Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions epicshop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions epicshop/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"type": "module",
"scripts": {
"postinstall": "node ./patch-workshop-app.js",
"test": "node --test ./patch-workshop-app.test.js"
},
"dependencies": {
"@epic-web/workshop-app": "^6.90.3",
"@epic-web/workshop-utils": "^6.90.3",
"epicshop": "^6.90.3",
"@epic-web/workshop-app": "^6.90.4",
"@epic-web/workshop-utils": "^6.90.4",
"epicshop": "^6.90.4",
"execa": "^8.0.1",
"fs-extra": "^11.2.0"
}
Expand Down
93 changes: 93 additions & 0 deletions epicshop/patch-workshop-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import fs from 'node:fs/promises'
import { createRequire } from 'node:module'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const require = createRequire(import.meta.url)
const catchAllRouteId = 'routes/$'

function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function getWorkshopAppServerBuildPath() {
const packageJsonPath = require.resolve('@epic-web/workshop-app/package.json')
return path.join(path.dirname(packageJsonPath), 'build/server/index.js')
}

export function patchServerBuild(contents) {
let patchedContents = contents
const routeModuleName = contents.match(
/const routes = \{[\s\S]*?"routes\/\$":\s*\{[\s\S]*?module:\s*(route\d+)/,
)?.[1]

if (!routeModuleName) {
throw new Error(`Unable to find ${catchAllRouteId} module in workshop app build`)
}

const routeModulePattern = new RegExp(
`const ${escapeRegExp(routeModuleName)} = /\\* @__PURE__ \\*/ Object\\.freeze\\(/\\* @__PURE__ \\*/ Object\\.defineProperty\\(\\{([\\s\\S]*?)\\n\\}, Symbol\\.toStringTag`,
)
const routeModuleMatch = contents.match(routeModulePattern)

if (!routeModuleMatch) {
throw new Error(`Unable to find ${catchAllRouteId} route module declaration`)
}

const routeModuleBody = routeModuleMatch[1]
const loaderName = routeModuleBody.match(
/\n\s+loader:\s*([A-Za-z_$][\w$]*)/,
)?.[1]

if (!loaderName) {
throw new Error(`Unable to find ${catchAllRouteId} loader export`)
}

let patchedRouteAction = false
if (!/\n\s+action:/.test(routeModuleBody)) {
const patchedRouteModuleBody = routeModuleBody.replace(
/\n(\s+)loader:\s*([A-Za-z_$][\w$]*)/,
(_match, indent, loader) =>
`\n${indent}action: ${loaderName},\n${indent}loader: ${loader}`,
)
const routeModuleBodyStart =
routeModuleMatch.index + routeModuleMatch[0].indexOf(routeModuleBody)
patchedContents =
patchedContents.slice(0, routeModuleBodyStart) +
patchedRouteModuleBody +
patchedContents.slice(routeModuleBodyStart + routeModuleBody.length)
patchedRouteAction = true
}

let patchedManifest = false
patchedContents = patchedContents.replace(
/"routes\/\$": \{([\s\S]{0,600}?)"hasAction": false/g,
(match, routeManifestPrefix) => {
patchedManifest = true
return `"routes/$": {${routeManifestPrefix}"hasAction": true`
},
)

return { contents: patchedContents, patchedRouteAction, patchedManifest }
}

export async function patchInstalledWorkshopApp() {
const serverBuildPath = getWorkshopAppServerBuildPath()
const originalContents = await fs.readFile(serverBuildPath, 'utf8')
const result = patchServerBuild(originalContents)

if (result.contents !== originalContents) {
await fs.writeFile(serverBuildPath, result.contents)
}

const actionStatus = result.patchedRouteAction ? 'added' : 'already present'
const manifestStatus = result.patchedManifest ? 'updated' : 'already current'
console.log(
`Patched @epic-web/workshop-app ${catchAllRouteId}: action ${actionStatus}, manifest ${manifestStatus}.`,
)
}

const currentFilePath = fileURLToPath(import.meta.url)
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
await patchInstalledWorkshopApp()
}
38 changes: 38 additions & 0 deletions epicshop/patch-workshop-app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { patchServerBuild } from './patch-workshop-app.js'

const fixture = `
const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
ErrorBoundary: ErrorBoundary$7,
default: $,
loader: loader$L
}, Symbol.toStringTag, { value: "Module" }));
const serverManifest = { "routes": { "routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "hasAction": false, "hasLoader": true } } };
const routes = {
"routes/$": {
id: "routes/$",
parentId: "root",
path: "*",
module: route1
}
};
`

test('patches the workshop app catch-all route with an action', () => {
const result = patchServerBuild(fixture)

assert.equal(result.patchedRouteAction, true)
assert.equal(result.patchedManifest, true)
assert.match(result.contents, /action: loader\$L,\n loader: loader\$L/)
assert.match(result.contents, /"routes\/\$": \{[^}]*"hasAction": true/)
})

test('leaves an already patched catch-all route unchanged', () => {
const once = patchServerBuild(fixture)
const twice = patchServerBuild(once.contents)

assert.equal(twice.patchedRouteAction, false)
assert.equal(twice.contents, once.contents)
})
6 changes: 5 additions & 1 deletion exercises/07.error-handling/05.problem.not-found/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ As we're following the `remix-flat-routes` convention, to create a route that
matches `/*`, 🐨 we'll create a file at <InlineFile file="app/routes/$.tsx" />.

With that file created, you need to 🐨 create a loader that throws a `404`
response:
response. Also export an action that uses the same behavior so non-GET requests
to missing routes get the expected 404 response instead of falling through to a
framework "no action" error:

```tsx
export async function loader() {
throw new Response('Not found', { status: 404 })
}

export const action = loader
```

Next, let's 🐨 export the `ErrorBoundary`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export async function loader() {
throw new Response('Not found', { status: 404 })
}

export const action = loader

export default function NotFound() {
// due to the loader, this component will never be rendered, but we'll return
// the error boundary just in case.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@ import { test, expect } from '@playwright/test'

test('can visit the home page', async ({ page }) => {
await page.goto('/')
await expect(page.getByText('Hello World')).toBeVisible()
await expect(page.getByRole('heading', { name: 'Epic Notes' })).toBeVisible()
})

test('post requests to missing routes return the not found response', async ({
request,
}) => {
const response = await request.post('/connectors/resource/index.php')

expect(response.status()).toBe(404)
expect(await response.text()).toContain('Not found')
})
Loading