diff --git a/e2e-tests/directory.kv.test.ts b/e2e-tests/directory.kv.test.ts index 4eb5c7c0..ee9b8480 100644 --- a/e2e-tests/directory.kv.test.ts +++ b/e2e-tests/directory.kv.test.ts @@ -87,3 +87,61 @@ test('GET `/dist` redirects to `/dist/`', async () => { expect(res.status).toBe(301); expect(res.headers.get('location')).toStrictEqual('https://localhost/dist/'); }); + +for (const prefix of ['/dist', '/download/release']) { + test(`GET \`${prefix}/docs/\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); + + // Dir that actually exists in `nodejs/docs/` + test(`GET \`${prefix}/docs/v0.0.1/\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); + + // Dir that's just a symlink `nodejs/docs/` -> `nodejs/release/v20.0.0/docs/` + test(`GET \`${prefix}/docs/v20.0.0/\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v20.0.0/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); +} diff --git a/e2e-tests/directory.s3.test.ts b/e2e-tests/directory.s3.test.ts index f1bca3e8..2152ad16 100644 --- a/e2e-tests/directory.s3.test.ts +++ b/e2e-tests/directory.s3.test.ts @@ -153,3 +153,67 @@ test('GET `/dist` redirects to `/dist/`', async () => { expect(res.status).toBe(301); expect(res.headers.get('location')).toStrictEqual('https://localhost/dist/'); }); + +for (const prefix of ['/dist', '/download/release']) { + test(`GET \`${prefix}/docs/\` returns 200`, async () => { + mockS3Response(S3_DIRECTORY_RESULT); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); + + // Dir that actually exists in `nodejs/docs/` + test(`GET \`${prefix}/docs/v0.0.1/\` returns 200`, async () => { + mockS3Response(S3_DIRECTORY_RESULT); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); + + // Dir that's just a symlink `nodejs/docs/` -> `nodejs/release/v20.0.0/docs/` + test(`GET \`${prefix}/docs/v20.0.0/\` returns 200`, async () => { + mockS3Response(S3_DIRECTORY_RESULT); + + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v20.0.0/`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.mutable + ); + }); +} diff --git a/e2e-tests/file.test.ts b/e2e-tests/file.test.ts index a4522a15..2b0008b3 100644 --- a/e2e-tests/file.test.ts +++ b/e2e-tests/file.test.ts @@ -184,6 +184,64 @@ test('GET `/dist/asd123.json` returns 404', async () => { expect(await res.text()).toStrictEqual('File not found'); }); +for (const prefix of ['/dist', '/download/release']) { + test(`GET \`${prefix}/docs/index.html\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/index.html`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.immutable + ); + }); + + // Dir that actually exists in `nodejs/docs/` + test(`GET \`${prefix}/docs/v0.0.1/index.html\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v0.0.1/index.html`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.immutable + ); + }); + + // Dir that's just a symlink `nodejs/docs/` -> `nodejs/release/v20.0.0/docs/` + test(`GET \`${prefix}/docs/v20.0.0/api/index.html\` returns 200`, async () => { + const ctx = createExecutionContext(); + + const res = await worker.fetch( + new Request(`https://localhost${prefix}/docs/v20.0.0/api/index.html`), + mockedEnv, + ctx + ); + + // Consume the body promise + await res.text(); + + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toStrictEqual( + CACHE_HEADERS.immutable + ); + }); +} + test('`if-modified-since` header', async () => { const ctx = createExecutionContext(); diff --git a/src/routes/index.ts b/src/routes/index.ts index ef685c2d..a9f0d050 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -48,6 +48,27 @@ export function registerRoutes(router: Router): void { router.head('/llms.txt', r2Middleware); router.get('/llms.txt', r2Middleware); + // `/dist/docs/...` is a symlink to `nodejs/docs/` + const distDocsDirToDocsDirRewrite = new SubtitutionMiddleware( + router, + '/dist/docs', + '/docs' + ); + router.head('/dist/docs*', distDocsDirToDocsDirRewrite); + router.get('/dist/docs*', distDocsDirToDocsDirRewrite); + + // `/download/release/docs/...` is a symlink to `nodejs/docs/` + const mainlineReleaseDocsDirToDocsDirRewrite = new SubtitutionMiddleware( + router, + '/download/release/docs', + '/docs' + ); + router.head( + '/download/release/docs*', + mainlineReleaseDocsDirToDocsDirRewrite + ); + router.get('/download/release/docs*', mainlineReleaseDocsDirToDocsDirRewrite); + router.head('/dist/:filePath+?', r2Middleware); router.get('/dist/:filePath+?', r2Middleware); diff --git a/src/utils/cache.test.ts b/src/utils/cache.test.ts index 503c46e1..db8ff830 100644 --- a/src/utils/cache.test.ts +++ b/src/utils/cache.test.ts @@ -7,6 +7,8 @@ describe('isImmutablePath', () => { '/dist/v20.0.0/node-v20.0.0-linux-x64.tar.gz', '/download/release/v20.0.0/node-v20.0.0.pkg', '/docs/v18.0.0/api/fs.html', + '/dist/docs/v20.0.0/api/index.html', + '/download/release/v20.0.0/api/index.html', ])('returns true for immutable asset `%s`', path => { expect(isImmutablePath(path)).toEqual(true); }); @@ -31,6 +33,10 @@ describe('isImmutablePath', () => { // `latest` aliases point at moving content '/dist/latest/node-v20.0.0-linux-x64.tar.gz', '/docs/latest/api/fs.html', + '/dist/docs/latest/api/index.html', + '/download/release/latest/api/index.html', + '/dist/docs/', + '/download/docs/', '/download/release/latest/win-x64/node_pdb.7z', '/dist/latest-v20.x/node-v20.0.0.tar.gz', // Legacy `latest-v0.X.x` aliases