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
1 change: 1 addition & 0 deletions epicshop/package-lock.json

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

4 changes: 4 additions & 0 deletions epicshop/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"type": "module",
"scripts": {
"postinstall": "node ./patch-workshop-app.js",
"test:patch-workshop-app": "node --test ./patch-workshop-app.test.js"
},
"dependencies": {
"@epic-web/workshop-app": "^6.90.3",
"@epic-web/workshop-utils": "^6.90.3",
Expand Down
193 changes: 193 additions & 0 deletions epicshop/patch-workshop-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

const here = path.dirname(fileURLToPath(import.meta.url))
const defaultServerBuildPath = path.join(
here,
'node_modules',
'@epic-web',
'workshop-app',
'build',
'server',
'index.js',
)
const defaultServerRuntimePath = path.join(
here,
'node_modules',
'@epic-web',
'workshop-app',
'dist',
'server',
'index.js',
)

const notFoundActionFunctionName = 'action$splatNotFound'
const notFoundActionFunction = `async function ${notFoundActionFunctionName}() {
throw new Response("Not found", {
status: 404
});
}
`

const loaderEndNeedle = ` throw new Response("Not found", {
status: 404
});
}
const $ = UNSAFE_withComponentProps`

const patchedLoaderEndNeedle = ` throw new Response("Not found", {
status: 404
});
}
${notFoundActionFunction}const $ = UNSAFE_withComponentProps`

const routeModuleNeedle = ` ErrorBoundary: ErrorBoundary$7,
default: $,
loader: loader$L
}, Symbol.toStringTag`

const patchedRouteModuleNeedle = ` ErrorBoundary: ErrorBoundary$7,
action: ${notFoundActionFunctionName},
default: $,
loader: loader$L
}, Symbol.toStringTag`

const routeManifestNeedle =
'"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": false,'

const patchedRouteManifestNeedle =
'"routes/$": { "id": "routes/$", "parentId": "root", "path": "*", "index": void 0, "caseSensitive": void 0, "hasAction": true,'

const repeatedSlashMiddleware = `app.use((req, res, next) => {
const requestPath = req.originalUrl.split("?")[0];
if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS" && /^\\/{2,}$/.test(requestPath)) {
res.status(404).send("Not found");
return;
}
next();
});
`

const requestContextMiddlewareNeedle =
'app.use((_req, _res, next) => requestContext.run({}, next));\n'

const patchedRequestContextMiddlewareNeedle = `${requestContextMiddlewareNeedle}${repeatedSlashMiddleware}`

export function patchWorkshopAppServerBuild(source) {
const hasActionFunction = source.includes(
`async function ${notFoundActionFunctionName}()`,
)
const hasActionExport = source.includes(
`action: ${notFoundActionFunctionName}`,
)
const hasActionManifest = source.includes(patchedRouteManifestNeedle)

if (hasActionFunction && hasActionExport && hasActionManifest) {
return { patched: false, source }
}

if (hasActionFunction !== hasActionExport) {
throw new Error(
'Found a partial workshop-app splat action patch. Reinstall dependencies and rerun the patch.',
)
}

let patchedSource = source
let patched = false

if (!hasActionFunction) {
if (!patchedSource.includes(loaderEndNeedle)) {
throw new Error(
'Could not find the workshop-app splat route loader to patch.',
)
}

if (!patchedSource.includes(routeModuleNeedle)) {
throw new Error(
'Could not find the workshop-app splat route module to patch.',
)
}

patchedSource = patchedSource
.replace(loaderEndNeedle, patchedLoaderEndNeedle)
.replace(routeModuleNeedle, patchedRouteModuleNeedle)
patched = true
}

if (!hasActionManifest) {
if (!patchedSource.includes(routeManifestNeedle)) {
throw new Error(
'Could not find the workshop-app splat route manifest entry to patch.',
)
}

patchedSource = patchedSource.replace(
routeManifestNeedle,
patchedRouteManifestNeedle,
)
patched = true
}

return {
patched,
source: patchedSource,
}
}

export function patchWorkshopAppServerRuntime(source) {
if (source.includes(repeatedSlashMiddleware)) {
return { patched: false, source }
}

if (!source.includes(requestContextMiddlewareNeedle)) {
throw new Error(
'Could not find the workshop-app request context middleware to patch.',
)
}

return {
patched: true,
source: source.replace(
requestContextMiddlewareNeedle,
patchedRequestContextMiddlewareNeedle,
),
}
}

export async function patchWorkshopApp({
serverBuildPath = defaultServerBuildPath,
serverRuntimePath = defaultServerRuntimePath,
} = {}) {
const [serverBuildSource, serverRuntimeSource] = await Promise.all([
fs.readFile(serverBuildPath, 'utf8'),
fs.readFile(serverRuntimePath, 'utf8'),
])
const serverBuildResult = patchWorkshopAppServerBuild(serverBuildSource)
const serverRuntimeResult = patchWorkshopAppServerRuntime(serverRuntimeSource)

await Promise.all([
serverBuildResult.patched
? fs.writeFile(serverBuildPath, serverBuildResult.source)
: null,
serverRuntimeResult.patched
? fs.writeFile(serverRuntimePath, serverRuntimeResult.source)
: null,
])

if (serverBuildResult.patched || serverRuntimeResult.patched) {
console.log(
'Patched @epic-web/workshop-app splat route to return a normal 404 for POST scanner traffic.',
)
}

return {
patched: serverBuildResult.patched || serverRuntimeResult.patched,
serverBuild: serverBuildResult,
serverRuntime: serverRuntimeResult,
}
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
await patchWorkshopApp()
}
96 changes: 96 additions & 0 deletions epicshop/patch-workshop-app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
patchWorkshopApp,
patchWorkshopAppServerBuild,
patchWorkshopAppServerRuntime,
} from './patch-workshop-app.js'

