diff --git a/epicshop/package.json b/epicshop/package.json index c666045fc..3ff444456 100644 --- a/epicshop/package.json +++ b/epicshop/package.json @@ -1,6 +1,8 @@ { "type": "module", "scripts": { + "postinstall": "node ./patch-workshop-app.js", + "test:patch": "node --test ./patch-workshop-app.test.js", "test:setup": "playwright install chromium --with-deps", "test": "playwright test" }, diff --git a/epicshop/patch-workshop-app.js b/epicshop/patch-workshop-app.js new file mode 100644 index 000000000..663cca7a6 --- /dev/null +++ b/epicshop/patch-workshop-app.js @@ -0,0 +1,74 @@ +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const workshopAppServerBuildPath = path.join( + 'node_modules', + '@epic-web', + 'workshop-app', + 'build', + 'server', + 'index.js', +) + +const catchAllActionSource = `async function action$catchAll() { + throw new Response("Not Found", { status: 404 }); +} +` + +const routeWithoutAction = `const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + ErrorBoundary: ErrorBoundary$7, + default: $, + loader: loader$L +}, Symbol.toStringTag, { value: "Module" }));` + +const routeWithAction = `const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + ErrorBoundary: ErrorBoundary$7, + default: $, + action: action$catchAll, + loader: loader$L +}, Symbol.toStringTag, { value: "Module" }));` + +const manifestWithoutAction = `"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false,` +const manifestWithAction = `"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": true,` + +export function patchServerBuild(contents) { + if (contents.includes('action: action$catchAll')) { + return contents + } + + if (!contents.includes(routeWithoutAction)) { + throw new Error('Could not find the workshop app catch-all route module.') + } + + if (!contents.includes(manifestWithoutAction)) { + throw new Error('Could not find the workshop app catch-all route manifest.') + } + + return contents + .replace(routeWithoutAction, `${catchAllActionSource}${routeWithAction}`) + .replace(manifestWithoutAction, manifestWithAction) +} + +export async function patchWorkshopApp({ cwd = __dirname } = {}) { + const serverBuildPath = path.join(cwd, workshopAppServerBuildPath) + const currentContents = await readFile(serverBuildPath, 'utf8') + const patchedContents = patchServerBuild(currentContents) + + if (patchedContents === currentContents) { + return { serverBuildPath, patched: false } + } + + await writeFile(serverBuildPath, patchedContents) + return { serverBuildPath, patched: true } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const { serverBuildPath, patched } = await patchWorkshopApp() + console.log( + `${patched ? 'Patched' : 'Already patched'} @epic-web/workshop-app catch-all route: ${serverBuildPath}`, + ) +} diff --git a/epicshop/patch-workshop-app.test.js b/epicshop/patch-workshop-app.test.js new file mode 100644 index 000000000..33296901d --- /dev/null +++ b/epicshop/patch-workshop-app.test.js @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { patchServerBuild } from './patch-workshop-app.js' + +const serverBuildFixture = `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": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true } } };` + +test('patches the workshop app catch-all route with a 404 action', () => { + const patched = patchServerBuild(serverBuildFixture) + + assert.match( + patched, + /async function action\$catchAll\(\) {\n throw new Response\("Not Found", { status: 404 }\);\n}/, + ) + assert.match(patched, /action: action\$catchAll,/) + assert.match( + patched, + /"routes\/\$": { "id": "routes\/\$", "parentId": "root", "path": "\*", "index": void 0, "caseSensitive": void 0, "hasAction": true,/, + ) +}) + +test('patching the server build is idempotent', () => { + const patched = patchServerBuild(serverBuildFixture) + + assert.equal(patchServerBuild(patched), patched) +})