From 70f9e7a7fac29e9c1f24defad563636852dff839 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:10:09 -0800 Subject: [PATCH 1/3] src: fix symlink issue, subtitution middleware hack Fixes #163 --- .github/workflows/update-links.yml | 2 +- scripts/build-r2-symlinks.mjs | 270 +++ ...e-handlebars.js => compile-handlebars.mjs} | 0 scripts/constants.mjs | 4 + scripts/promote-release.mjs | 135 -- scripts/update-latest-versions.js | 64 - src/constants/README.md | 12 + src/constants/cachedDirectories.json | 2138 +++++++++++++++++ src/constants/docsDirectory.json | 87 + src/middleware/r2Middleware.ts | 24 +- src/middleware/subtituteMiddleware.ts | 8 +- src/providers/r2Provider.ts | 24 + src/routes/request.ts | 2 +- src/routes/router.ts | 39 +- tests/e2e/fallback.test.ts | 2 +- .../middleware/substituteMiddleware.test.ts | 20 +- 16 files changed, 2589 insertions(+), 242 deletions(-) create mode 100644 scripts/build-r2-symlinks.mjs rename scripts/{compile-handlebars.js => compile-handlebars.mjs} (100%) delete mode 100755 scripts/promote-release.mjs delete mode 100644 scripts/update-latest-versions.js create mode 100644 src/constants/README.md create mode 100644 src/constants/cachedDirectories.json create mode 100644 src/constants/docsDirectory.json diff --git a/.github/workflows/update-links.yml b/.github/workflows/update-links.yml index efd0a6c..ad864d3 100644 --- a/.github/workflows/update-links.yml +++ b/.github/workflows/update-links.yml @@ -37,7 +37,7 @@ jobs: run: npm install && npm update nodejs-latest-linker --save - name: Update Redirect Links - run: node scripts/update-latest-versions.js && npm run format + run: node scripts/build-r2-symlinks.mjs && npm run format env: CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} diff --git a/scripts/build-r2-symlinks.mjs b/scripts/build-r2-symlinks.mjs new file mode 100644 index 0000000..29e02f3 --- /dev/null +++ b/scripts/build-r2-symlinks.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +'use strict'; + +import { join } from 'node:path'; +import { writeFile } from 'node:fs/promises'; +import { + HeadObjectCommand, + ListObjectsV2Command, + S3Client, +} from '@aws-sdk/client-s3'; +import { Linker } from 'nodejs-latest-linker/common.js'; +import { DOCS_DIR, ENDPOINT, PROD_BUCKET, RELEASE_DIR } from './constants.mjs'; + +const DOCS_DIRECTORY_OUT = join( + import.meta.dirname, + '..', + 'src', + 'constants', + 'docsDirectory.json' +); + +const LATEST_VERSIONS_OUT = join( + import.meta.dirname, + '..', + 'src', + 'constants', + 'latestVersions.json' +); + +const CACHED_DIRECTORIES_OUT = join( + import.meta.dirname, + '..', + 'src', + 'constants', + 'cachedDirectories.json' +); + +if (!process.env.CF_ACCESS_KEY_ID) { + throw new TypeError('CF_ACCESS_KEY_ID missing'); +} + +if (!process.env.CF_ACCESS_KEY_ID) { + throw new TypeError('CF_SECRET_ACCESS_KEY missing'); +} + +const client = new S3Client({ + endpoint: ENDPOINT, + region: 'auto', + credentials: { + accessKeyId: process.env.CF_ACCESS_KEY_ID, + secretAccessKey: process.env.CF_SECRET_ACCESS_KEY, + }, +}); + +// Cache the contents of `nodejs/docs/` so we can reference it in the worker +await writeDocsDirectoryFile(client); + +// Grab all of the files & directories in `nodejs/release/` +const releases = await listDirectory(client, RELEASE_DIR); + +// Create the latest version mapping with the contents of `nodejs/release/` +const latestVersions = await getLatestVersionMapping(client, releases); + +// Write it so we can use it in the worker +await writeFile(LATEST_VERSIONS_OUT, JSON.stringify(latestVersions)); + +// Filter the latest version map so we only have the `latest-*` directories +const latestVersionDirectories = Object.keys(latestVersions).map(version => + version === 'node-latest.tar.gz' ? version : `${version}/` +); + +// Create the complete listing of `nodejs/release/` by adding what R2 returned +// and the latest version directories (which are the symlinks) +const releaseDirectorySubdirectories = releases.subdirectories + .concat(latestVersionDirectories) + .sort(); + +// This is the path in R2 for the latest tar archive of Node. +const nodeLatestPath = `nodejs/release/${latestVersions['node-latest.tar.gz'].replaceAll('latest', latestVersions['latest'])}`; + +// Stat the file that `node-latest.tar.gz` points to so we can have accurate +// size & last modified info for the directory listing +const nodeLatest = await headFile(client, nodeLatestPath); +if (!nodeLatest) { + throw new TypeError( + `node-latest.tar.gz points to ${latestVersions['node-latest.tar.gz']} which doesn't exist in the prod bucket` + ); +} + +/** + * Preprocess these directories since they have symlinks in them that don't + * actually exist in R2 but need to be present when we give a directory listing + * result + * @type {Record} + */ +const cachedDirectories = { + 'nodejs/release/': { + subdirectories: releaseDirectorySubdirectories, + hasIndexHtmlFile: false, + files: [ + ...releases.files, + { + name: 'node-latest.tar.gz', + lastModified: nodeLatest.lastModified, + size: nodeLatest.size, + }, + ], + lastModified: releases.lastModified, + }, + 'nodejs/docs/': { + // We reuse the releases listing result here instead of listing the docs + // directory since it's more complete. The docs folder does have some actual + // directories in it, but most of it is symlinks and aren't present in R2. + subdirectories: releaseDirectorySubdirectories, + hasIndexHtmlFile: false, + files: [], + lastModified: releases.lastModified, + }, +}; + +await writeFile(CACHED_DIRECTORIES_OUT, JSON.stringify(cachedDirectories)); + +/** + * @param {S3Client} client + * @param {string} directory + * @returns {Promise} + */ +async function listDirectory(client, directory) { + /** + * @type {Array} + */ + const subdirectories = []; + + /** + * @type {Array} + */ + const files = []; + + let hasIndexHtmlFile = false; + + let truncated = true; + let continuationToken; + let lastModified = new Date(0); + while (truncated) { + const data = await client.send( + new ListObjectsV2Command({ + Bucket: PROD_BUCKET, + Delimiter: '/', + Prefix: directory, + ContinuationToken: continuationToken, + }) + ); + + if (data.CommonPrefixes) { + data.CommonPrefixes.forEach(value => { + if (value.Prefix) { + subdirectories.push(value.Prefix.substring(directory.length)); + } + }); + } + + if (data.Contents) { + data.Contents.forEach(value => { + if (value.Key) { + if (value.Key.match(/index.htm(?:l)$/)) { + hasIndexHtmlFile = true; + } + + files.push({ + name: value.Key.substring(directory.length), + lastModified: value.LastModified, + size: value.Size, + }); + + if (value.LastModified > lastModified) { + lastModified = value.LastModified; + } + } + }); + } + + truncated = data.IsTruncated; + continuationToken = data.NextContinuationToken; + } + + return { subdirectories, hasIndexHtmlFile, files, lastModified }; +} + +/** + * @param {S3Client} client + * @param {string} path + * @returns {Promise} + */ +async function headFile(client, path) { + const data = await client.send( + new HeadObjectCommand({ + Bucket: PROD_BUCKET, + Key: path, + }) + ); + + if (!data.LastModified || !data.ContentLength) { + return undefined; + } + + return { + name: path, + lastModified: data.LastModified, + size: data.ContentLength, + }; +} + +/** + * @param {S3Client} client + */ +async function writeDocsDirectoryFile(client) { + // Grab all of the directories in `nodejs/docs/` + const docs = await listDirectory(client, DOCS_DIR); + + // Cache the contents of `nodejs/docs/` so we can refer to it in the worker w/o + // making a call to R2. + await writeFile( + DOCS_DIRECTORY_OUT, + JSON.stringify( + docs.subdirectories.map(subdirectory => + subdirectory.substring(0, subdirectory.length - 1) + ) + ) + ); +} + +/** + * @param {S3Client} client + * @param {import('../src/providers/provider.js').ReadDirectoryResult} releases + * @returns {Promise>} + */ +async function getLatestVersionMapping(client, releases) { + const linker = new Linker({ baseDir: RELEASE_DIR, docs: DOCS_DIR }); + + /** + * Creates mappings to the latest versions of Node + * @type {Map} + * @example { 'nodejs/release/latest-v20.x': 'nodejs/release/v20.x.x' } + */ + const links = await linker.getLinks( + [...releases.subdirectories, ...releases.files.map(file => file.name)], + async directory => { + const { subdirectories, files } = await listDirectory( + client, + `${directory}/` + ); + return [...subdirectories, ...files.map(file => file.name)]; + } + ); + + /** + * @type {Record} + * @example {'latest-v20.x': 'v20.x.x'} + */ + const latestVersions = {}; + + for (const [key, value] of links) { + const trimmedKey = key.substring(RELEASE_DIR.length); + const trimmedValue = value.substring(RELEASE_DIR.length); + + latestVersions[trimmedKey] = trimmedValue; + } + + return latestVersions; +} diff --git a/scripts/compile-handlebars.js b/scripts/compile-handlebars.mjs similarity index 100% rename from scripts/compile-handlebars.js rename to scripts/compile-handlebars.mjs diff --git a/scripts/constants.mjs b/scripts/constants.mjs index f1a7be9..977fdc1 100644 --- a/scripts/constants.mjs +++ b/scripts/constants.mjs @@ -9,3 +9,7 @@ export const PROD_BUCKET = process.env.PROD_BUCKET ?? 'dist-prod'; export const STAGING_BUCKET = process.env.STAGING_BUCKET ?? 'dist-staging'; export const R2_RETRY_COUNT = 3; + +export const RELEASE_DIR = 'nodejs/release/'; + +export const DOCS_DIR = 'nodejs/docs/'; diff --git a/scripts/promote-release.mjs b/scripts/promote-release.mjs deleted file mode 100755 index 2bcbc66..0000000 --- a/scripts/promote-release.mjs +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -/** - * Usage: `promote-release [--recursive]` - * ex/ `promote-release nodejs/release/v20.0.0/ --recursive` - */ - -import { - S3Client, - ListObjectsV2Command, - CopyObjectCommand, -} from '@aws-sdk/client-s3'; -import { - ENDPOINT, - PROD_BUCKET, - STAGING_BUCKET, - R2_RETRY_COUNT, -} from './constants.mjs'; - -if (process.argv.length !== 3 && process.argv.length !== 4) { - console.error( - `usage: promote-release [--recursive]` - ); - process.exit(1); -} - -if (!process.env.CF_ACCESS_KEY_ID) { - console.error('CF_ACCESS_KEY_ID missing'); - process.exit(1); -} - -if (!process.env.CF_SECRET_ACCESS_KEY) { - console.error('CF_SECRET_ACCESS_KEY missing'); - process.exit(1); -} - -const client = new S3Client({ - endpoint: ENDPOINT, - region: 'auto', - credentials: { - accessKeyId: process.env.CF_ACCESS_KEY_ID, - secretAccessKey: process.env.CF_SECRET_ACCESS_KEY, - }, -}); - -const path = process.argv[2]; -const recursive = - process.argv.length === 4 && process.argv[3] === '--recursive'; - -if (recursive) { - const files = await getFilesToPromote(path); - - for (const file of files) { - await promoteFile(file); - } -} else { - await promoteFile(file); -} - -/** - * @param {string} path - * @returns {string[]} - */ -async function getFilesToPromote(path) { - let paths = []; - - let truncated = true; - let continuationToken; - while (truncated) { - const data = await retryWrapper(async () => { - return await client.send( - new ListObjectsV2Command({ - Bucket: STAGING_BUCKET, - Delimiter: '/', - Prefix: path, - ContinuationToken: continuationToken, - }) - ); - }); - - if (data.CommonPrefixes) { - for (const directory of data.CommonPrefixes) { - paths.push(...(await getFilesToPromote(directory.Prefix))); - } - } - - if (data.Contents) { - for (const object of data.Contents) { - paths.push(object.Key); - } - } - - truncated = data.IsTruncated ?? false; - continuationToken = data.NextContinuationToken; - } - - return paths; -} - -/** - * @param {string} file - */ -async function promoteFile(file) { - console.log(`Promoting ${file}`); - - await retryWrapper(async () => { - return await client.send( - new CopyObjectCommand({ - Bucket: PROD_BUCKET, - CopySource: `${STAGING_BUCKET}/${file}`, - Key: file, - }) - ); - }, R2_RETRY_COUNT); -} - -/** - * @param {() => Promise} request - * @returns {Promise} - */ -async function retryWrapper(request, retryLimit) { - let r2Error; - - for (let i = 0; i < R2_RETRY_COUNT; i++) { - try { - const result = await request(); - return result; - } catch (err) { - r2Error = err; - process.emitWarning(`error when contacting r2: ${err}`); - } - } - - throw r2Error; -} diff --git a/scripts/update-latest-versions.js b/scripts/update-latest-versions.js deleted file mode 100644 index de84ee3..0000000 --- a/scripts/update-latest-versions.js +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env node - -import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3'; -import { Linker } from 'nodejs-latest-linker/common.js'; -import { writeFile } from 'node:fs/promises'; - -const ENDPOINT = - 'https://07be8d2fbc940503ca1be344714cb0d1.r2.cloudflarestorage.com'; -const BUCKET = 'dist-prod'; -const RELEASE_DIR = 'nodejs/release/'; -const DOCS_DIR = 'nodejs/docs/'; - -const client = new S3Client({ - endpoint: ENDPOINT, - region: 'auto', - credentials: { - accessKeyId: process.env.CF_ACCESS_KEY_ID, - secretAccessKey: process.env.CF_SECRET_ACCESS_KEY, - }, -}); - -(async function main() { - const allDirs = await listDirectory(RELEASE_DIR); - const linker = new Linker({ baseDir: RELEASE_DIR }); - const allLinks = await linker.getLinks(allDirs, dir => - listDirectory(`${dir}/`) - ); - - const latestLinks = Array.from(allLinks).filter(link => - link[0].startsWith('nodejs/release') - ); - latestLinks.forEach(link => { - link[0] = link[0].substring('nodejs/release/'.length); - link[1] = link[1].substring('nodejs/release/'.length); - }); - - await writeFile( - './src/constants/latestVersions.json', - JSON.stringify(Object.fromEntries(latestLinks), null, 2) + '\n' - ); -})(); - -async function listDirectory(dir) { - let truncated = true; - let continuationToken; - let items = []; - while (truncated) { - const data = await client.send( - new ListObjectsV2Command({ - Bucket: BUCKET, - Delimiter: '/', - Prefix: dir, - ContinuationToken: continuationToken, - }) - ); - items = items.concat(data.CommonPrefixes ?? []).concat(data.Contents ?? []); - truncated = data.IsTruncated; - continuationToken = data.NextContinuationToken; - } - return items.map(d => { - const path = d.Prefix ?? d.Key; - return path.substring(dir.length); - }); -} diff --git a/src/constants/README.md b/src/constants/README.md new file mode 100644 index 0000000..dba9392 --- /dev/null +++ b/src/constants/README.md @@ -0,0 +1,12 @@ +# constants + +Various constants used throughout the worker + +## Table of Contents + +- [cache.ts](./cache.ts) - Caching directives +- [cachedDirectories.json](./cachedDirectories.json) - Directories that have their listing result cached because they have symlinks in them. These are updated by the [build-r2-symlinks](../scripts/build-r2-symlinks.mjs) script. +- [docsDirectory.json](./docsDirectory.json) - The contents of `nodejs/docs/` in the `dist-prod` bucket. This is updated by the [build-r2-symlinks](../scripts/build-r2-symlinks.mjs) script. +- [files.ts](./files.ts) - Constants related to files the worker serves. +- [latestVersions.json] - Map of `latest-*` directories to their actual latest versions. This is updated by the [build-r2-symlinks](../scripts/build-r2-symlinks.mjs) script. +- [limits.ts](./limits.ts) - Hardcap limits that the worker shouldn't exceed. diff --git a/src/constants/cachedDirectories.json b/src/constants/cachedDirectories.json new file mode 100644 index 0000000..d702f16 --- /dev/null +++ b/src/constants/cachedDirectories.json @@ -0,0 +1,2138 @@ +{ + "nodejs/release/": { + "subdirectories": [ + "latest-argon/", + "latest-boron/", + "latest-carbon/", + "latest-dubnium/", + "latest-erbium/", + "latest-fermium/", + "latest-gallium/", + "latest-hydrogen/", + "latest-iron/", + "latest-jod/", + "latest-v0.10.x/", + "latest-v0.12.x/", + "latest-v10.x/", + "latest-v11.x/", + "latest-v12.x/", + "latest-v13.x/", + "latest-v14.x/", + "latest-v15.x/", + "latest-v16.x/", + "latest-v17.x/", + "latest-v18.x/", + "latest-v19.x/", + "latest-v20.x/", + "latest-v21.x/", + "latest-v22.x/", + "latest-v23.x/", + "latest-v4.x/", + "latest-v5.x/", + "latest-v6.x/", + "latest-v7.x/", + "latest-v8.x/", + "latest-v9.x/", + "latest/", + "node-latest.tar.gz", + "npm/", + "patch/", + "v0.1.100/", + "v0.1.101/", + "v0.1.102/", + "v0.1.103/", + "v0.1.104/", + "v0.1.14/", + "v0.1.15/", + "v0.1.16/", + "v0.1.17/", + "v0.1.18/", + "v0.1.19/", + "v0.1.20/", + "v0.1.21/", + "v0.1.22/", + "v0.1.23/", + "v0.1.24/", + "v0.1.25/", + "v0.1.26/", + "v0.1.27/", + "v0.1.28/", + "v0.1.29/", + "v0.1.30/", + "v0.1.31/", + "v0.1.32/", + "v0.1.33/", + "v0.1.90/", + "v0.1.91/", + "v0.1.92/", + "v0.1.93/", + "v0.1.94/", + "v0.1.95/", + "v0.1.96/", + "v0.1.97/", + "v0.1.98/", + "v0.1.99/", + "v0.10.0/", + "v0.10.1/", + "v0.10.10/", + "v0.10.11/", + "v0.10.12/", + "v0.10.13/", + "v0.10.14/", + "v0.10.15/", + "v0.10.16-isaacs-manual/", + "v0.10.16/", + "v0.10.17/", + "v0.10.18/", + "v0.10.19/", + "v0.10.2/", + "v0.10.20/", + "v0.10.21/", + "v0.10.22/", + "v0.10.23/", + "v0.10.24/", + "v0.10.25/", + "v0.10.26/", + "v0.10.27/", + "v0.10.28/", + "v0.10.29/", + "v0.10.3/", + "v0.10.30/", + "v0.10.31/", + "v0.10.32/", + "v0.10.33/", + "v0.10.34/", + "v0.10.35/", + "v0.10.36/", + "v0.10.37/", + "v0.10.38/", + "v0.10.39/", + "v0.10.4/", + "v0.10.40/", + "v0.10.41/", + "v0.10.42/", + "v0.10.43/", + "v0.10.44/", + "v0.10.45/", + "v0.10.46/", + "v0.10.47/", + "v0.10.48/", + "v0.10.5/", + "v0.10.6/", + "v0.10.7/", + "v0.10.8/", + "v0.10.9/", + "v0.11.0/", + "v0.11.1/", + "v0.11.10/", + "v0.11.11/", + "v0.11.12/", + "v0.11.13/", + "v0.11.14/", + "v0.11.15/", + "v0.11.16/", + "v0.11.2/", + "v0.11.3/", + "v0.11.4/", + "v0.11.5/", + "v0.11.6/", + "v0.11.7/", + "v0.11.8/", + "v0.11.9/", + "v0.12.0/", + "v0.12.1/", + "v0.12.10/", + "v0.12.11/", + "v0.12.12/", + "v0.12.13/", + "v0.12.14/", + "v0.12.15/", + "v0.12.16/", + "v0.12.17/", + "v0.12.18/", + "v0.12.2/", + "v0.12.3/", + "v0.12.4/", + "v0.12.5/", + "v0.12.6/", + "v0.12.7/", + "v0.12.8/", + "v0.12.9/", + "v0.2.0/", + "v0.2.1/", + "v0.2.2/", + "v0.2.3/", + "v0.2.4/", + "v0.2.5/", + "v0.2.6/", + "v0.3.0/", + "v0.3.1/", + "v0.3.2/", + "v0.3.3/", + "v0.3.4/", + "v0.3.5/", + "v0.3.6/", + "v0.3.7/", + "v0.3.8/", + "v0.4.0/", + "v0.4.1/", + "v0.4.10/", + "v0.4.11/", + "v0.4.12/", + "v0.4.2/", + "v0.4.3/", + "v0.4.4/", + "v0.4.5/", + "v0.4.6/", + "v0.4.7/", + "v0.4.8/", + "v0.4.9/", + "v0.5.0/", + "v0.5.1/", + "v0.5.10/", + "v0.5.2/", + "v0.5.3/", + "v0.5.4/", + "v0.5.5/", + "v0.5.6/", + "v0.5.7/", + "v0.5.8/", + "v0.5.9/", + "v0.6.0/", + "v0.6.1/", + "v0.6.10/", + "v0.6.11/", + "v0.6.12/", + "v0.6.13/", + "v0.6.14/", + "v0.6.15/", + "v0.6.16/", + "v0.6.17/", + "v0.6.18/", + "v0.6.19/", + "v0.6.2/", + "v0.6.20/", + "v0.6.21/", + "v0.6.3/", + "v0.6.4/", + "v0.6.5/", + "v0.6.6/", + "v0.6.7/", + "v0.6.8/", + "v0.6.9/", + "v0.7.0/", + "v0.7.1/", + "v0.7.10/", + "v0.7.11/", + "v0.7.12/", + "v0.7.2/", + "v0.7.3/", + "v0.7.4/", + "v0.7.5/", + "v0.7.6/", + "v0.7.7/", + "v0.7.8/", + "v0.7.9/", + "v0.8.0/", + "v0.8.1/", + "v0.8.10/", + "v0.8.11/", + "v0.8.12/", + "v0.8.13/", + "v0.8.14/", + "v0.8.15/", + "v0.8.16/", + "v0.8.17/", + "v0.8.18/", + "v0.8.19/", + "v0.8.2/", + "v0.8.20/", + "v0.8.21/", + "v0.8.22/", + "v0.8.23/", + "v0.8.24/", + "v0.8.25/", + "v0.8.26/", + "v0.8.27/", + "v0.8.28/", + "v0.8.3/", + "v0.8.4/", + "v0.8.5/", + "v0.8.6/", + "v0.8.7/", + "v0.8.8/", + "v0.8.9/", + "v0.9.0/", + "v0.9.1/", + "v0.9.10/", + "v0.9.11/", + "v0.9.12/", + "v0.9.2/", + "v0.9.3/", + "v0.9.4/", + "v0.9.5/", + "v0.9.6/", + "v0.9.7/", + "v0.9.8/", + "v0.9.9/", + "v10.0.0/", + "v10.1.0/", + "v10.10.0/", + "v10.11.0/", + "v10.12.0/", + "v10.13.0/", + "v10.14.0/", + "v10.14.1/", + "v10.14.2/", + "v10.15.0/", + "v10.15.1/", + "v10.15.2/", + "v10.15.3/", + "v10.16.0/", + "v10.16.1/", + "v10.16.2/", + "v10.16.3/", + "v10.17.0/", + "v10.18.0/", + "v10.18.1/", + "v10.19.0/", + "v10.2.0/", + "v10.2.1/", + "v10.20.0/", + "v10.20.1/", + "v10.21.0/", + "v10.22.0/", + "v10.22.1/", + "v10.23.0/", + "v10.23.1/", + "v10.23.2/", + "v10.23.3/", + "v10.24.0/", + "v10.24.1/", + "v10.3.0/", + "v10.4.0/", + "v10.4.1/", + "v10.5.0/", + "v10.6.0/", + "v10.7.0/", + "v10.8.0/", + "v10.9.0/", + "v11.0.0/", + "v11.1.0/", + "v11.10.0/", + "v11.10.1/", + "v11.11.0/", + "v11.12.0/", + "v11.13.0/", + "v11.14.0/", + "v11.15.0/", + "v11.2.0/", + "v11.3.0/", + "v11.4.0/", + "v11.5.0/", + "v11.6.0/", + "v11.7.0/", + "v11.8.0/", + "v11.9.0/", + "v12.0.0/", + "v12.1.0/", + "v12.10.0/", + "v12.11.0/", + "v12.11.1/", + "v12.12.0/", + "v12.13.0/", + "v12.13.1/", + "v12.14.0/", + "v12.14.1/", + "v12.15.0/", + "v12.16.0/", + "v12.16.1/", + "v12.16.2/", + "v12.16.3/", + "v12.17.0/", + "v12.18.0/", + "v12.18.1/", + "v12.18.2/", + "v12.18.3/", + "v12.18.4/", + "v12.19.0/", + "v12.19.1/", + "v12.2.0/", + "v12.20.0/", + "v12.20.1/", + "v12.20.2/", + "v12.21.0/", + "v12.22.0/", + "v12.22.1/", + "v12.22.10/", + "v12.22.11/", + "v12.22.12/", + "v12.22.2/", + "v12.22.3/", + "v12.22.4/", + "v12.22.5/", + "v12.22.6/", + "v12.22.7/", + "v12.22.8/", + "v12.22.9/", + "v12.3.0/", + "v12.3.1/", + "v12.4.0/", + "v12.5.0/", + "v12.6.0/", + "v12.7.0/", + "v12.8.0/", + "v12.8.1/", + "v12.9.0/", + "v12.9.1/", + "v13.0.0/", + "v13.0.1/", + "v13.1.0/", + "v13.10.0/", + "v13.10.1/", + "v13.11.0/", + "v13.12.0/", + "v13.13.0/", + "v13.14.0/", + "v13.2.0/", + "v13.3.0/", + "v13.4.0/", + "v13.5.0/", + "v13.6.0/", + "v13.7.0/", + "v13.8.0/", + "v13.9.0/", + "v14.0.0/", + "v14.1.0/", + "v14.10.0/", + "v14.10.1/", + "v14.11.0/", + "v14.12.0/", + "v14.13.0/", + "v14.13.1/", + "v14.14.0/", + "v14.15.0/", + "v14.15.1/", + "v14.15.2/", + "v14.15.3/", + "v14.15.4/", + "v14.15.5/", + "v14.16.0/", + "v14.16.1/", + "v14.17.0/", + "v14.17.1/", + "v14.17.2/", + "v14.17.3/", + "v14.17.4/", + "v14.17.5/", + "v14.17.6/", + "v14.18.0/", + "v14.18.1/", + "v14.18.2/", + "v14.18.3/", + "v14.19.0/", + "v14.19.1/", + "v14.19.2/", + "v14.19.3/", + "v14.2.0/", + "v14.20.0/", + "v14.20.1/", + "v14.21.0/", + "v14.21.1/", + "v14.21.2/", + "v14.21.3/", + "v14.3.0/", + "v14.4.0/", + "v14.5.0/", + "v14.6.0/", + "v14.7.0/", + "v14.8.0/", + "v14.9.0/", + "v15.0.0/", + "v15.0.1/", + "v15.1.0/", + "v15.10.0/", + "v15.11.0/", + "v15.12.0/", + "v15.13.0/", + "v15.14.0/", + "v15.2.0/", + "v15.2.1/", + "v15.3.0/", + "v15.4.0/", + "v15.5.0/", + "v15.5.1/", + "v15.6.0/", + "v15.7.0/", + "v15.8.0/", + "v15.9.0/", + "v16.0.0/", + "v16.1.0/", + "v16.10.0/", + "v16.11.0/", + "v16.11.1/", + "v16.12.0/", + "v16.13.0/", + "v16.13.1/", + "v16.13.2/", + "v16.14.0/", + "v16.14.1/", + "v16.14.2/", + "v16.15.0/", + "v16.15.1/", + "v16.16.0/", + "v16.17.0/", + "v16.17.1/", + "v16.18.0/", + "v16.18.1/", + "v16.19.0/", + "v16.19.1/", + "v16.2.0/", + "v16.20.0/", + "v16.20.1/", + "v16.20.2/", + "v16.3.0/", + "v16.4.0/", + "v16.4.1/", + "v16.4.2/", + "v16.5.0/", + "v16.6.0/", + "v16.6.1/", + "v16.6.2/", + "v16.7.0/", + "v16.8.0/", + "v16.9.0/", + "v16.9.1/", + "v17.0.0/", + "v17.0.1/", + "v17.1.0/", + "v17.2.0/", + "v17.3.0/", + "v17.3.1/", + "v17.4.0/", + "v17.5.0/", + "v17.6.0/", + "v17.7.0/", + "v17.7.1/", + "v17.7.2/", + "v17.8.0/", + "v17.9.0/", + "v17.9.1/", + "v18.0.0/", + "v18.1.0/", + "v18.10.0/", + "v18.11.0/", + "v18.12.0/", + "v18.12.1/", + "v18.13.0/", + "v18.14.0/", + "v18.14.1/", + "v18.14.2/", + "v18.15.0/", + "v18.16.0/", + "v18.16.1/", + "v18.17.0/", + "v18.17.1/", + "v18.18.0/", + "v18.18.1/", + "v18.18.2/", + "v18.19.0/", + "v18.19.1/", + "v18.2.0/", + "v18.20.0/", + "v18.20.1/", + "v18.20.2/", + "v18.20.3/", + "v18.20.4/", + "v18.20.5/", + "v18.3.0/", + "v18.4.0/", + "v18.5.0/", + "v18.6.0/", + "v18.7.0/", + "v18.8.0/", + "v18.9.0/", + "v18.9.1/", + "v19.0.0/", + "v19.0.1/", + "v19.1.0/", + "v19.2.0/", + "v19.3.0/", + "v19.4.0/", + "v19.5.0/", + "v19.6.0/", + "v19.6.1/", + "v19.7.0/", + "v19.8.0/", + "v19.8.1/", + "v19.9.0/", + "v20.0.0/", + "v20.1.0/", + "v20.10.0/", + "v20.11.0/", + "v20.11.1/", + "v20.12.0/", + "v20.12.1/", + "v20.12.2/", + "v20.13.0/", + "v20.13.1/", + "v20.14.0/", + "v20.15.0/", + "v20.15.1/", + "v20.16.0/", + "v20.17.0/", + "v20.18.0/", + "v20.18.1/", + "v20.2.0/", + "v20.3.0/", + "v20.3.1/", + "v20.4.0/", + "v20.5.0/", + "v20.5.1/", + "v20.6.0/", + "v20.6.1/", + "v20.7.0/", + "v20.8.0/", + "v20.8.1/", + "v20.9.0/", + "v21.0.0/", + "v21.1.0/", + "v21.2.0/", + "v21.3.0/", + "v21.4.0/", + "v21.5.0/", + "v21.6.0/", + "v21.6.1/", + "v21.6.2/", + "v21.7.0/", + "v21.7.1/", + "v21.7.2/", + "v21.7.3/", + "v22.0.0/", + "v22.1.0/", + "v22.10.0/", + "v22.11.0/", + "v22.12.0/", + "v22.2.0/", + "v22.3.0/", + "v22.4.0/", + "v22.4.1/", + "v22.5.0/", + "v22.5.1/", + "v22.6.0/", + "v22.7.0/", + "v22.8.0/", + "v22.9.0/", + "v23.0.0/", + "v23.1.0/", + "v23.2.0/", + "v23.3.0/", + "v23.4.0/", + "v23.5.0/", + "v4.0.0/", + "v4.1.0/", + "v4.1.1/", + "v4.1.2/", + "v4.2.0/", + "v4.2.1/", + "v4.2.2/", + "v4.2.3/", + "v4.2.4/", + "v4.2.5/", + "v4.2.6/", + "v4.3.0/", + "v4.3.1/", + "v4.3.2/", + "v4.4.0/", + "v4.4.1/", + "v4.4.2/", + "v4.4.3/", + "v4.4.4/", + "v4.4.5/", + "v4.4.6/", + "v4.4.7/", + "v4.5.0/", + "v4.6.0/", + "v4.6.1/", + "v4.6.2/", + "v4.7.0/", + "v4.7.1/", + "v4.7.2/", + "v4.7.3/", + "v4.8.0/", + "v4.8.1/", + "v4.8.2/", + "v4.8.3/", + "v4.8.4/", + "v4.8.5/", + "v4.8.6/", + "v4.8.7/", + "v4.9.0/", + "v4.9.1/", + "v5.0.0/", + "v5.1.0/", + "v5.1.1/", + "v5.10.0/", + "v5.10.1/", + "v5.11.0/", + "v5.11.1/", + "v5.12.0/", + "v5.2.0/", + "v5.3.0/", + "v5.4.0/", + "v5.4.1/", + "v5.5.0/", + "v5.6.0/", + "v5.7.0/", + "v5.7.1/", + "v5.8.0/", + "v5.9.0/", + "v5.9.1/", + "v6.0.0/", + "v6.1.0/", + "v6.10.0/", + "v6.10.1/", + "v6.10.2/", + "v6.10.3/", + "v6.11.0/", + "v6.11.1/", + "v6.11.2/", + "v6.11.3/", + "v6.11.4/", + "v6.11.5/", + "v6.12.0/", + "v6.12.1/", + "v6.12.2/", + "v6.12.3/", + "v6.13.0/", + "v6.13.1/", + "v6.14.0/", + "v6.14.1/", + "v6.14.2/", + "v6.14.3/", + "v6.14.4/", + "v6.15.0/", + "v6.15.1/", + "v6.16.0/", + "v6.17.0/", + "v6.17.1/", + "v6.2.0/", + "v6.2.1/", + "v6.2.2/", + "v6.3.0/", + "v6.3.1/", + "v6.4.0/", + "v6.5.0/", + "v6.6.0/", + "v6.7.0/", + "v6.8.0/", + "v6.8.1/", + "v6.9.0/", + "v6.9.1/", + "v6.9.2/", + "v6.9.3/", + "v6.9.4/", + "v6.9.5/", + "v7.0.0/", + "v7.1.0/", + "v7.10.0/", + "v7.10.1/", + "v7.2.0/", + "v7.2.1/", + "v7.3.0/", + "v7.4.0/", + "v7.5.0/", + "v7.6.0/", + "v7.7.0/", + "v7.7.1/", + "v7.7.2/", + "v7.7.3/", + "v7.7.4/", + "v7.8.0/", + "v7.9.0/", + "v8.0.0/", + "v8.1.0/", + "v8.1.1/", + "v8.1.2/", + "v8.1.3/", + "v8.1.4/", + "v8.10.0/", + "v8.11.0/", + "v8.11.1/", + "v8.11.2/", + "v8.11.3/", + "v8.11.4/", + "v8.12.0/", + "v8.13.0/", + "v8.14.0/", + "v8.14.1/", + "v8.15.0/", + "v8.15.1/", + "v8.16.0/", + "v8.16.1/", + "v8.16.2/", + "v8.17.0/", + "v8.2.0/", + "v8.2.1/", + "v8.3.0/", + "v8.4.0/", + "v8.5.0/", + "v8.6.0/", + "v8.7.0/", + "v8.8.0/", + "v8.8.1/", + "v8.9.0/", + "v8.9.1/", + "v8.9.2/", + "v8.9.3/", + "v8.9.4/", + "v9.0.0/", + "v9.1.0/", + "v9.10.0/", + "v9.10.1/", + "v9.11.0/", + "v9.11.1/", + "v9.11.2/", + "v9.2.0/", + "v9.2.1/", + "v9.3.0/", + "v9.4.0/", + "v9.5.0/", + "v9.6.0/", + "v9.6.1/", + "v9.7.0/", + "v9.7.1/", + "v9.8.0/", + "v9.9.0/" + ], + "hasIndexHtmlFile": false, + "files": [ + { + "name": "index.json", + "lastModified": "2024-12-19T18:54:38.831Z", + "size": 288500 + }, + { + "name": "index.tab", + "lastModified": "2024-12-19T18:54:39.891Z", + "size": 181997 + }, + { + "name": "node-0.0.1.tar.gz", + "lastModified": "2024-11-04T16:54:04.949Z", + "size": 2846972 + }, + { + "name": "node-0.0.2.tar.gz", + "lastModified": "2024-11-04T16:54:04.938Z", + "size": 2847748 + }, + { + "name": "node-0.0.3.tar.gz", + "lastModified": "2024-11-04T16:54:04.956Z", + "size": 2891652 + }, + { + "name": "node-0.0.4.tar.gz", + "lastModified": "2024-11-04T16:54:04.957Z", + "size": 2891533 + }, + { + "name": "node-0.0.5.tar.gz", + "lastModified": "2024-11-04T16:54:04.946Z", + "size": 2926727 + }, + { + "name": "node-0.0.6.tar.gz", + "lastModified": "2024-11-04T16:54:04.917Z", + "size": 2952115 + }, + { + "name": "node-0.1.0.tar.gz", + "lastModified": "2024-11-04T16:54:05.154Z", + "size": 3331341 + }, + { + "name": "node-0.1.1.tar.gz", + "lastModified": "2024-11-04T16:54:05.201Z", + "size": 3390971 + }, + { + "name": "node-0.1.10.tar.gz", + "lastModified": "2024-11-04T16:54:05.199Z", + "size": 3674389 + }, + { + "name": "node-0.1.11.tar.gz", + "lastModified": "2024-11-04T16:54:05.268Z", + "size": 3683687 + }, + { + "name": "node-0.1.12.tar.gz", + "lastModified": "2024-11-04T16:54:05.244Z", + "size": 3699939 + }, + { + "name": "node-0.1.13.tar.gz", + "lastModified": "2024-11-04T16:54:05.252Z", + "size": 3718928 + }, + { + "name": "node-0.1.2.tar.gz", + "lastModified": "2024-11-04T16:54:05.229Z", + "size": 3516421 + }, + { + "name": "node-0.1.3.tar.gz", + "lastModified": "2024-11-04T16:54:05.205Z", + "size": 3527371 + }, + { + "name": "node-0.1.4.tar.gz", + "lastModified": "2024-11-04T16:54:05.405Z", + "size": 3567057 + }, + { + "name": "node-0.1.5.tar.gz", + "lastModified": "2024-11-04T16:54:05.429Z", + "size": 3598128 + }, + { + "name": "node-0.1.6.tar.gz", + "lastModified": "2024-11-04T16:54:05.488Z", + "size": 3598051 + }, + { + "name": "node-0.1.7.tar.gz", + "lastModified": "2024-11-04T16:54:05.465Z", + "size": 3599626 + }, + { + "name": "node-0.1.8.tar.gz", + "lastModified": "2024-11-04T16:54:05.499Z", + "size": 3639185 + }, + { + "name": "node-0.1.9.tar.gz", + "lastModified": "2024-11-04T16:54:05.499Z", + "size": 3639588 + }, + { + "name": "node-v0.1.100.tar.gz", + "lastModified": "2024-11-04T16:54:05.515Z", + "size": 3813493 + }, + { + "name": "node-v0.1.101.tar.gz", + "lastModified": "2024-11-04T16:54:05.566Z", + "size": 3825097 + }, + { + "name": "node-v0.1.102.tar.gz", + "lastModified": "2024-11-04T16:54:05.690Z", + "size": 3847409 + }, + { + "name": "node-v0.1.103.tar.gz", + "lastModified": "2024-11-04T16:54:05.687Z", + "size": 3843666 + }, + { + "name": "node-v0.1.104.tar.gz", + "lastModified": "2024-11-04T16:54:05.745Z", + "size": 3859322 + }, + { + "name": "node-v0.1.14.tar.gz", + "lastModified": "2024-11-04T16:54:05.826Z", + "size": 3736523 + }, + { + "name": "node-v0.1.15.tar.gz", + "lastModified": "2024-11-04T16:54:05.731Z", + "size": 3766716 + }, + { + "name": "node-v0.1.16.tar.gz", + "lastModified": "2024-11-04T16:54:05.965Z", + "size": 3827870 + }, + { + "name": "node-v0.1.17.tar.gz", + "lastModified": "2024-11-04T16:54:05.719Z", + "size": 3826866 + }, + { + "name": "node-v0.1.18.tar.gz", + "lastModified": "2024-11-04T16:54:05.805Z", + "size": 3537137 + }, + { + "name": "node-v0.1.19.tar.gz", + "lastModified": "2024-11-04T16:54:05.954Z", + "size": 3574916 + }, + { + "name": "node-v0.1.20.tar.gz", + "lastModified": "2024-11-04T16:54:05.980Z", + "size": 3575681 + }, + { + "name": "node-v0.1.21.tar.gz", + "lastModified": "2024-11-04T16:54:05.997Z", + "size": 3597096 + }, + { + "name": "node-v0.1.22.tar.gz", + "lastModified": "2024-11-04T16:54:05.990Z", + "size": 3604861 + }, + { + "name": "node-v0.1.23.tar.gz", + "lastModified": "2024-11-04T16:54:06.017Z", + "size": 3605321 + }, + { + "name": "node-v0.1.24.tar.gz", + "lastModified": "2024-11-04T16:54:06.086Z", + "size": 3649641 + }, + { + "name": "node-v0.1.25.tar.gz", + "lastModified": "2024-11-04T16:54:06.152Z", + "size": 3666675 + }, + { + "name": "node-v0.1.26.tar.gz", + "lastModified": "2024-11-04T16:54:06.291Z", + "size": 3704254 + }, + { + "name": "node-v0.1.27.tar.gz", + "lastModified": "2024-11-04T16:54:06.253Z", + "size": 3766114 + }, + { + "name": "node-v0.1.28.tar.gz", + "lastModified": "2024-11-04T16:54:06.273Z", + "size": 3804602 + }, + { + "name": "node-v0.1.29.tar.gz", + "lastModified": "2024-11-04T16:54:06.289Z", + "size": 3807335 + }, + { + "name": "node-v0.1.30.tar.gz", + "lastModified": "2024-11-04T16:54:06.307Z", + "size": 3912671 + }, + { + "name": "node-v0.1.31.tar.gz", + "lastModified": "2024-11-04T16:54:06.277Z", + "size": 3933441 + }, + { + "name": "node-v0.1.32.tar.gz", + "lastModified": "2024-11-04T16:54:06.366Z", + "size": 3984949 + }, + { + "name": "node-v0.1.33.tar.gz", + "lastModified": "2024-11-04T16:54:06.411Z", + "size": 4016600 + }, + { + "name": "node-v0.1.90.tar.gz", + "lastModified": "2024-11-04T16:54:06.555Z", + "size": 6452573 + }, + { + "name": "node-v0.1.91.tar.gz", + "lastModified": "2024-11-04T16:54:06.525Z", + "size": 6488023 + }, + { + "name": "node-v0.1.92.tar.gz", + "lastModified": "2024-11-04T16:54:06.601Z", + "size": 6535942 + }, + { + "name": "node-v0.1.93.tar.gz", + "lastModified": "2024-11-04T16:54:06.574Z", + "size": 6528767 + }, + { + "name": "node-v0.1.94.tar.gz", + "lastModified": "2024-11-04T16:54:06.605Z", + "size": 6691437 + }, + { + "name": "node-v0.1.95.tar.gz", + "lastModified": "2024-11-04T16:54:06.563Z", + "size": 3691396 + }, + { + "name": "node-v0.1.96.tar.gz", + "lastModified": "2024-11-04T16:54:06.659Z", + "size": 3697128 + }, + { + "name": "node-v0.1.97.tar.gz", + "lastModified": "2024-11-04T16:54:06.666Z", + "size": 3725213 + }, + { + "name": "node-v0.1.98.tar.gz", + "lastModified": "2024-11-04T16:54:06.781Z", + "size": 3770749 + }, + { + "name": "node-v0.1.99.tar.gz", + "lastModified": "2024-11-04T16:54:06.849Z", + "size": 3796156 + }, + { + "name": "node-v0.2.0.tar.gz", + "lastModified": "2024-11-04T16:54:06.783Z", + "size": 3869705 + }, + { + "name": "node-v0.2.1.tar.gz", + "lastModified": "2024-11-04T16:54:06.870Z", + "size": 3874229 + }, + { + "name": "node-v0.2.2.tar.gz", + "lastModified": "2024-11-04T16:54:06.870Z", + "size": 3876164 + }, + { + "name": "node-v0.2.3.tar.gz", + "lastModified": "2024-11-04T16:54:06.886Z", + "size": 3877908 + }, + { + "name": "node-v0.2.4.tar.gz", + "lastModified": "2024-11-04T16:54:06.933Z", + "size": 4002347 + }, + { + "name": "node-v0.2.5.tar.gz", + "lastModified": "2024-11-04T16:54:06.935Z", + "size": 4008314 + }, + { + "name": "node-v0.2.6.tar.gz", + "lastModified": "2024-11-04T16:54:07.042Z", + "size": 4010320 + }, + { + "name": "node-v0.3.0.tar.gz", + "lastModified": "2024-11-04T16:54:07.315Z", + "size": 4054239 + }, + { + "name": "node-v0.3.1.tar.gz", + "lastModified": "2024-11-04T16:54:07.121Z", + "size": 4147683 + }, + { + "name": "node-v0.3.2.tar.gz", + "lastModified": "2024-11-04T16:54:07.127Z", + "size": 4433878 + }, + { + "name": "node-v0.3.3.tar.gz", + "lastModified": "2024-11-04T16:54:07.178Z", + "size": 4582669 + }, + { + "name": "node-v0.3.4.tar.gz", + "lastModified": "2024-11-04T16:54:07.187Z", + "size": 4610441 + }, + { + "name": "node-v0.3.5.tar.gz", + "lastModified": "2024-11-04T16:54:07.206Z", + "size": 4655131 + }, + { + "name": "node-v0.3.6.tar.gz", + "lastModified": "2024-11-04T16:54:07.186Z", + "size": 4727071 + }, + { + "name": "node-v0.3.7.tar.gz", + "lastModified": "2024-11-04T16:54:07.341Z", + "size": 4748183 + }, + { + "name": "node-v0.3.8.tar.gz", + "lastModified": "2024-11-04T16:54:07.382Z", + "size": 4779447 + }, + { + "name": "node-v0.4.0.tar.gz", + "lastModified": "2024-11-04T16:54:07.443Z", + "size": 4827760 + }, + { + "name": "node-v0.4.1.tar.gz", + "lastModified": "2024-11-04T16:54:07.458Z", + "size": 4855576 + }, + { + "name": "node-v0.4.10.tar.gz", + "lastModified": "2024-10-30T17:10:04.482Z", + "size": 12410018 + }, + { + "name": "node-v0.4.11.tar.gz", + "lastModified": "2024-10-30T17:10:04.314Z", + "size": 12419274 + }, + { + "name": "node-v0.4.12.tar.gz", + "lastModified": "2024-10-30T17:10:04.446Z", + "size": 12421469 + }, + { + "name": "node-v0.4.2.tar.gz", + "lastModified": "2024-11-04T16:54:07.495Z", + "size": 4922523 + }, + { + "name": "node-v0.4.3.tar.gz", + "lastModified": "2024-11-04T16:54:07.523Z", + "size": 4991966 + }, + { + "name": "node-v0.4.4.tar.gz", + "lastModified": "2024-11-04T16:54:07.540Z", + "size": 4995935 + }, + { + "name": "node-v0.4.5.tar.gz", + "lastModified": "2024-11-04T16:54:07.613Z", + "size": 5001301 + }, + { + "name": "node-v0.4.6.tar.gz", + "lastModified": "2024-11-04T16:54:07.593Z", + "size": 5008110 + }, + { + "name": "node-v0.4.7.tar.gz", + "lastModified": "2024-11-04T16:54:07.623Z", + "size": 5011520 + }, + { + "name": "node-v0.4.8.tar.gz", + "lastModified": "2024-11-04T16:54:07.714Z", + "size": 4991396 + }, + { + "name": "node-v0.4.9.tar.gz", + "lastModified": "2024-11-04T16:54:07.694Z", + "size": 4994552 + }, + { + "name": "node-v0.4.tar.gz", + "lastModified": "2024-11-04T16:54:07.867Z", + "size": 4991396 + }, + { + "name": "node-v0.5.0.tar.gz", + "lastModified": "2024-11-04T16:54:07.817Z", + "size": 5357945 + }, + { + "name": "node-v0.6.1.tar.gz", + "lastModified": "2024-10-30T17:10:04.441Z", + "size": 9276847 + }, + { + "name": "node-v0.6.10.tar.gz", + "lastModified": "2024-10-30T17:10:04.988Z", + "size": 10545272 + }, + { + "name": "node-v0.6.11.tar.gz", + "lastModified": "2024-10-30T17:10:05.655Z", + "size": 10555423 + }, + { + "name": "node-v0.6.12.tar.gz", + "lastModified": "2024-10-30T17:10:04.999Z", + "size": 10452498 + }, + { + "name": "node-v0.6.13.tar.gz", + "lastModified": "2024-10-30T17:10:05.019Z", + "size": 10757157 + }, + { + "name": "node-v0.6.2.tar.gz", + "lastModified": "2024-10-30T17:10:05.258Z", + "size": 9286655 + }, + { + "name": "node-v0.6.3.tar.gz", + "lastModified": "2024-10-30T17:10:05.663Z", + "size": 10048403 + }, + { + "name": "node-v0.6.4.tar.gz", + "lastModified": "2024-10-30T17:10:05.703Z", + "size": 10195975 + }, + { + "name": "node-v0.6.5.tar.gz", + "lastModified": "2024-10-30T17:10:06.293Z", + "size": 10195654 + }, + { + "name": "node-v0.6.6.tar.gz", + "lastModified": "2024-10-30T17:10:06.182Z", + "size": 10446671 + }, + { + "name": "node-v0.6.7.tar.gz", + "lastModified": "2024-10-30T17:10:05.973Z", + "size": 10473188 + }, + { + "name": "node-v0.6.8.tar.gz", + "lastModified": "2024-10-30T17:10:06.318Z", + "size": 10488841 + }, + { + "name": "node-v0.6.9.tar.gz", + "lastModified": "2024-10-30T17:10:06.804Z", + "size": 10544243 + }, + { + "name": "npm-versions.txt", + "lastModified": "2024-11-04T16:54:07.949Z", + "size": 1676 + }, + { + "name": "node-latest.tar.gz", + "lastModified": "2024-12-19T18:54:11.000Z", + "size": 99908524 + } + ], + "lastModified": "2024-12-19T18:54:39.891Z" + }, + "nodejs/docs/": { + "subdirectories": [ + "latest-argon/", + "latest-boron/", + "latest-carbon/", + "latest-dubnium/", + "latest-erbium/", + "latest-fermium/", + "latest-gallium/", + "latest-hydrogen/", + "latest-iron/", + "latest-jod/", + "latest-v0.10.x/", + "latest-v0.12.x/", + "latest-v10.x/", + "latest-v11.x/", + "latest-v12.x/", + "latest-v13.x/", + "latest-v14.x/", + "latest-v15.x/", + "latest-v16.x/", + "latest-v17.x/", + "latest-v18.x/", + "latest-v19.x/", + "latest-v20.x/", + "latest-v21.x/", + "latest-v22.x/", + "latest-v23.x/", + "latest-v4.x/", + "latest-v5.x/", + "latest-v6.x/", + "latest-v7.x/", + "latest-v8.x/", + "latest-v9.x/", + "latest/", + "node-latest.tar.gz", + "npm/", + "patch/", + "v0.1.100/", + "v0.1.101/", + "v0.1.102/", + "v0.1.103/", + "v0.1.104/", + "v0.1.14/", + "v0.1.15/", + "v0.1.16/", + "v0.1.17/", + "v0.1.18/", + "v0.1.19/", + "v0.1.20/", + "v0.1.21/", + "v0.1.22/", + "v0.1.23/", + "v0.1.24/", + "v0.1.25/", + "v0.1.26/", + "v0.1.27/", + "v0.1.28/", + "v0.1.29/", + "v0.1.30/", + "v0.1.31/", + "v0.1.32/", + "v0.1.33/", + "v0.1.90/", + "v0.1.91/", + "v0.1.92/", + "v0.1.93/", + "v0.1.94/", + "v0.1.95/", + "v0.1.96/", + "v0.1.97/", + "v0.1.98/", + "v0.1.99/", + "v0.10.0/", + "v0.10.1/", + "v0.10.10/", + "v0.10.11/", + "v0.10.12/", + "v0.10.13/", + "v0.10.14/", + "v0.10.15/", + "v0.10.16-isaacs-manual/", + "v0.10.16/", + "v0.10.17/", + "v0.10.18/", + "v0.10.19/", + "v0.10.2/", + "v0.10.20/", + "v0.10.21/", + "v0.10.22/", + "v0.10.23/", + "v0.10.24/", + "v0.10.25/", + "v0.10.26/", + "v0.10.27/", + "v0.10.28/", + "v0.10.29/", + "v0.10.3/", + "v0.10.30/", + "v0.10.31/", + "v0.10.32/", + "v0.10.33/", + "v0.10.34/", + "v0.10.35/", + "v0.10.36/", + "v0.10.37/", + "v0.10.38/", + "v0.10.39/", + "v0.10.4/", + "v0.10.40/", + "v0.10.41/", + "v0.10.42/", + "v0.10.43/", + "v0.10.44/", + "v0.10.45/", + "v0.10.46/", + "v0.10.47/", + "v0.10.48/", + "v0.10.5/", + "v0.10.6/", + "v0.10.7/", + "v0.10.8/", + "v0.10.9/", + "v0.11.0/", + "v0.11.1/", + "v0.11.10/", + "v0.11.11/", + "v0.11.12/", + "v0.11.13/", + "v0.11.14/", + "v0.11.15/", + "v0.11.16/", + "v0.11.2/", + "v0.11.3/", + "v0.11.4/", + "v0.11.5/", + "v0.11.6/", + "v0.11.7/", + "v0.11.8/", + "v0.11.9/", + "v0.12.0/", + "v0.12.1/", + "v0.12.10/", + "v0.12.11/", + "v0.12.12/", + "v0.12.13/", + "v0.12.14/", + "v0.12.15/", + "v0.12.16/", + "v0.12.17/", + "v0.12.18/", + "v0.12.2/", + "v0.12.3/", + "v0.12.4/", + "v0.12.5/", + "v0.12.6/", + "v0.12.7/", + "v0.12.8/", + "v0.12.9/", + "v0.2.0/", + "v0.2.1/", + "v0.2.2/", + "v0.2.3/", + "v0.2.4/", + "v0.2.5/", + "v0.2.6/", + "v0.3.0/", + "v0.3.1/", + "v0.3.2/", + "v0.3.3/", + "v0.3.4/", + "v0.3.5/", + "v0.3.6/", + "v0.3.7/", + "v0.3.8/", + "v0.4.0/", + "v0.4.1/", + "v0.4.10/", + "v0.4.11/", + "v0.4.12/", + "v0.4.2/", + "v0.4.3/", + "v0.4.4/", + "v0.4.5/", + "v0.4.6/", + "v0.4.7/", + "v0.4.8/", + "v0.4.9/", + "v0.5.0/", + "v0.5.1/", + "v0.5.10/", + "v0.5.2/", + "v0.5.3/", + "v0.5.4/", + "v0.5.5/", + "v0.5.6/", + "v0.5.7/", + "v0.5.8/", + "v0.5.9/", + "v0.6.0/", + "v0.6.1/", + "v0.6.10/", + "v0.6.11/", + "v0.6.12/", + "v0.6.13/", + "v0.6.14/", + "v0.6.15/", + "v0.6.16/", + "v0.6.17/", + "v0.6.18/", + "v0.6.19/", + "v0.6.2/", + "v0.6.20/", + "v0.6.21/", + "v0.6.3/", + "v0.6.4/", + "v0.6.5/", + "v0.6.6/", + "v0.6.7/", + "v0.6.8/", + "v0.6.9/", + "v0.7.0/", + "v0.7.1/", + "v0.7.10/", + "v0.7.11/", + "v0.7.12/", + "v0.7.2/", + "v0.7.3/", + "v0.7.4/", + "v0.7.5/", + "v0.7.6/", + "v0.7.7/", + "v0.7.8/", + "v0.7.9/", + "v0.8.0/", + "v0.8.1/", + "v0.8.10/", + "v0.8.11/", + "v0.8.12/", + "v0.8.13/", + "v0.8.14/", + "v0.8.15/", + "v0.8.16/", + "v0.8.17/", + "v0.8.18/", + "v0.8.19/", + "v0.8.2/", + "v0.8.20/", + "v0.8.21/", + "v0.8.22/", + "v0.8.23/", + "v0.8.24/", + "v0.8.25/", + "v0.8.26/", + "v0.8.27/", + "v0.8.28/", + "v0.8.3/", + "v0.8.4/", + "v0.8.5/", + "v0.8.6/", + "v0.8.7/", + "v0.8.8/", + "v0.8.9/", + "v0.9.0/", + "v0.9.1/", + "v0.9.10/", + "v0.9.11/", + "v0.9.12/", + "v0.9.2/", + "v0.9.3/", + "v0.9.4/", + "v0.9.5/", + "v0.9.6/", + "v0.9.7/", + "v0.9.8/", + "v0.9.9/", + "v10.0.0/", + "v10.1.0/", + "v10.10.0/", + "v10.11.0/", + "v10.12.0/", + "v10.13.0/", + "v10.14.0/", + "v10.14.1/", + "v10.14.2/", + "v10.15.0/", + "v10.15.1/", + "v10.15.2/", + "v10.15.3/", + "v10.16.0/", + "v10.16.1/", + "v10.16.2/", + "v10.16.3/", + "v10.17.0/", + "v10.18.0/", + "v10.18.1/", + "v10.19.0/", + "v10.2.0/", + "v10.2.1/", + "v10.20.0/", + "v10.20.1/", + "v10.21.0/", + "v10.22.0/", + "v10.22.1/", + "v10.23.0/", + "v10.23.1/", + "v10.23.2/", + "v10.23.3/", + "v10.24.0/", + "v10.24.1/", + "v10.3.0/", + "v10.4.0/", + "v10.4.1/", + "v10.5.0/", + "v10.6.0/", + "v10.7.0/", + "v10.8.0/", + "v10.9.0/", + "v11.0.0/", + "v11.1.0/", + "v11.10.0/", + "v11.10.1/", + "v11.11.0/", + "v11.12.0/", + "v11.13.0/", + "v11.14.0/", + "v11.15.0/", + "v11.2.0/", + "v11.3.0/", + "v11.4.0/", + "v11.5.0/", + "v11.6.0/", + "v11.7.0/", + "v11.8.0/", + "v11.9.0/", + "v12.0.0/", + "v12.1.0/", + "v12.10.0/", + "v12.11.0/", + "v12.11.1/", + "v12.12.0/", + "v12.13.0/", + "v12.13.1/", + "v12.14.0/", + "v12.14.1/", + "v12.15.0/", + "v12.16.0/", + "v12.16.1/", + "v12.16.2/", + "v12.16.3/", + "v12.17.0/", + "v12.18.0/", + "v12.18.1/", + "v12.18.2/", + "v12.18.3/", + "v12.18.4/", + "v12.19.0/", + "v12.19.1/", + "v12.2.0/", + "v12.20.0/", + "v12.20.1/", + "v12.20.2/", + "v12.21.0/", + "v12.22.0/", + "v12.22.1/", + "v12.22.10/", + "v12.22.11/", + "v12.22.12/", + "v12.22.2/", + "v12.22.3/", + "v12.22.4/", + "v12.22.5/", + "v12.22.6/", + "v12.22.7/", + "v12.22.8/", + "v12.22.9/", + "v12.3.0/", + "v12.3.1/", + "v12.4.0/", + "v12.5.0/", + "v12.6.0/", + "v12.7.0/", + "v12.8.0/", + "v12.8.1/", + "v12.9.0/", + "v12.9.1/", + "v13.0.0/", + "v13.0.1/", + "v13.1.0/", + "v13.10.0/", + "v13.10.1/", + "v13.11.0/", + "v13.12.0/", + "v13.13.0/", + "v13.14.0/", + "v13.2.0/", + "v13.3.0/", + "v13.4.0/", + "v13.5.0/", + "v13.6.0/", + "v13.7.0/", + "v13.8.0/", + "v13.9.0/", + "v14.0.0/", + "v14.1.0/", + "v14.10.0/", + "v14.10.1/", + "v14.11.0/", + "v14.12.0/", + "v14.13.0/", + "v14.13.1/", + "v14.14.0/", + "v14.15.0/", + "v14.15.1/", + "v14.15.2/", + "v14.15.3/", + "v14.15.4/", + "v14.15.5/", + "v14.16.0/", + "v14.16.1/", + "v14.17.0/", + "v14.17.1/", + "v14.17.2/", + "v14.17.3/", + "v14.17.4/", + "v14.17.5/", + "v14.17.6/", + "v14.18.0/", + "v14.18.1/", + "v14.18.2/", + "v14.18.3/", + "v14.19.0/", + "v14.19.1/", + "v14.19.2/", + "v14.19.3/", + "v14.2.0/", + "v14.20.0/", + "v14.20.1/", + "v14.21.0/", + "v14.21.1/", + "v14.21.2/", + "v14.21.3/", + "v14.3.0/", + "v14.4.0/", + "v14.5.0/", + "v14.6.0/", + "v14.7.0/", + "v14.8.0/", + "v14.9.0/", + "v15.0.0/", + "v15.0.1/", + "v15.1.0/", + "v15.10.0/", + "v15.11.0/", + "v15.12.0/", + "v15.13.0/", + "v15.14.0/", + "v15.2.0/", + "v15.2.1/", + "v15.3.0/", + "v15.4.0/", + "v15.5.0/", + "v15.5.1/", + "v15.6.0/", + "v15.7.0/", + "v15.8.0/", + "v15.9.0/", + "v16.0.0/", + "v16.1.0/", + "v16.10.0/", + "v16.11.0/", + "v16.11.1/", + "v16.12.0/", + "v16.13.0/", + "v16.13.1/", + "v16.13.2/", + "v16.14.0/", + "v16.14.1/", + "v16.14.2/", + "v16.15.0/", + "v16.15.1/", + "v16.16.0/", + "v16.17.0/", + "v16.17.1/", + "v16.18.0/", + "v16.18.1/", + "v16.19.0/", + "v16.19.1/", + "v16.2.0/", + "v16.20.0/", + "v16.20.1/", + "v16.20.2/", + "v16.3.0/", + "v16.4.0/", + "v16.4.1/", + "v16.4.2/", + "v16.5.0/", + "v16.6.0/", + "v16.6.1/", + "v16.6.2/", + "v16.7.0/", + "v16.8.0/", + "v16.9.0/", + "v16.9.1/", + "v17.0.0/", + "v17.0.1/", + "v17.1.0/", + "v17.2.0/", + "v17.3.0/", + "v17.3.1/", + "v17.4.0/", + "v17.5.0/", + "v17.6.0/", + "v17.7.0/", + "v17.7.1/", + "v17.7.2/", + "v17.8.0/", + "v17.9.0/", + "v17.9.1/", + "v18.0.0/", + "v18.1.0/", + "v18.10.0/", + "v18.11.0/", + "v18.12.0/", + "v18.12.1/", + "v18.13.0/", + "v18.14.0/", + "v18.14.1/", + "v18.14.2/", + "v18.15.0/", + "v18.16.0/", + "v18.16.1/", + "v18.17.0/", + "v18.17.1/", + "v18.18.0/", + "v18.18.1/", + "v18.18.2/", + "v18.19.0/", + "v18.19.1/", + "v18.2.0/", + "v18.20.0/", + "v18.20.1/", + "v18.20.2/", + "v18.20.3/", + "v18.20.4/", + "v18.20.5/", + "v18.3.0/", + "v18.4.0/", + "v18.5.0/", + "v18.6.0/", + "v18.7.0/", + "v18.8.0/", + "v18.9.0/", + "v18.9.1/", + "v19.0.0/", + "v19.0.1/", + "v19.1.0/", + "v19.2.0/", + "v19.3.0/", + "v19.4.0/", + "v19.5.0/", + "v19.6.0/", + "v19.6.1/", + "v19.7.0/", + "v19.8.0/", + "v19.8.1/", + "v19.9.0/", + "v20.0.0/", + "v20.1.0/", + "v20.10.0/", + "v20.11.0/", + "v20.11.1/", + "v20.12.0/", + "v20.12.1/", + "v20.12.2/", + "v20.13.0/", + "v20.13.1/", + "v20.14.0/", + "v20.15.0/", + "v20.15.1/", + "v20.16.0/", + "v20.17.0/", + "v20.18.0/", + "v20.18.1/", + "v20.2.0/", + "v20.3.0/", + "v20.3.1/", + "v20.4.0/", + "v20.5.0/", + "v20.5.1/", + "v20.6.0/", + "v20.6.1/", + "v20.7.0/", + "v20.8.0/", + "v20.8.1/", + "v20.9.0/", + "v21.0.0/", + "v21.1.0/", + "v21.2.0/", + "v21.3.0/", + "v21.4.0/", + "v21.5.0/", + "v21.6.0/", + "v21.6.1/", + "v21.6.2/", + "v21.7.0/", + "v21.7.1/", + "v21.7.2/", + "v21.7.3/", + "v22.0.0/", + "v22.1.0/", + "v22.10.0/", + "v22.11.0/", + "v22.12.0/", + "v22.2.0/", + "v22.3.0/", + "v22.4.0/", + "v22.4.1/", + "v22.5.0/", + "v22.5.1/", + "v22.6.0/", + "v22.7.0/", + "v22.8.0/", + "v22.9.0/", + "v23.0.0/", + "v23.1.0/", + "v23.2.0/", + "v23.3.0/", + "v23.4.0/", + "v23.5.0/", + "v4.0.0/", + "v4.1.0/", + "v4.1.1/", + "v4.1.2/", + "v4.2.0/", + "v4.2.1/", + "v4.2.2/", + "v4.2.3/", + "v4.2.4/", + "v4.2.5/", + "v4.2.6/", + "v4.3.0/", + "v4.3.1/", + "v4.3.2/", + "v4.4.0/", + "v4.4.1/", + "v4.4.2/", + "v4.4.3/", + "v4.4.4/", + "v4.4.5/", + "v4.4.6/", + "v4.4.7/", + "v4.5.0/", + "v4.6.0/", + "v4.6.1/", + "v4.6.2/", + "v4.7.0/", + "v4.7.1/", + "v4.7.2/", + "v4.7.3/", + "v4.8.0/", + "v4.8.1/", + "v4.8.2/", + "v4.8.3/", + "v4.8.4/", + "v4.8.5/", + "v4.8.6/", + "v4.8.7/", + "v4.9.0/", + "v4.9.1/", + "v5.0.0/", + "v5.1.0/", + "v5.1.1/", + "v5.10.0/", + "v5.10.1/", + "v5.11.0/", + "v5.11.1/", + "v5.12.0/", + "v5.2.0/", + "v5.3.0/", + "v5.4.0/", + "v5.4.1/", + "v5.5.0/", + "v5.6.0/", + "v5.7.0/", + "v5.7.1/", + "v5.8.0/", + "v5.9.0/", + "v5.9.1/", + "v6.0.0/", + "v6.1.0/", + "v6.10.0/", + "v6.10.1/", + "v6.10.2/", + "v6.10.3/", + "v6.11.0/", + "v6.11.1/", + "v6.11.2/", + "v6.11.3/", + "v6.11.4/", + "v6.11.5/", + "v6.12.0/", + "v6.12.1/", + "v6.12.2/", + "v6.12.3/", + "v6.13.0/", + "v6.13.1/", + "v6.14.0/", + "v6.14.1/", + "v6.14.2/", + "v6.14.3/", + "v6.14.4/", + "v6.15.0/", + "v6.15.1/", + "v6.16.0/", + "v6.17.0/", + "v6.17.1/", + "v6.2.0/", + "v6.2.1/", + "v6.2.2/", + "v6.3.0/", + "v6.3.1/", + "v6.4.0/", + "v6.5.0/", + "v6.6.0/", + "v6.7.0/", + "v6.8.0/", + "v6.8.1/", + "v6.9.0/", + "v6.9.1/", + "v6.9.2/", + "v6.9.3/", + "v6.9.4/", + "v6.9.5/", + "v7.0.0/", + "v7.1.0/", + "v7.10.0/", + "v7.10.1/", + "v7.2.0/", + "v7.2.1/", + "v7.3.0/", + "v7.4.0/", + "v7.5.0/", + "v7.6.0/", + "v7.7.0/", + "v7.7.1/", + "v7.7.2/", + "v7.7.3/", + "v7.7.4/", + "v7.8.0/", + "v7.9.0/", + "v8.0.0/", + "v8.1.0/", + "v8.1.1/", + "v8.1.2/", + "v8.1.3/", + "v8.1.4/", + "v8.10.0/", + "v8.11.0/", + "v8.11.1/", + "v8.11.2/", + "v8.11.3/", + "v8.11.4/", + "v8.12.0/", + "v8.13.0/", + "v8.14.0/", + "v8.14.1/", + "v8.15.0/", + "v8.15.1/", + "v8.16.0/", + "v8.16.1/", + "v8.16.2/", + "v8.17.0/", + "v8.2.0/", + "v8.2.1/", + "v8.3.0/", + "v8.4.0/", + "v8.5.0/", + "v8.6.0/", + "v8.7.0/", + "v8.8.0/", + "v8.8.1/", + "v8.9.0/", + "v8.9.1/", + "v8.9.2/", + "v8.9.3/", + "v8.9.4/", + "v9.0.0/", + "v9.1.0/", + "v9.10.0/", + "v9.10.1/", + "v9.11.0/", + "v9.11.1/", + "v9.11.2/", + "v9.2.0/", + "v9.2.1/", + "v9.3.0/", + "v9.4.0/", + "v9.5.0/", + "v9.6.0/", + "v9.6.1/", + "v9.7.0/", + "v9.7.1/", + "v9.8.0/", + "v9.9.0/" + ], + "hasIndexHtmlFile": false, + "files": [], + "lastModified": "2024-12-19T18:54:39.891Z" + } +} diff --git a/src/constants/docsDirectory.json b/src/constants/docsDirectory.json new file mode 100644 index 0000000..9125c53 --- /dev/null +++ b/src/constants/docsDirectory.json @@ -0,0 +1,87 @@ +[ + "v0.0.1", + "v0.0.2", + "v0.0.3", + "v0.0.4", + "v0.0.5", + "v0.0.6", + "v0.1.0", + "v0.1.1", + "v0.1.10", + "v0.1.100", + "v0.1.101", + "v0.1.102", + "v0.1.103", + "v0.1.104", + "v0.1.11", + "v0.1.12", + "v0.1.13", + "v0.1.14", + "v0.1.15", + "v0.1.16", + "v0.1.17", + "v0.1.18", + "v0.1.19", + "v0.1.2", + "v0.1.20", + "v0.1.21", + "v0.1.22", + "v0.1.23", + "v0.1.24", + "v0.1.25", + "v0.1.26", + "v0.1.27", + "v0.1.28", + "v0.1.29", + "v0.1.3", + "v0.1.30", + "v0.1.31", + "v0.1.32", + "v0.1.33", + "v0.1.4", + "v0.1.5", + "v0.1.6", + "v0.1.7", + "v0.1.8", + "v0.1.9", + "v0.1.90", + "v0.1.91", + "v0.1.92", + "v0.1.93", + "v0.1.94", + "v0.1.95", + "v0.1.96", + "v0.1.97", + "v0.1.98", + "v0.1.99", + "v0.2.0", + "v0.2.1", + "v0.2.2", + "v0.2.3", + "v0.2.4", + "v0.2.5", + "v0.2.6", + "v0.3.0", + "v0.3.1", + "v0.3.2", + "v0.3.3", + "v0.3.4", + "v0.3.5", + "v0.3.6", + "v0.3.7", + "v0.3.8", + "v0.4.0", + "v0.4.1", + "v0.4.10", + "v0.4.11", + "v0.4.12", + "v0.4.2", + "v0.4.3", + "v0.4.4", + "v0.4.5", + "v0.4.6", + "v0.4.7", + "v0.4.8", + "v0.4.9", + "v0.5.0" +] diff --git a/src/middleware/r2Middleware.ts b/src/middleware/r2Middleware.ts index dd6ad8f..539fe9b 100644 --- a/src/middleware/r2Middleware.ts +++ b/src/middleware/r2Middleware.ts @@ -1,4 +1,5 @@ import { CACHE_HEADERS } from '../constants/cache'; +import docsDirectory from '../constants/docsDirectory.json' assert { type: 'json' }; import type { Context } from '../context'; import type { GetFileResult } from '../providers/provider'; import { R2Provider } from '../providers/r2Provider'; @@ -32,12 +33,6 @@ export class R2Middleware implements Middleware { } } -function shouldListFiles(path: string): boolean { - // /docs lists the nodejs/release directory - don't want to include the - // files in there for that path - return !path.startsWith('/docs'); -} - async function handleDirectory( request: Request, r2Path: string, @@ -45,13 +40,12 @@ async function handleDirectory( ): Promise { if (!hasTrailingSlash(request.urlObj.pathname)) { // We always want directory listing requests to have a trailing slash - const url = request.unsubtitutedUrl ?? request.urlObj; + const url = request.unsubstitutedUrl ?? request.urlObj; return Response.redirect(`${url}/`, 301); } - const result = await getProvider(ctx).readDirectory(r2Path, { - listFiles: shouldListFiles(request.urlObj.pathname), - }); + // todo remove listpaths option? + const result = await getProvider(ctx).readDirectory(r2Path); if (result === undefined) { return responses.directoryNotFound(request.method); @@ -65,7 +59,7 @@ async function handleDirectory( let responseBody; if (request.method === 'GET') { responseBody = renderDirectoryListing( - request.unsubtitutedUrl ?? request.urlObj, + request.unsubstitutedUrl ?? request.urlObj, result ); } @@ -164,10 +158,16 @@ function getR2Path({ } else if (pathname.startsWith('/docs')) { if (params.version !== undefined) { // /docs/vX.X.X at minimum + + // Older version, docs exist in the docs folder + if (docsDirectory.includes(params.version)) { + return `nodejs/docs/${params.version}/${filePath}`; + } + return `nodejs/release/${params.version}/docs/${filePath}`; } else { // Just /docs - return `nodejs/release/`; + return `nodejs/docs/`; } } else if (pathname.startsWith('/metrics')) { // Substring to cut off the leading / diff --git a/src/middleware/subtituteMiddleware.ts b/src/middleware/subtituteMiddleware.ts index ab79b28..7fb37fe 100644 --- a/src/middleware/subtituteMiddleware.ts +++ b/src/middleware/subtituteMiddleware.ts @@ -34,7 +34,11 @@ export class SubtitutionMiddleware implements Middleware { }, }); - // todo fix this - return Promise.resolve(Response.redirect(newUrl)); + const substitutedRequest = new Request( + newUrl, + new Request(request) + ); + + return this.router.handle(substitutedRequest, ctx); } } diff --git a/src/providers/r2Provider.ts b/src/providers/r2Provider.ts index c72114a..6c4b725 100644 --- a/src/providers/r2Provider.ts +++ b/src/providers/r2Provider.ts @@ -1,5 +1,6 @@ import { CACHE_HEADERS } from '../constants/cache'; import { R2_RETRY_LIMIT } from '../constants/limits'; +import CACHED_DIRECTORIES from '../constants/cachedDirectories.json' assert { type: 'json' }; import type { Context } from '../context'; import { objectHasBody } from '../utils/object'; import { retryWrapper } from '../utils/provider'; @@ -85,9 +86,32 @@ export class R2Provider implements Provider { path: string, options?: ReadDirectoryOptions ): Promise { + if (path in CACHED_DIRECTORIES) { + // @ts-expect-error + const result: ReadDirectoryResult = CACHED_DIRECTORIES[path]; + + for (const file of result.files) { + if (typeof file.lastModified === 'string') { + file.lastModified = new Date(file.lastModified); + } + } + + if (typeof result.lastModified === 'string') { + result.lastModified = new Date(result.lastModified); + } + + return Promise.resolve({ + subdirectories: result.subdirectories, + files: result.files, + hasIndexHtmlFile: result.hasIndexHtmlFile, + lastModified: new Date(result.lastModified), + }); + } + const s3Provider = new S3Provider({ ctx: this.ctx, }); + return s3Provider.readDirectory(path, options); } } diff --git a/src/routes/request.ts b/src/routes/request.ts index 00252a1..82817d5 100644 --- a/src/routes/request.ts +++ b/src/routes/request.ts @@ -6,5 +6,5 @@ export interface Request extends IRequest { /** * Set by {@link SubtitutionMiddleware} if it's used */ - unsubtitutedUrl?: URL; + unsubstitutedUrl?: URL; } diff --git a/src/routes/router.ts b/src/routes/router.ts index c9cf507..5d12d18 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -11,41 +11,49 @@ import type { Request as WorkerRequest } from './request'; * @see {Middleware} */ export class Router { - private itty = IttyRouter(); - - handle(request: Request, ctx: Context): Promise { - return this.itty.fetch(request, ctx); + private itty = IttyRouter< + WorkerRequest, + [Context, URL | undefined], + Response + >(); + + handle( + request: Request, + ctx: Context, + unsubstitutedUrl?: URL + ): Promise { + return this.itty.fetch(request, ctx, unsubstitutedUrl); } all(endpoint: string, middlewares: Middleware[]): void { const middlewareChain = buildMiddlewareChain(middlewares); - this.itty.all(endpoint, (req, ctx) => { - return callMiddlewareChain(middlewareChain, req, ctx); + this.itty.all(endpoint, (req, ctx, unsubstitutedUrl) => { + return callMiddlewareChain(middlewareChain, req, ctx, unsubstitutedUrl); }); } options(endpoint: string, middlewares: Middleware[]): void { const middlewareChain = buildMiddlewareChain(middlewares); - this.itty.options(endpoint, (req, ctx) => { - return callMiddlewareChain(middlewareChain, req, ctx); + this.itty.options(endpoint, (req, ctx, unsubstitutedUrl) => { + return callMiddlewareChain(middlewareChain, req, ctx, unsubstitutedUrl); }); } head(endpoint: string, middlewares: Middleware[]): void { const middlewareChain = buildMiddlewareChain(middlewares); - this.itty.head(endpoint, (req, ctx) => { - return callMiddlewareChain(middlewareChain, req, ctx); + this.itty.head(endpoint, (req, ctx, unsubstitutedUrl) => { + return callMiddlewareChain(middlewareChain, req, ctx, unsubstitutedUrl); }); } get(endpoint: string, middlewares: Middleware[]): void { const middlewareChain = buildMiddlewareChain(middlewares); - this.itty.get(endpoint, (req, ctx) => { - return callMiddlewareChain(middlewareChain, req, ctx); + this.itty.get(endpoint, (req, ctx, unsubstitutedUrl) => { + return callMiddlewareChain(middlewareChain, req, ctx, unsubstitutedUrl); }); } } @@ -86,7 +94,8 @@ function buildMiddlewareChain(middlewares: Middleware[]): MiddlewareChain { async function callMiddlewareChain( chain: MiddlewareChain, request: WorkerRequest, - ctx: Context + ctx: Context, + unsubstitutedUrl: URL | undefined ): Promise { // Parse url here so we don't have to do it multiple times later on const url = parseUrl(request); @@ -96,6 +105,10 @@ async function callMiddlewareChain( request.urlObj = url; + if (unsubstitutedUrl) { + request.unsubstitutedUrl = unsubstitutedUrl; + } + return chain(request, ctx); } diff --git a/tests/e2e/fallback.test.ts b/tests/e2e/fallback.test.ts index 8f8580e..3fe02a9 100644 --- a/tests/e2e/fallback.test.ts +++ b/tests/e2e/fallback.test.ts @@ -6,7 +6,7 @@ import http from 'node:http'; const FILE_PATH_TO_TEST = 'dist/index.json'; let fallbackFilePathHit = false; -const DIRECTORY_TO_TEST = 'download/release/'; +const DIRECTORY_TO_TEST = 'download/v1.0.0/'; let fallbackDirectoryPathHit = false; function startfallbackMock(): http.Server { diff --git a/tests/unit/middleware/substituteMiddleware.test.ts b/tests/unit/middleware/substituteMiddleware.test.ts index 893bf2a..ad92dc4 100644 --- a/tests/unit/middleware/substituteMiddleware.test.ts +++ b/tests/unit/middleware/substituteMiddleware.test.ts @@ -1,20 +1,17 @@ import assert from 'node:assert'; import { it } from 'node:test'; -import type { Request as WorkerRequest } from '../../../src/routes/request'; import { SubtitutionMiddleware } from '../../../src/middleware/subtituteMiddleware'; -import { Router } from '../../../src/routes'; +import type { Request as WorkerRequest } from '../../../src/routes/request'; +import type { Router } from '../../../src/routes'; it('correctly substitutes url `/dist/latest` to `/dist/v1.0.0`', async () => { const originalUrl = 'https://localhost/dist/latest'; - const originalRequest: Partial = { - ...new Request(originalUrl), - url: originalUrl, - urlObj: new URL(originalUrl), - }; + const originalRequest: Partial = new Request(originalUrl); + originalRequest.urlObj = new URL(originalUrl); const router: Partial = { - handle: (substitutedRequest: WorkerRequest) => { + handle: (substitutedRequest: WorkerRequest, _, unsubstitutedUrl) => { // Is the url is now substituted (latest -> v1.0.0) assert.strictEqual( substitutedRequest.url, @@ -22,17 +19,14 @@ it('correctly substitutes url `/dist/latest` to `/dist/v1.0.0`', async () => { ); // Did we save the unsubstituted path? - assert.strictEqual( - substitutedRequest.unsubtitutedUrl, - originalRequest.urlObj - ); + assert.strictEqual(unsubstitutedUrl, originalRequest.urlObj); return Promise.resolve(new Response()); }, }; // Pre-checks for sanity - assert.strictEqual(originalRequest.unsubtitutedUrl, undefined); + assert.strictEqual(originalRequest.unsubstitutedUrl, undefined); assert.strictEqual(originalRequest.urlObj!.pathname, '/dist/latest'); // @ts-expect-error full router not needed From 019f2cf790add24e6eff0e44ef08eefaff51075a Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:02:37 -0800 Subject: [PATCH 2/3] fix tests Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- scripts/build-r2-symlinks.mjs | 2 +- src/middleware/subtituteMiddleware.ts | 2 +- src/providers/r2Provider.ts | 3 +- tests/e2e/directory.test.ts | 31 ++++++++++++++----- tests/e2e/test-data/expected-html/dist.txt | 6 ++-- .../expected-s3/ListObjectsV2-exists.xml | 4 +-- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/scripts/build-r2-symlinks.mjs b/scripts/build-r2-symlinks.mjs index 29e02f3..38a36b0 100644 --- a/scripts/build-r2-symlinks.mjs +++ b/scripts/build-r2-symlinks.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env node 'use strict'; import { join } from 'node:path'; diff --git a/src/middleware/subtituteMiddleware.ts b/src/middleware/subtituteMiddleware.ts index 7fb37fe..4258b48 100644 --- a/src/middleware/subtituteMiddleware.ts +++ b/src/middleware/subtituteMiddleware.ts @@ -39,6 +39,6 @@ export class SubtitutionMiddleware implements Middleware { new Request(request) ); - return this.router.handle(substitutedRequest, ctx); + return this.router.handle(substitutedRequest, ctx, request.urlObj); } } diff --git a/src/providers/r2Provider.ts b/src/providers/r2Provider.ts index 6c4b725..019c702 100644 --- a/src/providers/r2Provider.ts +++ b/src/providers/r2Provider.ts @@ -87,7 +87,8 @@ export class R2Provider implements Provider { options?: ReadDirectoryOptions ): Promise { if (path in CACHED_DIRECTORIES) { - // @ts-expect-error + // @ts-expect-error dates may not be parsed at this point, we take care + // of it below const result: ReadDirectoryResult = CACHED_DIRECTORIES[path]; for (const file of result.files) { diff --git a/tests/e2e/directory.test.ts b/tests/e2e/directory.test.ts index 1bbecfd..97a7783 100644 --- a/tests/e2e/directory.test.ts +++ b/tests/e2e/directory.test.ts @@ -1,6 +1,6 @@ import { after, before, describe, it } from 'node:test'; import assert from 'node:assert'; -import { readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import http from 'http'; import { Miniflare } from 'miniflare'; @@ -24,9 +24,12 @@ async function startS3Mock(): Promise { const r2Prefix = url.searchParams.get('prefix')!; let doesFolderExist = - ['nodejs/release/', 'nodejs/', 'nodejs/docs/', 'metrics/'].includes( - r2Prefix - ) || r2Prefix.endsWith('/docs/api/'); + [ + 'nodejs/release/v1.0.0/', + 'nodejs/', + 'nodejs/docs/', + 'metrics/', + ].includes(r2Prefix) || r2Prefix.endsWith('/docs/api/'); if (doesFolderExist) { xmlFilePath += 'ListObjectsV2-exists.xml'; @@ -76,15 +79,27 @@ describe('Directory Tests (Restricted Directory Listing)', () => { }); it('redirects `/dist` to `/dist/` and returns expected html', async () => { - const [originalRes, expectedHtml] = await Promise.all([ - mf.dispatchFetch(`${url}dist`, { redirect: 'manual' }), + const originalRes = await mf.dispatchFetch(`${url}dist`, { + redirect: 'manual', + }); + + assert.strictEqual(originalRes.status, 301); + const res = await mf.dispatchFetch(originalRes.headers.get('location')!); + assert.strictEqual(res.status, 200); + assert.strictEqual( + res.headers.get('cache-control'), + 'public, max-age=3600, s-maxage=14400' + ); + }); + + it('`/dist/v1.0.0/` returns expected html', async () => { + const [res, expectedHtml] = await Promise.all([ + mf.dispatchFetch(`${url}dist/v1.0.0/`), readFile('./tests/e2e/test-data/expected-html/dist.txt', { encoding: 'utf-8', }), ]); - assert.strictEqual(originalRes.status, 301); - const res = await mf.dispatchFetch(originalRes.headers.get('location')!); assert.strictEqual(res.status, 200); assert.strictEqual( res.headers.get('cache-control'), diff --git a/tests/e2e/test-data/expected-html/dist.txt b/tests/e2e/test-data/expected-html/dist.txt index 0725de9..43a623e 100644 --- a/tests/e2e/test-data/expected-html/dist.txt +++ b/tests/e2e/test-data/expected-html/dist.txt @@ -1,6 +1,6 @@ - Index of /dist/ + Index of /dist/v1.0.0/ -

