@thomas.f/prnext 0.1.16-alpha → 0.1.17-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/AI.md +63 -0
- package/README.md +384 -9
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +5 -0
- package/build/analyze.mjs +184 -0
- package/build/app-config.mjs +7 -8
- package/build/app-static.mjs +12 -7
- package/build/app.mjs +23 -10
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +20 -5
- package/build/check.mjs +56 -11
- package/build/compiler.mjs +25 -1
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +21 -13
- package/build/css-modules.mjs +15 -4
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +83 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +55 -4
- package/build/edge.mjs +3 -2
- package/build/framework-imports.mjs +40 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +96 -101
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +2 -3
- package/build/migrate.mjs +60 -57
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +97 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-pages.mjs +90 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +80 -0
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +15 -3
- package/build/transform-cache.mjs +37 -4
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +91 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +22 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +5 -4
- package/build/webpack-graph.mjs +164 -27
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +105 -121
- package/compat/index.d.ts +21 -7
- package/compat/link.d.cts +4 -3
- package/compat/navigation.d.cts +4 -3
- package/compat/router.d.cts +4 -3
- package/compat/typed-route.d.cts +17 -0
- package/docs/ai/compatibility.md +106 -0
- package/docs/ai/data.md +128 -0
- package/docs/ai/deployment.md +111 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +146 -0
- package/docs/ai/routing.md +132 -0
- package/docs/ai/troubleshooting.md +120 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +25 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +17 -6
- package/runtime/app-render.mjs +12 -0
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +239 -45
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +186 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +18 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +307 -0
- package/runtime/env.mjs +2 -1
- package/runtime/pages-client.mjs +10 -1
- package/runtime/pages-navigation.mjs +6 -1
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/rsc-worker.mjs +7 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +12 -5
- package/runtime/worker.mjs +36 -2
package/build/loader-cache.mjs
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
import { rename } from '../runtime/fs.mjs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { readFile,
|
|
4
|
+
import { readFile, open, unlink, readdir, stat, lstat, realpath } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import {privateCacheDirectory,readCacheRecord} from './persistent-cache.mjs';
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
8
9
|
const MAX_ENTRY = 16 * 1024 * 1024, MAX_TOTAL = 64 * 1024 * 1024;
|
|
10
|
+
const MAX_DEPENDENCIES=10000;
|
|
9
11
|
async function fileHash(file) {
|
|
10
12
|
try { if ((await stat(file)).size > MAX_ENTRY) return null; return hash(await readFile(file)); }
|
|
11
13
|
catch(error) { if(error.code === 'ENOENT') return 'missing'; throw error; }
|
|
12
14
|
}
|
|
13
15
|
async function directoryHash(root) {
|
|
14
|
-
|
|
16
|
+
try{if(!(await stat(root)).isDirectory())return null;}catch(error){if(error.code==='ENOENT')return 'missing';throw error;}
|
|
17
|
+
const records = [],seen=new Set(); let count = 0;
|
|
15
18
|
async function walk(directory) {
|
|
19
|
+
const canonical=await realpath(directory);if(seen.has(canonical))throw new Error('Cyclic loader context dependency');seen.add(canonical);
|
|
16
20
|
for (const name of (await readdir(directory)).sort()) {
|
|
17
|
-
if (++count >
|
|
21
|
+
if (++count > MAX_DEPENDENCIES) throw new Error('Loader context dependency exceeds 10000 files');
|
|
18
22
|
const file=path.join(directory,name), metadata=await stat(file);
|
|
19
|
-
|
|
23
|
+
const digest=metadata.isDirectory()?'directory':await fileHash(file);
|
|
24
|
+
if(digest===null||digest==='missing')throw new Error('Unstable or excessive loader context dependency');
|
|
25
|
+
records.push([path.relative(root,file),digest]);
|
|
20
26
|
if(metadata.isDirectory())await walk(file);
|
|
21
27
|
}
|
|
28
|
+
seen.delete(canonical);
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
catch(error) { if(error.code==='ENOENT')return 'missing'; throw error; }
|
|
30
|
+
await walk(root);return hash(JSON.stringify(records));
|
|
25
31
|
}
|
|
26
32
|
function loaderModules(loaders) {
|
|
27
33
|
const files=new Set();
|
|
@@ -30,41 +36,62 @@ function loaderModules(loaders) {
|
|
|
30
36
|
return [...files];
|
|
31
37
|
}
|
|
32
38
|
async function snapshot(files,contexts,missing) {
|
|
39
|
+
if(files.length+contexts.length+missing.length>MAX_DEPENDENCIES)throw new Error('Excessive loader dependencies');
|
|
33
40
|
return {files:await Promise.all([...new Set([...files,...missing])].sort().map(async file=>[file,await fileHash(file)])),
|
|
34
41
|
contexts:await Promise.all([...new Set(contexts)].sort().map(async file=>[file,await directoryHash(file)]))};
|
|
35
42
|
}
|
|
36
43
|
async function valid(snapshot) {
|
|
37
|
-
if(snapshot
|
|
44
|
+
if(!validSnapshot(snapshot))return false;
|
|
38
45
|
for(const [file,digest] of snapshot.files)if(await fileHash(file)!==digest)return false;
|
|
39
46
|
for(const [file,digest] of snapshot.contexts)if(await directoryHash(file)!==digest)return false;
|
|
40
47
|
return true;
|
|
41
48
|
}
|
|
42
49
|
async function prune(directory) {
|
|
43
|
-
const entries
|
|
50
|
+
const entries=[];
|
|
51
|
+
for(const name of await readdir(directory))if(/^[a-f0-9]{64}\.json$/.test(name)){
|
|
52
|
+
const info=await lstat(path.join(directory,name)).catch(()=>null);
|
|
53
|
+
if(info?.isFile()&&!info.isSymbolicLink())entries.push({name,size:info.size,mtimeMs:info.mtimeMs});
|
|
54
|
+
}
|
|
44
55
|
entries.sort((a,b)=>b.mtimeMs-a.mtimeMs);let bytes=0;
|
|
45
|
-
for(let index=0;index<entries.length;index++){bytes+=entries[index].size;if(index>=256||bytes>MAX_TOTAL)await
|
|
56
|
+
for(let index=0;index<entries.length;index++){bytes+=entries[index].size;if(index>=256||bytes>MAX_TOTAL)await unlink(path.join(directory,entries[index].name)).catch(()=>{});}
|
|
57
|
+
}
|
|
58
|
+
const validFile=value=>typeof value==='string'&&path.isAbsolute(value)&&!value.includes('\0')&&value.length<=32768;
|
|
59
|
+
const validFiles=value=>Array.isArray(value)&&value.length<=MAX_DEPENDENCIES&&value.every(validFile);
|
|
60
|
+
const digest=value=>value==='missing'||typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
|
|
61
|
+
function validSnapshot(value){return !!value&&typeof value==='object'&&['files','contexts'].every(key=>Array.isArray(value[key])&&value[key].length<=MAX_DEPENDENCIES&&value[key].every(item=>Array.isArray(item)&&item.length===2&&validFile(item[0])&&digest(item[1])));}
|
|
62
|
+
function validResult(value){
|
|
63
|
+
return !!value&&typeof value==='object'&&typeof value.source==='string'&&value.source.length<=MAX_ENTRY&&
|
|
64
|
+
['dependencies','contexts','missing'].every(key=>validFiles(value[key]))&&
|
|
65
|
+
Array.isArray(value.warnings)&&value.warnings.length<=MAX_DEPENDENCIES&&value.warnings.every(item=>typeof item==='string')&&
|
|
66
|
+
Array.isArray(value.assets)&&value.assets.length<=4096&&value.assets.every(item=>item&&typeof item.name==='string'&&!path.isAbsolute(item.name)&&!/[\\\0?#]/.test(item.name)&&item.name.split('/').every(part=>part&&part!=='.'&&part!=='..')&&typeof item.data==='string'&&item.data.length<=MAX_ENTRY&&item.data.length%4===0&&/^[A-Za-z0-9+/]*={0,2}$/.test(item.data))&&
|
|
67
|
+
(value.map===undefined||value.map&&typeof value.map==='object'&&!Array.isArray(value.map)&&value.map.version===3)&&
|
|
68
|
+
(value.cacheable===undefined||typeof value.cacheable==='boolean');
|
|
69
|
+
}
|
|
70
|
+
async function publish(directory,key,value){
|
|
71
|
+
const payload=JSON.stringify(value),body=JSON.stringify({version:1,key,checksum:hash(payload),payload});
|
|
72
|
+
if(Buffer.byteLength(body)>MAX_ENTRY)return;
|
|
73
|
+
const destination=path.join(directory,key+'.json'),temporary=destination+'.'+randomUUID()+'.tmp';
|
|
74
|
+
try{const file=await open(temporary,'wx',0o600);try{await file.writeFile(body);}finally{await file.close();}await rename(temporary,destination);await prune(directory);}
|
|
75
|
+
finally{await unlink(temporary).catch(()=>{});}
|
|
46
76
|
}
|
|
47
77
|
/** Cache only loaders which opt into dependency tracking; no render output enters this cache. */
|
|
48
78
|
export async function cachedLoader(options,run) {
|
|
79
|
+
const execute=()=>{for(const file of loaderModules(options.loaders))delete require.cache[file];return run();};
|
|
80
|
+
if(process.env.PRNEXT_BUILD_CACHE==='0')return execute();
|
|
49
81
|
let serializable = true;
|
|
50
82
|
try { JSON.stringify(options,(_key,value)=>{if(typeof value==='function'||typeof value==='symbol'||typeof value==='bigint'||value instanceof RegExp)serializable=false;return value;}); } catch { serializable=false; }
|
|
51
|
-
if (!serializable) return
|
|
52
|
-
|
|
53
|
-
const key=hash(JSON.stringify({...options,env:hash(JSON.stringify(Object.entries(process.env).sort()))}));
|
|
54
|
-
const file=path.join(directory,key+'.json');
|
|
83
|
+
if (!serializable) return execute();
|
|
84
|
+
let directory,key;
|
|
55
85
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const result=await
|
|
60
|
-
if(result
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const temporary=file+'.'+randomUUID()+'.tmp';
|
|
67
|
-
try {await writeFile(temporary,body);await rename(temporary,file);}finally{await rm(temporary,{force:true});}
|
|
68
|
-
await prune(directory);
|
|
86
|
+
directory=await privateCacheDirectory(options.root,'loaders-v2');
|
|
87
|
+
if(directory){key=hash(JSON.stringify({...options,node:process.version,env:hash(JSON.stringify(Object.entries(process.env).sort()))}));const cached=await readCacheRecord(directory,key,MAX_ENTRY);if(cached?.format===2&&validResult(cached.result)&&await valid(cached.snapshot))return cached.result;}
|
|
88
|
+
} catch{/* Cache availability and malformed records never prevent a loader run. */}
|
|
89
|
+
const result=await execute();
|
|
90
|
+
if(!directory||!key||result?.cacheable===false||!validResult(result))return result;
|
|
91
|
+
try{
|
|
92
|
+
const dependencies=[...new Set([options.file,...result.dependencies,...loaderModules(options.loaders),...['package.json','package-lock.json','npm-shrinkwrap.json','pnpm-lock.yaml','yarn.lock','bun.lock','bun.lockb'].map(name=>path.join(options.root,name))])];
|
|
93
|
+
const current=await snapshot(dependencies,result.contexts,result.missing);
|
|
94
|
+
if(validSnapshot(current))await publish(directory,key,{format:2,snapshot:current,result:{...result,dependencies}});
|
|
95
|
+
}catch{/* Successful loader output remains usable when snapshot/publication fails. */}
|
|
69
96
|
return result;
|
|
70
97
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import {analyzeSource} from './source-analysis.mjs';
|
|
5
5
|
import { validateAppConfig, mergeAppConfig } from './app-config.mjs';
|
|
6
6
|
import { scanMetadataImages } from './metadata-images.mjs';
|
|
7
7
|
import { pageExtensionPattern, validatePageSource } from './page-extensions.mjs';
|
|
@@ -50,8 +50,7 @@ export async function scanMetadataRoutes({ root, appRoot, files, routes, pattern
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function metadataExportNames(source, file) {
|
|
53
|
-
|
|
54
|
-
return ast.program.body.flatMap(node => node.type === 'ExportNamedDeclaration' ? [node.declaration?.id?.name, ...(node.declaration?.declarations || []).map(item => item.id.name), ...node.specifiers.map(item => item.exported?.name)] : []);
|
|
53
|
+
return analyzeSource(source,file).program.body.flatMap(node => node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type' ? [node.declaration?.id?.name, ...(node.declaration?.declarations || []).map(item => item.id.name), ...node.specifiers.filter(item=>item.exportKind!=='type').map(item => item.exported?.name ?? item.exported?.value)] : []).filter(Boolean);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export async function prepareMetadataRoutes(project, stage) {
|
package/build/migrate.mjs
CHANGED
|
@@ -2,23 +2,16 @@ import { rename } from '../runtime/fs.mjs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import {
|
|
6
|
-
import { constants } from 'node:fs';
|
|
7
|
-
import { readFile, writeFile, rm, lstat, realpath, access } from 'node:fs/promises';
|
|
5
|
+
import { readFile, writeFile, rm, lstat, realpath } from 'node:fs/promises';
|
|
8
6
|
import { checkProject } from './check.mjs';
|
|
9
7
|
import { sourceCheckout } from '../native/resolve.mjs';
|
|
8
|
+
import { migrationInstallContext, regularMigrationFile, installWithManager, executeManager } from './package-manager.mjs';
|
|
10
9
|
|
|
11
10
|
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
12
11
|
const dependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies'];
|
|
13
|
-
const locks = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'];
|
|
14
12
|
const own = (object, key) => Object.hasOwn(object, key);
|
|
15
13
|
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
14
|
|
|
17
|
-
async function optionalFile(file) {
|
|
18
|
-
try { return await readFile(file); }
|
|
19
|
-
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
export function parseMigrationArgs(args) {
|
|
23
16
|
const options = { dryRun: false, install: true, json: false };
|
|
24
17
|
let directory;
|
|
@@ -172,31 +165,14 @@ export function migrationPackage(input, framework, spec) {
|
|
|
172
165
|
return { package: output, changes, notes };
|
|
173
166
|
}
|
|
174
167
|
|
|
175
|
-
async function installContext(root, pkg) {
|
|
176
|
-
const present = [];
|
|
177
|
-
for (const name of locks) if (await optionalFile(path.join(root, name))) present.push(name);
|
|
178
|
-
const manager = pkg.packageManager?.split('@')[0];
|
|
179
|
-
if (manager && manager !== 'npm') return { allowed: false, reason: `This project uses ${manager}. Use --no-install, then install with ${manager}.`, locks: present };
|
|
180
|
-
if (present.some(name => !['package-lock.json', 'npm-shrinkwrap.json'].includes(name))) {
|
|
181
|
-
return { allowed: false, reason: 'A non-npm lockfile is present. Use --no-install and the existing package manager to preserve its lockfile.', locks: present };
|
|
182
|
-
}
|
|
183
|
-
for (let directory = root; ; directory = path.dirname(directory)) {
|
|
184
|
-
const contents = directory === root ? null : await optionalFile(path.join(directory, 'package.json'));
|
|
185
|
-
let ancestor;
|
|
186
|
-
try { ancestor = directory === root ? pkg : contents && JSON.parse(contents); } catch { ancestor = null; }
|
|
187
|
-
if (ancestor?.workspaces) return { allowed: false, reason: 'A workspace package manager must be run from its workspace root. Use --no-install, then install from that root.', locks: present };
|
|
188
|
-
if (directory === path.dirname(directory)) break;
|
|
189
|
-
}
|
|
190
|
-
return { allowed: true, locks: present };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
168
|
async function backup(file, contents) {
|
|
194
169
|
for (let index = 0; index < 1000; index++) {
|
|
195
170
|
const destination = `${file}.prnext-backup${index ? '.' + index : ''}`;
|
|
196
171
|
try { await writeFile(destination, contents, { flag: 'wx', mode: 0o600 }); return destination; }
|
|
197
172
|
catch (error) {
|
|
198
173
|
if (error.code !== 'EEXIST') throw error;
|
|
199
|
-
|
|
174
|
+
const existing = await regularMigrationFile(destination);
|
|
175
|
+
if (existing?.contents.equals(contents)) return destination;
|
|
200
176
|
}
|
|
201
177
|
}
|
|
202
178
|
throw new Error(`Too many migration backups for ${file}.`);
|
|
@@ -206,34 +182,23 @@ async function replacePackage(file, original, text, mode) {
|
|
|
206
182
|
const temporary = `${file}.prnext-${randomUUID()}.tmp`;
|
|
207
183
|
try {
|
|
208
184
|
await writeFile(temporary, text, { flag: 'wx', mode });
|
|
185
|
+
if (!(await lstat(file)).isFile()) throw new Error('package.json is no longer a regular file.');
|
|
209
186
|
if (!(await readFile(file)).equals(original)) throw new Error('package.json changed during migration. Retry after the other edit finishes.');
|
|
210
187
|
await rename(temporary, file);
|
|
211
188
|
} finally { await rm(temporary, { force: true }); }
|
|
212
189
|
}
|
|
213
190
|
|
|
214
|
-
export async function installMigrationDependencies(root, { json = false, local = false } = {}) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const command = npm && /(?:^|[/\\])npm(?:-cli)?\.(?:c?js)$/.test(npm) ? process.execPath : process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
219
|
-
const argv = command === process.execPath ? [npm, ...args] : args;
|
|
220
|
-
await new Promise((resolve, reject) => {
|
|
221
|
-
// Windows .cmd launchers require a shell; argv contains only fixed flags,
|
|
222
|
-
// never project data. npx/npm invocations use npm-cli.js directly instead.
|
|
223
|
-
const child = spawn(command, argv, { cwd: root, stdio: json ? ['inherit', 2, 2] : 'inherit', shell: process.platform === 'win32' && command === 'npm.cmd' });
|
|
224
|
-
child.once('error', reject);
|
|
225
|
-
child.once('exit', (code, signal) => code === 0 ? resolve() : reject(new Error(`npm install failed (${signal || code}). Backups are available; fix the dependency conflict and retry. No --force or --legacy-peer-deps override was used.`)));
|
|
226
|
-
});
|
|
227
|
-
for (const name of ['prn', 'prnext']) {
|
|
228
|
-
const executable = path.join(root, 'node_modules/.bin', name + (process.platform === 'win32' ? '.cmd' : ''));
|
|
229
|
-
try { await access(executable, process.platform === 'win32' ? constants.F_OK : constants.X_OK); }
|
|
230
|
-
catch { throw new Error(`npm did not install the ${name} executable. Check npm's bin-links setting and the local PRNext dependency path, then retry.`); }
|
|
231
|
-
}
|
|
191
|
+
export async function installMigrationDependencies(root, { json = false, local = false, context } = {}) {
|
|
192
|
+
context ||= await migrationInstallContext(root, JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')));
|
|
193
|
+
if (!context.allowed) throw new Error(context.reason);
|
|
194
|
+
return installWithManager(context, { json, local });
|
|
232
195
|
}
|
|
233
196
|
|
|
234
197
|
export async function migrateProject(directory, { dryRun = false, install = true, json = false } = {}, services = {}) {
|
|
235
198
|
const root = path.resolve(directory), file = path.join(root, 'package.json');
|
|
236
199
|
const report = { root, ok: false, status: 'checking', dryRun, changes: [], backups: [], notes: [], preflight: null };
|
|
200
|
+
const snapshots = new Map();
|
|
201
|
+
let appliedText, physicalFile, installationStarted = false;
|
|
237
202
|
try {
|
|
238
203
|
const stat = await lstat(file);
|
|
239
204
|
if (!stat.isFile()) throw new Error('Migration requires a regular package.json file, not a symlink or directory.');
|
|
@@ -244,15 +209,23 @@ export async function migrateProject(directory, { dryRun = false, install = true
|
|
|
244
209
|
const framework = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
245
210
|
if ([framework.name, 'prnext', 'prnext-monorepo'].includes(input.name)) throw new Error('Choose the Next.js application directory, not the PRNext framework repository.');
|
|
246
211
|
const local = sourceCheckout() !== null;
|
|
212
|
+
const context = await migrationInstallContext(root, input);
|
|
247
213
|
// npm resolves file dependencies from the physical working directory. A
|
|
248
214
|
// symlink such as macOS /var -> /private/var otherwise changes ../ depth.
|
|
249
215
|
const relative = path.relative(await realpath(root), await realpath(packageRoot)).split(path.sep).join('/') || '.';
|
|
250
|
-
|
|
216
|
+
// Preserve a live checkout for local candidates: file: is copied into
|
|
217
|
+
// pnpm/Yarn stores and would lose the repository's native build context.
|
|
218
|
+
const protocol = context.manager === 'npm' ? 'file' : context.manager === 'yarn' && context.berry ? 'portal' : 'link';
|
|
219
|
+
const spec = local ? `${protocol}:${relative}` : framework.version;
|
|
251
220
|
const plan = migrationPackage(input, framework, spec);
|
|
252
221
|
report.changes = plan.changes;
|
|
253
222
|
report.notes = plan.notes;
|
|
254
223
|
if (local) report.notes.push(`This unpublished checkout uses a local PRNext dependency (${spec}); keep this checkout available. Rust is required on the first native build.`);
|
|
255
|
-
|
|
224
|
+
physicalFile = path.join(context.target, 'package.json');
|
|
225
|
+
report.installation = { manager: context.manager, version: context.version, root: context.root, workspace: context.workspace,
|
|
226
|
+
target: context.workspaceTarget, command: context.command };
|
|
227
|
+
report.notes.push(...context.notes);
|
|
228
|
+
for (const stateFile of context.stateFiles) snapshots.set(stateFile, await regularMigrationFile(stateFile));
|
|
256
229
|
if (!context.allowed) {
|
|
257
230
|
report.notes.push(context.reason);
|
|
258
231
|
if (install && !dryRun) throw new Error(context.reason);
|
|
@@ -264,11 +237,8 @@ export async function migrateProject(directory, { dryRun = false, install = true
|
|
|
264
237
|
if (dryRun) { report.ok = true; report.status = 'preview'; return report; }
|
|
265
238
|
if (plan.changes.length || install) {
|
|
266
239
|
report.backups.push(await backup(file, original));
|
|
267
|
-
for (const
|
|
268
|
-
|
|
269
|
-
const stat = await lstat(lockFile);
|
|
270
|
-
if (!stat.isFile()) throw new Error(`Migration requires a regular ${name} file.`);
|
|
271
|
-
report.backups.push(await backup(lockFile, await readFile(lockFile)));
|
|
240
|
+
for (const lockFile of context.locks) {
|
|
241
|
+
report.backups.push(await backup(lockFile, snapshots.get(lockFile).contents));
|
|
272
242
|
}
|
|
273
243
|
}
|
|
274
244
|
if (plan.changes.length) {
|
|
@@ -276,23 +246,56 @@ export async function migrateProject(directory, { dryRun = false, install = true
|
|
|
276
246
|
const newline = source.includes('\r\n') ? '\r\n' : '\n';
|
|
277
247
|
const text = (source.startsWith('\uFEFF') ? '\uFEFF' : '') + JSON.stringify(plan.package, null, indent).replaceAll('\n', newline) + (/\r?\n$/.test(source) ? newline : '');
|
|
278
248
|
await replacePackage(file, original, text, stat.mode & 0o777);
|
|
249
|
+
appliedText = Buffer.from(text);
|
|
279
250
|
}
|
|
280
251
|
report.status = plan.changes.length ? 'prepared' : 'unchanged';
|
|
281
252
|
if (!install) {
|
|
282
253
|
report.ok = true;
|
|
283
|
-
report.notes.push(
|
|
254
|
+
report.notes.push(`Dependency installation and installed-version validation are pending. Run ${context.command.name} ${context.command.args.map(value => /\s/.test(value) ? JSON.stringify(value) : value).join(' ')} from ${context.root}, then run prnext check and prnext build.`);
|
|
284
255
|
return report;
|
|
285
256
|
}
|
|
286
257
|
report.status = 'installing';
|
|
287
|
-
|
|
288
|
-
|
|
258
|
+
// A concurrent editor must not have its manifest or lockfile silently
|
|
259
|
+
// replaced by an install planned from an earlier snapshot.
|
|
260
|
+
for (const [stateFile, snapshot] of snapshots) {
|
|
261
|
+
const current = await regularMigrationFile(stateFile), expected = stateFile === physicalFile && appliedText ? appliedText : snapshot?.contents;
|
|
262
|
+
if (expected ? !current?.contents.equals(expected) : current !== null) throw new Error(`Migration file changed before installation: ${stateFile}`);
|
|
263
|
+
}
|
|
264
|
+
if (await realpath(context.target) !== context.target || await realpath(context.root) !== context.root) throw new Error('Workspace directories changed during migration.');
|
|
265
|
+
installationStarted = true;
|
|
266
|
+
const installed = await (services.install || installMigrationDependencies)(root, { json, local, context });
|
|
267
|
+
if (installed?.pnp && !services.check) {
|
|
268
|
+
const result = await executeManager(context, ['node', path.join(packageRoot, 'cli.mjs'), 'check', context.target, '--json'], { cwd: context.target });
|
|
269
|
+
report.preflight = JSON.parse(result.stdout);
|
|
270
|
+
} else report.preflight = await check(root);
|
|
289
271
|
if (!report.preflight.ok) throw new Error(`Dependencies were installed, but final preflight failed: ${report.preflight.errors.join(' ')}`);
|
|
290
272
|
report.ok = true;
|
|
291
273
|
report.status = 'migrated';
|
|
292
|
-
report.notes.push(
|
|
274
|
+
report.notes.push(`Migration prepared and dependencies checked. Run ${context.manager} run build and test your application before deploying.`);
|
|
293
275
|
return report;
|
|
294
276
|
} catch (error) {
|
|
295
277
|
report.error = error.message;
|
|
278
|
+
if (appliedText || report.status === 'installing') {
|
|
279
|
+
report.rollback = { ok: true, restored: [], errors: [] };
|
|
280
|
+
for (const [stateFile, snapshot] of snapshots) {
|
|
281
|
+
try {
|
|
282
|
+
if (await realpath(path.dirname(stateFile)) !== path.dirname(stateFile)) throw new Error('Migration directory became a symbolic link; preserved the current file.');
|
|
283
|
+
const current = await regularMigrationFile(stateFile);
|
|
284
|
+
if (stateFile !== physicalFile && !installationStarted) continue;
|
|
285
|
+
if (stateFile === physicalFile && current && !current.contents.equals(appliedText || snapshot.contents) && !current.contents.equals(snapshot.contents)) throw new Error('package.json was edited after migration; the newer edit was preserved.');
|
|
286
|
+
if (stateFile !== physicalFile && path.basename(stateFile) === 'package.json' && current && snapshot && !current.contents.equals(snapshot.contents)) throw new Error('Workspace package.json changed outside the selected application; the newer edit was preserved.');
|
|
287
|
+
if (snapshot) {
|
|
288
|
+
if (!current?.contents.equals(snapshot.contents)) {
|
|
289
|
+
const temporary = stateFile + `.prnext-${randomUUID()}.tmp`;
|
|
290
|
+
try { await writeFile(temporary, snapshot.contents, { flag: 'wx', mode: snapshot.mode }); await rename(temporary, stateFile); }
|
|
291
|
+
finally { await rm(temporary, { force: true }); }
|
|
292
|
+
report.rollback.restored.push(stateFile);
|
|
293
|
+
}
|
|
294
|
+
} else if (current) { await rm(stateFile); report.rollback.restored.push(stateFile); }
|
|
295
|
+
} catch (rollbackError) { report.rollback.ok = false; report.rollback.errors.push(`${stateFile}: ${rollbackError.message}`); }
|
|
296
|
+
}
|
|
297
|
+
report.notes.push(report.rollback.ok ? 'Restored the selected manifest and lockfiles after the failed migration. Dependency directories/caches may contain partial installation results; retry the same package manager.' : 'Some rollback files changed or became unsafe. Preserved those files; use the exact backups to review recovery.');
|
|
298
|
+
}
|
|
296
299
|
if (report.status === 'checking') report.status = 'blocked';
|
|
297
300
|
else report.status = 'incomplete';
|
|
298
301
|
return report;
|
package/build/npm-packages.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { builtinModules, createRequire } from 'node:module';
|
|
4
|
-
import {
|
|
5
|
-
import { VISITOR_KEYS } from '@babel/types';
|
|
4
|
+
import {analyzeSource} from './source-analysis.mjs';
|
|
6
5
|
import { createNativeDiscovery } from './native-discovery.mjs';
|
|
7
6
|
import {frameworkImportPattern} from './framework-imports.mjs';
|
|
8
7
|
|
|
@@ -26,22 +25,8 @@ export function npmPackagesPlugin({ pages = false, projectRoot, transpilePackage
|
|
|
26
25
|
sourceBytes += Buffer.byteLength(source);
|
|
27
26
|
if (sourceBytes > 64 * 1024 * 1024) throw new Error('npm compilation analysis exceeded 64 MiB of source. Separate large runtime dependencies using serverExternalPackages.');
|
|
28
27
|
if (Buffer.byteLength(source) > 8 * 1024 * 1024) throw new Error(`npm source ${file} exceeds the 8 MiB analysis limit. Configure serverExternalPackages for packages that do not use Next APIs.`);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
function visit(node) {
|
|
32
|
-
if (!node || typeof node !== 'object') return;
|
|
33
|
-
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type) && node.source && node.importKind !== 'type' && node.exportKind !== 'type') result.imports.push({ path: node.source.value, kind: 'import-statement' });
|
|
34
|
-
if (node.type === 'CallExpression' && (node.callee.type === 'Import' || node.callee.type === 'Identifier' && node.callee.name === 'require')) {
|
|
35
|
-
const argument = node.arguments[0];
|
|
36
|
-
if (argument?.type === 'StringLiteral') result.imports.push({ path: argument.value, kind: node.callee.type === 'Import' ? 'dynamic-import' : 'require-call' });
|
|
37
|
-
else if (node.callee.type !== 'Import') result.computedRequire = true;
|
|
38
|
-
}
|
|
39
|
-
for (const key of VISITOR_KEYS[node.type] || []) {
|
|
40
|
-
if (Array.isArray(node[key])) node[key].forEach(visit); else visit(node[key]);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
visit(ast.program);
|
|
44
|
-
return result;
|
|
28
|
+
const {imports,computedRequire,frameworkBoundary}=analyzeSource(source,file);
|
|
29
|
+
return {imports,computedRequire,frameworkBoundary};
|
|
45
30
|
})());
|
|
46
31
|
return sources.get(file);
|
|
47
32
|
}
|