@thomas.f/prnext 0.1.17-alpha → 0.1.18-alpha
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 +26 -869
- package/build/actions.mjs +1 -1
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +81 -26
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +31 -8
- package/build/config.mjs +30 -1
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-update.mjs +4 -2
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/index.mjs +59 -26
- package/build/instrumentation.mjs +30 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +58 -9
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +6 -3
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +60 -4
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +68 -3
- package/compat/link.cjs +12 -9
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +171 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +82 -0
- package/docs/ai/troubleshooting.md +37 -2
- package/package.json +26 -7
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +212 -65
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +7 -2
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/development.mjs +29 -12
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
package/build/create.mjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readFile, readdir, realpath, unlink } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { packageManifest } from '../native/resolve.mjs';
|
|
6
|
+
import { supportedNode } from '../native/diagnostics.mjs';
|
|
7
|
+
import { npmCommand } from '../native/npm.mjs';
|
|
8
|
+
|
|
9
|
+
export const createHelp = `Usage: prnext create <directory> [--js] [--no-install] [--json]
|
|
10
|
+
|
|
11
|
+
Create a minimal App Router project and install its dependencies with npm.
|
|
12
|
+
TypeScript is the default; --js creates a JavaScript project.
|
|
13
|
+
The directory must be absent or completely empty. Existing files are never replaced.
|
|
14
|
+
--no-install prepares files without running npm or accessing the network.
|
|
15
|
+
Inside an existing workspace, use --no-install and its own package manager.
|
|
16
|
+
|
|
17
|
+
npx @thomas.f/prnext create my-app
|
|
18
|
+
cd my-app
|
|
19
|
+
npm run dev
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export function parseCreateArgs(args, cwd = process.cwd()) {
|
|
23
|
+
const flags = new Set(), positional = [];
|
|
24
|
+
let literal = false;
|
|
25
|
+
for (const arg of args) {
|
|
26
|
+
if (literal) positional.push(arg);
|
|
27
|
+
else if (arg === '--') literal = true;
|
|
28
|
+
else if (['--no-install', '--js', '--json'].includes(arg)) {
|
|
29
|
+
if (flags.has(arg)) throw new Error(`Duplicate create option: ${arg}`);
|
|
30
|
+
flags.add(arg);
|
|
31
|
+
} else if (arg.startsWith('-')) throw new Error(`Unknown create option: ${arg}`);
|
|
32
|
+
else positional.push(arg);
|
|
33
|
+
}
|
|
34
|
+
if (positional.length !== 1 || !positional[0].trim() || /[\0\r\n]/.test(positional[0])) throw new Error('Provide one project directory: prnext create my-app');
|
|
35
|
+
return { directory: path.resolve(cwd, positional[0]), options: { install: !flags.has('--no-install'), typescript: !flags.has('--js'), json: flags.has('--json') } };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
39
|
+
const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
40
|
+
async function info(file, options) {
|
|
41
|
+
try { return await lstat(file, options); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
42
|
+
}
|
|
43
|
+
async function physicalDirectory(directory, missing = false) {
|
|
44
|
+
// macOS exposes standard directories through root-owned /private aliases.
|
|
45
|
+
// Resolve only those exact system targets; every user-controlled descendant
|
|
46
|
+
// still goes through the strict no-symlink walk below, on every write.
|
|
47
|
+
if (process.platform === 'darwin') {
|
|
48
|
+
const alias = /^\/(?:var|tmp|etc)(?=\/|$)/.exec(directory)?.[0];
|
|
49
|
+
if (alias && (await info(alias))?.isSymbolicLink() && await realpath(alias) === `/private${alias}`) {
|
|
50
|
+
directory = `/private${directory}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const root = path.parse(directory).root;
|
|
54
|
+
const inspected = [];
|
|
55
|
+
let current = root;
|
|
56
|
+
for (const part of path.relative(root, directory).split(path.sep).filter(Boolean)) {
|
|
57
|
+
if (process.platform === 'win32' && (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part) || /[<>:"|?*]|[. ]$/.test(part))) throw new Error(`Invalid Windows directory name: ${part}`);
|
|
58
|
+
current = path.join(current, part);
|
|
59
|
+
const entry = await info(current, { bigint: true });
|
|
60
|
+
if (!entry && missing) continue;
|
|
61
|
+
if (!entry?.isDirectory() || entry.isSymbolicLink()) throw new Error(`Project directories must not be files, symbolic links or junctions: ${current}`);
|
|
62
|
+
inspected.push([current, entry]);
|
|
63
|
+
}
|
|
64
|
+
if (!missing) {
|
|
65
|
+
const canonical = await realpath(directory);
|
|
66
|
+
if (!samePath(canonical, directory)) {
|
|
67
|
+
if (process.platform !== 'win32') throw new Error('Use a physical project directory without symbolic links or junctions.');
|
|
68
|
+
// DOS 8.3 names are directory aliases, not reparse points. Accept the
|
|
69
|
+
// expanded name only after rechecking every observed directory identity
|
|
70
|
+
// and proving that the canonical target is the same physical directory.
|
|
71
|
+
for (const [file, original] of inspected) {
|
|
72
|
+
const observed = await info(file, { bigint: true });
|
|
73
|
+
if (!observed?.isDirectory() || observed.isSymbolicLink() || !sameFile(original, observed)) throw new Error(`Project directory changed during creation: ${file}`);
|
|
74
|
+
}
|
|
75
|
+
const original = inspected.at(-1)?.[1] || await info(directory, { bigint: true });
|
|
76
|
+
const resolved = await info(canonical, { bigint: true });
|
|
77
|
+
if (!original?.isDirectory() || original.isSymbolicLink() || original.ino === 0n || !resolved?.isDirectory() || resolved.isSymbolicLink() || !sameFile(original, resolved)) throw new Error('Use a physical project directory without symbolic links or junctions.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function parentWorkspace(directory) {
|
|
83
|
+
for (let current = path.dirname(directory); ; current = path.dirname(current)) {
|
|
84
|
+
const workspace = await info(path.join(current, 'pnpm-workspace.yaml'));
|
|
85
|
+
if (workspace) return current;
|
|
86
|
+
const file = path.join(current, 'package.json'), entry = await info(file);
|
|
87
|
+
if (entry) {
|
|
88
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 1024 * 1024) throw new Error(`Cannot safely inspect parent package.json: ${file}`);
|
|
89
|
+
let manifest;
|
|
90
|
+
try { manifest = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, '')); }
|
|
91
|
+
catch { throw new Error(`Parent package.json is invalid: ${file}`); }
|
|
92
|
+
if (manifest.workspaces) return current;
|
|
93
|
+
}
|
|
94
|
+
if (current === path.dirname(current)) return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function projectName(directory) {
|
|
99
|
+
const name = path.basename(directory).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '').slice(0, 100);
|
|
100
|
+
return !name || ['node_modules', 'favicon.ico'].includes(name) ? 'prnext-app' : name;
|
|
101
|
+
}
|
|
102
|
+
const json = value => JSON.stringify(value, null, 2) + '\n';
|
|
103
|
+
|
|
104
|
+
function projectFiles(name, typescript) {
|
|
105
|
+
const extension = typescript ? 'tsx' : 'jsx';
|
|
106
|
+
const scripts = { dev: 'prnext dev', build: 'prnext build', start: 'prnext start', check: 'prnext check', ...(typescript ? { typecheck: 'prnext typecheck' } : {}) };
|
|
107
|
+
const manifest = { name, version: '0.0.0', private: true, type: 'module', scripts,
|
|
108
|
+
dependencies: { [packageManifest.name]: packageManifest.version, ...Object.fromEntries(Object.entries(packageManifest.peerDependencies).filter(([name])=>!packageManifest.peerDependenciesMeta?.[name]?.optional)) },
|
|
109
|
+
...(typescript ? { devDependencies: { typescript: '^5.9.0', '@types/node': '^22.0.0', '@types/react': '^19.0.0', '@types/react-dom': '^19.0.0' } } : {}),
|
|
110
|
+
engines: { node: packageManifest.engines.node } };
|
|
111
|
+
const files = new Map([
|
|
112
|
+
['package.json', json(manifest)],
|
|
113
|
+
[`app/layout.${extension}`, `import './globals.css';\n${typescript ? "import type { ReactNode } from 'react';\n" : ''}
|
|
114
|
+
export const metadata = { title: 'My PRNext app', description: 'A React application built with PRNext.' };
|
|
115
|
+
|
|
116
|
+
export default function RootLayout({ children }${typescript ? ': { children: ReactNode }' : ''}) {
|
|
117
|
+
return <html lang="en"><body>{children}</body></html>;
|
|
118
|
+
}
|
|
119
|
+
`],
|
|
120
|
+
[`app/page.${extension}`, `export default function Home() {
|
|
121
|
+
return <main><h1>PRNext</h1><p>Edit <code>app/page.${extension}</code> to get started.</p></main>;
|
|
122
|
+
}
|
|
123
|
+
`],
|
|
124
|
+
['app/globals.css', `:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #0a0a0a; color: #ededed; }
|
|
125
|
+
* { box-sizing: border-box; }
|
|
126
|
+
body { margin: 0; }
|
|
127
|
+
main { max-width: 64rem; margin: auto; padding: clamp(2rem, 8vw, 6rem) 1.5rem; }
|
|
128
|
+
h1 { font-size: clamp(2.5rem, 8vw, 4rem); letter-spacing: -.04em; }
|
|
129
|
+
p { line-height: 1.6; }
|
|
130
|
+
code { font-size: .95em; }
|
|
131
|
+
`],
|
|
132
|
+
['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
|
|
133
|
+
['README.md', `# ${name}\n\nA minimal PRNext application.\n\nRun \`npm install\` if dependencies have not been installed, then:\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nEdit \`app/page.${extension}\`. Use the local URL printed by the server.\n\n\`\`\`sh\nnpm run check\n${typescript ? 'npm run typecheck\n' : ''}npm run build\nnpm start\n\`\`\`\n\nUse another port with \`npm run dev -- --port 3001\` when needed.\n\n[PRNext documentation](https://prnext.dev) · Installed AI guide: \`npx --no-install prnext docs --ai\`.\nPRNext is experimental; Next.js compatibility is partial.\n`],
|
|
134
|
+
['AGENTS.md', `# PRNext project\n\nThis application uses \`@thomas.f/prnext\`. Before changing routing, configuration, data fetching or deployment, read the installed guide at \`node_modules/@thomas.f/prnext/AI.md\` or run:\n\n\`\`\`sh\nnpx --no-install prnext docs --ai\nnpx --no-install prnext docs --ai --list\n\`\`\`\n\nRead only the relevant topic with \`npx --no-install prnext docs --ai <topic>\`. The installed documentation is version-specific; do not assume full Next.js compatibility. Preserve the project's package manager and lockfile.\n`],
|
|
135
|
+
]);
|
|
136
|
+
if (typescript) files.set('tsconfig.json', json({ compilerOptions: { target: 'ES2022', lib: ['DOM', 'DOM.Iterable', 'ES2022'], module: 'ESNext', moduleResolution: 'Bundler', jsx: 'preserve', strict: true, noEmit: true, esModuleInterop: true, skipLibCheck: true }, include: ['**/*.ts', '**/*.tsx'], exclude: ['node_modules', '.prnext', '.prnext-dev', '.prnext-cache'] }));
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function abortError() { const error = new Error('Project creation canceled.'); error.name = 'AbortError'; return error; }
|
|
141
|
+
function aborted(signal) { if (signal?.aborted) throw abortError(); }
|
|
142
|
+
|
|
143
|
+
/** Run npm without a shell; a canceled command waits for its own installer tree. */
|
|
144
|
+
export async function installProject(directory, { signal, onOutput = () => {} } = {}) {
|
|
145
|
+
aborted(signal);
|
|
146
|
+
// npm resolves its working directory physically. Keep its environment on
|
|
147
|
+
// that same path so macOS aliases cannot split the lock tree.
|
|
148
|
+
directory = await realpath(directory);
|
|
149
|
+
aborted(signal);
|
|
150
|
+
const env = { ...process.env, INIT_CWD: directory, PWD: directory };
|
|
151
|
+
for (const key of Object.keys(env)) if (/^npm_config_(?:workspace|workspaces|global|prefix)$/i.test(key)) delete env[key];
|
|
152
|
+
const npm = npmCommand(env);
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
// The generated package.json already anchors npm's local project here.
|
|
155
|
+
// npm 10 on Windows treats an explicit --prefix as its global top and can
|
|
156
|
+
// add the project to its own dependencies even with --global=false.
|
|
157
|
+
const child = spawn(npm.command, [...npm.args, 'install', '--workspaces=false', '--global=false'], { cwd: directory, env, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
158
|
+
let tail = '', timer, canceling = false;
|
|
159
|
+
const receive = stream => chunk => { tail = (tail + chunk).slice(-16000); onOutput(String(chunk), stream); };
|
|
160
|
+
child.stdout.on('data', receive('stdout'));
|
|
161
|
+
child.stderr.on('data', receive('stderr'));
|
|
162
|
+
function terminate(force) {
|
|
163
|
+
if (!child.pid) return;
|
|
164
|
+
if (process.platform === 'win32') {
|
|
165
|
+
if (child.exitCode !== null || child.signalCode) return;
|
|
166
|
+
const executable = path.join(env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe');
|
|
167
|
+
const killer = spawn(executable, ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
168
|
+
killer.on('error', () => child.kill());
|
|
169
|
+
} else {
|
|
170
|
+
try { process.kill(-child.pid, force ? 'SIGKILL' : 'SIGTERM'); } catch (error) { if (error.code !== 'ESRCH') child.kill(force ? 'SIGKILL' : 'SIGTERM'); }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function cancel() { canceling = true; terminate(false); timer ||= setTimeout(() => terminate(true), 5000); }
|
|
174
|
+
function cleanup() { clearTimeout(timer); signal?.removeEventListener('abort', cancel); }
|
|
175
|
+
child.once('error', error => { cleanup(); reject(error); });
|
|
176
|
+
child.once('close', (code, childSignal) => {
|
|
177
|
+
// A detached lifecycle script can close npm's pipes before it exits.
|
|
178
|
+
// Do not let npm's exit cancel escalation for that remaining group.
|
|
179
|
+
if ((canceling || signal?.aborted) && process.platform !== 'win32') terminate(true);
|
|
180
|
+
cleanup();
|
|
181
|
+
if (canceling || signal?.aborted) reject(abortError());
|
|
182
|
+
else if (code === 0) resolve();
|
|
183
|
+
else reject(new Error(`npm install failed (${childSignal || code}).${tail.trim() ? '\n' + tail.trim() : ''}`));
|
|
184
|
+
});
|
|
185
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
186
|
+
if (signal?.aborted) cancel();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function createProject(directory, options = {}, services = {}) {
|
|
191
|
+
const root = path.resolve(directory), typescript = options.typescript !== false, install = options.install !== false;
|
|
192
|
+
aborted(options.signal);
|
|
193
|
+
const nodeVersion = services.nodeVersion || process.version;
|
|
194
|
+
if (!supportedNode(nodeVersion)) throw new Error(`Node ${nodeVersion} is unsupported. Use Node ${packageManifest.engines.node}.`);
|
|
195
|
+
await physicalDirectory(root, true);
|
|
196
|
+
const existing = await info(root);
|
|
197
|
+
if (existing && (await readdir(root)).length) throw new Error(`Project directory must be empty; no files were changed: ${root}`);
|
|
198
|
+
if (install && await parentWorkspace(root)) throw new Error('Automatic installation inside an existing workspace is not supported. Use --no-install, then add the project with the workspace package manager. No files were changed.');
|
|
199
|
+
aborted(options.signal);
|
|
200
|
+
await mkdir(root, { recursive: true });
|
|
201
|
+
await physicalDirectory(root);
|
|
202
|
+
const lockPath = path.join(root, '.prnext-create.lock');
|
|
203
|
+
const lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW || 0), 0o600);
|
|
204
|
+
const locked = await lock.stat();
|
|
205
|
+
try {
|
|
206
|
+
if ((await readdir(root)).some(name => name !== '.prnext-create.lock')) throw new Error(`Project directory is no longer empty; existing files were preserved: ${root}`);
|
|
207
|
+
const name = projectName(root), files = projectFiles(name, typescript);
|
|
208
|
+
for (const [relative, content] of files) {
|
|
209
|
+
aborted(options.signal);
|
|
210
|
+
await physicalDirectory(root);
|
|
211
|
+
const target = path.join(root, relative), parent = path.dirname(target);
|
|
212
|
+
await physicalDirectory(parent, true);
|
|
213
|
+
await mkdir(parent, { recursive: true });
|
|
214
|
+
await physicalDirectory(parent);
|
|
215
|
+
const file = await open(target, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW || 0), 0o644);
|
|
216
|
+
try { await file.writeFile(content, 'utf8'); } finally { await file.close(); }
|
|
217
|
+
}
|
|
218
|
+
const report = { ok: true, status: 'prepared', root, name, typescript, files: [...files.keys()], installed: false, next: install ? ['npm run dev'] : ['npm install', 'npm run dev'] };
|
|
219
|
+
if (install) {
|
|
220
|
+
try {
|
|
221
|
+
aborted(options.signal);
|
|
222
|
+
await (services.runInstall || installProject)(root, { signal: options.signal, onOutput: options.onOutput });
|
|
223
|
+
aborted(options.signal);
|
|
224
|
+
report.installed = true;
|
|
225
|
+
report.status = 'created';
|
|
226
|
+
} catch (error) {
|
|
227
|
+
report.ok = false;
|
|
228
|
+
report.status = error.name === 'AbortError' || options.signal?.aborted ? 'cancelled' : 'install-failed';
|
|
229
|
+
report.error = error.message;
|
|
230
|
+
report.next = ['npm install', 'npm run dev'];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return report;
|
|
234
|
+
} finally {
|
|
235
|
+
await lock.close();
|
|
236
|
+
const current = await info(lockPath);
|
|
237
|
+
if (current?.isFile() && !current.isSymbolicLink() && sameFile(current, locked)) await unlink(lockPath);
|
|
238
|
+
}
|
|
239
|
+
}
|
package/build/css-modules.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { build as bundle } from 'esbuild';
|
|
2
|
-
import { mkdir, writeFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, writeFile, realpath, readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { runInNewContext } from 'node:vm';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { createStyleProcessor, stylesheetPattern, moduleStylesheetPattern } from './styles.mjs';
|
|
7
|
+
import { createStyleProcessor, resolveStandardBrowserTargets, stylesheetPattern, moduleStylesheetPattern } from './styles.mjs';
|
|
8
8
|
import {rename} from '../runtime/fs.mjs';
|
|
9
9
|
import {randomUUID} from 'node:crypto';
|
|
10
10
|
import {rm} from 'node:fs/promises';
|
|
11
|
+
import {cachedCompilation} from './compilation-cache.mjs';
|
|
12
|
+
import {compactCssModule,globalCssNames} from './compact-css.mjs';
|
|
11
13
|
|
|
12
14
|
const assetLoaders = Object.fromEntries(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'].map(ext => [ext, 'file']));
|
|
13
15
|
|
|
@@ -23,13 +25,34 @@ async function resolveCss(esbuild, args) {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** One cached CSS compilation supplies identical class maps to the server and the browser. */
|
|
26
|
-
export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/assets', sassOptions, cssEngine }) {
|
|
27
|
-
const cache = new Map();
|
|
28
|
-
|
|
28
|
+
export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/assets', sassOptions, cssEngine, dev = false, config = {} }) {
|
|
29
|
+
const cache = new Map(), compactNames = new Map();
|
|
30
|
+
let targets;
|
|
31
|
+
const browserTargets=()=>targets ??= Promise.resolve().then(()=>resolveStandardBrowserTargets(projectRoot));
|
|
32
|
+
const processor = createStyleProcessor({ projectRoot, sassOptions, cssEngine, browserTargets });
|
|
29
33
|
const hash = file => createHash('sha256').update(path.relative(projectRoot, file).replaceAll(path.sep, '/')).digest('hex').slice(0, 12);
|
|
34
|
+
let eligibility;
|
|
35
|
+
async function cacheEligible() {
|
|
36
|
+
if (dev || cssEngine==='postcss' || config.webpack || Object.keys(config.turbopack?.rules || {}).length || process.env.BROWSERSLIST_CONFIG || process.env.BROWSERSLIST_STATS || /\b(?:extends|stats)\b/i.test(process.env.BROWSERSLIST || '')) return false;
|
|
37
|
+
// A user PostCSS plugin can read arbitrary files, glob directories or use
|
|
38
|
+
// network/time without declaring dependencies. Its output is never cached.
|
|
39
|
+
const names = await readdir(projectRoot);
|
|
40
|
+
if (names.some(name => /^(?:postcss\.config|\.postcssrc)(?:\.|$)/.test(name))) return false;
|
|
41
|
+
try { if (JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8')).postcss !== undefined) return false; }
|
|
42
|
+
catch (error) { if (error.code !== 'ENOENT') return false; }
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const validMapping = value => {try{const parsed=JSON.parse(value);return parsed&&typeof parsed==='object'&&!Array.isArray(parsed)&&Object.values(parsed).every(value=>typeof value==='string');}catch{return false;}};
|
|
46
|
+
const validResult = value => value && typeof value.mapping === 'string' && value.mapping.length <= 16*1024*1024 && validMapping(value.mapping) && typeof value.css === 'string' && value.css.length <= 16*1024*1024
|
|
47
|
+
&& Array.isArray(value.compactNames) && value.compactNames.length <= 65536 && value.compactNames.every(item=>Array.isArray(item)&&item.length===2&&typeof item[0]==='string'&&item[0].length<=4096&&typeof item[1]==='string'&&/^p[\w-]{10}$/.test(item[1]))
|
|
48
|
+
&& Array.isArray(value.watchFiles) && value.watchFiles.length <= 4096 && value.watchFiles.every(file => typeof file==='string' && path.isAbsolute(file) && !file.includes('\0') && !/\.s[ac]ss$/i.test(file))
|
|
49
|
+
&& Array.isArray(value.assets) && value.assets.length <= 4096 && value.assets.every(asset => asset && typeof asset.name === 'string'
|
|
50
|
+
&& asset.name && asset.name === path.basename(asset.name) && !/[\\/\0<>:"|?*#]/.test(asset.name) && asset.name !== '.' && asset.name !== '..'
|
|
51
|
+
&& typeof asset.data === 'string' && asset.data.length<=24*1024*1024 && asset.data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(asset.data))
|
|
52
|
+
&& new Set(value.assets.map(asset=>asset.name)).size===value.assets.length && value.assets.reduce((sum,asset)=>sum+asset.data.length,0)<=48*1024*1024;
|
|
30
53
|
|
|
31
54
|
async function compile(file) {
|
|
32
|
-
const dependencies=new Set([file]);
|
|
55
|
+
const dependencies=new Set([file]), globals = new Set();
|
|
33
56
|
const result = await bundle({
|
|
34
57
|
absWorkingDir: projectRoot,
|
|
35
58
|
stdin: { contents: `import styles from ${JSON.stringify(file)}; export default styles;`, resolveDir: projectRoot, loader: 'js' },
|
|
@@ -57,11 +80,13 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
57
80
|
});
|
|
58
81
|
esbuild.onLoad({ filter: /.*/, namespace: 'prnext-local-css' }, async args => {
|
|
59
82
|
const transformed = await processor.process(args.pluginData.source);
|
|
83
|
+
if (!dev) globalCssNames(transformed.css, {local:true}, globals);
|
|
60
84
|
for(const file of transformed.watchFiles)dependencies.add(file);
|
|
61
85
|
return { contents: transformed.css, loader: 'local-css', resolveDir: path.dirname(args.pluginData.source), watchFiles: transformed.watchFiles };
|
|
62
86
|
});
|
|
63
87
|
esbuild.onLoad({ filter: stylesheetPattern, namespace: 'file' }, async args => {
|
|
64
88
|
const transformed = await processor.process(args.path);
|
|
89
|
+
if (!dev) globalCssNames(transformed.css, {local:false}, globals);
|
|
65
90
|
for(const file of transformed.watchFiles)dependencies.add(file);
|
|
66
91
|
return { contents: transformed.css, loader: 'css', resolveDir: path.dirname(args.path), watchFiles: transformed.watchFiles };
|
|
67
92
|
});
|
|
@@ -77,18 +102,37 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
77
102
|
// The isolated context deliberately has no require, process, network, or filesystem.
|
|
78
103
|
const module = { exports: {} };
|
|
79
104
|
runInNewContext(javascript.text, { module }, { timeout: 1000, filename: 'prnext-css-exports.cjs' });
|
|
80
|
-
const
|
|
81
|
-
|
|
105
|
+
const compact = dev ? {mapping:module.exports.default,css:stylesheet.text,names:[]} : compactCssModule(stylesheet.text,module.exports.default,{globals});
|
|
106
|
+
const mapping = JSON.stringify({ ...compact.mapping, ...(await processor.process(file)).exports });
|
|
107
|
+
const assets = [];
|
|
82
108
|
for (const output of result.outputFiles) {
|
|
83
109
|
if (output === javascript || output === stylesheet) continue;
|
|
84
|
-
|
|
85
|
-
try{await writeFile(temporary,output.contents);await rename(temporary,target);}finally{await rm(temporary,{force:true});}
|
|
110
|
+
assets.push({name:path.basename(output.path),data:Buffer.from(output.contents).toString('base64')});
|
|
86
111
|
}
|
|
87
|
-
return { mapping, css:
|
|
112
|
+
return { mapping, css: compact.css,compactNames:compact.names,watchFiles:[...dependencies],assets };
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
function get(file) {
|
|
91
|
-
if (!cache.has(file)) cache.set(file,
|
|
116
|
+
if (!cache.has(file)) cache.set(file, (async () => {
|
|
117
|
+
eligibility ??= cacheEligible();
|
|
118
|
+
const enabled = await eligibility && /\.css$/i.test(file);
|
|
119
|
+
const result = await cachedCompilation({root:projectRoot,kind:'css-modules',inputs:[file],excluded:[path.resolve(projectRoot,config.distDir || '.prnext'),...(config.output==='export'?[path.join(projectRoot,'out')]:[])],styleTargets:true,settings:{file,assetBase,cssEngine:cssEngine || 'auto',...(enabled?{browserTargets:await browserTargets()}:{})},
|
|
120
|
+
enabled,reason:dev?'development':'custom-style-pipeline',validate:validResult,
|
|
121
|
+
bytes:value=>Buffer.byteLength(value.mapping)+Buffer.byteLength(value.css)+Buffer.byteLength(JSON.stringify(value.compactNames))+value.assets.reduce((sum,asset)=>sum+Buffer.byteLength(asset.data,'base64'),0),
|
|
122
|
+
produce:async()=>{const value=await compile(file);return {value,dependencies:value.watchFiles};}});
|
|
123
|
+
await mkdir(path.join(stage,'assets'),{recursive:true});
|
|
124
|
+
for(const [original,short] of result.compactNames || []) {
|
|
125
|
+
if(compactNames.has(short)&&compactNames.get(short)!==original)throw new Error('CSS Module compact-name collision across modules.');
|
|
126
|
+
compactNames.set(short,original);
|
|
127
|
+
}
|
|
128
|
+
for (const asset of result.assets) {
|
|
129
|
+
const target=path.join(stage,'assets',asset.name),temporary=target+'.'+randomUUID()+'.tmp';
|
|
130
|
+
try{await writeFile(temporary,Buffer.from(asset.data,'base64'));await rename(temporary,target);}finally{await rm(temporary,{force:true});}
|
|
131
|
+
}
|
|
132
|
+
// Keep the original public result and release replay-only binary payloads.
|
|
133
|
+
// The per-build mapping cache must not retain a base64 copy of every asset.
|
|
134
|
+
return {mapping:result.mapping,css:result.css,watchFiles:result.watchFiles};
|
|
135
|
+
})());
|
|
92
136
|
return cache.get(file);
|
|
93
137
|
}
|
|
94
138
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const INDEX = 'deployment-assets.json';
|
|
6
|
+
const MAX_FILES = 1024, MAX_BYTES = 64 * 1024 * 1024, MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const MAX_AGE = 60 * 60 * 1000, MAX_GENERATIONS = 2;
|
|
8
|
+
// Only build-owned, immutable code/styles. Never JSON, maps, HTML, public/ or server/.
|
|
9
|
+
const safeName = value => typeof value === 'string' && value.length < 240 && /^[a-zA-Z0-9_.-]+-[a-zA-Z0-9]{8,64}\.(?:js|mjs|css|woff2?|ttf|otf|eot|png|jpe?g|avif|webp|gif|svg|ico|wasm)$/.test(value);
|
|
10
|
+
const assetKey = name => process.platform === 'win32' ? name.toLowerCase() : name;
|
|
11
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
async function regular(file, limit) {
|
|
13
|
+
const info = await lstat(file);
|
|
14
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > limit) return;
|
|
15
|
+
return info;
|
|
16
|
+
}
|
|
17
|
+
async function indexAt(directory) {
|
|
18
|
+
try {
|
|
19
|
+
if (!await regular(path.join(directory, INDEX), 1024 * 1024)) return;
|
|
20
|
+
const value = JSON.parse(await readFile(path.join(directory, INDEX), 'utf8'));
|
|
21
|
+
if (value.version !== 1 || !Array.isArray(value.generations) || value.generations.length > 6) return;
|
|
22
|
+
for (const generation of value.generations) {
|
|
23
|
+
if (typeof generation.id !== 'string' || generation.id.length > 128 || !Number.isSafeInteger(generation.createdAt) || !Array.isArray(generation.files) || generation.files.length > MAX_FILES) return;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const file of generation.files) {
|
|
26
|
+
if (!safeName(file.name) || seen.has(assetKey(file.name)) || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > MAX_FILE_BYTES || !/^[a-f0-9]{64}$/.test(file.hash)) return;
|
|
27
|
+
seen.add(assetKey(file.name));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) { /* Corrupt previous metadata is an optional retention miss. */ } }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Copy only public immutable assets into a stage, before its atomic publication. */
|
|
35
|
+
export async function retainDeploymentAssets({ stage, destination, manifest, policy, now = Date.now() }) {
|
|
36
|
+
if (manifest.dev) return { generations: 0, files: 0, bytes: 0 };
|
|
37
|
+
const maxAge = (policy && policy.maxAge !== undefined ? policy.maxAge * 1000 : MAX_AGE);
|
|
38
|
+
const maxGenerations = policy && policy.maxGenerations !== undefined ? policy.maxGenerations : MAX_GENERATIONS;
|
|
39
|
+
const maxBytes = policy && policy.maxBytes !== undefined ? policy.maxBytes : MAX_BYTES;
|
|
40
|
+
const assets = path.join(stage, 'assets');
|
|
41
|
+
const assetInfo = await lstat(assets);
|
|
42
|
+
if (!assetInfo.isDirectory() || assetInfo.isSymbolicLink()) throw new Error('Deployment assets must be a regular directory');
|
|
43
|
+
const current = { id: manifest.cacheId, createdAt: now, eligible: true, files: [] };
|
|
44
|
+
let indexedBytes = 0;
|
|
45
|
+
for (const name of (await readdir(assets)).sort()) {
|
|
46
|
+
if (!safeName(name)) {
|
|
47
|
+
// Maps/manifests do not participate in executable chunk graphs. Unknown
|
|
48
|
+
// content-addressed media and auxiliary directories make retention unsafe.
|
|
49
|
+
if (/\.(?:map|json|html|txt|gz|br)$/.test(name)) continue;
|
|
50
|
+
current.eligible = false; current.files = []; break;
|
|
51
|
+
}
|
|
52
|
+
const file = path.join(assets, name), info = await regular(file, MAX_FILE_BYTES);
|
|
53
|
+
if (!info || (indexedBytes += info.size) > 256 * 1024 * 1024) { current.eligible = false; current.files = []; break; }
|
|
54
|
+
const bytes = await readFile(file);
|
|
55
|
+
current.files.push({ name, bytes: bytes.length, hash: hash(bytes) });
|
|
56
|
+
if (current.files.length > MAX_FILES) { current.eligible = false; current.files = []; break; }
|
|
57
|
+
}
|
|
58
|
+
const previousRoot = await lstat(path.join(destination, 'assets')).catch(() => null);
|
|
59
|
+
const previous = !current.eligible || policy === false || !previousRoot?.isDirectory() || previousRoot.isSymbolicLink() ? undefined : await indexAt(destination), generations = [current], known = new Map(current.files.map(file => [assetKey(file.name), file]));
|
|
60
|
+
const retained = Object.create(null); let bytes = 0, count = 0;
|
|
61
|
+
for (const generation of previous?.generations || []) {
|
|
62
|
+
if (generation.eligible !== true || generations.length > maxGenerations || generation.id === current.id || generation.createdAt > now || now - generation.createdAt >= maxAge) continue;
|
|
63
|
+
if (generation.files.some(file => { const value = known.get(assetKey(file.name)); return value && (value.hash !== file.hash || value.bytes !== file.bytes); })) continue;
|
|
64
|
+
const candidates = generation.files.filter(file => !known.has(assetKey(file.name)));
|
|
65
|
+
const size = candidates.reduce((sum, file) => sum + file.bytes, 0);
|
|
66
|
+
if (bytes + size > maxBytes || count + candidates.length > MAX_FILES) continue;
|
|
67
|
+
const verified = [];
|
|
68
|
+
for (const file of candidates) {
|
|
69
|
+
const source = path.join(destination, 'assets', file.name);
|
|
70
|
+
try {
|
|
71
|
+
if (!await regular(source, MAX_FILE_BYTES)) continue;
|
|
72
|
+
const contents = await readFile(source);
|
|
73
|
+
if (contents.length !== file.bytes || hash(contents) !== file.hash) continue;
|
|
74
|
+
verified.push({file, contents});
|
|
75
|
+
} catch { /* An unreadable previous asset is an optional retention miss. */ }
|
|
76
|
+
}
|
|
77
|
+
// An incomplete previous generation cannot promise a working chunk graph.
|
|
78
|
+
if (verified.length !== candidates.length) continue;
|
|
79
|
+
for (const {file, contents} of verified) {
|
|
80
|
+
await writeFile(path.join(assets, file.name), contents, {flag:'wx'});
|
|
81
|
+
known.set(assetKey(file.name), file); retained[file.name] = generation.createdAt + maxAge;
|
|
82
|
+
bytes += file.bytes; count++;
|
|
83
|
+
}
|
|
84
|
+
generations.push(generation);
|
|
85
|
+
}
|
|
86
|
+
manifest.config.retainedAssets = retained;
|
|
87
|
+
await writeFile(path.join(stage, INDEX), JSON.stringify({ version: 1, generations }) + '\n');
|
|
88
|
+
return { generations: generations.length - 1, files: count, bytes };
|
|
89
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const defaults=Object.freeze({maxAge:3600,maxGenerations:2,maxBytes:64*1024*1024});
|
|
2
|
+
const limits={maxAge:[1,86400],maxGenerations:[1,5],maxBytes:[1024,256*1024*1024]};
|
|
3
|
+
|
|
4
|
+
/** Retain only bounded, immutable public assets; false revokes all carry-over. */
|
|
5
|
+
export function validateDeploymentRetention(value){
|
|
6
|
+
if(value===false)return false;
|
|
7
|
+
if(value===undefined)return {...defaults};
|
|
8
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||![Object.prototype,null].includes(Object.getPrototypeOf(value)))throw new TypeError('deploymentRetention must be false or an object.');
|
|
9
|
+
const output={...defaults};
|
|
10
|
+
for(const key of Reflect.ownKeys(value)){
|
|
11
|
+
if(!Object.hasOwn(limits,key))throw new TypeError(`deploymentRetention.${String(key)} is not supported.`);
|
|
12
|
+
if(value[key]===undefined)continue;
|
|
13
|
+
const [minimum,maximum]=limits[key];
|
|
14
|
+
if(!Number.isSafeInteger(value[key])||value[key]<minimum||value[key]>maximum)throw new TypeError(`deploymentRetention.${key} must be an integer from ${minimum} to ${maximum}.`);
|
|
15
|
+
output[key]=value[key];
|
|
16
|
+
}
|
|
17
|
+
return output;
|
|
18
|
+
}
|
package/build/dev-css-update.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
31
31
|
for(const dependency of manifest.devDependencies||[])for(const file of dependency.files)if(file.endsWith('.css'))known.add(path.join(root,file));
|
|
32
32
|
if(changed.some(file=>!known.has(path.join(root,file))))return fallback('unknown-style-dependency');
|
|
33
33
|
if(!selected.length)return fallback('unknown-style-dependency');
|
|
34
|
-
const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine});
|
|
34
|
+
const stage=destination,modules=createCssModules({projectRoot:root,stage,assetBase:manifest.config.assetBase,cssEngine,dev:true});
|
|
35
35
|
try{
|
|
36
36
|
await snapshot(source,stage);
|
|
37
37
|
for(const previous of manifest.devStyles.modules.filter(module=>(module.watchFiles||[module.file]).some(file=>changedPaths.has(file)))){
|
|
@@ -70,7 +70,9 @@ export async function tryDevCssUpdate({root,source,destination,manifest,changed,
|
|
|
70
70
|
old.buildId=buildId;
|
|
71
71
|
const changedNames=new Set(selected.map(([name])=>name));
|
|
72
72
|
const revise=urls=>urls.map(url=>changedNames.has(path.posix.basename(url.split('?')[0]))?url.split('?')[0]+'?v='+buildId:url);
|
|
73
|
-
|
|
73
|
+
const reviseScopes=scopes=>scopes?.map(scope=>({...scope,css:revise(scope.css)}));
|
|
74
|
+
for(const page of old.pages)page.css=revise(page.css);if(old.app){old.app.css=revise(old.app.css);for(const route of old.app.routes||[]){route.css=revise(route.css);if(route.cssScopes)route.cssScopes=reviseScopes(route.cssScopes);}}
|
|
75
|
+
next.routes=next.routes.map(route=>({...route,css:revise(route.css||[]),...(route.cssScopes?{cssScopes:reviseScopes(route.cssScopes)}:{})}));
|
|
74
76
|
await writeFile(path.join(stage,'assets',path.basename(next.devClient)),JSON.stringify(old));
|
|
75
77
|
const protectedNames=new Set([path.basename(baseClient),path.basename(next.devClient)]),history=[];
|
|
76
78
|
for(const name of await readdir(path.join(stage,'assets')))if(/^dev-manifest-.*\.json$/.test(name)&&!protectedNames.has(name)){const info=await stat(path.join(stage,'assets',name));history.push({name,size:info.size,time:info.mtimeMs});}
|
|
@@ -8,7 +8,7 @@ export function recordDevDependencies(state,options,result){
|
|
|
8
8
|
for(const item of input.imports||[])if(!item.external)imports.add(absolute(item.path));
|
|
9
9
|
state.devDependencies.set(file,imports);
|
|
10
10
|
}
|
|
11
|
-
for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')){
|
|
11
|
+
for(const [name,output]of Object.entries(result.metafile.outputs))if(name.endsWith('.css')&&path.resolve(options.absWorkingDir||state.root,name).startsWith(path.join(state.stage,'assets')+path.sep)){
|
|
12
12
|
const inputs=Object.keys(output.inputs||{}).filter(file=>file!=='<stdin>'),imported=new Set();
|
|
13
13
|
for(const input of inputs)for(const item of result.metafile.inputs[input]?.imports||[])if(item.kind==='import-rule')imported.add(item.path);
|
|
14
14
|
const roots=inputs.filter(input=>!imported.has(input));
|
package/build/dev-discovery.mjs
CHANGED
|
@@ -19,9 +19,9 @@ export async function discoverDevelopment(root) {
|
|
|
19
19
|
try {
|
|
20
20
|
const config=await loadProjectConfig(root,{dev:true,envMode});
|
|
21
21
|
const project=await scanProject(root,{basePath:config.basePath,pageExtensions:config.pageExtensions,i18n:config.i18n,devAppFiles:new Set()});
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const eager
|
|
22
|
+
// Destinations unknown before routing are represented by deferred entries.
|
|
23
|
+
// Their worker asks the compiler only after middleware has run once.
|
|
24
|
+
const eager=false;
|
|
25
25
|
return {project,eager,cssFast:!config.webpack&&!Object.keys(config.turbopack?.rules||{}).length,config:{basePath:config.basePath,assetPrefix:config.assetPrefix,i18n:config.i18n,distDir:config.distDir,
|
|
26
26
|
onDemandEntries:config.onDemandEntries,typescript:config.typescript,typedRoutes:config.typedRoutes,cssEngine:config.cssEngine}};
|
|
27
27
|
} finally {restore();}
|
package/build/dev.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { cachedAsyncTransform } from './transform-cache.mjs';
|
|
8
|
-
import {inlineSourceMap} from './source-maps.mjs';
|
|
8
|
+
import {inlineSourceMap,extractSourceMap} from './source-maps.mjs';
|
|
9
|
+
import {TraceMap,originalPositionFor} from '@jridgewell/trace-mapping';
|
|
9
10
|
|
|
10
11
|
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
11
12
|
export const devClientFile = path.join(packageRoot, 'runtime/dev-client.mjs');
|
|
@@ -25,10 +26,14 @@ function namedDefaultComponents({types:t}){
|
|
|
25
26
|
}
|
|
26
27
|
}}}};
|
|
27
28
|
}
|
|
28
|
-
function sourceElements(id){return ({types:t})=>({visitor:{JSXOpeningElement(node){
|
|
29
|
+
function sourceElements(id,trace){return ({types:t})=>({visitor:{JSXOpeningElement(node){
|
|
29
30
|
const name=node.node.name;
|
|
30
31
|
if(name.type!=='JSXIdentifier'||!/^[a-z]/.test(name.name)||node.node.attributes.some(attribute=>attribute.type==='JSXAttribute'&&attribute.name.name==='data-prnext-source'))return;
|
|
31
|
-
|
|
32
|
+
let start=node.node.loc?.start;if(!start)return;
|
|
33
|
+
// React Compiler and custom loaders may move JSX before instrumentation.
|
|
34
|
+
// Follow their map back to the editable source; generated, unmapped elements
|
|
35
|
+
// must not send the inspector to an invented line in the user's component.
|
|
36
|
+
if(trace){const original=originalPositionFor(trace,start);if(original.line==null||original.column==null)return;start=original;}
|
|
32
37
|
node.node.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-prnext-source'),t.stringLiteral(`${id}:${start.line}:${start.column+1}`)));
|
|
33
38
|
}}});}
|
|
34
39
|
|
|
@@ -41,8 +46,9 @@ export function createSourceLocationTransform(projectRoot){
|
|
|
41
46
|
const root=filename.startsWith(projectRoot+path.sep)?projectRoot:await canonicalRoot;
|
|
42
47
|
if(!filename.startsWith(root+path.sep))return source;
|
|
43
48
|
const id=path.relative(root,filename).split(path.sep).join('/');
|
|
44
|
-
return cachedAsyncTransform('dev-source-elements',source,[root,filename,'
|
|
45
|
-
const
|
|
49
|
+
return cachedAsyncTransform('dev-source-elements',source,[root,filename,'v2-original-locations'],async()=>{
|
|
50
|
+
const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
|
|
51
|
+
const result=await transformAsync(source,{filename,babelrc:false,configFile:false,sourceMaps:true,sourceFileName:filename,sourceType:'unambiguous',parserOpts:{plugins:['jsx',...(/\.[cm]?tsx?$/.test(filename)?['typescript']:[])]},plugins:[sourceElements(id,trace)]});
|
|
46
52
|
return inlineSourceMap(result.code,result.map);
|
|
47
53
|
});
|
|
48
54
|
};
|
|
@@ -96,12 +102,13 @@ export function createRefreshTransform(projectRoot, { generation,forceGeneration
|
|
|
96
102
|
const root = filename.startsWith(projectRoot + path.sep) ? projectRoot : await canonicalRoot;
|
|
97
103
|
if (!filename.startsWith(root + path.sep)) return source;
|
|
98
104
|
const hash = createHash('sha256').update(source).digest('hex');
|
|
99
|
-
const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-
|
|
105
|
+
const transformed = await cachedAsyncTransform('fast-refresh',source,[projectRoot,filename,'source-elements-v3-original-locations'],async () => {
|
|
100
106
|
const id = path.relative(root, filename).replaceAll(path.sep, '/');
|
|
107
|
+
const input=extractSourceMap(source),trace=input.map?new TraceMap(input.map):undefined;
|
|
101
108
|
const result = await transformAsync(source, { filename, babelrc: false, configFile: false,
|
|
102
109
|
ast: true, sourceMaps: true, sourceFileName:filename, sourceType: 'unambiguous',
|
|
103
|
-
parserOpts: { plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
104
|
-
plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id)],
|
|
110
|
+
parserOpts: { plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] },
|
|
111
|
+
plugins: [namedDefaultComponents,[refresh, { skipEnvCheck: true, refreshReg: '__prnextRefreshReg', refreshSig: '__prnextRefreshSig' }],sourceElements(id,trace)],
|
|
105
112
|
});
|
|
106
113
|
// An ESM import changes webpack's interpretation of module.exports.
|
|
107
114
|
// Keep script/CommonJS inputs in their original module format.
|
package/build/dynamic.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export function transformDynamicImports(source, filename, { projectRoot, mode =
|
|
|
67
67
|
|
|
68
68
|
function transformDynamicImportsUncached(source, filename, {projectRoot,mode}) {
|
|
69
69
|
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: filename,
|
|
70
|
-
plugins: ['jsx', ...(/\.tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
70
|
+
plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] });
|
|
71
71
|
const relative = path.relative(projectRoot || process.cwd(), filename).replaceAll(path.sep, '/');
|
|
72
72
|
let changed = false;
|
|
73
73
|
traverse(ast, {
|
package/build/edge.mjs
CHANGED
|
@@ -60,7 +60,7 @@ function validateUncached(source,file,reactMode) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Bundle the entire application graph as Web ESM before entering the V8 realm. */
|
|
63
|
-
export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource }) {
|
|
63
|
+
export async function compileEdge({ file, outfile, projectRoot, dev = false, defineEnvironment = {}, defaultExport = false, reactMode, plugins = [], transformSource, transformTypedHelpers = false }) {
|
|
64
64
|
const runtime = path.resolve(path.dirname(outfile), '../runtime');
|
|
65
65
|
await mkdir(runtime, { recursive: true });
|
|
66
66
|
try { await access(path.join(runtime, 'edge-vm.cjs')); }
|
|
@@ -85,7 +85,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
|
|
|
85
85
|
mainFields: ['browser', 'module', 'main'], logLevel: 'silent', metafile: true,
|
|
86
86
|
define: { 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'), ...defineEnvironment, 'process.env.NEXT_RUNTIME': '"edge"' },
|
|
87
87
|
plugins: [...plugins, { name: 'prnext-edge', setup(builder) {
|
|
88
|
-
applicationEntry(builder,readCompilerFile);
|
|
88
|
+
applicationEntry(builder,readCompilerFile,{transformTypedHelpers});
|
|
89
89
|
builder.onResolve({ filter: /.*/ }, args => {
|
|
90
90
|
if (builtin.has(args.path) || args.path.startsWith('node:')) return { errors: [{ text: `Node.js module ${args.path} cannot run in the Edge Runtime (${args.importer}).` }] };
|
|
91
91
|
if (reactMode && ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom', 'react-server-dom-webpack/server', 'react-server-dom-webpack/server.node'].includes(args.path)) return { path: args.path, namespace: 'prnext-edge-react' };
|
|
@@ -112,7 +112,7 @@ export async function compileEdge({ file, outfile, projectRoot, dev = false, def
|
|
|
112
112
|
: names[args.path].map(name => `export const ${name}=globalThis.__PRNEXT_EDGE_HOST.${args.path}.${name};`).join('\n') }));
|
|
113
113
|
builder.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async args => {
|
|
114
114
|
let source = args.prnextSource ?? await readFile(args.path, 'utf8');
|
|
115
|
-
if (transformSource) source = await transformSource(source, args.path, builder);
|
|
115
|
+
if (transformSource) source = await transformSource(source, args.path, builder, {loaderOutput:args.prnextSource!==undefined});
|
|
116
116
|
if (!trusted.has(args.path)) source = validate(source, args.path, reactMode);
|
|
117
117
|
return { contents: source, loader: /\.[cm]?tsx?$/.test(args.path) ? 'tsx' : 'jsx', resolveDir: path.dirname(args.path) };
|
|
118
118
|
});
|
package/build/fonts.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export function createFonts({ projectRoot, stage, assetBase }) {
|
|
|
51
51
|
}
|
|
52
52
|
async function transformSource(source, file) {
|
|
53
53
|
if (/(?:^|[/\\])pages[/\\]_document\.[cm]?[jt]sx?$/.test(file)) throw new Error('next/font cannot be used in pages/_document. Define fonts in pages/_app or a page component.');
|
|
54
|
-
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
54
|
+
const ast = parse(source, { sourceType: 'unambiguous', sourceFilename: file, plugins: ['jsx', ...(/\.[cm]?tsx?$/.test(file) ? ['typescript'] : [])] });
|
|
55
55
|
let program;
|
|
56
56
|
traverse(ast, { Program(value) { program = value; value.stop(); } });
|
|
57
57
|
const imports = [], calls = [];
|