@thomas.f/prnext 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/build/action-exports.mjs +107 -0
- package/build/actions.mjs +214 -0
- package/build/app-config.mjs +138 -0
- package/build/app-routing.mjs +119 -0
- package/build/app-static.mjs +158 -0
- package/build/app.mjs +536 -0
- package/build/cache-components.mjs +91 -0
- package/build/cache-handlers.mjs +54 -0
- package/build/check.mjs +40 -0
- package/build/compiler.mjs +222 -0
- package/build/config.mjs +193 -0
- package/build/css-modules.mjs +114 -0
- package/build/custom-routes.mjs +363 -0
- package/build/dev-worker.mjs +18 -0
- package/build/dev.mjs +76 -0
- package/build/directory-cache.mjs +29 -0
- package/build/dynamic.mjs +125 -0
- package/build/edge.mjs +141 -0
- package/build/font-data.LICENSE.txt +25 -0
- package/build/font-data.json +1 -0
- package/build/font-files.mjs +221 -0
- package/build/fonts.mjs +131 -0
- package/build/framework-imports.mjs +17 -0
- package/build/i18n.mjs +39 -0
- package/build/images-config.mjs +62 -0
- package/build/images.mjs +86 -0
- package/build/incremental.mjs +76 -0
- package/build/index.mjs +356 -0
- package/build/loader-cache.mjs +69 -0
- package/build/metadata-images.mjs +72 -0
- package/build/metadata-routes.mjs +77 -0
- package/build/middleware.mjs +231 -0
- package/build/migrate.mjs +289 -0
- package/build/module-resolution.mjs +78 -0
- package/build/native-discovery.mjs +134 -0
- package/build/native-packages.mjs +105 -0
- package/build/npm-packages.mjs +110 -0
- package/build/og.mjs +14 -0
- package/build/page-extensions.mjs +12 -0
- package/build/passthrough-loader.cjs +4 -0
- package/build/precompress.mjs +122 -0
- package/build/scan.mjs +178 -0
- package/build/script-workers.mjs +25 -0
- package/build/server-actions-config.mjs +27 -0
- package/build/source-maps.mjs +34 -0
- package/build/standalone-config.mjs +19 -0
- package/build/standalone-runner.mjs +21 -0
- package/build/standalone-worker.mjs +5 -0
- package/build/standalone.mjs +240 -0
- package/build/static-export.mjs +93 -0
- package/build/styles.mjs +174 -0
- package/build/transform-cache.mjs +50 -0
- package/build/transform.mjs +155 -0
- package/build/turbopack-conditions.mjs +59 -0
- package/build/webpack-bridge.cjs +3 -0
- package/build/webpack-graph.mjs +256 -0
- package/build/webpack-loader.cjs +9 -0
- package/cli.mjs +210 -0
- package/compat/app-client-page.cjs +32 -0
- package/compat/app-context.cjs +40 -0
- package/compat/app-context.d.cts +20 -0
- package/compat/app-dynamic.cjs +34 -0
- package/compat/app-error-boundary.cjs +53 -0
- package/compat/app-global-error-boundary.cjs +34 -0
- package/compat/app-layout-context.cjs +27 -0
- package/compat/app-navigation-boundary.cjs +47 -0
- package/compat/app.cjs +33 -0
- package/compat/app.d.cts +9 -0
- package/compat/cache-handlers.cjs +223 -0
- package/compat/cache-life.cjs +34 -0
- package/compat/cache.cjs +99 -0
- package/compat/cache.d.cts +36 -0
- package/compat/compat/router.d.cts +2 -0
- package/compat/compat-router.cjs +7 -0
- package/compat/constants.cjs +8 -0
- package/compat/constants.d.cts +7 -0
- package/compat/cookies.cjs +129 -0
- package/compat/data-cache.cjs +389 -0
- package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
- package/compat/dist/shared/lib/router-context.d.cts +1 -0
- package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
- package/compat/document.cjs +73 -0
- package/compat/document.d.cts +41 -0
- package/compat/draft.cjs +28 -0
- package/compat/dynamic-bailout.cjs +13 -0
- package/compat/dynamic.cjs +119 -0
- package/compat/dynamic.d.cts +32 -0
- package/compat/error.cjs +36 -0
- package/compat/error.d.cts +15 -0
- package/compat/fetch.d.cts +10 -0
- package/compat/font/google.d.cts +1 -0
- package/compat/font/local.d.cts +2 -0
- package/compat/font-google.cjs +2 -0
- package/compat/font-google.d.cts +1944 -0
- package/compat/font-local.cjs +5 -0
- package/compat/font-local.d.cts +3 -0
- package/compat/font-types.d.cts +25 -0
- package/compat/head.cjs +180 -0
- package/compat/head.d.cts +3 -0
- package/compat/headers.cjs +89 -0
- package/compat/headers.d.cts +26 -0
- package/compat/image-config.cjs +3 -0
- package/compat/image-imports.d.ts +12 -0
- package/compat/image-shared.cjs +62 -0
- package/compat/image.cjs +64 -0
- package/compat/image.d.cts +35 -0
- package/compat/incremental-cache.cjs +186 -0
- package/compat/index.d.ts +159 -0
- package/compat/instant-samples.cjs +39 -0
- package/compat/instant.cjs +13 -0
- package/compat/link.cjs +68 -0
- package/compat/link.d.cts +17 -0
- package/compat/locale.cjs +30 -0
- package/compat/metadata.d.cts +77 -0
- package/compat/navigation-controls.cjs +27 -0
- package/compat/navigation-server.cjs +25 -0
- package/compat/navigation.cjs +52 -0
- package/compat/navigation.d.cts +28 -0
- package/compat/next-app-router-context.cjs +6 -0
- package/compat/next-router-context.cjs +3 -0
- package/compat/og.cjs +32 -0
- package/compat/og.d.cts +1 -0
- package/compat/paths.cjs +33 -0
- package/compat/preview.cjs +41 -0
- package/compat/rewrite.cjs +41 -0
- package/compat/route-request.cjs +64 -0
- package/compat/router.cjs +137 -0
- package/compat/router.d.cts +58 -0
- package/compat/script-context.cjs +5 -0
- package/compat/script-loader.cjs +135 -0
- package/compat/script.cjs +69 -0
- package/compat/script.d.cts +17 -0
- package/compat/server-primitives.cjs +109 -0
- package/compat/server.cjs +13 -0
- package/compat/server.d.cts +37 -0
- package/compat/static-generation.cjs +108 -0
- package/compat/use-cache.cjs +129 -0
- package/index.mjs +1 -0
- package/native/platforms.json +6 -0
- package/native/resolve.mjs +81 -0
- package/package.json +127 -0
- package/release/verify.mjs +59 -0
- package/runtime/action-client.mjs +19 -0
- package/runtime/action-crypto.mjs +138 -0
- package/runtime/action-reference.mjs +2 -0
- package/runtime/action-server.mjs +161 -0
- package/runtime/action-ssr.mjs +5 -0
- package/runtime/api.mjs +326 -0
- package/runtime/app-client.mjs +539 -0
- package/runtime/app-content.mjs +8 -0
- package/runtime/app-errors.mjs +12 -0
- package/runtime/app-instant.mjs +30 -0
- package/runtime/app-metadata.mjs +410 -0
- package/runtime/app-partial-model.mjs +228 -0
- package/runtime/app-partial.mjs +130 -0
- package/runtime/app-prefetch.mjs +34 -0
- package/runtime/app-recovery.mjs +87 -0
- package/runtime/app-render.mjs +544 -0
- package/runtime/app-routing-access.mjs +143 -0
- package/runtime/app-routing-context.mjs +35 -0
- package/runtime/app-routing-state.mjs +61 -0
- package/runtime/app-routing.mjs +323 -0
- package/runtime/app-segments.mjs +22 -0
- package/runtime/app-static-params.mjs +120 -0
- package/runtime/app-static.mjs +86 -0
- package/runtime/app-stream-channel.mjs +92 -0
- package/runtime/app-stream-html.mjs +279 -0
- package/runtime/build-directory.mjs +48 -0
- package/runtime/cache-arguments.mjs +33 -0
- package/runtime/cache-components-rsc.mjs +64 -0
- package/runtime/cache-request.mjs +23 -0
- package/runtime/client-navigation.mjs +40 -0
- package/runtime/dev-client.mjs +129 -0
- package/runtime/dev-file-changes.mjs +110 -0
- package/runtime/document.mjs +96 -0
- package/runtime/edge.mjs +105 -0
- package/runtime/env.mjs +68 -0
- package/runtime/fetch-cache.mjs +379 -0
- package/runtime/font-preload.mjs +4 -0
- package/runtime/http.mjs +190 -0
- package/runtime/incremental-cache.mjs +83 -0
- package/runtime/metadata-route.mjs +83 -0
- package/runtime/middleware.mjs +186 -0
- package/runtime/module-loader.mjs +21 -0
- package/runtime/navigation.mjs +16 -0
- package/runtime/og-loader.mjs +17 -0
- package/runtime/pages-client.mjs +197 -0
- package/runtime/pages-error-props.mjs +6 -0
- package/runtime/pages-errors.mjs +85 -0
- package/runtime/pages-initial-props.mjs +43 -0
- package/runtime/pages-navigation.mjs +565 -0
- package/runtime/pages-paths.mjs +80 -0
- package/runtime/pages-revalidate.mjs +33 -0
- package/runtime/partial-artifact-cache.mjs +37 -0
- package/runtime/partial-template.mjs +174 -0
- package/runtime/profile.mjs +9 -0
- package/runtime/profiles.json +47 -0
- package/runtime/render.mjs +304 -0
- package/runtime/request-transport.mjs +41 -0
- package/runtime/request-work.mjs +7 -0
- package/runtime/route-static.mjs +117 -0
- package/runtime/rsc-worker.mjs +426 -0
- package/runtime/script-html.mjs +61 -0
- package/runtime/stream-select.mjs +25 -0
- package/runtime/stream-utils.mjs +127 -0
- package/runtime/transport.mjs +108 -0
- package/runtime/watch-project.mjs +130 -0
- package/runtime/worker-artifacts.mjs +22 -0
- package/runtime/worker-metadata.mjs +31 -0
- package/runtime/worker.mjs +197 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {validateCondition} from './turbopack-conditions.mjs';
|
|
3
|
+
import {frameworkImportPattern} from './framework-imports.mjs';
|
|
4
|
+
|
|
5
|
+
const reserved = name => frameworkImportPattern.test(name) || /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/.test(name);
|
|
6
|
+
export function validateWebpackResolution(input = {}) {
|
|
7
|
+
for(const key of Object.keys(input))if(!['alias','extensions'].includes(key))throw new Error(`webpack.resolve.${key} is not implemented`);
|
|
8
|
+
const aliases=input.alias || {};
|
|
9
|
+
if (!aliases || typeof aliases!=='object' || Array.isArray(aliases) || Object.keys(aliases).length>256)throw new TypeError('webpack.resolve.alias must contain at most 256 aliases');
|
|
10
|
+
for(const [key,value]of Object.entries(aliases)){
|
|
11
|
+
const name=key.endsWith('$')?key.slice(0,-1):key;
|
|
12
|
+
if (!name || name.length>4096 || /[\0*$]/.test(name) || reserved(name))throw new TypeError(`Unsupported webpack.resolve.alias key ${key}`);
|
|
13
|
+
const values=Array.isArray(value)?value:[value];
|
|
14
|
+
if(!values.length || values.length>32 || values.some(value=>value!==false && (typeof value!=='string' || !value || value.length>4096 || /[\0*]/.test(value))))throw new TypeError(`Invalid webpack.resolve.alias.${key}`);
|
|
15
|
+
}
|
|
16
|
+
const extensions=validateTurbopack({resolveExtensions:input.extensions}).resolveExtensions;
|
|
17
|
+
return {resolveAlias:aliases,aliasesRelativeToImporter:true,...(extensions?{resolveExtensions:extensions}:{})};
|
|
18
|
+
}
|
|
19
|
+
export function validateTurbopack(input = {}) {
|
|
20
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('turbopack must be an object');
|
|
21
|
+
for (const key of Object.keys(input)) if (!['root', 'resolveAlias', 'resolveExtensions', 'rules'].includes(key)) throw new Error(`turbopack.${key} is not implemented by PRNext`);
|
|
22
|
+
if (input.root !== undefined && (typeof input.root !== 'string' || !path.isAbsolute(input.root) || input.root.includes('\0'))) throw new TypeError('turbopack.root must be an absolute filesystem path');
|
|
23
|
+
const aliases = input.resolveAlias ?? {};
|
|
24
|
+
if (!aliases || typeof aliases !== 'object' || Array.isArray(aliases) || Object.keys(aliases).length > 256) throw new TypeError('turbopack.resolveAlias must contain at most 256 aliases');
|
|
25
|
+
const validTarget = target => typeof target === 'string' && target.length > 0 && target.length <= 4096 && !/[\0*]/.test(target);
|
|
26
|
+
for (const [name, target] of Object.entries(aliases)) {
|
|
27
|
+
if (!name || name.length > 4096 || /[\0*$]/.test(name) || reserved(name)) throw new TypeError(`Unsupported turbopack.resolveAlias key ${name}; framework aliases and wildcard aliases cannot be overridden`);
|
|
28
|
+
if (!validTarget(target) && (!target || typeof target !== 'object' || Array.isArray(target) || Object.keys(target).length !== 1 || !validTarget(target.browser))) throw new TypeError(`turbopack.resolveAlias.${name} must be a path/package string or {browser: string}`);
|
|
29
|
+
}
|
|
30
|
+
const extensions = input.resolveExtensions;
|
|
31
|
+
if (extensions !== undefined && (!Array.isArray(extensions) || !extensions.length || extensions.length > 32 || extensions.some(value => typeof value !== 'string' || !/^\.[a-zA-Z0-9.]+$/.test(value)))) throw new TypeError('turbopack.resolveExtensions must be an array of at most 32 file extensions');
|
|
32
|
+
const rules = input.rules;
|
|
33
|
+
if (rules !== undefined) {
|
|
34
|
+
if (!rules || typeof rules !== 'object' || Array.isArray(rules) || Object.keys(rules).length > 256) throw new TypeError('turbopack.rules must contain at most 256 patterns');
|
|
35
|
+
let count=0;
|
|
36
|
+
for (const [pattern, entries] of Object.entries(rules)) {
|
|
37
|
+
const variants=Array.isArray(entries)?entries:[entries];
|
|
38
|
+
if (!variants.length || (count+=variants.length)>256) throw new TypeError('turbopack.rules must contain at most 256 nonempty rule variants');
|
|
39
|
+
for (const rule of variants) {
|
|
40
|
+
if (!pattern || pattern.length > 1024 || !rule || typeof rule !== 'object' || Object.keys(rule).some(key => !['loaders', 'as', 'condition'].includes(key)) || !Array.isArray(rule.loaders) || !rule.loaders.length || rule.loaders.length > 32 || rule.loaders.some(loader => typeof loader !== 'string' && (!loader || typeof loader.loader !== 'string' || Object.keys(loader).some(key => !['loader','options'].includes(key))))) throw new TypeError(`Invalid turbopack.rules ${pattern}; expected a loaders array and optional as/condition`);
|
|
41
|
+
if (rule.as !== undefined && !['*.js', '*.jsx'].includes(rule.as)) throw new Error('Turbopack loader output must be JavaScript (*.js or *.jsx)');
|
|
42
|
+
if (rule.condition !== undefined) validateCondition(rule.condition);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { ...(input.root !== undefined ? {root:path.resolve(input.root)} : {}), ...(rules ? {rules} : {}), resolveAlias: aliases, ...(extensions ? { resolveExtensions: [...new Set(extensions)] } : {}) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve aliases before boundary analysis so every graph uses the same module identity. */
|
|
50
|
+
export function moduleResolutionPlugin(config = {}, projectRoot) {
|
|
51
|
+
const aliases = Object.entries(config.resolveAlias || {}).sort(([a], [b]) => b.length - a.length);
|
|
52
|
+
return { name: 'prnext-module-resolution', setup(build) {
|
|
53
|
+
if (config.resolveExtensions) build.initialOptions.resolveExtensions = config.resolveExtensions;
|
|
54
|
+
if (!aliases.length) return;
|
|
55
|
+
build.onResolve({ filter: /.*/ }, async args => {
|
|
56
|
+
if (args.pluginData?.prnextAlias || args.namespace && args.namespace !== 'file') return;
|
|
57
|
+
const match = aliases.find(([name]) => name.endsWith('$') ? args.path===name.slice(0,-1) : args.path === name || args.path.startsWith(name + '/'));
|
|
58
|
+
if (!match) return;
|
|
59
|
+
const [name, value] = match;
|
|
60
|
+
const targets = Array.isArray(value)?value:[typeof value === 'string' || value===false ? value : build.initialOptions.platform === 'browser' ? value.browser : undefined];
|
|
61
|
+
let resolved;
|
|
62
|
+
for(const target of targets){
|
|
63
|
+
if (target===false) return {path:args.path,namespace:'prnext-ignored-alias'};
|
|
64
|
+
if (target===undefined) return;
|
|
65
|
+
const replacement = target + args.path.slice(name.endsWith('$')?name.length-1:name.length);
|
|
66
|
+
const resolveDir = config.aliasesRelativeToImporter ? args.resolveDir || projectRoot : config.root || projectRoot;
|
|
67
|
+
resolved = await build.resolve(replacement.startsWith('.') ? path.resolve(resolveDir, replacement) : replacement, {
|
|
68
|
+
importer: args.importer, namespace: args.namespace, resolveDir,
|
|
69
|
+
kind: args.kind, with: args.with, pluginData: { ...args.pluginData, prnextAlias: true },
|
|
70
|
+
});
|
|
71
|
+
if(!resolved.errors.length)break;
|
|
72
|
+
}
|
|
73
|
+
// Do not leak the recursion guard to imports made by the aliased module.
|
|
74
|
+
return { ...resolved, pluginData: args.pluginData };
|
|
75
|
+
});
|
|
76
|
+
build.onLoad({filter:/.*/,namespace:'prnext-ignored-alias'},()=>({contents:'module.exports = {};',loader:'js'}));
|
|
77
|
+
} };
|
|
78
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** Inspect only packages reached through imports and declared runtime dependencies. */
|
|
6
|
+
export function createNativeDiscovery({ projectRoot, isClientSource }) {
|
|
7
|
+
const metadata = new Map();
|
|
8
|
+
const packageRoots = new Map();
|
|
9
|
+
const packageFiles = new Map();
|
|
10
|
+
const dependencyRoots = new Map();
|
|
11
|
+
const clientBoundaries = new Map();
|
|
12
|
+
|
|
13
|
+
function readMetadata(directory) {
|
|
14
|
+
if (!metadata.has(directory)) metadata.set(directory, readFile(path.join(directory, 'package.json'), 'utf8').then(JSON.parse).catch(error => {
|
|
15
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return;
|
|
16
|
+
throw error;
|
|
17
|
+
}));
|
|
18
|
+
return metadata.get(directory);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function packageFor(file) {
|
|
22
|
+
let directory = path.dirname(file);
|
|
23
|
+
const visited = [];
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (packageRoots.has(directory)) {
|
|
26
|
+
const cached = packageRoots.get(directory);
|
|
27
|
+
for (const current of visited) packageRoots.set(current, cached);
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
visited.push(directory);
|
|
31
|
+
const info = await readMetadata(directory);
|
|
32
|
+
if (typeof info?.name === 'string') {
|
|
33
|
+
const result = { directory, name: info.name, metadata: info };
|
|
34
|
+
for (const current of visited) packageRoots.set(current, result);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
const parent = path.dirname(directory);
|
|
38
|
+
if (parent === directory) return;
|
|
39
|
+
directory = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function filesFor(directory) {
|
|
44
|
+
if (!packageFiles.has(directory)) packageFiles.set(directory, (async () => {
|
|
45
|
+
const result = { native: false, css: false, sources: [] };
|
|
46
|
+
async function walk(current) {
|
|
47
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
48
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
49
|
+
const file = path.join(current, entry.name);
|
|
50
|
+
// Follow declared dependencies independently; avoid walking symlinked
|
|
51
|
+
// directories into another package or revisiting directory cycles.
|
|
52
|
+
if (entry.isDirectory()) await walk(file);
|
|
53
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
54
|
+
if (entry.name.endsWith('.node')) result.native = true;
|
|
55
|
+
if (/\.(?:css|scss|sass)$/i.test(entry.name)) result.css = true;
|
|
56
|
+
if (/\.(?:[cm]?js|jsx|tsx?)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) result.sources.push(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await walk(directory);
|
|
61
|
+
return result;
|
|
62
|
+
})());
|
|
63
|
+
return packageFiles.get(directory);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveDependency(directory, name) {
|
|
67
|
+
const key = directory + '\0' + name;
|
|
68
|
+
if (!dependencyRoots.has(key)) dependencyRoots.set(key, (async () => {
|
|
69
|
+
const require = createRequire(path.join(directory, 'package.json'));
|
|
70
|
+
try {
|
|
71
|
+
const file = require.resolve(name);
|
|
72
|
+
if (path.isAbsolute(file)) return packageFor(file);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) throw error;
|
|
75
|
+
}
|
|
76
|
+
// Packages with only conditional/subpath exports still have runtime
|
|
77
|
+
// dependencies; locate their metadata without importing or evaluating them.
|
|
78
|
+
for (const search of require.resolve.paths(name) || []) {
|
|
79
|
+
const candidate = path.join(search, name);
|
|
80
|
+
const info = await readMetadata(candidate);
|
|
81
|
+
if (typeof info?.name === 'string') {
|
|
82
|
+
const directory = await realpath(candidate);
|
|
83
|
+
return { directory, name: info.name, metadata: info };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})());
|
|
87
|
+
return dependencyRoots.get(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function nativeReachable(owner, visited = new Set()) {
|
|
91
|
+
if (visited.has(owner.directory)) return false;
|
|
92
|
+
visited.add(owner.directory);
|
|
93
|
+
if ((await filesFor(owner.directory)).native) return true;
|
|
94
|
+
const names = new Set([...Object.keys(owner.metadata.dependencies || {}), ...Object.keys(owner.metadata.optionalDependencies || {})]);
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
const dependency = await resolveDependency(owner.directory, name);
|
|
97
|
+
if (dependency && await nativeReachable(dependency, visited)) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasClientBoundary(directory, sources) {
|
|
103
|
+
if (!clientBoundaries.has(directory)) clientBoundaries.set(directory, (async () => {
|
|
104
|
+
for (const file of sources) {
|
|
105
|
+
const source = await readFile(file, 'utf8');
|
|
106
|
+
if ((source.includes('use client') || source.includes('use server')) && isClientSource(source, file)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
})());
|
|
110
|
+
return clientBoundaries.get(directory);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function canExternalizeDirectory(directory, mode) {
|
|
114
|
+
const ownFiles = await filesFor(directory);
|
|
115
|
+
return !ownFiles.css && !await hasClientBoundary(directory, ownFiles.sources);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
canExternalizeDirectory,
|
|
120
|
+
async externalOwner(file, mode) {
|
|
121
|
+
const owner = await packageFor(file);
|
|
122
|
+
if (!owner) return;
|
|
123
|
+
const application = path.relative(owner.directory, projectRoot);
|
|
124
|
+
if (application === '' || (!application.startsWith('..' + path.sep) && application !== '..' && !path.isAbsolute(application))) return;
|
|
125
|
+
if (!await nativeReachable(owner)) return;
|
|
126
|
+
// Keep CSS extraction and Server Component client-reference transforms in
|
|
127
|
+
// the compiler. A native dependency imported by this package is examined
|
|
128
|
+
// separately when its own entry is resolved.
|
|
129
|
+
if (!await canExternalizeDirectory(owner.directory, mode)) return;
|
|
130
|
+
if (!/\.[cm]?js$/.test(file)) return;
|
|
131
|
+
return { directory: owner.directory, name: owner.name };
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createNativeDiscovery } from './native-discovery.mjs';
|
|
4
|
+
|
|
5
|
+
function contains(directory, file) {
|
|
6
|
+
const relative = path.relative(directory, file);
|
|
7
|
+
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function packageAt(directory) {
|
|
11
|
+
try {
|
|
12
|
+
const metadata = JSON.parse(await readFile(path.join(directory, 'package.json'), 'utf8'));
|
|
13
|
+
return typeof metadata.name === 'string' ? { directory, name: metadata.name } : undefined;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Keep npm native loaders and their adjacent files under Node's package resolution. */
|
|
21
|
+
export function createNativePackages(projectRoot, { isClientSource }) {
|
|
22
|
+
const discovery = createNativeDiscovery({ projectRoot, isClientSource });
|
|
23
|
+
const external = new Map();
|
|
24
|
+
const references = new Map();
|
|
25
|
+
|
|
26
|
+
async function ownerOf(file) {
|
|
27
|
+
// Externalize the outer package for nested node_modules so a transitive addon
|
|
28
|
+
// remains resolvable from its original npm parent after deployment.
|
|
29
|
+
const parts = file.split(path.sep);
|
|
30
|
+
for (let index = 0; index < parts.length - 1; index++) {
|
|
31
|
+
if (parts[index] !== 'node_modules') continue;
|
|
32
|
+
const end = index + (parts[index + 1]?.startsWith('@') ? 3 : 2);
|
|
33
|
+
const candidate = await packageAt(parts.slice(0, end).join(path.sep));
|
|
34
|
+
if (candidate && contains(candidate.directory, file)) return candidate;
|
|
35
|
+
}
|
|
36
|
+
// Workspace packages may resolve through symlinks outside node_modules.
|
|
37
|
+
for (let directory = path.dirname(file); directory !== path.dirname(directory); directory = path.dirname(directory)) {
|
|
38
|
+
if (directory === projectRoot) break;
|
|
39
|
+
const candidate = await packageAt(directory);
|
|
40
|
+
if (candidate && [...references.keys()].some(entry => contains(directory, entry))) return candidate;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function discoverOwner(file, mode) {
|
|
45
|
+
const discovered = await discovery.externalOwner(file, mode);
|
|
46
|
+
if (!discovered) return;
|
|
47
|
+
const outer = await ownerOf(file);
|
|
48
|
+
if (!outer || outer.directory === discovered.directory) return discovered;
|
|
49
|
+
if (!await discovery.canExternalizeDirectory(outer.directory, mode)) {
|
|
50
|
+
throw new Error(`Native npm dependency ${discovered.name} is nested inside ${outer.name}, which contains CSS or Client Components. Install ${discovered.name} as a direct application dependency so it can remain external without bypassing ${outer.name}'s compilation.`);
|
|
51
|
+
}
|
|
52
|
+
return outer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ownerFor(file) { return [...external.values()].find(owner => contains(owner.directory, file)); }
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
get version() { return external.size; },
|
|
59
|
+
assertCompatibleCss(cssFiles) {
|
|
60
|
+
for (const css of cssFiles) {
|
|
61
|
+
const owner = ownerFor(css);
|
|
62
|
+
if (owner) throw new Error(`Native npm package ${owner.name} also imports CSS (${css}). PRNext cannot yet externalize this mixed package for App Router rendering. Move the native dependency into a separate server-only npm package or a Pages API route.`);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
plugin(mode) {
|
|
66
|
+
return {
|
|
67
|
+
name: `prnext-native-packages-${mode}`,
|
|
68
|
+
setup(esbuild) {
|
|
69
|
+
esbuild.onResolve({ filter: /\.node$/ }, async args => {
|
|
70
|
+
if (args.pluginData?.prnextNativeResolving) return;
|
|
71
|
+
if (mode === 'browser') return { errors: [{ text: `Native Node addon ${args.path} cannot run in a Client Component (${args.importer}). Keep the native dependency in a Server Component or route handler.` }] };
|
|
72
|
+
const resolved = await esbuild.resolve(args.path, { importer: args.importer, resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextNativeResolving: true } });
|
|
73
|
+
if (resolved.errors.length) return resolved;
|
|
74
|
+
const owner = await ownerOf(resolved.path);
|
|
75
|
+
if (!owner) return { errors: [{ text: `Local native addon ${args.path} in ${args.importer} cannot be relocated by the App Router compiler. Package the addon and its loader as an npm dependency, then import that package by name.` }] };
|
|
76
|
+
external.set(owner.directory, owner);
|
|
77
|
+
return { errors: [{ text: `Native npm package ${owner.name} requires an external server import.` }] };
|
|
78
|
+
});
|
|
79
|
+
if (mode === 'browser') return;
|
|
80
|
+
esbuild.onResolve({ filter: /.*/ }, async args => {
|
|
81
|
+
if (args.pluginData?.prnextNativeResolving || args.namespace !== 'file') return;
|
|
82
|
+
if (path.isAbsolute(args.path)) {
|
|
83
|
+
const reference = references.get(args.path);
|
|
84
|
+
const owner = ownerFor(args.path) || (reference && await discoverOwner(args.path, mode));
|
|
85
|
+
if (!owner) return;
|
|
86
|
+
external.set(owner.directory, owner);
|
|
87
|
+
if (!reference) return { errors: [{ text: `Native npm package ${owner.name} is imported using a filesystem path (${args.path}). Import its public npm entry by name so the deployed build can resolve its native files.` }] };
|
|
88
|
+
return { path: reference.specifier, external: true };
|
|
89
|
+
}
|
|
90
|
+
if (args.path.startsWith('.') || args.path.startsWith('#')) return;
|
|
91
|
+
const resolved = await esbuild.resolve(args.path, { importer: args.importer, resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextNativeResolving: true } });
|
|
92
|
+
if (resolved.errors.length || resolved.external || resolved.namespace !== 'file') return resolved;
|
|
93
|
+
references.set(resolved.path, { specifier: args.path, importer: args.importer });
|
|
94
|
+
const owner = ownerFor(resolved.path) || await discoverOwner(resolved.path, mode);
|
|
95
|
+
if (owner) {
|
|
96
|
+
external.set(owner.directory, owner);
|
|
97
|
+
return { path: args.path, external: true };
|
|
98
|
+
}
|
|
99
|
+
return resolved;
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { builtinModules } from 'node:module';
|
|
4
|
+
import { parse } from '@babel/parser';
|
|
5
|
+
import { VISITOR_KEYS } from '@babel/types';
|
|
6
|
+
import { createNativeDiscovery } from './native-discovery.mjs';
|
|
7
|
+
import {frameworkImportPattern} from './framework-imports.mjs';
|
|
8
|
+
|
|
9
|
+
const builtins = new Set([...builtinModules, ...builtinModules.map(name => 'node:' + name)]);
|
|
10
|
+
const react = /^(?:react|react-dom|react-server-dom-webpack)(?:\/|$)/;
|
|
11
|
+
const framework = name => frameworkImportPattern.test(name) || /^(?:server-only|client-only)$/.test(name);
|
|
12
|
+
const packageName = name => name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0];
|
|
13
|
+
const bare = name => !name.startsWith('.') && !name.startsWith('#') && !path.isAbsolute(name);
|
|
14
|
+
|
|
15
|
+
/** Inspect reached imports once per compiler graph; never execute npm modules. */
|
|
16
|
+
export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackages = [], serverExternalPackages = [] }) {
|
|
17
|
+
const transpile = new Set(transpilePackages), external = new Set(serverExternalPackages);
|
|
18
|
+
const sources = new Map(), decisions = new Map();
|
|
19
|
+
let sourceBytes = 0;
|
|
20
|
+
const native = createNativeDiscovery({ projectRoot, isClientSource: source => /['"]use (?:client|server)['"]/.test(source) });
|
|
21
|
+
function imports(file) {
|
|
22
|
+
if (!sources.has(file)) sources.set(file, (async () => {
|
|
23
|
+
if (!/\.(?:[cm]?js|jsx|tsx?)$/.test(file)) return { imports: [] };
|
|
24
|
+
const source = await readFile(file, 'utf8');
|
|
25
|
+
sourceBytes += Buffer.byteLength(source);
|
|
26
|
+
if (sourceBytes > 64 * 1024 * 1024) throw new Error('npm compilation analysis exceeded 64 MiB of source. Separate large runtime dependencies using serverExternalPackages.');
|
|
27
|
+
if (Buffer.byteLength(source) > 8 * 1024 * 1024) throw new Error(`npm source ${file} exceeds the 8 MiB analysis limit. Configure serverExternalPackages for packages that do not use Next APIs.`);
|
|
28
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, allowReturnOutsideFunction: true, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
29
|
+
const result = { imports: [], computedRequire: false, frameworkBoundary: ast.program.directives.some(item => ['use client', 'use server'].includes(item.value.value)) };
|
|
30
|
+
function visit(node) {
|
|
31
|
+
if (!node || typeof node !== 'object') return;
|
|
32
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type) && node.source && node.importKind !== 'type' && node.exportKind !== 'type') result.imports.push({ path: node.source.value, kind: 'import-statement' });
|
|
33
|
+
if (node.type === 'CallExpression' && (node.callee.type === 'Import' || node.callee.type === 'Identifier' && node.callee.name === 'require')) {
|
|
34
|
+
const argument = node.arguments[0];
|
|
35
|
+
if (argument?.type === 'StringLiteral') result.imports.push({ path: argument.value, kind: node.callee.type === 'Import' ? 'dynamic-import' : 'require-call' });
|
|
36
|
+
else if (node.callee.type !== 'Import') result.computedRequire = true;
|
|
37
|
+
}
|
|
38
|
+
for (const key of VISITOR_KEYS[node.type] || []) {
|
|
39
|
+
if (Array.isArray(node[key])) node[key].forEach(visit); else visit(node[key]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
visit(ast.program);
|
|
43
|
+
return result;
|
|
44
|
+
})());
|
|
45
|
+
return sources.get(file);
|
|
46
|
+
}
|
|
47
|
+
return { name: 'prnext-npm-packages', setup(build) {
|
|
48
|
+
const resolving = { prnextNpmResolving: true, prnextNativeResolving: true };
|
|
49
|
+
const resolve = (name, importer, kind) => build.resolve(name, { importer, resolveDir: path.dirname(importer), kind, pluginData: resolving });
|
|
50
|
+
function classify(file) {
|
|
51
|
+
if (!decisions.has(file)) decisions.set(file, (async () => {
|
|
52
|
+
const queue = [file], visited = new Set();
|
|
53
|
+
let directory = path.dirname(file);
|
|
54
|
+
for (;;) {
|
|
55
|
+
try { await readFile(path.join(directory, 'package.json')); break; }
|
|
56
|
+
catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; }
|
|
57
|
+
const parent = path.dirname(directory);
|
|
58
|
+
if (parent === directory) break;
|
|
59
|
+
directory = parent;
|
|
60
|
+
}
|
|
61
|
+
let needsCompiler = false, computedRequire = false;
|
|
62
|
+
while (queue.length) {
|
|
63
|
+
const current = queue.pop();
|
|
64
|
+
if (visited.has(current)) continue;
|
|
65
|
+
visited.add(current);
|
|
66
|
+
if (visited.size > 4096) throw new Error(`npm dependency graph for ${file} exceeds 4096 source files. Use serverExternalPackages for packages that do not use Next APIs.`);
|
|
67
|
+
const info = await imports(current);
|
|
68
|
+
needsCompiler ||= info.frameworkBoundary;
|
|
69
|
+
const relative = path.relative(directory, current);
|
|
70
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative) && !relative.split(path.sep).includes('node_modules')) computedRequire ||= info.computedRequire;
|
|
71
|
+
for (const dependency of info.imports) {
|
|
72
|
+
if (framework(dependency.path)) { needsCompiler = true; continue; }
|
|
73
|
+
if (builtins.has(dependency.path) || react.test(dependency.path)) continue;
|
|
74
|
+
if (bare(dependency.path)) {
|
|
75
|
+
if (transpile.has(packageName(dependency.path))) needsCompiler = true;
|
|
76
|
+
}
|
|
77
|
+
const resolved = await resolve(dependency.path, current, dependency.kind);
|
|
78
|
+
if (!resolved.errors.length && !resolved.external && resolved.namespace === 'file') queue.push(resolved.path);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { needsCompiler, computedRequire };
|
|
82
|
+
})());
|
|
83
|
+
return decisions.get(file);
|
|
84
|
+
}
|
|
85
|
+
build.onResolve({ filter: /.*/ }, async args => {
|
|
86
|
+
if (args.pluginData?.prnextNpmResolving || args.namespace !== 'file' || !bare(args.path) || framework(args.path)) return;
|
|
87
|
+
if (builtins.has(args.path) || react.test(args.path)) return { path: args.path, external: true };
|
|
88
|
+
const name = packageName(args.path);
|
|
89
|
+
// App graphs already compile npm by default. Explicit externals need the
|
|
90
|
+
// same validation as Pages to avoid bypassing compatibility transforms.
|
|
91
|
+
if (!pages && !external.has(name)) return;
|
|
92
|
+
const resolved = await resolve(args.path, args.importer || path.join(projectRoot, 'package.json'), args.kind);
|
|
93
|
+
if (resolved.errors.length || resolved.external || resolved.namespace !== 'file') return resolved;
|
|
94
|
+
const decision = await classify(resolved.path);
|
|
95
|
+
const fromRoot = await resolve(args.path, path.join(projectRoot, 'package.json'), args.kind);
|
|
96
|
+
const nestedResolution = fromRoot.errors.length || fromRoot.path !== resolved.path;
|
|
97
|
+
const bundle = transpile.has(name) || decision.needsCompiler || nestedResolution;
|
|
98
|
+
if (external.has(name) && decision.needsCompiler) throw new Error(`serverExternalPackages includes ${name}, but its imports require Next/PRNext compilation. Remove it from serverExternalPackages.`);
|
|
99
|
+
if (external.has(name) && nestedResolution) throw new Error(`External package ${name} resolves differently from the application root. Install the required version as a direct application dependency.`);
|
|
100
|
+
if (bundle && !external.has(name)) {
|
|
101
|
+
const owner = await native.externalOwner(resolved.path, 'ssr');
|
|
102
|
+
if (owner && decision.needsCompiler) throw new Error(`npm package ${name} combines Next imports with a native addon. Separate the native loader into a server-only dependency so its Next imports can be compiled.`);
|
|
103
|
+
if (owner) throw new Error(`transpilePackages cannot bundle native npm package ${name}. Keep its native loader external.`);
|
|
104
|
+
if (decision.computedRequire) throw new Error(`npm package ${name} needs compilation but reaches a computed require(). Use static imports or separate its runtime loader into a serverExternalPackages dependency.`);
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
return { path: args.path, external: true };
|
|
108
|
+
});
|
|
109
|
+
} };
|
|
110
|
+
}
|
package/build/og.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export async function prepareImageResponse(stage) {
|
|
5
|
+
const directory = path.dirname(createRequire(import.meta.url).resolve('@vercel/og'));
|
|
6
|
+
const target = path.join(stage, 'runtime/og');
|
|
7
|
+
await mkdir(target, { recursive: true });
|
|
8
|
+
await writeFile(path.join(target, 'index.node.mjs'), `import {createRequire as __prnextRequire} from 'node:module';import {fileURLToPath as __prnextFile} from 'node:url';import {dirname as __prnextDir} from 'node:path';const require=__prnextRequire(import.meta.url),__filename=__prnextFile(import.meta.url),__dirname=__prnextDir(__filename);\n` + await readFile(path.join(directory, 'index.node.js'), 'utf8'));
|
|
9
|
+
for (const name of ['Geist-Regular.ttf', 'resvg.wasm']) await cp(path.join(directory, name), path.join(target, name));
|
|
10
|
+
await cp(path.join(directory, '../LICENSE'), path.join(target, 'LICENSE'));
|
|
11
|
+
await writeFile(path.join(target, 'NOTICE.txt'), 'Image renderer: @vercel/og 1.0.3 (https://github.com/vercel/og). PRNext adds Node CommonJS globals to its bundled ESM entry and includes the adjacent HarfBuzz WASM needed by that entry. Upstream bundled notices are preserved in index.node.mjs.\n');
|
|
12
|
+
const satori = createRequire(path.join(directory, 'index.node.js')).resolve('satori');
|
|
13
|
+
await cp(createRequire(satori).resolve('harfbuzzjs/hb.wasm'), path.join(target, 'hb.wasm'));
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const defaultPageExtensions = ['tsx', 'ts', 'jsx', 'js', 'mjs', 'cjs'];
|
|
2
|
+
|
|
3
|
+
export function pageExtensionPattern(extensions = defaultPageExtensions) {
|
|
4
|
+
if (!Array.isArray(extensions) || !extensions.length || extensions.length > 64 || extensions.some(value => typeof value !== 'string' || value.length > 100 || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(value))) {
|
|
5
|
+
throw new Error('pageExtensions must be a non-empty array of file suffixes without a leading dot or path separator.');
|
|
6
|
+
}
|
|
7
|
+
return new RegExp(`\\.(?:${[...new Set(extensions)].sort((a, b) => b.length - a.length).map(value => value.replaceAll('.', '\\.')).join('|')})$`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validatePageSource(file) {
|
|
11
|
+
if (!/\.(?:[cm]?js|jsx|tsx?)$/.test(file)) throw new Error(`The pageExtensions entry ${file} requires an unsupported source loader. PRNext currently compiles JavaScript and TypeScript, including compound suffixes such as .page.tsx.`);
|
|
12
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { readdir, rm, stat, utimes, mkdir, copyFile, rename } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { createGzip, createGunzip } from 'node:zlib';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
async function digest(stream, limit = Infinity) {
|
|
9
|
+
const hash = createHash('sha256');
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
bytes += chunk.length;
|
|
13
|
+
if (bytes > limit) throw Object.assign(new Error('Precompression cache exceeds source size'), { code: 'CACHE_SIZE' });
|
|
14
|
+
hash.update(chunk);
|
|
15
|
+
}
|
|
16
|
+
return hash.digest('hex');
|
|
17
|
+
}
|
|
18
|
+
async function trimCache(directory) {
|
|
19
|
+
const entries = (await Promise.all((await readdir(directory)).filter(name => /^[a-f0-9]{64}\.gz$/.test(name)).map(async name => {
|
|
20
|
+
try { return {name, ...await stat(path.join(directory,name))}; } catch(error) { if(error.code==='ENOENT')return null; throw error; }
|
|
21
|
+
}))).filter(Boolean).sort((a,b)=>b.mtimeMs-a.mtimeMs);
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
for (let index=0;index<entries.length;index++) {
|
|
24
|
+
bytes += entries[index].size;
|
|
25
|
+
if(index>=512 || bytes>64*1024*1024)await rm(path.join(directory,entries[index].name),{force:true});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Images, archives and WOFF fonts already contain compressed data. Only build
|
|
30
|
+
// outputs with useful gzip representations are candidates; public stays mutable.
|
|
31
|
+
const compressibleExtensions = new Set([
|
|
32
|
+
'.html', '.htm', '.js', '.mjs', '.cjs', '.css', '.json', '.map', '.svg',
|
|
33
|
+
'.xml', '.txt', '.csv', '.md', '.webmanifest', '.wasm', '.ttf', '.otf', '.eot',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
async function* files(directory, additional) {
|
|
37
|
+
let entries;
|
|
38
|
+
try { entries = await readdir(directory, { withFileTypes: true }); }
|
|
39
|
+
catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const filename = path.join(directory, entry.name);
|
|
42
|
+
if (entry.isDirectory()) yield* files(filename, additional);
|
|
43
|
+
else if (entry.isFile() && (compressibleExtensions.has(path.extname(entry.name).toLowerCase()) || additional.has(filename))) yield filename;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function compress(filename, cacheDirectory) {
|
|
48
|
+
const original = await stat(filename);
|
|
49
|
+
if (!original.size) return 'skipped';
|
|
50
|
+
const sidecar = filename + '.gz';
|
|
51
|
+
const key = cacheDirectory && original.size <= 64*1024*1024 ? await digest(createReadStream(filename)) : null;
|
|
52
|
+
const cached = key && path.join(cacheDirectory,key+'.gz');
|
|
53
|
+
let reused = false;
|
|
54
|
+
try {
|
|
55
|
+
if (cached) {
|
|
56
|
+
try {
|
|
57
|
+
if ((await stat(cached)).size <= 4*1024*1024) {
|
|
58
|
+
// Validate cached content, including gzip CRC, using bounded buffers.
|
|
59
|
+
// A corrupt cache is a miss, never a corrupt published response.
|
|
60
|
+
const input = createReadStream(cached), unzip = createGunzip();
|
|
61
|
+
const verified = digest(unzip, original.size);
|
|
62
|
+
const pumping = pipeline(input,unzip);
|
|
63
|
+
const [actual] = await Promise.all([verified,pumping]);
|
|
64
|
+
if (actual === key) { await copyFile(cached,sidecar); reused = true; }
|
|
65
|
+
}
|
|
66
|
+
} catch (error) { if (!['ENOENT','ENOTDIR','Z_DATA_ERROR','Z_BUF_ERROR','CACHE_SIZE'].includes(error.code)) throw error; }
|
|
67
|
+
}
|
|
68
|
+
if (!reused) await pipeline(createReadStream(filename), createGzip(), createWriteStream(sidecar, { flags: 'wx' }));
|
|
69
|
+
if ((await stat(sidecar)).size >= original.size) {
|
|
70
|
+
await rm(sidecar);
|
|
71
|
+
return 'skipped';
|
|
72
|
+
}
|
|
73
|
+
// Node cannot set arbitrary nanosecond timestamps. Use the same conversion
|
|
74
|
+
// for both immutable outputs so the server's exact freshness check succeeds.
|
|
75
|
+
const accessed = original.atimeMs / 1000;
|
|
76
|
+
const modified = original.mtimeMs / 1000;
|
|
77
|
+
await utimes(filename, accessed, modified);
|
|
78
|
+
await utimes(sidecar, accessed, modified);
|
|
79
|
+
if (cached && !reused && (await stat(sidecar)).size <= 4*1024*1024) {
|
|
80
|
+
await mkdir(cacheDirectory,{recursive:true});
|
|
81
|
+
const temporary=cached+'.'+randomUUID()+'.tmp';
|
|
82
|
+
try { await copyFile(sidecar,temporary); await rename(temporary,cached); }
|
|
83
|
+
finally { await rm(temporary,{force:true}); }
|
|
84
|
+
await trimCache(cacheDirectory);
|
|
85
|
+
}
|
|
86
|
+
return reused ? 'reused' : 'compressed';
|
|
87
|
+
} catch (error) {
|
|
88
|
+
await rm(sidecar, { force: true });
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Precompress unpublished build assets with bounded buffers and two workers. */
|
|
94
|
+
export async function precompressBuild(stage, manifest, {cacheDirectory} = {}) {
|
|
95
|
+
const stats = {compressed:0,reused:0,skipped:0};
|
|
96
|
+
// Route Handler bodies are opaque bytes. Only consider published, unencoded
|
|
97
|
+
// response artifacts; the size check also rejects incompressible binary data.
|
|
98
|
+
const additional = new Set((manifest?.prerendered || []).filter(record =>
|
|
99
|
+
!record.dataFile && record.file?.endsWith('.body') &&
|
|
100
|
+
!Object.keys(record.headers || {}).some(name => name.toLowerCase() === 'content-encoding')
|
|
101
|
+
).map(record => path.resolve(stage, record.file)));
|
|
102
|
+
async function* candidates() {
|
|
103
|
+
yield* files(path.resolve(stage, 'static'), additional);
|
|
104
|
+
yield* files(path.resolve(stage, 'assets'), additional);
|
|
105
|
+
}
|
|
106
|
+
const iterator = candidates();
|
|
107
|
+
let failure;
|
|
108
|
+
const worker = async () => {
|
|
109
|
+
while (!failure) {
|
|
110
|
+
try {
|
|
111
|
+
const next = await iterator.next();
|
|
112
|
+
if (next.done) return;
|
|
113
|
+
stats[await compress(next.value, cacheDirectory)]++;
|
|
114
|
+
} catch (error) { failure ||= error; }
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
// Wait for both pipelines before reporting failure: the caller can then
|
|
118
|
+
// discard the staging directory without racing another compressor.
|
|
119
|
+
await Promise.all([worker(), worker()]);
|
|
120
|
+
if (failure) throw failure;
|
|
121
|
+
return stats;
|
|
122
|
+
}
|