@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,54 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {frameworkImportName} from './framework-imports.mjs';
|
|
5
|
+
|
|
6
|
+
export function validateCacheHandler(input) {
|
|
7
|
+
if (input === undefined) return undefined;
|
|
8
|
+
if (typeof input !== 'string' || !input || input.includes('\0') || Buffer.byteLength(input) > 4096) throw new TypeError('cacheHandler must be a module path');
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function prepareCacheHandler(options) {
|
|
13
|
+
if (!options.config.cacheHandler) return undefined;
|
|
14
|
+
const handlers = await prepareCacheHandlers({ ...options, config: { cacheHandlers: { __legacy: options.config.cacheHandler } } });
|
|
15
|
+
return handlers.__legacy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateCacheHandlers(input) {
|
|
19
|
+
if (input === undefined) return {};
|
|
20
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('cacheHandlers must be an object of module paths');
|
|
21
|
+
if (Object.keys(input).length > 16) throw new TypeError('cacheHandlers accepts at most 16 handlers');
|
|
22
|
+
const result = Object.create(null);
|
|
23
|
+
for (const [name, value] of Object.entries(input)) {
|
|
24
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(name) || ['private', '__proto__', 'constructor', 'prototype'].includes(name)) throw new TypeError(`Invalid cache handler name: ${name}`);
|
|
25
|
+
if (value === undefined) continue;
|
|
26
|
+
if (typeof value !== 'string' || !value || value.includes('\0') || Buffer.byteLength(value) > 4096) throw new TypeError(`cacheHandlers.${name} must be a module path`);
|
|
27
|
+
result[name] = value;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Private server entries travel with the build; no handler code is sent to browsers. */
|
|
33
|
+
export async function prepareCacheHandlers({ config, projectRoot, stage, environment = {} }) {
|
|
34
|
+
const result = {};
|
|
35
|
+
const compiled = new Map();
|
|
36
|
+
const require = createRequire(path.join(projectRoot, 'package.json'));
|
|
37
|
+
for (const [name, source] of Object.entries(config.cacheHandlers || {})) {
|
|
38
|
+
let entry;
|
|
39
|
+
try { entry = require.resolve(source.startsWith('.') || path.isAbsolute(source) ? path.resolve(projectRoot, source) : source); }
|
|
40
|
+
catch (error) { throw new Error(`Cannot resolve cacheHandlers.${name}: ${source}`, { cause: error }); }
|
|
41
|
+
if (compiled.has(entry)) { result[name] = compiled.get(entry); continue; }
|
|
42
|
+
const output = `server/cache-handler-${name}.mjs`;
|
|
43
|
+
await build({ absWorkingDir: projectRoot, entryPoints: [entry], outfile: path.join(stage, output), bundle: true,
|
|
44
|
+
platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent', define: environment,
|
|
45
|
+
banner: { js: "import {createRequire as __prnextCreateRequire} from 'node:module';const require=__prnextCreateRequire(import.meta.url);" },
|
|
46
|
+
plugins: [{ name: 'cache-handler-compatibility', setup(builder) {
|
|
47
|
+
builder.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/(cache|headers|server)(?:\.js)?$/ }, ({ path: specifier }) => ({ path: `../compat/${frameworkImportName(specifier)}.cjs`, external: true }));
|
|
48
|
+
} }],
|
|
49
|
+
});
|
|
50
|
+
result[name] = output;
|
|
51
|
+
compiled.set(entry, output);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
package/build/check.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { loadProjectConfig } from './config.mjs';
|
|
5
|
+
import { validateCompilerConfig } from './compiler.mjs';
|
|
6
|
+
import { scanProject } from './scan.mjs';
|
|
7
|
+
import { compileCustomRoutes } from './custom-routes.mjs';
|
|
8
|
+
import { validateReactVersions } from './app.mjs';
|
|
9
|
+
import { snapshotEnvConfig } from '../runtime/env.mjs';
|
|
10
|
+
|
|
11
|
+
/** Preflight an existing project without rewriting sources, dependencies or build output. */
|
|
12
|
+
export async function checkProject(directory, { dev = false, validateDependencies = true } = {}) {
|
|
13
|
+
const root = path.resolve(directory);
|
|
14
|
+
const report = { root, ok: false, routes: [], versions: {}, errors: [], notes: [
|
|
15
|
+
'Preflight validates configuration, route conventions and installed React versions. A successful production build and browser tests are still required.',
|
|
16
|
+
'Webpack plugin hooks are validated during compilation; preflight only validates plugin shape and translated configuration.',
|
|
17
|
+
'Rust handles HTTP, routing, static files, images and persistent caches. React and npm JavaScript execute on Node/V8.',
|
|
18
|
+
] };
|
|
19
|
+
const restore = snapshotEnvConfig();
|
|
20
|
+
try {
|
|
21
|
+
const config = await loadProjectConfig(root, {dev});
|
|
22
|
+
if (config.onDemandEntries) report.notes.push('onDemandEntries is accepted for Next plugin compatibility. PRNext compiles all development routes and does not use Next\'s in-memory page eviction queue; these retention settings do not change its memory limits.');
|
|
23
|
+
await compileCustomRoutes(config);
|
|
24
|
+
const project = await scanProject(root, {basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n});
|
|
25
|
+
await validateCompilerConfig(config,root,{dev,edge:project.middleware?.runtime==='edge'||project.routes.some(route=>route.cacheConfig?.runtime==='edge'||route.handlerConfig?.runtime==='edge')});
|
|
26
|
+
report.routes = project.routes.filter(route=>!route.internal).map(({pattern,kind,router})=>({pattern,kind,router:router||'pages'}));
|
|
27
|
+
const resolve = createRequire(path.join(root,'package.json'));
|
|
28
|
+
for (const name of ['next','react','react-dom','react-server-dom-webpack']) {
|
|
29
|
+
try { report.versions[name] = JSON.parse(await readFile(resolve.resolve(`${name}/package.json`),'utf8')).version; }
|
|
30
|
+
catch { report.versions[name] = null; }
|
|
31
|
+
}
|
|
32
|
+
if (validateDependencies) {
|
|
33
|
+
if (project.routes.some(route=>route.router==='app')) await validateReactVersions(resolve);
|
|
34
|
+
else if (!report.versions.react || !report.versions['react-dom']) throw new Error('Install react and react-dom in the project before building Pages routes.');
|
|
35
|
+
} else report.notes.push('Installed React versions have not been validated yet; migration checks them after dependency installation.');
|
|
36
|
+
report.ok = true;
|
|
37
|
+
} catch (error) { report.errors.push(error.message); }
|
|
38
|
+
finally { restore(); }
|
|
39
|
+
return report;
|
|
40
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { build as esbuild } from 'esbuild';
|
|
3
|
+
import { runLoaders } from 'loader-runner';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { moduleResolutionPlugin, validateWebpackResolution } from './module-resolution.mjs';
|
|
9
|
+
import { cachedLoader } from './loader-cache.mjs';
|
|
10
|
+
import { incrementalBuild } from './incremental.mjs';
|
|
11
|
+
import { webpackGraph } from './webpack-graph.mjs';
|
|
12
|
+
import { turbopackRules, sourceReader } from './turbopack-conditions.mjs';
|
|
13
|
+
import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.mjs';
|
|
14
|
+
|
|
15
|
+
let validateOptions;
|
|
16
|
+
const compilation = new AsyncLocalStorage();
|
|
17
|
+
export const withCompilation = callback => withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback));
|
|
18
|
+
export function configureCompiler(options) { Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps); }
|
|
19
|
+
export async function compilerSource(file, fallback) {
|
|
20
|
+
return compilation.getStore()?.loaded.get(file)?.source ?? fallback ?? await readFile(file, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
const framework = /^(?:prnext-pages-compatibility|prnext-app-(?:rsc|ssr|browser)|prnext-edge-app|prnext-edge|prnext-middleware-compatibility)$/;
|
|
23
|
+
function condition(value, input) {
|
|
24
|
+
if (value === undefined) return true;
|
|
25
|
+
if (value instanceof RegExp) { value.lastIndex = 0; return value.test(input); }
|
|
26
|
+
if (typeof value === 'string') return input.startsWith(value);
|
|
27
|
+
if (typeof value === 'function') return !!value(input);
|
|
28
|
+
if (Array.isArray(value)) return value.some(item => condition(item, input));
|
|
29
|
+
if (value && typeof value === 'object' && Object.keys(value).some(key=>!['and','or','not'].includes(key))) throw new Error('Unsupported webpack rule condition field');
|
|
30
|
+
if (value && typeof value === 'object') return (!value.and || value.and.every(item => condition(item,input))) && (!value.or || value.or.some(item => condition(item,input))) && (!value.not || !condition(value.not,input));
|
|
31
|
+
throw new Error('Unsupported webpack rule condition');
|
|
32
|
+
}
|
|
33
|
+
function applies(rule,file,issuer,query) { return condition(rule.test,file) && condition(rule.include,file) && !(rule.exclude && condition(rule.exclude,file)) && condition(rule.issuer,issuer) && condition(rule.resourceQuery,query); }
|
|
34
|
+
async function matching(rules, file, issuer = '', query = '', output = [], read = sourceReader(file)) {
|
|
35
|
+
for (const rule of rules) {
|
|
36
|
+
if (!applies(rule,file,issuer,query)) continue;
|
|
37
|
+
if (rule.prnextCondition && !await rule.prnextCondition(file,read)) continue;
|
|
38
|
+
if (rule.oneOf) { const selected = rule.oneOf.find(item => applies(item,file,issuer,query)); if (selected) await matching([selected],file,issuer,query,output,read); }
|
|
39
|
+
if (rule.rules) await matching(rule.rules,file,issuer,query,output,read);
|
|
40
|
+
const use = typeof rule.use === 'function' ? rule.use({resource:file,resourceQuery:query,issuer}) : rule.use ?? (rule.loader ? {loader:rule.loader,options:rule.options} : []);
|
|
41
|
+
for (const item of Array.isArray(use) ? use : [use]) output.push({ ...(typeof item === 'string' ? {loader:item} : item), enforce:rule.enforce });
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
function validateRules(rules) {
|
|
46
|
+
if (!Array.isArray(rules) || rules.length > 256) throw new Error('webpack.module.rules must contain at most 256 rules');
|
|
47
|
+
for (const rule of rules) {
|
|
48
|
+
if (!rule || typeof rule !== 'object') throw new Error('Invalid webpack rule');
|
|
49
|
+
for (const key of Object.keys(rule)) if (!['test','include','exclude','issuer','resourceQuery','use','loader','options','oneOf','rules','enforce','type','resolve'].includes(key)) throw new Error(`webpack rule ${key} is not implemented`);
|
|
50
|
+
// The bridge already parses JavaScript as auto and resolves extensionless
|
|
51
|
+
// ESM requests. Accept the equivalent rule injected by Contentlayer.
|
|
52
|
+
if (rule.type !== undefined && rule.type !== 'javascript/auto') throw new Error(`webpack rule type ${rule.type} is not implemented`);
|
|
53
|
+
if (rule.resolve !== undefined && (!rule.resolve || typeof rule.resolve !== 'object' || Array.isArray(rule.resolve) || Object.keys(rule.resolve).some(key => key !== 'fullySpecified') || rule.resolve.fullySpecified !== false)) throw new Error('webpack rule resolve only supports fullySpecified: false');
|
|
54
|
+
if (rule.enforce && !['pre','post'].includes(rule.enforce)) throw new Error('Invalid webpack rule enforce');
|
|
55
|
+
if (rule.rules) validateRules(rule.rules);
|
|
56
|
+
if (rule.oneOf) validateRules(rule.oneOf);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function targetConfig(state, target) {
|
|
60
|
+
if (!state.targets.has(target)) state.targets.set(target,(async()=>{
|
|
61
|
+
const webpack = state.config.webpack ? (await import('webpack')).default : undefined;
|
|
62
|
+
const base = { mode:state.dev?'development':'production', context:state.root, resolve:{alias:{},extensions:['.tsx','.ts','.jsx','.js','.mjs','.json']},module:{rules:[]},plugins:[] };
|
|
63
|
+
const configured = state.config.webpack ? await state.config.webpack(base,{buildId:state.buildId,dev:state.dev,isServer:target!=='browser',nextRuntime:target==='browser'?undefined:target==='edge'?'edge':'nodejs',webpack,
|
|
64
|
+
defaultLoaders:{babel:{loader:new URL('./passthrough-loader.cjs',import.meta.url).pathname}}}) : base;
|
|
65
|
+
if (!configured || typeof configured !== 'object') throw new Error('webpack hook must return its configuration');
|
|
66
|
+
for (const key of Object.keys(configured)) if (!['mode','context','resolve','module','plugins','watchOptions'].includes(key)) throw new Error(`webpack.${key} cannot be translated to the PRNext compiler`);
|
|
67
|
+
const watchOptions = configured.watchOptions;
|
|
68
|
+
if (watchOptions !== undefined) {
|
|
69
|
+
if (!watchOptions || typeof watchOptions !== 'object' || Array.isArray(watchOptions) || Object.keys(watchOptions).some(key => key !== 'ignored')) throw new Error('webpack.watchOptions only supports ignored');
|
|
70
|
+
const ignored = watchOptions.ignored;
|
|
71
|
+
if (ignored !== undefined && !(ignored instanceof RegExp) && typeof ignored !== 'string' && !(Array.isArray(ignored) && ignored.every(value => typeof value === 'string'))) throw new Error('webpack.watchOptions.ignored must be a string, RegExp or array of strings');
|
|
72
|
+
}
|
|
73
|
+
if (configured.mode !== base.mode || configured.context !== state.root) throw new Error('webpack mode/context cannot be changed');
|
|
74
|
+
for (const key of Object.keys(configured.resolve || {})) if (!['alias','extensions'].includes(key)) throw new Error(`webpack.resolve.${key} is not implemented`);
|
|
75
|
+
for (const key of Object.keys(configured.module || {})) if (key !== 'rules') throw new Error(`webpack.module.${key} is not implemented`);
|
|
76
|
+
const rules = state.config.webpack ? configured.module?.rules || [] : turbopackRules(state.config.turbopack?.rules,state,target);
|
|
77
|
+
if (state.config.webpack) validateRules(rules);
|
|
78
|
+
const resolution = validateWebpackResolution(configured.resolve);
|
|
79
|
+
const define = {}, graphPlugins = [], banners = [], footers = [];
|
|
80
|
+
for (let plugin of configured.plugins || []) {
|
|
81
|
+
if (plugin instanceof webpack.EnvironmentPlugin) {
|
|
82
|
+
const values = {};
|
|
83
|
+
for (const key of plugin.keys) {
|
|
84
|
+
const value = process.env[key] ?? plugin.defaultValues[key];
|
|
85
|
+
if (value === undefined) throw new Error(`EnvironmentPlugin: ${key} environment variable is undefined`);
|
|
86
|
+
values[`process.env.${key}`] = JSON.stringify(value);
|
|
87
|
+
values[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
88
|
+
}
|
|
89
|
+
plugin = new webpack.DefinePlugin(values);
|
|
90
|
+
}
|
|
91
|
+
if (plugin instanceof webpack.BannerPlugin) {
|
|
92
|
+
if (typeof plugin.options.banner !== 'string' || Object.keys(plugin.options).some(key => !['banner','raw','footer'].includes(key))) {graphPlugins.push(plugin);continue;}
|
|
93
|
+
(plugin.options.footer ? footers : banners).push(plugin.banner({}));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!(plugin instanceof webpack.DefinePlugin)) {
|
|
97
|
+
if (!plugin || typeof plugin.apply !== 'function') throw new Error('Webpack plugins must implement apply(compiler)');
|
|
98
|
+
graphPlugins.push(plugin); continue;
|
|
99
|
+
}
|
|
100
|
+
const validateDefinitions = (values,prefix='') => {for(const [name,value] of Object.entries(values)) {
|
|
101
|
+
const key=prefix+name;
|
|
102
|
+
if (/^(?:process\.env\.(?:NODE_ENV|NEXT_RUNTIME|PRNEXT_)|__prnext)/.test(key.replace(/^typeof /,''))) throw new Error(`DefinePlugin cannot override reserved ${key}`);
|
|
103
|
+
if(value && Object.getPrototypeOf(value)===Object.prototype)validateDefinitions(value,key+'.');
|
|
104
|
+
}};
|
|
105
|
+
validateDefinitions(plugin.definitions);
|
|
106
|
+
if(Object.entries(plugin.definitions).some(([key,value])=>key.startsWith('typeof ') || !['string','number','boolean'].includes(typeof value))) {graphPlugins.push(plugin);continue;}
|
|
107
|
+
for (const [key,value] of Object.entries(plugin.definitions)) {
|
|
108
|
+
define[key] = String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {rules,resolution,define,graphPlugins,banners,footers,watchOptions,graph:graphPlugins.length>0 || !!state.config.webpack && rules.length>0};
|
|
112
|
+
})());
|
|
113
|
+
return state.targets.get(target);
|
|
114
|
+
}
|
|
115
|
+
export async function validateCompilerConfig(config, root, {dev=false,edge=false}={}) {
|
|
116
|
+
if (!config.webpack) return;
|
|
117
|
+
const state={config,root,dev,buildId:'prnext-check',targets:new Map()};
|
|
118
|
+
for (const target of ['node','browser',...(edge?['edge']:[])]) await targetConfig(state,target);
|
|
119
|
+
}
|
|
120
|
+
export async function build(options) {
|
|
121
|
+
const state = compilation.getStore();
|
|
122
|
+
// esbuild's shared service dispatches callbacks from its own async context.
|
|
123
|
+
// Re-enter this build so transforms never inherit another project's map mode,
|
|
124
|
+
// action registry or configuration from the first service invocation.
|
|
125
|
+
const run=AsyncLocalStorage.snapshot();
|
|
126
|
+
options={...options,plugins:(options.plugins || []).map(plugin=>({...plugin,setup(builder){
|
|
127
|
+
return run(()=>{
|
|
128
|
+
const hooks={};
|
|
129
|
+
for(const name of ['onResolve','onLoad'])hooks[name]=(settings,callback)=>builder[name](settings,(...args)=>run(callback,...args));
|
|
130
|
+
for(const name of ['onStart','onEnd','onDispose'])hooks[name]=callback=>builder[name]((...args)=>run(callback,...args));
|
|
131
|
+
return plugin.setup({...builder,...hooks});
|
|
132
|
+
});
|
|
133
|
+
}}))};
|
|
134
|
+
// Runtime vendor bundles are framework-owned, not project webpack targets.
|
|
135
|
+
const projectGraph = (options.plugins || []).some(plugin => framework.test(plugin.name));
|
|
136
|
+
const compile = input => state?.incremental && state.dev && (projectGraph || !input.plugins?.length) ? incrementalBuild(input,{root:state.root,stage:state.stage,group:projectGraph?'project':'auxiliary',run:callback=>compilation.run(state,callback)}) : esbuild(input);
|
|
137
|
+
if (!(state?.config?.webpack || Object.keys(state?.config?.turbopack?.rules || {}).length) || !projectGraph) return compile(options);
|
|
138
|
+
const target = options.define?.['process.env.NEXT_RUNTIME'] === '"edge"' ? 'edge' : options.platform === 'browser' ? 'browser' : 'node';
|
|
139
|
+
const config = await targetConfig(state,target);
|
|
140
|
+
validateOptions ||= (await import('schema-utils')).validate;
|
|
141
|
+
const callbacks = [];
|
|
142
|
+
const require = createRequire(path.join(state.root,'package.json'));
|
|
143
|
+
const emitted = new Map();
|
|
144
|
+
const loaderPlugin = {name:'prnext-webpack-loaders',setup(builder) {
|
|
145
|
+
builder.onResolve({filter:/.*/},async args=>{
|
|
146
|
+
if (args.pluginData?.prnextLoaderResolution || args.namespace && args.namespace !== 'file') return;
|
|
147
|
+
if (args.path.includes('!')) throw new Error('Inline loader requests require the webpack backend; configure webpack.module.rules or a graph plugin');
|
|
148
|
+
const result = await builder.resolve(args.path,{resolveDir:args.resolveDir,importer:args.importer,kind:args.kind,with:args.with,pluginData:{...args.pluginData,prnextLoaderResolution:true}});
|
|
149
|
+
if (result.errors.length || result.external || !path.isAbsolute(result.path)) return;
|
|
150
|
+
const loaders = await matching(config.rules,result.path,args.importer,result.suffix || '');
|
|
151
|
+
if (!loaders.length) return;
|
|
152
|
+
return {path:result.path,namespace:'file',suffix:result.suffix,pluginData:{prnextLoaders:loaders,issuer:args.importer}};
|
|
153
|
+
});
|
|
154
|
+
builder.onLoad({filter:/.*/,namespace:'file'},async args=>{
|
|
155
|
+
let selected = args.pluginData?.prnextLoaders || await matching(config.rules,args.path,'',args.suffix);
|
|
156
|
+
if(args.inlinePrefix)selected=selected.filter(item=>args.inlinePrefix==='!'?!!item.enforce:args.inlinePrefix==='-!'?item.enforce==='post':false);
|
|
157
|
+
if (!selected.length) return;
|
|
158
|
+
const loaders = selected.sort((a,b)=>({post:0,pre:2}[a.enforce]??1)-({post:0,pre:2}[b.enforce]??1)).map(item=>({loader:require.resolve(item.loader),options:item.options}));
|
|
159
|
+
const execute = ()=>new Promise((resolve,reject)=>{
|
|
160
|
+
const assets = [], warnings = [], errors = [];
|
|
161
|
+
function resolveModule(context,request,callback){
|
|
162
|
+
if (args.webpackContext) {this.cacheable(false);return args.webpackContext.getResolve()(context,request,callback);}
|
|
163
|
+
const promise = builder.resolve(request,{resolveDir:context,kind:'import-statement',pluginData:{prnextLoaderResolution:true}}).then(value=>{if(value.errors.length || !value.path)throw new Error(`Cannot resolve ${request} from ${context}`);return value.path;});
|
|
164
|
+
if (callback) promise.then(value=>callback(null,value),callback); else return promise;
|
|
165
|
+
}
|
|
166
|
+
runLoaders({resource:args.path+(args.suffix || ''),loaders,readResource:(file,done)=>args.prnextSource!==undefined && file===args.path ? done(null,Buffer.from(args.prnextSource),args.prnextSourceMap) : fs.readFile(file,done),context:{
|
|
167
|
+
version:2,rootContext:state.root,mode:state.dev?'development':'production',target:target==='node'?'node':'web',sourceMap:!!options.sourcemap,fs,
|
|
168
|
+
getOptions(schema){const value=this.query && typeof this.query==='object'?this.query:this.query?Object.fromEntries(new URLSearchParams(this.query.slice(1))):{};if(schema)validateOptions(schema,value,{name:this.loaders[this.loaderIndex].path});return value;},
|
|
169
|
+
resolve:resolveModule,getResolve(settings){
|
|
170
|
+
if (!settings || !Object.keys(settings).length) return resolveModule.bind(this);
|
|
171
|
+
if (!args.webpackContext) throw new Error('Custom getResolve options require a webpack configuration');
|
|
172
|
+
const resolve=args.webpackContext.getResolve(settings),loader=this;
|
|
173
|
+
return (context,request,callback)=>{
|
|
174
|
+
// Native resolution tracks dependencies on the webpack module, not
|
|
175
|
+
// the disk runner: do not persist an incomplete dependency snapshot.
|
|
176
|
+
loader.cacheable(false);
|
|
177
|
+
return resolve(context,request,callback);
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
getLogger:()=>console,emitWarning:error=>warnings.push(String(error?.message||error)),emitError:error=>errors.push(error instanceof Error?error:new Error(String(error))),
|
|
181
|
+
emitFile(name,content){if(typeof name!=='string'||path.isAbsolute(name)||name.split(/[\\/]/).some(part=>part==='..'||!part)||/[\0?#]/.test(name))throw new Error(`Unsafe loader asset path: ${name}`);assets.push({name,data:Buffer.from(content).toString('base64')});},
|
|
182
|
+
importModule(...values){if(!args.webpackContext)throw new Error('loader importModule requires a webpack configuration');this.cacheable(false);return args.webpackContext.importModule(...values);},
|
|
183
|
+
loadModule(...values){if(!args.webpackContext)throw new Error('loader loadModule requires a webpack configuration');this.cacheable(false);return args.webpackContext.loadModule(...values);},
|
|
184
|
+
}},(error,result)=>{
|
|
185
|
+
if(error||errors.length)return reject(error||errors[0]);
|
|
186
|
+
if(!result?.result || !['string','object'].includes(typeof result.result[0]))return reject(new Error('Loader must return JavaScript source'));
|
|
187
|
+
const source=Buffer.isBuffer(result.result[0])?result.result[0].toString():result.result[0];
|
|
188
|
+
if(typeof source!=='string')return reject(new Error('Loader must return a string or Buffer'));
|
|
189
|
+
resolve({source,...(options.sourcemap && result.result[1]?{map:typeof result.result[1]==='string'?JSON.parse(result.result[1]):result.result[1]}:{}),assets,warnings,cacheable:result.cacheable,dependencies:result.fileDependencies,contexts:result.contextDependencies,missing:result.missingDependencies});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
// A loader installed by a graph plugin may already have changed the input.
|
|
193
|
+
// Its source is not represented by the disk-loader cache's fingerprint.
|
|
194
|
+
const transformed = args.prnextSource!==undefined ? await execute() : await cachedLoader({root:state.root,file:args.path,query:args.suffix || '',loaders,target,dev:state.dev,sourceMap:!!options.sourcemap,mapPipeline:1,resolution:config.resolution,turbopackResolution:{resolveAlias:state.config.turbopack?.resolveAlias,resolveExtensions:state.config.turbopack?.resolveExtensions}},execute);
|
|
195
|
+
const mappedSource=inlineSourceMap(transformed.source,transformed.map);
|
|
196
|
+
for(const asset of transformed.assets){const previous=emitted.get(asset.name);if(previous && previous!==asset.data)throw new Error(`Conflicting loader asset: ${asset.name}`);emitted.set(asset.name,asset.data);}
|
|
197
|
+
state.loaded.set(args.path,{...transformed,source:mappedSource});
|
|
198
|
+
for (const callback of callbacks) {
|
|
199
|
+
const result=await callback({...args,prnextSource:mappedSource});
|
|
200
|
+
if(result)return {...result,loader:result.loader || 'jsx',watchFiles:[...(result.watchFiles||[]),...transformed.dependencies],watchDirs:transformed.contexts,warnings:[...(result.warnings||[]),...transformed.warnings.map(text=>({text}))]};
|
|
201
|
+
}
|
|
202
|
+
return {contents:mappedSource,loader:'jsx',resolveDir:path.dirname(args.path),watchFiles:transformed.dependencies,watchDirs:transformed.contexts,warnings:transformed.warnings.map(text=>({text}))};
|
|
203
|
+
});
|
|
204
|
+
}};
|
|
205
|
+
const plugins = (options.plugins || []).map(plugin=>!framework.test(plugin.name)?plugin:{...plugin,setup(builder){
|
|
206
|
+
return plugin.setup({...builder,onLoad(settings,callback){if(settings.namespace==='file')callbacks.push(callback);builder.onLoad(settings,callback);}});
|
|
207
|
+
}});
|
|
208
|
+
if (config.graph) return webpackGraph({...options,
|
|
209
|
+
...(config.banners.length ? {banner:{...options.banner,js:[options.banner?.js,...config.banners].filter(Boolean).join('\n')}} : {}),
|
|
210
|
+
...(config.footers.length ? {footer:{...options.footer,js:[options.footer?.js,...config.footers].filter(Boolean).join('\n')}} : {}),
|
|
211
|
+
define:{...options.define,...config.define},plugins:[moduleResolutionPlugin(config.resolution,state.root),loaderPlugin,...plugins],
|
|
212
|
+
}, {state,config,target}).then(async result=>{
|
|
213
|
+
for(const [name,data] of emitted){const file=path.join(state.stage,'assets',name);await mkdir(path.dirname(file),{recursive:true});await writeFile(file,Buffer.from(data,'base64'));}
|
|
214
|
+
return result;
|
|
215
|
+
});
|
|
216
|
+
const result = await compile({...options,
|
|
217
|
+
...(config.banners.length ? {banner:{...options.banner,js:[options.banner?.js,...config.banners].filter(Boolean).join('\n')}} : {}),
|
|
218
|
+
...(config.footers.length ? {footer:{...options.footer,js:[options.footer?.js,...config.footers].filter(Boolean).join('\n')}} : {}),
|
|
219
|
+
define:{...options.define,...config.define,__webpack_public_path__:JSON.stringify(state.assetBase+'/')},plugins:[moduleResolutionPlugin(config.resolution,state.root),loaderPlugin,...plugins]});
|
|
220
|
+
for(const [name,data] of emitted){const file=path.join(state.stage,'assets',name);await mkdir(path.dirname(file),{recursive:true});await writeFile(file,Buffer.from(data,'base64'));}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
package/build/config.mjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { build as bundle } from 'esbuild';
|
|
2
|
+
import { readdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { loadEnvConfig } from '../runtime/env.mjs';
|
|
7
|
+
import constants from '../compat/constants.cjs';
|
|
8
|
+
import cacheProfiles from '../compat/cache-life.cjs';
|
|
9
|
+
import { validateDistDir } from '../runtime/build-directory.mjs';
|
|
10
|
+
import { validateCacheHandler, validateCacheHandlers } from './cache-handlers.mjs';
|
|
11
|
+
import { validateImagesConfig } from './images-config.mjs';
|
|
12
|
+
import { pageExtensionPattern, defaultPageExtensions } from './page-extensions.mjs';
|
|
13
|
+
import { validateServerActions } from './server-actions-config.mjs';
|
|
14
|
+
import { validateStandaloneConfig } from './standalone-config.mjs';
|
|
15
|
+
import { validateTurbopack } from './module-resolution.mjs';
|
|
16
|
+
import { validateI18n } from './i18n.mjs';
|
|
17
|
+
import { frameworkPackages } from './framework-imports.mjs';
|
|
18
|
+
|
|
19
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
|
|
21
|
+
const supported = new Set(['env', 'redirects', 'headers', 'rewrites', 'generateBuildId', 'compress', 'poweredByHeader', 'productionBrowserSourceMaps', 'basePath', 'assetPrefix', 'experimental', 'sassOptions', 'transpilePackages', 'serverExternalPackages', 'cacheComponents', 'cacheLife', 'images', 'reactStrictMode', 'pageExtensions', 'trailingSlash', 'skipTrailingSlashRedirect', 'skipMiddlewareUrlNormalize', 'skipProxyUrlNormalize', 'cacheHandlers', 'distDir']);
|
|
22
|
+
const defaults = { distDir: '.prnext', env: {}, compress: true, poweredByHeader: true, productionBrowserSourceMaps: false, basePath: '', assetPrefix: '', trailingSlash: false, skipTrailingSlashRedirect: false, skipMiddlewareUrlNormalize: false, experimental: { nextScriptWorkers: false } };
|
|
23
|
+
supported.add('cacheHandler');
|
|
24
|
+
supported.add('cacheMaxMemorySize');
|
|
25
|
+
supported.add('turbopack');
|
|
26
|
+
supported.add('i18n');
|
|
27
|
+
supported.add('webpack');
|
|
28
|
+
supported.add('onDemandEntries');
|
|
29
|
+
for (const key of ['output', 'outputFileTracingRoot', 'outputFileTracingIncludes', 'outputFileTracingExcludes']) supported.add(key);
|
|
30
|
+
|
|
31
|
+
function validPrefixPath(value, where, { root = false } = {}) {
|
|
32
|
+
if (/[^\x00-\x7f]/.test(value)) throw new Error(`${where} must use an ASCII URL pathname; percent-encode non-ASCII characters explicitly.`);
|
|
33
|
+
if ((!root && value === '/') || !value.startsWith('/') || value.startsWith('//') || /[\s\\?#\u0000-\u001f\u007f]/.test(value) ||
|
|
34
|
+
value.replace(/\/+$/, '').split('/').some((segment, index) => index && (!segment || segment === '.' || segment === '..')) || /%(?![\da-f]{2})/i.test(value)) {
|
|
35
|
+
throw new Error(`${where} must be a URL pathname prefix without query, fragment, whitespace, backslashes, empty interior segments or dot segments.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function publicAssetBase(config) {
|
|
40
|
+
return (config.assetPrefix || config.basePath || '').replace(/\/+$/, '') + '/_prnext/assets';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function configPlugin() {
|
|
44
|
+
return {
|
|
45
|
+
name: 'prnext-project-config',
|
|
46
|
+
setup(build) {
|
|
47
|
+
build.onResolve({ filter: /^(?:next|prnext|@thomas\.f\/prnext)\/constants(?:\.js)?$/ }, () => ({ path: path.join(packageRoot, 'compat/constants.cjs') }));
|
|
48
|
+
build.onLoad({ filter: /\.(?:[cm]?js|ts)$/ }, async ({ path: filename }) => {
|
|
49
|
+
if (filename.includes(`${path.sep}node_modules${path.sep}`)) return;
|
|
50
|
+
// Preserve filesystem paths after bundling config's relative helpers.
|
|
51
|
+
const prefix = `const __prnext_config_url=${JSON.stringify(pathToFileURL(filename).href)};const __prnext_config_file=${JSON.stringify(filename)};const __prnext_config_dir=${JSON.stringify(path.dirname(filename))};\n`;
|
|
52
|
+
const source = (await readFile(filename, 'utf8')).replace(/^#![^\n]*(?:\n|$)/, '');
|
|
53
|
+
return { contents: prefix + source, loader: filename.endsWith('.ts') ? 'ts' : 'js', resolveDir: path.dirname(filename) };
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function importConfig(filename) {
|
|
60
|
+
const temporary = path.join(path.dirname(filename), `.prnext-config-${randomUUID()}.mjs`);
|
|
61
|
+
try {
|
|
62
|
+
const result = await bundle({ entryPoints: [filename], absWorkingDir: path.dirname(filename), bundle: true, write: false, platform: 'node', target: 'node22', format: 'esm', packages: 'external', logLevel: 'silent',
|
|
63
|
+
define: { 'import.meta.url': '__prnext_config_url', __filename: '__prnext_config_file', __dirname: '__prnext_config_dir' },
|
|
64
|
+
banner: { js: `import {createRequire as __prnext_create_require} from 'node:module';const require=__prnext_create_require(${JSON.stringify(pathToFileURL(filename).href)});` },
|
|
65
|
+
plugins: [configPlugin()],
|
|
66
|
+
});
|
|
67
|
+
await writeFile(temporary, result.outputFiles[0].contents, { flag: 'wx' });
|
|
68
|
+
const imported = await import(pathToFileURL(temporary).href);
|
|
69
|
+
return imported.default;
|
|
70
|
+
} finally { await rm(temporary, { force: true }); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateProjectConfig(input, filename = 'project config') {
|
|
74
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error(`${filename} must export a configuration object or a function returning one.`);
|
|
75
|
+
for (const key of Object.keys(input)) {
|
|
76
|
+
if (!supported.has(key)) throw new Error(`${filename}: option "${key}" is not supported by PRNext. It cannot be silently ignored.`);
|
|
77
|
+
}
|
|
78
|
+
// Optional properties explicitly set to undefined still use their defaults,
|
|
79
|
+
// as in common process.env-based Next configuration wrappers.
|
|
80
|
+
const config = { ...defaults, ...Object.fromEntries(Object.entries(input).filter(([,value]) => value !== undefined)) };
|
|
81
|
+
if (config.onDemandEntries !== undefined) {
|
|
82
|
+
const entries = config.onDemandEntries;
|
|
83
|
+
if (!entries || typeof entries !== 'object' || Array.isArray(entries)) throw new TypeError('onDemandEntries must be an object');
|
|
84
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
85
|
+
if (!['maxInactiveAge', 'pagesBufferLength'].includes(key)) throw new TypeError(`onDemandEntries.${key} is not supported`);
|
|
86
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) throw new TypeError(`onDemandEntries.${key} must be a non-negative safe integer`);
|
|
87
|
+
}
|
|
88
|
+
config.onDemandEntries = { maxInactiveAge: entries.maxInactiveAge ?? 60000, pagesBufferLength: entries.pagesBufferLength ?? 5 };
|
|
89
|
+
}
|
|
90
|
+
if (config.webpack !== undefined && typeof config.webpack !== 'function') throw new TypeError('webpack must be a configuration function');
|
|
91
|
+
config.turbopack = validateTurbopack(config.turbopack);
|
|
92
|
+
config.i18n = validateI18n(config.i18n);
|
|
93
|
+
if (config.i18n && config.output === 'export') throw new Error("i18n and output: 'export' cannot be combined; use explicit locale routes for static exports");
|
|
94
|
+
validateStandaloneConfig(config);
|
|
95
|
+
config.distDir = validateDistDir(config.distDir);
|
|
96
|
+
if (config.output === 'export' && (config.distDir === 'out' || config.distDir.startsWith('out/'))) throw new Error("output: 'export' reserves out/ for exported files; choose a different distDir");
|
|
97
|
+
pageExtensionPattern(config.pageExtensions);
|
|
98
|
+
config.pageExtensions = [...new Set(config.pageExtensions || defaultPageExtensions)];
|
|
99
|
+
if (config.reactStrictMode !== undefined && typeof config.reactStrictMode !== 'boolean') throw new Error(`${filename}: reactStrictMode must be a boolean.`);
|
|
100
|
+
config.cacheComponents ??= false;
|
|
101
|
+
if (typeof config.cacheComponents !== 'boolean') throw new TypeError('cacheComponents must be a boolean');
|
|
102
|
+
config.cacheLife = cacheProfiles.profiles(config.cacheLife);
|
|
103
|
+
for (const key of ['transpilePackages', 'serverExternalPackages']) {
|
|
104
|
+
const names = config[key] ?? [];
|
|
105
|
+
if (!Array.isArray(names) || names.some(name => typeof name !== 'string' || !/^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9_.-]+$/.test(name) || name.startsWith('.') || frameworkPackages.includes(name) || /^(?:react|react-dom|react-server-dom-webpack)$/.test(name))) throw new Error(`${filename}: ${key} must be an array of npm package names excluding framework and React packages.`);
|
|
106
|
+
config[key] = [...new Set(names)];
|
|
107
|
+
}
|
|
108
|
+
for (const name of config.transpilePackages) if (config.serverExternalPackages.includes(name)) throw new Error(`${filename}: ${name} cannot be in both transpilePackages and serverExternalPackages.`);
|
|
109
|
+
if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
|
|
110
|
+
if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
|
|
111
|
+
for (const key of Object.keys(config.experimental)) {
|
|
112
|
+
if (!['nextScriptWorkers', 'serverActions'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
113
|
+
}
|
|
114
|
+
const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
|
|
115
|
+
if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
|
|
116
|
+
config.experimental = { nextScriptWorkers, serverActions: validateServerActions(config.experimental.serverActions) };
|
|
117
|
+
for (const key of ['basePath', 'assetPrefix']) {
|
|
118
|
+
if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
|
|
119
|
+
}
|
|
120
|
+
if (config.basePath) {
|
|
121
|
+
validPrefixPath(config.basePath, `${filename}: basePath`);
|
|
122
|
+
if (config.basePath.endsWith('/')) throw new Error(`${filename}: basePath must not end with /.`);
|
|
123
|
+
}
|
|
124
|
+
if (config.assetPrefix) {
|
|
125
|
+
if (/^https?:\/\//i.test(config.assetPrefix)) {
|
|
126
|
+
let url;
|
|
127
|
+
try { url = new URL(config.assetPrefix); } catch { throw new Error(`${filename}: assetPrefix must be a valid HTTP(S) URL or pathname prefix.`); }
|
|
128
|
+
if (url.username || url.password || /[\s\\?#]/.test(config.assetPrefix)) throw new Error(`${filename}: assetPrefix cannot contain credentials, query, fragment, whitespace or backslashes.`);
|
|
129
|
+
const rawPathname = /^https?:\/\/[^/]*(\/.*)?$/i.exec(config.assetPrefix)?.[1] || '/';
|
|
130
|
+
validPrefixPath(rawPathname, `${filename}: assetPrefix pathname`, { root: true });
|
|
131
|
+
config.assetPrefix = url.href.replace(/\/+$/, '');
|
|
132
|
+
} else {
|
|
133
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(config.assetPrefix)) throw new Error(`${filename}: assetPrefix only supports HTTP(S) URLs and pathname prefixes.`);
|
|
134
|
+
const prefix = config.assetPrefix.startsWith('/') ? config.assetPrefix : '/' + config.assetPrefix;
|
|
135
|
+
validPrefixPath(prefix, `${filename}: assetPrefix`, { root: true });
|
|
136
|
+
// An explicit / selects root assets even when basePath is configured.
|
|
137
|
+
config.assetPrefix = prefix.replace(/\/+$/, '') || '/';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const key of ['compress', 'poweredByHeader', 'productionBrowserSourceMaps', 'trailingSlash', 'skipTrailingSlashRedirect', 'skipMiddlewareUrlNormalize']) {
|
|
141
|
+
if (typeof config[key] !== 'boolean') throw new Error(`${filename}: ${key} must be a boolean.`);
|
|
142
|
+
}
|
|
143
|
+
for (const key of ['redirects', 'headers', 'rewrites', 'generateBuildId']) {
|
|
144
|
+
if (config[key] !== undefined && typeof config[key] !== 'function') throw new Error(`${filename}: ${key} must be a function.`);
|
|
145
|
+
}
|
|
146
|
+
if (!config.env || typeof config.env !== 'object' || Array.isArray(config.env)) throw new Error(`${filename}: env must be an object of string values.`);
|
|
147
|
+
const environment = Object.create(null);
|
|
148
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
149
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key) || /^(?:NODE_|__)/i.test(key) || key === 'NEXT_RUNTIME' || key.startsWith('PRNEXT_')) throw new Error(`${filename}: env key "${key}" is reserved or not a JavaScript identifier.`);
|
|
150
|
+
if (value === undefined) continue;
|
|
151
|
+
if (typeof value !== 'string') throw new Error(`${filename}: env.${key} must be a string or undefined.`);
|
|
152
|
+
environment[key] = value;
|
|
153
|
+
}
|
|
154
|
+
config.env = environment;
|
|
155
|
+
if (config.skipProxyUrlNormalize !== undefined) {
|
|
156
|
+
if (typeof config.skipProxyUrlNormalize !== 'boolean') throw new TypeError('skipProxyUrlNormalize must be a boolean');
|
|
157
|
+
config.skipMiddlewareUrlNormalize = config.skipProxyUrlNormalize;
|
|
158
|
+
}
|
|
159
|
+
config.cacheHandlers = validateCacheHandlers(config.cacheHandlers);
|
|
160
|
+
config.cacheHandler = validateCacheHandler(config.cacheHandler);
|
|
161
|
+
if (config.cacheMaxMemorySize !== undefined && (!Number.isSafeInteger(config.cacheMaxMemorySize) || config.cacheMaxMemorySize < 0 || config.cacheMaxMemorySize > 1024 * 1024 * 1024)) throw new TypeError('cacheMaxMemorySize must be an integer between 0 and 1 GiB');
|
|
162
|
+
config.images = validateImagesConfig(config.images, { basePath: config.basePath, assetPrefix: config.assetPrefix, trailingSlash: config.trailingSlash });
|
|
163
|
+
return config;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Config and its relative imports are evaluated fresh on every build. */
|
|
167
|
+
export async function loadProjectConfig(projectRoot, { dev = false, envMode, phase = dev ? constants.PHASE_DEVELOPMENT_SERVER : constants.PHASE_PRODUCTION_BUILD } = {}) {
|
|
168
|
+
const root = path.resolve(projectRoot);
|
|
169
|
+
loadEnvConfig(root, { dev, mode: envMode });
|
|
170
|
+
const files = await readdir(root);
|
|
171
|
+
const names = files.filter(name => configName.test(name));
|
|
172
|
+
const unsupportedFiles = files.filter(name => /^(?:next|prnext)\.config\.(?:mts|cts|jsx|tsx|json)$/.test(name));
|
|
173
|
+
if (unsupportedFiles.length) throw new Error(`Unsupported configuration file ${unsupportedFiles.join(', ')}. Use .js, .mjs, .cjs or .ts.`);
|
|
174
|
+
if (names.length > 1) throw new Error(`Multiple configuration files found: ${names.sort().join(', ')}. Keep one next.config.* or prnext.config.* file.`);
|
|
175
|
+
if (!names.length) return validateProjectConfig({});
|
|
176
|
+
const filename = path.join(root, names[0]);
|
|
177
|
+
let input = await importConfig(filename);
|
|
178
|
+
if (typeof input === 'function') input = await input(phase, { defaultConfig: { ...defaults, env: {}, experimental: { ...defaults.experimental } } });
|
|
179
|
+
return validateProjectConfig(input, names[0]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function defineEnvironment(config, env = process.env) {
|
|
183
|
+
const publicValues = Object.fromEntries(Object.entries(env).filter(([key, value]) => key.startsWith('NEXT_PUBLIC_') && value !== undefined));
|
|
184
|
+
const values = { ...publicValues, ...config.env };
|
|
185
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function generateBuildId(config, { dev = false } = {}) {
|
|
189
|
+
const generated = !dev && config.generateBuildId ? await config.generateBuildId() : null;
|
|
190
|
+
if (generated === null) return randomUUID();
|
|
191
|
+
if (typeof generated !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(generated)) throw new Error('generateBuildId must return null or a nonempty URL-safe string of at most 128 characters (letters, digits, _ or -).');
|
|
192
|
+
return generated;
|
|
193
|
+
}
|