@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,114 @@
|
|
|
1
|
+
import { build as bundle } from 'esbuild';
|
|
2
|
+
import { mkdir, writeFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { runInNewContext } from 'node:vm';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { createStyleProcessor, stylesheetPattern, moduleStylesheetPattern } from './styles.mjs';
|
|
8
|
+
|
|
9
|
+
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
10
|
+
|
|
11
|
+
async function resolveCss(esbuild, args) {
|
|
12
|
+
const result = await esbuild.resolve(args.path, { resolveDir: args.resolveDir, kind: args.kind, pluginData: { prnextResolvingCss: true } });
|
|
13
|
+
if (result.errors.length) return result;
|
|
14
|
+
// Server packages are normally external, but a direct CSS import still needs compilation.
|
|
15
|
+
if (result.external) {
|
|
16
|
+
try { return { path: await realpath(createRequire(path.join(args.resolveDir, '__prnext_resolve.cjs')).resolve(args.path)) }; }
|
|
17
|
+
catch (error) { return { errors: [{ text: error.message }] }; }
|
|
18
|
+
}
|
|
19
|
+
return { path: await realpath(result.path) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One cached CSS compilation supplies identical class maps to the server and the browser. */
|
|
23
|
+
export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/assets', sassOptions }) {
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
const processor = createStyleProcessor({ projectRoot, sassOptions });
|
|
26
|
+
const hash = file => createHash('sha256').update(path.relative(projectRoot, file).replaceAll(path.sep, '/')).digest('hex').slice(0, 12);
|
|
27
|
+
|
|
28
|
+
async function compile(file) {
|
|
29
|
+
const result = await bundle({
|
|
30
|
+
absWorkingDir: projectRoot,
|
|
31
|
+
stdin: { contents: `import styles from ${JSON.stringify(file)}; export default styles;`, resolveDir: projectRoot, loader: 'js' },
|
|
32
|
+
outfile: path.join(stage, '.cssmodules', hash(file) + '.js'),
|
|
33
|
+
bundle: true,
|
|
34
|
+
write: false,
|
|
35
|
+
format: 'cjs',
|
|
36
|
+
platform: 'browser',
|
|
37
|
+
logLevel: 'silent',
|
|
38
|
+
loader: assetLoaders,
|
|
39
|
+
publicPath: assetBase,
|
|
40
|
+
assetNames: '[name]-[hash]',
|
|
41
|
+
// Preserve these identifiers; the final browser build may minify its JS freely.
|
|
42
|
+
minifyIdentifiers: false,
|
|
43
|
+
plugins: [{
|
|
44
|
+
name: 'prnext-deterministic-css-identifiers',
|
|
45
|
+
setup(esbuild) {
|
|
46
|
+
esbuild.onResolve({ filter: moduleStylesheetPattern }, async args => {
|
|
47
|
+
if (args.pluginData?.prnextResolvingCss || args.path.startsWith('prnext-css:')) return;
|
|
48
|
+
const resolved = await resolveCss(esbuild, args);
|
|
49
|
+
if (resolved.errors) return { errors: resolved.errors };
|
|
50
|
+
const basename = path.basename(resolved.path).replace(/\.module\.(?:css|scss|sass)$/i, '').replace(/[^\w-]/g, '_');
|
|
51
|
+
return { path: `r${hash(resolved.path)}_${basename}.module.css`, namespace: 'prnext-local-css', pluginData: { source: resolved.path } };
|
|
52
|
+
});
|
|
53
|
+
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-local-css' }, async args => {
|
|
54
|
+
const transformed = await processor.process(args.pluginData.source);
|
|
55
|
+
return { contents: transformed.css, loader: 'local-css', resolveDir: path.dirname(args.pluginData.source), watchFiles: transformed.watchFiles };
|
|
56
|
+
});
|
|
57
|
+
esbuild.onLoad({ filter: stylesheetPattern, namespace: 'file' }, async args => {
|
|
58
|
+
const transformed = await processor.process(args.path);
|
|
59
|
+
return { contents: transformed.css, loader: 'css', resolveDir: path.dirname(args.path), watchFiles: transformed.watchFiles };
|
|
60
|
+
});
|
|
61
|
+
esbuild.onResolve({ filter: /^\// }, args => args.kind === 'url-token' ? { path: args.path, external: true } : undefined);
|
|
62
|
+
},
|
|
63
|
+
}],
|
|
64
|
+
});
|
|
65
|
+
const javascript = result.outputFiles.find(output => output.path.endsWith('.js'));
|
|
66
|
+
const stylesheet = result.outputFiles.find(output => output.path.endsWith('.css'));
|
|
67
|
+
if (!javascript || !stylesheet) throw new Error(`Unable to compile CSS Module ${file}.`);
|
|
68
|
+
// This is esbuild's generated export map, whose only inputs are CSS files.
|
|
69
|
+
// The isolated context deliberately has no require, process, network, or filesystem.
|
|
70
|
+
const module = { exports: {} };
|
|
71
|
+
runInNewContext(javascript.text, { module }, { timeout: 1000, filename: 'prnext-css-exports.cjs' });
|
|
72
|
+
const mapping = JSON.stringify({ ...module.exports.default, ...(await processor.process(file)).exports });
|
|
73
|
+
await mkdir(path.join(stage, 'assets'), { recursive: true });
|
|
74
|
+
for (const output of result.outputFiles) {
|
|
75
|
+
if (output === javascript || output === stylesheet) continue;
|
|
76
|
+
await writeFile(path.join(stage, 'assets', path.basename(output.path)), output.contents);
|
|
77
|
+
}
|
|
78
|
+
return { mapping, css: stylesheet.text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function get(file) {
|
|
82
|
+
if (!cache.has(file)) cache.set(file, compile(file));
|
|
83
|
+
return cache.get(file);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
plugin(browser, { emitStyles = () => true } = {}) {
|
|
88
|
+
return {
|
|
89
|
+
name: 'prnext-shared-css-modules',
|
|
90
|
+
setup(esbuild) {
|
|
91
|
+
esbuild.onResolve({ filter: moduleStylesheetPattern }, async args => {
|
|
92
|
+
if (args.pluginData?.prnextResolvingCss || args.path.startsWith('prnext-css:')) return;
|
|
93
|
+
const resolved = await resolveCss(esbuild, args);
|
|
94
|
+
if (resolved.errors) return { errors: resolved.errors };
|
|
95
|
+
return { path: resolved.path, namespace: 'prnext-css-mapping' };
|
|
96
|
+
});
|
|
97
|
+
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-css-mapping' }, async args => {
|
|
98
|
+
const compiled = await get(args.path);
|
|
99
|
+
return { contents: `${browser && emitStyles(args.path) ? `import ${JSON.stringify('prnext-css:' + args.path)};` : ''} export default ${compiled.mapping};`, loader: 'js' };
|
|
100
|
+
});
|
|
101
|
+
if (browser) {
|
|
102
|
+
esbuild.onLoad({ filter: stylesheetPattern, namespace: 'file' }, async args => {
|
|
103
|
+
const transformed = await processor.process(args.path);
|
|
104
|
+
return { contents: transformed.css, loader: 'css', resolveDir: path.dirname(args.path), watchFiles: transformed.watchFiles };
|
|
105
|
+
});
|
|
106
|
+
esbuild.onResolve({ filter: /^prnext-css:/ }, args => ({ path: args.path.slice('prnext-css:'.length), namespace: 'prnext-compiled-css' }));
|
|
107
|
+
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-compiled-css' }, async args => ({ contents: (await get(args.path)).css, loader: 'css', resolveDir: path.dirname(args.path) }));
|
|
108
|
+
esbuild.onResolve({ filter: /^\// }, args => args.kind === 'url-token' ? { path: args.path, external: true } : undefined);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { parse, tokensToRegexp } from 'path-to-regexp';
|
|
2
|
+
import { withLocale } from '../compat/locale.cjs';
|
|
3
|
+
|
|
4
|
+
const MAX_ROUTES = 1000;
|
|
5
|
+
const MAX_STRING = 4096;
|
|
6
|
+
const MAX_CONDITIONS = 16;
|
|
7
|
+
const MAX_PARAMS = 64;
|
|
8
|
+
const MAX_HEADERS = 64;
|
|
9
|
+
const MAX_MANIFEST_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
11
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
12
|
+
const STATUS = new Set([301, 302, 303, 307, 308]);
|
|
13
|
+
|
|
14
|
+
function fail(where, message) { throw new Error(`Invalid custom route ${where}: ${message}`); }
|
|
15
|
+
function record(value, where) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
17
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail(where, 'expected an object');
|
|
18
|
+
}
|
|
19
|
+
function keys(value, allowed, where) {
|
|
20
|
+
const unknown = Object.keys(value).filter(key => !allowed.includes(key));
|
|
21
|
+
if (unknown.length) fail(where, `unsupported fields: ${unknown.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
function string(value, where, { empty = false, max = MAX_STRING, header = false } = {}) {
|
|
24
|
+
if (typeof value !== 'string' || (!empty && !value.length) || Buffer.byteLength(value) > max) {
|
|
25
|
+
fail(where, `expected ${empty ? 'a' : 'a nonempty'} string of at most ${max} bytes`);
|
|
26
|
+
}
|
|
27
|
+
if ((header ? /[\u0000-\u0008\u000a-\u001f\u007f]/ : CONTROL).test(value)) fail(where, 'control characters are forbidden');
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function parseTokens(value, where, delimiter = '/') {
|
|
31
|
+
try { return parse(value, { delimiter }); }
|
|
32
|
+
catch (error) { fail(where, `invalid path pattern (${error.message})`); }
|
|
33
|
+
}
|
|
34
|
+
function addParam(params, name, where, info = {}) {
|
|
35
|
+
if (params.has(name)) fail(where, `duplicate parameter ${JSON.stringify(name)}`);
|
|
36
|
+
if (params.size >= MAX_PARAMS) fail(where, `at most ${MAX_PARAMS} parameters are supported`);
|
|
37
|
+
params.set(name, info);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Keep these checks aligned with custom_routes.rs::js_regex. JavaScript accepts
|
|
41
|
+
// identity escapes which have different meanings in Rust, and the native
|
|
42
|
+
// matcher deliberately limits insensitive character classes to ASCII rather
|
|
43
|
+
// than silently applying Rust's broader Unicode case folding.
|
|
44
|
+
function validateNativeRegex(pattern, insensitive, where) {
|
|
45
|
+
let inClass = false;
|
|
46
|
+
let classText = '';
|
|
47
|
+
const escapedLetters = new Set('dDwWsSbBnrtvfxuk');
|
|
48
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
49
|
+
let value = pattern[index];
|
|
50
|
+
if (value === '\\') {
|
|
51
|
+
const kind = pattern[++index];
|
|
52
|
+
if (kind === undefined) fail(where, 'trailing regular-expression escape');
|
|
53
|
+
if (/[A-Za-z]/.test(kind) && !escapedLetters.has(kind)) {
|
|
54
|
+
fail(where, `unsupported ECMAScript escape \\${kind} in the native custom-route matcher`);
|
|
55
|
+
}
|
|
56
|
+
if (kind === 'x' || kind === 'u') {
|
|
57
|
+
const length = kind === 'x' ? 2 : 4;
|
|
58
|
+
const digits = pattern.slice(index + 1, index + length + 1);
|
|
59
|
+
if (digits.length !== length || !/^[0-9A-Fa-f]+$/.test(digits)) fail(where, 'unsupported ECMAScript hex/Unicode escape; use \\xHH or \\uHHHH');
|
|
60
|
+
const code = Number.parseInt(digits, 16);
|
|
61
|
+
if (code >= 0xd800 && code <= 0xdfff) fail(where, 'UTF-16 surrogate regex escapes are unsupported by the native matcher');
|
|
62
|
+
value = String.fromCharCode(code);
|
|
63
|
+
index += length;
|
|
64
|
+
} else value = `\\${kind}`;
|
|
65
|
+
if (inClass) classText += value;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!inClass && value === '[') { inClass = true; classText = ''; }
|
|
69
|
+
else if (inClass && value === ']') {
|
|
70
|
+
if (insensitive && (/[^\x00-\x7f]/.test(classText) || classText.includes('\\u'))) {
|
|
71
|
+
fail(where, 'case-insensitive Unicode character classes are unsupported; use Unicode literals or a case-sensitive has/missing condition');
|
|
72
|
+
}
|
|
73
|
+
inClass = false;
|
|
74
|
+
} else if (inClass) classText += value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compileSource(source, params, where) {
|
|
79
|
+
string(source, `${where}.source`);
|
|
80
|
+
if (!source.startsWith('/') || source.startsWith('//')) fail(where, 'source must start with a single /');
|
|
81
|
+
const tokens = parseTokens(source, `${where}.source`);
|
|
82
|
+
const captures = [];
|
|
83
|
+
let regex;
|
|
84
|
+
try {
|
|
85
|
+
regex = tokensToRegexp(tokens, captures, { strict: true, sensitive: false, delimiter: '/' }).source;
|
|
86
|
+
} catch (error) { fail(where, `invalid source (${error.message})`); }
|
|
87
|
+
// Match Next's optional final slash without allowing query/hash delimiters.
|
|
88
|
+
regex = regex.replace(/\$$/, '(?:/)?$').replace(/\\\//g, '/');
|
|
89
|
+
if (Buffer.byteLength(regex) > MAX_STRING) fail(where, 'compiled source exceeds 4096 bytes');
|
|
90
|
+
validateNativeRegex(regex, true, `${where}.source`);
|
|
91
|
+
const compiledKeys = captures.map(token => {
|
|
92
|
+
const repeat = token.modifier === '*' || token.modifier === '+';
|
|
93
|
+
const name = typeof token.name === 'number' ? null : token.name;
|
|
94
|
+
if (name !== null) addParam(params, name, where, { repeat, optional: token.modifier === '?' || token.modifier === '*' });
|
|
95
|
+
return { name, repeat, separator: `${token.prefix}${token.suffix}` };
|
|
96
|
+
});
|
|
97
|
+
if (captures.length > MAX_PARAMS) fail(where, `at most ${MAX_PARAMS} captures are supported`);
|
|
98
|
+
return { source, regex, keys: compiledKeys };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function regexCaptures(pattern, where) {
|
|
102
|
+
try { new RegExp(pattern); }
|
|
103
|
+
catch (error) { fail(where, `invalid condition regex (${error.message})`); }
|
|
104
|
+
const captures = [];
|
|
105
|
+
let count = 0;
|
|
106
|
+
let inClass = false;
|
|
107
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
108
|
+
const char = pattern[index];
|
|
109
|
+
if (char === '\\') { index++; continue; }
|
|
110
|
+
if (char === '[') inClass = true;
|
|
111
|
+
else if (char === ']') inClass = false;
|
|
112
|
+
else if (char === '(' && !inClass) {
|
|
113
|
+
if (pattern[index + 1] !== '?') count++;
|
|
114
|
+
else if (pattern[index + 2] === '<' && !['=', '!'].includes(pattern[index + 3])) {
|
|
115
|
+
const end = pattern.indexOf('>', index + 3);
|
|
116
|
+
const name = pattern.slice(index + 3, end);
|
|
117
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) fail(where, 'named captures must use ASCII identifier names');
|
|
118
|
+
captures.push({ name, index: ++count });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (count > MAX_PARAMS) fail(where, `at most ${MAX_PARAMS} regex captures are supported`);
|
|
123
|
+
return captures;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function conditions(value, params, where, expose) {
|
|
127
|
+
if (value === undefined) return [];
|
|
128
|
+
if (!Array.isArray(value) || value.length > MAX_CONDITIONS) fail(where, `expected at most ${MAX_CONDITIONS} conditions`);
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
return value.map((item, index) => {
|
|
131
|
+
const location = `${where}[${index}]`;
|
|
132
|
+
record(item, location); keys(item, ['type', 'key', 'value'], location);
|
|
133
|
+
if (!['header', 'cookie', 'query', 'host'].includes(item.type)) fail(location, 'unknown condition type');
|
|
134
|
+
const result = { type: item.type, captures: [] };
|
|
135
|
+
if (item.type === 'host') {
|
|
136
|
+
if (item.key !== undefined) fail(location, 'host conditions do not accept key');
|
|
137
|
+
if (!item.value) fail(location, 'host conditions require a nonempty value');
|
|
138
|
+
} else {
|
|
139
|
+
string(item.key, `${location}.key`, { max: 256 });
|
|
140
|
+
if ((item.type === 'header' || item.type === 'cookie') && !HEADER_NAME.test(item.key)) fail(location, 'invalid header/cookie key');
|
|
141
|
+
result.key = item.type === 'header' ? item.key.toLowerCase() : item.key;
|
|
142
|
+
}
|
|
143
|
+
if (item.value !== undefined) {
|
|
144
|
+
string(item.value, `${location}.value`, { empty: true, max: 1024 });
|
|
145
|
+
if (item.value) {
|
|
146
|
+
// A noncapturing wrapper keeps alternations anchored as a whole.
|
|
147
|
+
result.regex = `^(?:${item.value})$`;
|
|
148
|
+
result.captures = regexCaptures(result.regex, location);
|
|
149
|
+
validateNativeRegex(result.regex, false, `${location}.value`);
|
|
150
|
+
if (item.type === 'host' && !result.captures.length) result.capture = 'host';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!result.regex && result.key) {
|
|
154
|
+
const name = result.key.replace(/[^A-Za-z]/g, '');
|
|
155
|
+
if (name) result.capture = name;
|
|
156
|
+
}
|
|
157
|
+
const identity = JSON.stringify(result);
|
|
158
|
+
if (seen.has(identity)) fail(where, 'duplicate condition');
|
|
159
|
+
seen.add(identity);
|
|
160
|
+
if (expose) {
|
|
161
|
+
for (const capture of result.captures) addParam(params, capture.name, location);
|
|
162
|
+
if (result.capture) addParam(params, result.capture, location);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function template(value, params, where, { path = false, delimiter = '/', strict = false } = {}) {
|
|
169
|
+
const tokens = path ? parseTokens(value, where, delimiter) : nonPathTokens(value, params, strict, where);
|
|
170
|
+
return tokens.map(token => {
|
|
171
|
+
if (typeof token === 'string') return token;
|
|
172
|
+
if (typeof token.name === 'number' || !params.has(token.name)) fail(where, `unknown destination parameter ${JSON.stringify(token.name)}`);
|
|
173
|
+
if (params.get(token.name).repeat && !['*', '+'].includes(token.modifier)) {
|
|
174
|
+
fail(where, `repeated parameter :${token.name} requires * or + in the destination`);
|
|
175
|
+
}
|
|
176
|
+
return { param: token.name, prefix: token.prefix, suffix: token.suffix, modifier: token.modifier,
|
|
177
|
+
...(token.join !== undefined ? { join: token.join } : {}) };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nonPathTokens(value, params, strict, where) {
|
|
182
|
+
const result = [];
|
|
183
|
+
let text = '';
|
|
184
|
+
const flush = () => { if (text) result.push(text); text = ''; };
|
|
185
|
+
for (let index = 0; index < value.length;) {
|
|
186
|
+
if (value[index] === '\\' && value[index + 1] === ':') { text += ':'; index += 2; continue; }
|
|
187
|
+
const match = value[index] === ':' && /^:([A-Za-z0-9_]+)([?*+]?)/.exec(value.slice(index));
|
|
188
|
+
if (!match) { text += value[index++]; continue; }
|
|
189
|
+
if (!params.has(match[1])) {
|
|
190
|
+
// Non-path strings routinely contain literal colons (CSP, URLs, times).
|
|
191
|
+
// Reject unresolved parameter-like query placeholders, but retain URI
|
|
192
|
+
// schemes and numeric ports rather than guessing they are parameters.
|
|
193
|
+
if (strict && /^[A-Za-z_]/.test(match[1])) fail(where, `unknown destination parameter ${JSON.stringify(match[1])}`);
|
|
194
|
+
text += match[0]; index += match[0].length; continue;
|
|
195
|
+
}
|
|
196
|
+
flush();
|
|
197
|
+
// Next compiles non-path values with an artificial leading slash, then
|
|
198
|
+
// removes it. Arrays at the beginning consequently join with '/', whereas
|
|
199
|
+
// embedded arrays use the preceding path-to-regexp prefix ('.' or '/').
|
|
200
|
+
const join = index === 0 || value[index - 1] === '/' ? '/' : value[index - 1] === '.' ? '.' : '';
|
|
201
|
+
result.push({ name: match[1], prefix: '', suffix: '', modifier: match[2],
|
|
202
|
+
...(['*', '+'].includes(match[2]) ? { join } : {}) });
|
|
203
|
+
index += match[0].length;
|
|
204
|
+
}
|
|
205
|
+
flush(); return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function splitDestination(value, where) {
|
|
209
|
+
let rest = value;
|
|
210
|
+
const hashIndex = rest.indexOf('#');
|
|
211
|
+
const hash = hashIndex === -1 ? '' : rest.slice(hashIndex + 1);
|
|
212
|
+
if (hashIndex !== -1) rest = rest.slice(0, hashIndex);
|
|
213
|
+
const queryIndex = rest.indexOf('?');
|
|
214
|
+
const search = queryIndex === -1 ? '' : rest.slice(queryIndex + 1);
|
|
215
|
+
if (queryIndex !== -1) rest = rest.slice(0, queryIndex);
|
|
216
|
+
let protocol, hostname, port;
|
|
217
|
+
const external = /^https?:\/\//i.test(rest);
|
|
218
|
+
if (external) {
|
|
219
|
+
const match = /^(https?):\/\/([^/]+)(\/.*)?$/i.exec(rest);
|
|
220
|
+
if (!match) fail(where, 'invalid external destination');
|
|
221
|
+
protocol = match[1].toLowerCase();
|
|
222
|
+
let authority = match[2];
|
|
223
|
+
if (authority.includes('@') || authority.includes('\\')) fail(where, 'external credentials and backslashes are unsupported');
|
|
224
|
+
if (authority.startsWith('[')) {
|
|
225
|
+
const ipv6 = /^(\[[0-9a-fA-F:.]+\])(?::(\d+))?$/.exec(authority);
|
|
226
|
+
if (!ipv6) fail(where, 'invalid IPv6 destination');
|
|
227
|
+
hostname = ipv6[1]; port = ipv6[2];
|
|
228
|
+
} else {
|
|
229
|
+
const numericPort = /:(\d+)$/.exec(authority);
|
|
230
|
+
if (numericPort) { port = numericPort[1]; authority = authority.slice(0, -numericPort[0].length); }
|
|
231
|
+
hostname = authority;
|
|
232
|
+
if (!hostname || /[\s%]/.test(hostname)) fail(where, 'invalid external hostname');
|
|
233
|
+
}
|
|
234
|
+
if (port && (+port > 65535 || +port === 0)) fail(where, 'invalid destination port');
|
|
235
|
+
rest = match[3] || '/';
|
|
236
|
+
} else if (!rest.startsWith('/') || rest.startsWith('//')) {
|
|
237
|
+
fail(where, 'destination must start with /, http:// or https://');
|
|
238
|
+
}
|
|
239
|
+
if (rest.includes('\\') && /\\(?![:*+?(){}\\])/.test(rest)) fail(where, 'invalid destination escape');
|
|
240
|
+
return { external, protocol, hostname, port, pathname: rest, search, hash };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function destination(value, params, where, rewrite) {
|
|
244
|
+
string(value, where);
|
|
245
|
+
const pieces = splitDestination(value, where);
|
|
246
|
+
const pathname = template(pieces.pathname, params, `${where}.pathname`, { path: true });
|
|
247
|
+
const hash = template(pieces.hash, params, `${where}.hash`, { path: true });
|
|
248
|
+
const result = { external: pieces.external, pathname, query: [], hash };
|
|
249
|
+
if (pieces.external) {
|
|
250
|
+
result.protocol = pieces.protocol;
|
|
251
|
+
result.hostname = pieces.hostname.startsWith('[') ? [pieces.hostname] : template(pieces.hostname, params, `${where}.hostname`, { path: true, delimiter: '.' });
|
|
252
|
+
if (pieces.port) result.port = pieces.port;
|
|
253
|
+
}
|
|
254
|
+
const query = new URLSearchParams(pieces.search);
|
|
255
|
+
if ([...query].length > 128) fail(where, 'at most 128 destination query entries are supported');
|
|
256
|
+
for (const [key, value] of query) {
|
|
257
|
+
string(key, `${where}.query key`, { empty: true }); string(value, `${where}.query value`, { empty: true });
|
|
258
|
+
result.query.push({ key, value: template(value, params, `${where}.query`, { strict: true }) });
|
|
259
|
+
}
|
|
260
|
+
result.appendParamsToQuery = rewrite && ![...pathname, ...hash, ...(result.hostname || [])].some(token => typeof token !== 'string');
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function matcher(route, params, where, expose = true) {
|
|
265
|
+
const result = compileSource(route.source, params, where);
|
|
266
|
+
result.has = conditions(route.has, params, `${where}.has`, expose);
|
|
267
|
+
result.missing = conditions(route.missing, params, `${where}.missing`, false);
|
|
268
|
+
const existing = new Set(result.has.map(item => JSON.stringify(item)));
|
|
269
|
+
if (result.missing.some(item => existing.has(JSON.stringify(item)))) fail(where, 'the same condition cannot be required and missing');
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// A deployment pathname is literal, not additional path-to-regexp syntax.
|
|
274
|
+
function mountedPattern(source, basePath) {
|
|
275
|
+
if (!basePath) return source;
|
|
276
|
+
const prefix = basePath.replace(/[\\:*+?(){}[\]]/g, '\\$&');
|
|
277
|
+
return prefix + (source === '/' ? '' : source);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Middleware only needs a predicate; its captures are not destination params. */
|
|
281
|
+
export function compileRouteMatcher(input, where = 'middleware.matcher', { basePath = '' } = {}) {
|
|
282
|
+
const route = typeof input === 'string' ? { source: input } : input;
|
|
283
|
+
record(route, where);
|
|
284
|
+
keys(route, ['source', 'has', 'missing', 'locale'], where);
|
|
285
|
+
if (route.locale !== undefined && route.locale !== false) fail(where, 'locale only accepts false while that feature is unsupported');
|
|
286
|
+
// Validate the original source before adding a mount prefix, otherwise an
|
|
287
|
+
// invalid relative source could accidentally become an absolute pattern.
|
|
288
|
+
string(route.source, `${where}.source`);
|
|
289
|
+
if (!route.source.startsWith('/') || route.source.startsWith('//')) fail(where, 'source must start with a single /');
|
|
290
|
+
return matcher({ ...route, source: mountedPattern(route.source, basePath) }, new Map(), where, false);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function compileRoute(route, type, where, basePath = '') {
|
|
294
|
+
record(route, where);
|
|
295
|
+
keys(route, ['source', 'has', 'missing', 'basePath', 'locale', ...(type === 'header' ? ['headers'] : type === 'redirect' ? ['destination', 'permanent', 'statusCode'] : ['destination'])], where);
|
|
296
|
+
for (const option of ['basePath', 'locale']) if (route[option] !== undefined && route[option] !== false) fail(where, `${option} only accepts false`);
|
|
297
|
+
string(route.source, `${where}.source`);
|
|
298
|
+
if (!route.source.startsWith('/') || route.source.startsWith('//')) fail(where, 'source must start with a single /');
|
|
299
|
+
if (type === 'rewrite' && route.basePath === false && !/^https?:\/\//i.test(route.destination)) fail(where, 'basePath:false rewrites require an external HTTP(S) destination');
|
|
300
|
+
if (basePath && route.basePath !== false) {
|
|
301
|
+
route = { ...route, source: mountedPattern(route.source, basePath),
|
|
302
|
+
...(typeof route.destination === 'string' && route.destination.startsWith('/') && !route.destination.startsWith('//')
|
|
303
|
+
? { destination: mountedPattern(route.destination, basePath) } : {}) };
|
|
304
|
+
}
|
|
305
|
+
const params = new Map();
|
|
306
|
+
const result = matcher(route, params, where);
|
|
307
|
+
if (type === 'header') {
|
|
308
|
+
if (!Array.isArray(route.headers) || !route.headers.length || route.headers.length > MAX_HEADERS) fail(where, `headers must contain 1–${MAX_HEADERS} entries`);
|
|
309
|
+
result.headers = route.headers.map((header, index) => {
|
|
310
|
+
const location = `${where}.headers[${index}]`;
|
|
311
|
+
record(header, location); keys(header, ['key', 'value'], location);
|
|
312
|
+
string(header.key, `${location}.key`, { max: 256 });
|
|
313
|
+
string(header.value, `${location}.value`, { empty: true, header: true });
|
|
314
|
+
const key = template(header.key, params, `${location}.key`);
|
|
315
|
+
if (!HEADER_NAME.test(key.map(token => typeof token === 'string' ? token : 'param').join(''))) fail(location, 'invalid header name');
|
|
316
|
+
return { key, value: template(header.value, params, `${location}.value`) };
|
|
317
|
+
});
|
|
318
|
+
// Duplicate response header names intentionally retain source order: the
|
|
319
|
+
// later matching header wins, just as Next custom headers do.
|
|
320
|
+
} else {
|
|
321
|
+
result.destination = destination(route.destination, params, `${where}.destination`, type === 'rewrite');
|
|
322
|
+
if (type === 'redirect') {
|
|
323
|
+
if (route.permanent !== undefined && route.statusCode !== undefined) fail(where, 'permanent and statusCode are mutually exclusive');
|
|
324
|
+
if (route.statusCode !== undefined) {
|
|
325
|
+
if (!STATUS.has(route.statusCode)) fail(where, 'statusCode must be 301, 302, 303, 307 or 308');
|
|
326
|
+
result.statusCode = route.statusCode;
|
|
327
|
+
} else {
|
|
328
|
+
if (typeof route.permanent !== 'boolean') fail(where, 'redirect requires permanent or statusCode');
|
|
329
|
+
result.statusCode = route.permanent ? 308 : 307;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Compile Next custom route callbacks into the bounded native manifest. */
|
|
337
|
+
export async function compileCustomRoutes(config = {}) {
|
|
338
|
+
const loaded = await Promise.all(['headers', 'redirects', 'rewrites'].map(async name => {
|
|
339
|
+
if (config[name] === undefined) return [];
|
|
340
|
+
if (typeof config[name] !== 'function') fail(name, 'configuration must be a function');
|
|
341
|
+
return await config[name]();
|
|
342
|
+
}));
|
|
343
|
+
let total = 0;
|
|
344
|
+
const compileList = (value, type, where) => {
|
|
345
|
+
if (!Array.isArray(value)) fail(where, 'callback must return an array');
|
|
346
|
+
if (config.i18n) value = value.flatMap(route => route?.locale === false ? [route] : [...config.i18n.locales.filter(locale => locale !== config.i18n.defaultLocale), config.i18n.defaultLocale].map(locale => ({
|
|
347
|
+
...route, source: withLocale(route.source, locale, config.i18n.defaultLocale),
|
|
348
|
+
...(typeof route.destination === 'string' && route.destination.startsWith('/') && !route.destination.startsWith('//') ? {destination:withLocale(route.destination,locale,config.i18n.defaultLocale)} : {}),
|
|
349
|
+
})));
|
|
350
|
+
total += value.length;
|
|
351
|
+
if (total > MAX_ROUTES) fail(where, `at most ${MAX_ROUTES} custom routes are supported`);
|
|
352
|
+
return value.map((route, index) => compileRoute(route, type, `${where}[${index}]`, config.basePath || ''));
|
|
353
|
+
};
|
|
354
|
+
const headers = compileList(loaded[0], 'header', 'headers');
|
|
355
|
+
const redirects = compileList(loaded[1], 'redirect', 'redirects');
|
|
356
|
+
let rawRewrites = loaded[2];
|
|
357
|
+
if (Array.isArray(rawRewrites)) rawRewrites = { afterFiles: rawRewrites };
|
|
358
|
+
record(rawRewrites, 'rewrites'); keys(rawRewrites, ['beforeFiles', 'afterFiles', 'fallback'], 'rewrites');
|
|
359
|
+
const rewrites = Object.fromEntries(['beforeFiles', 'afterFiles', 'fallback'].map(phase => [phase, compileList(rawRewrites[phase] === undefined ? [] : rawRewrites[phase], 'rewrite', `rewrites.${phase}`)]));
|
|
360
|
+
const result = { version: 1, headers, redirects, rewrites };
|
|
361
|
+
if (Buffer.byteLength(JSON.stringify(result)) > MAX_MANIFEST_BYTES) fail('manifest', 'compiled routes exceed 2 MiB');
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {build} from './index.mjs';
|
|
2
|
+
import {closeIncrementalCompiler} from './incremental.mjs';
|
|
3
|
+
import {realpath} from 'node:fs/promises';
|
|
4
|
+
import {createDevInputSnapshot} from '../runtime/dev-file-changes.mjs';
|
|
5
|
+
let builds=0,busy=false;
|
|
6
|
+
process.on('message',async message=>{
|
|
7
|
+
if(message?.type!=='build'||busy)return;
|
|
8
|
+
busy=true;
|
|
9
|
+
try{
|
|
10
|
+
const started=performance.now();
|
|
11
|
+
const inputs=createDevInputSnapshot([message.root,await realpath(message.root)]);
|
|
12
|
+
await build(message.root,{dev:true,incremental:true,devInputs:inputs});
|
|
13
|
+
console.log(`PRNext built ${message.root} in ${((performance.now()-started)/1000).toFixed(2)}s`);
|
|
14
|
+
process.send({type:'result',ok:true,inputs:inputs.entries(),recycle:++builds>=24 || process.memoryUsage().rss>512*1024*1024});
|
|
15
|
+
}catch(error){process.send({type:'result',ok:false,error:error.stack||error.message,recycle:true});}
|
|
16
|
+
finally{busy=false;}
|
|
17
|
+
});
|
|
18
|
+
process.on('disconnect',async()=>{await closeIncrementalCompiler();process.exit();});
|
package/build/dev.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { transformAsync } from '@babel/core';
|
|
2
|
+
import refresh from 'react-refresh/babel';
|
|
3
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { cachedAsyncTransform } from './transform-cache.mjs';
|
|
8
|
+
import {inlineSourceMap} from './source-maps.mjs';
|
|
9
|
+
|
|
10
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
11
|
+
export const devClientFile = path.join(packageRoot, 'runtime/dev-client.mjs');
|
|
12
|
+
|
|
13
|
+
/** Keep React renderers and framework contexts identical across rebuilt ESM chunks. */
|
|
14
|
+
export function devSingletonsPlugin() {
|
|
15
|
+
return { name: 'prnext-development-singletons', setup(build) {
|
|
16
|
+
build.onLoad({ filter: /\.[cm]?js$/, namespace: 'file' }, async args => {
|
|
17
|
+
const filename = args.path.replaceAll(path.sep, '/');
|
|
18
|
+
if (!/\/node_modules\/(?:react|react-dom|react-refresh|react-server-dom-webpack)\//.test(filename) && !(args.path.startsWith(path.join(packageRoot, 'compat') + path.sep) && args.path.endsWith('.cjs'))) return;
|
|
19
|
+
const source = await readFile(args.path, 'utf8');
|
|
20
|
+
// These packages and adapters expose CommonJS entry points. ESM files are
|
|
21
|
+
// left alone; wrapping their declarations would change module semantics.
|
|
22
|
+
if (!/\b(?:module\.exports|exports\.)/.test(source)) return;
|
|
23
|
+
return { contents: `const __prnextModules=globalThis.__PRNEXT_DEV_MODULES__||=new Map();const __prnextKey=${JSON.stringify(filename)};if(__prnextModules.has(__prnextKey)){module.exports=__prnextModules.get(__prnextKey)}else{\n${source}\n__prnextModules.set(__prnextKey,module.exports);}`, loader: 'js', resolveDir: path.dirname(args.path) };
|
|
24
|
+
});
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function componentExports(program) {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const statement of program.body) {
|
|
31
|
+
if (statement.type === 'ExportAllDeclaration') return false;
|
|
32
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
33
|
+
count++;
|
|
34
|
+
const value = statement.declaration;
|
|
35
|
+
const name = value.id?.name || (value.type === 'Identifier' ? value.name : '');
|
|
36
|
+
if (!/^[A-Z]/.test(name)) return false;
|
|
37
|
+
}
|
|
38
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.exportKind === 'type') continue;
|
|
39
|
+
if (statement.source) return false;
|
|
40
|
+
const declaration = statement.declaration;
|
|
41
|
+
const names = declaration?.type === 'VariableDeclaration' ? declaration.declarations.map(item => item.id.name)
|
|
42
|
+
: declaration?.id ? [declaration.id.name] : statement.specifiers.filter(item => item.exportKind !== 'type').map(item => item.local?.name);
|
|
43
|
+
for (const name of names) { count++; if (!name || !/^[A-Z]/.test(name)) return false; }
|
|
44
|
+
}
|
|
45
|
+
return count > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The official React transform records hook signatures and refresh families. */
|
|
49
|
+
export function createRefreshTransform(projectRoot) {
|
|
50
|
+
projectRoot = path.resolve(projectRoot);
|
|
51
|
+
const canonicalRoot = realpath(projectRoot).catch(() => projectRoot);
|
|
52
|
+
return async (source, filename) => {
|
|
53
|
+
if (filename.includes(`${path.sep}node_modules${path.sep}`) || filename.includes(`${path.sep}.prnext-build-`)) return source;
|
|
54
|
+
// Bundlers resolve symlinks (including /var -> /private/var on macOS).
|
|
55
|
+
// Compare against both spellings so temporary/linked apps still refresh.
|
|
56
|
+
const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
|
|
57
|
+
if (!filename.startsWith(root + path.sep)) return source;
|
|
58
|
+
const hash = createHash('sha256').update(source).digest('hex');
|
|
59
|
+
return cachedAsyncTransform('fast-refresh',source,[projectRoot,filename],async () => {
|
|
60
|
+
const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
|
|
61
|
+
ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
|
|
62
|
+
parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
63
|
+
plugins: [[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }]],
|
|
64
|
+
});
|
|
65
|
+
const id = path.relative(root, filename).replaceAll(path.sep, '/');
|
|
66
|
+
// An ESM import changes webpack's interpretation of module.exports.
|
|
67
|
+
// Keep script/CommonJS inputs in their original module format.
|
|
68
|
+
const runtime = result.ast.program.sourceType === 'script'
|
|
69
|
+
? `const{register:__prnextRegister,signature:__prnextRefreshSig,registerModule:__prnextRegisterModule}=require(${JSON.stringify(devClientFile)});`
|
|
70
|
+
: `import{register as __prnextRegister,signature as __prnextRefreshSig,registerModule as __prnextRegisterModule}from ${JSON.stringify(devClientFile)};`;
|
|
71
|
+
const strict = result.ast.program.directives.some(item => item.value.value === 'use strict') ? '"use strict";' : '';
|
|
72
|
+
const prefix = `${strict}${runtime}function __prnextRefreshReg(type,id){__prnextRegister(type,${JSON.stringify(id + ':')}+id)};\n`;
|
|
73
|
+
return inlineSourceMap(prefix + result.code + `\n__prnextRegisterModule(${JSON.stringify(id)},${JSON.stringify(hash)},${componentExports(result.ast.program)});\n`,{...result.map,mappings:';'+result.map.mappings});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {readdir} from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
const entries=new Map();
|
|
4
|
+
let bytes=0,hits=0,misses=0;
|
|
5
|
+
const limit=1024*1024,countLimit=256;
|
|
6
|
+
export const directoryCacheStats=()=>({entries:entries.size,bytes,hits,misses});
|
|
7
|
+
export function clearDirectoryCache(){entries.clear();bytes=0;}
|
|
8
|
+
|
|
9
|
+
/** Validate names on every scan: Linux directory timestamps and sizes may be
|
|
10
|
+
* identical across rapid additions/renames. A stat-only cache can miss routes.
|
|
11
|
+
* Keep bounded snapshots, with one readdir instead of two stat calls on misses.
|
|
12
|
+
* File contents remain handled separately by the source/configuration caches. */
|
|
13
|
+
export async function readDirectory(directory){
|
|
14
|
+
const files=await readdir(directory,{withFileTypes:true});
|
|
15
|
+
const value=JSON.stringify(files.map(item=>[item.name,item.isDirectory()?'d':item.isFile()?'f':'o']));
|
|
16
|
+
let entry=entries.get(directory);
|
|
17
|
+
if(entry?.value===value){hits++;entries.delete(directory);entries.set(directory,entry);return decode(entry.value);}
|
|
18
|
+
misses++;
|
|
19
|
+
if(entry){entries.delete(directory);bytes-=entry.bytes;}
|
|
20
|
+
const size=2*(directory.length+value.length);
|
|
21
|
+
if(size<=64*1024){
|
|
22
|
+
while(entries.size && (entries.size>=countLimit || bytes+size>limit)){
|
|
23
|
+
const [key,old]=entries.entries().next().value;entries.delete(key);bytes-=old.bytes;
|
|
24
|
+
}
|
|
25
|
+
entries.set(directory,{value,bytes:size});bytes+=size;
|
|
26
|
+
}
|
|
27
|
+
return decode(value);
|
|
28
|
+
}
|
|
29
|
+
function decode(value){return JSON.parse(value).map(([name,kind])=>({name,isDirectory:()=>kind==='d',isFile:()=>kind==='f'}));}
|