const unpatchedServerBuildFixture = `async function loader$L({
params
}) {
const splat = params["*"];
const segments = splat?.split("/") ?? [];
if (segments.length > 0 && !isNaN(Number(segments[0]))) {
const newPath = \`/exercise/\${splat}\`;
return new Response(null, {
status: 302,
headers: {
Location: newPath
}
});
}
throw new Response("Not found", {
status: 404
});
}
const $ = UNSAFE_withComponentProps(function NotFound() {
return /* @__PURE__ */ jsx(ErrorBoundary$7, {});
});
const ErrorBoundary$7 = UNSAFE_withErrorBoundaryProps(function ErrorBoundary2() {
return null;
});
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 } } };
`

const unpatchedServerRuntimeFixture = `app.use((_req, _res, next) => requestContext.run({}, next));
app.options("*splat", (_req, res) => {
res.set("Allow", "GET, HEAD, POST, OPTIONS");
res.sendStatus(204);
});
`

test('adds a 404 action to the workshop-app splat route module', () => {
const result = patchWorkshopAppServerBuild(unpatchedServerBuildFixture)

assert.equal(result.patched, true)
assert.match(result.source, /async function action\$splatNotFound\(\)/)
assert.match(result.source, /action: action\$splatNotFound/)
assert.match(result.source, /"routes\/\$": .*"hasAction": true/)
})

test('does not modify an already patched server build', () => {
const firstResult = patchWorkshopAppServerBuild(unpatchedServerBuildFixture)
const secondResult = patchWorkshopAppServerBuild(firstResult.source)

assert.equal(secondResult.patched, false)
assert.equal(secondResult.source, firstResult.source)
})

test('fails loudly when the expected route shape changes', () => {
assert.throws(
() => patchWorkshopAppServerBuild('const route1 = {}'),
/Could not find the workshop-app splat route loader/,
)
})

test('adds a narrow repeated-slash POST guard before React Router', () => {
const result = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture)

assert.equal(result.patched, true)
assert.match(result.source, /requestPath = req\.originalUrl\.split/)
assert.match(result.source, /\^\\\/\{2,\}\$/)
})

test('does not modify an already patched server runtime', () => {
const firstResult = patchWorkshopAppServerRuntime(unpatchedServerRuntimeFixture)
const secondResult = patchWorkshopAppServerRuntime(firstResult.source)

assert.equal(secondResult.patched, false)
assert.equal(secondResult.source, firstResult.source)
})

test('patches the installed workshop-app build', async () => {
const result = await patchWorkshopApp()

assert.equal(typeof result.patched, 'boolean')
assert.match(result.serverBuild.source, /action: action\$splatNotFound/)
assert.match(result.serverBuild.source, /"routes\/\$": .*"hasAction": true/)
assert.match(result.serverRuntime.source, /\^\\\/\{2,\}\$/)
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test:e2e": "pkgmgr run test:e2e --silent --prefix playground",
"test:e2e:dev": "pkgmgr run test:e2e:dev --silent --prefix playground",
"test:e2e:run": "pkgmgr run test:e2e:run --silent --prefix playground",
"test:patch-workshop-app": "npm run test:patch-workshop-app --prefix epicshop",
"setup": "pkgmgrx epicshop setup",
"setup:custom": "node ./epicshop/setup-custom.js",
"lint": "eslint . --concurrency=auto",
Expand Down
Loading