Index of /dist/


../
+

Index of /dist/v1.0.0/


../
 latest/                                                           -                   -
-index.json                                         12 Sept 2023, 05:43                 18 B
+index.json                                         12 Sept 2023, 05:43                 18 B
 

diff --git a/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml b/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml index 5bed3e2..6787ab9 100644 --- a/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml +++ b/tests/e2e/test-data/expected-s3/ListObjectsV2-exists.xml @@ -5,11 +5,11 @@ 1000 false - nodejs/release/latest/ + nodejs/release/v1.0.0/latest/ "asd123" - nodejs/release/index.json + nodejs/release/v1.0.0/index.json 2023-09-12T05:43:00.000Z 18 From b0cdb16c5614b3f6ecc51c9658f93ba936a55c3a Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:20:08 -0800 Subject: [PATCH 3/3] review Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/providers/r2Provider.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/providers/r2Provider.ts b/src/providers/r2Provider.ts index 019c702..d68b16d 100644 --- a/src/providers/r2Provider.ts +++ b/src/providers/r2Provider.ts @@ -15,6 +15,19 @@ import type { } from './provider'; import { S3Provider } from './s3Provider'; +type CachedFile = { + name: string; + lastModified: string | Date; + size: number; +}; + +type CachedDirectory = { + subdirectories: string[]; + hasIndexHtmlFile: boolean; + files: CachedFile[] | File[]; + lastModified: string | Date; +}; + type R2ProviderCtorOptions = { ctx: Context; }; @@ -87,20 +100,19 @@ export class R2Provider implements Provider { options?: ReadDirectoryOptions ): Promise { if (path in CACHED_DIRECTORIES) { - // @ts-expect-error dates may not be parsed at this point, we take care - // of it below - const result: ReadDirectoryResult = CACHED_DIRECTORIES[path]; - - for (const file of result.files) { - if (typeof file.lastModified === 'string') { - file.lastModified = new Date(file.lastModified); - } - } + const result: CachedDirectory = + CACHED_DIRECTORIES[path as keyof typeof CACHED_DIRECTORIES]; if (typeof result.lastModified === 'string') { result.lastModified = new Date(result.lastModified); + + for (const file of result.files) { + // @ts-expect-error this isn't readonly + file.lastModified = new Date(file.lastModified); + } } + // @ts-expect-error at this point the result is parsed already return Promise.resolve({ subdirectories: result.subdirectories, files: result.files,