@thomas.f/prnext 0.1.19-alpha → 0.1.20-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/build/app.mjs +46 -78
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +3 -3
- package/build/config.mjs +23 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/index.mjs +30 -13
- package/build/native-ppr.mjs +32 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/cli.mjs +14 -5
- package/compat/app-layout-context.cjs +7 -3
- package/compat/flight-delta.cjs +47 -12
- package/compat/index.d.ts +24 -1
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +110 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/package.json +12 -9
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +150 -26
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +6 -3
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/development.mjs +27 -8
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +31 -5
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
|
@@ -14,15 +14,15 @@ const MAX_FILES = 4096, MAX_DIRECTORIES = 8192, MAX_NAMES = 32768;
|
|
|
14
14
|
const MAX_FILE_BYTES = 16 * 1024 * 1024, MAX_BYTES = 48 * 1024 * 1024;
|
|
15
15
|
const RECORD_LIMIT = 64 * 1024 * 1024;
|
|
16
16
|
const SHA = /^[a-f0-9]{64}$/;
|
|
17
|
-
const frameworkDirectory=name=>['.git','.prnext-cache','.prnext','.prnext-incremental-stage'].includes(name)||/^\.prnext-(?:build|backup|config|postcss)-/.test(name);
|
|
17
|
+
const frameworkDirectory=name=>['.git','.prnext-cache','.prnext','.prnext-gc','.prnext-incremental-stage'].includes(name)||/^\.prnext-(?:build|backup|config|postcss)-/.test(name);
|
|
18
18
|
const frameworkPointer=name=>/^\.prnext-output(?:\.json|-.+\.tmp)$/.test(name);
|
|
19
19
|
const ignored = entry => entry.isDirectory() && (entry.name==='node_modules'||frameworkDirectory(entry.name)) || frameworkPointer(entry.name);
|
|
20
20
|
const safePath = file => typeof file === 'string' && file.length < 32768 && path.isAbsolute(file) && !file.includes('\0');
|
|
21
21
|
const inside = (root, file) => { const relative = path.relative(root, file); return relative === '' || !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep); };
|
|
22
|
-
export const withCompilationGraphCacheReporting = run => reports.run({ kinds: new Map(), identities:new Map(), listings:new Map(),listingNames:0 }, run);
|
|
22
|
+
export const withCompilationGraphCacheReporting = run => reports.run({ kinds: new Map(), identities:new Map(), listings:new Map(),listingNames:0,discovery:{metadataReads:0,metadataHits:0,parentVisits:0} }, run);
|
|
23
23
|
export function compilationGraphCacheReport() {
|
|
24
24
|
const kinds = [...(reports.getStore()?.kinds || [])].map(([kind, value]) => ({ kind, ...value, reasons: { ...value.reasons } }));
|
|
25
|
-
return { version: 1, scope: 'eligible-production-compilation-graphs', kinds,
|
|
25
|
+
return { version: 1, scope: 'eligible-production-compilation-graphs', kinds,discovery:{...(reports.getStore()?.discovery||{metadataReads:0,metadataHits:0,parentVisits:0})},
|
|
26
26
|
hits: kinds.reduce((sum, item) => sum + item.hits, 0), misses: kinds.reduce((sum, item) => sum + item.misses, 0),
|
|
27
27
|
bypasses: kinds.reduce((sum, item) => sum + item.bypasses, 0), reusedBytes: kinds.reduce((sum, item) => sum + item.reusedBytes, 0) };
|
|
28
28
|
}
|
|
@@ -83,20 +83,34 @@ async function directoryDigest(directory, budget, started = Infinity, lookup = f
|
|
|
83
83
|
async function snapshot(root, dependencies, { started = Infinity, imports = [], rejectTsconfig = false, styleTargets=false, baseline, excluded = [] } = {}) {
|
|
84
84
|
if (!Array.isArray(dependencies) || dependencies.length > MAX_FILES || dependencies.some(file => !safePath(file))) throw Error('dependency-limit');
|
|
85
85
|
const files = new Set(dependencies), directories = new Set(), trees = new Set([root]), fullDirectories = new Set();
|
|
86
|
+
// Discovery metadata is shared only within this snapshot. The final source
|
|
87
|
+
// digests and directory listings below always perform their own validation.
|
|
88
|
+
const metadataCache=new Map(),expandedParents=new Set(),packageRoots=new Map(),stats=reports.getStore()?.discovery;
|
|
89
|
+
const metadata=async file=>{
|
|
90
|
+
if(metadataCache.has(file)){if(stats)stats.metadataHits++;return metadataCache.get(file);}
|
|
91
|
+
if(metadataCache.size>=MAX_DIRECTORIES*8)throw Error('resolution-limit');
|
|
92
|
+
if(stats)stats.metadataReads++;
|
|
93
|
+
const pending=lstat(file).catch(error=>{if(error.code==='ENOENT')return;throw error;});
|
|
94
|
+
metadataCache.set(file,pending);return pending;
|
|
95
|
+
};
|
|
86
96
|
if(dependencies.some(file=>path.dirname(file).split(path.sep).some(frameworkDirectory)||frameworkPointer(path.basename(file))||excluded.some(directory=>inside(directory,file))))throw Error('generated-input');
|
|
87
97
|
for(const name of ['package.json','package-lock.json','npm-shrinkwrap.json','pnpm-lock.yaml','yarn.lock','bun.lock','bun.lockb','.env','.env.local','.env.production','.env.production.local','.env.development','.env.development.local','.env.test','.env.test.local']){
|
|
88
|
-
const file=path.join(root,name)
|
|
98
|
+
const file=path.join(root,name),info=await metadata(file);if(info){if(!info.isFile()||info.isSymbolicLink())throw Error('unsafe-config');files.add(file);}
|
|
89
99
|
}
|
|
90
100
|
const addParents = async file => {
|
|
91
101
|
let directory = path.dirname(file);
|
|
92
102
|
while (true) {
|
|
103
|
+
if(expandedParents.has(directory))break;
|
|
104
|
+
expandedParents.add(directory);if(stats)stats.parentVisits++;
|
|
93
105
|
directories.add(directory);
|
|
94
106
|
// Node's package search includes missing nearer node_modules directories.
|
|
95
107
|
directories.add(path.join(directory, 'node_modules'));
|
|
108
|
+
if(directories.size>MAX_DIRECTORIES)throw Error('resolution-limit');
|
|
96
109
|
for (const name of ['package.json', 'tsconfig.json', 'jsconfig.json', ...(styleTargets?['.browserslistrc','browserslist']:[])]) {
|
|
97
110
|
const candidate = path.join(directory, name);
|
|
98
111
|
try {
|
|
99
|
-
const info = await
|
|
112
|
+
const info = await metadata(candidate);
|
|
113
|
+
if(!info)continue;
|
|
100
114
|
if(name==='browserslist'&&info.isDirectory())continue;
|
|
101
115
|
if (rejectTsconfig && /^(?:ts|js)config\.json$/.test(name)) throw Error('untracked-tsconfig');
|
|
102
116
|
if (!info.isFile() || info.isSymbolicLink() || info.size>MAX_FILE_BYTES) throw Error('unsafe-config');
|
|
@@ -111,16 +125,22 @@ async function snapshot(root, dependencies, { started = Infinity, imports = [],
|
|
|
111
125
|
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
112
126
|
}
|
|
113
127
|
};
|
|
128
|
+
const packageRoot=async file=>{
|
|
129
|
+
let directory=path.dirname(file),found,visited=[];
|
|
130
|
+
while(true){
|
|
131
|
+
if(packageRoots.has(directory)){found=packageRoots.get(directory);break;}
|
|
132
|
+
visited.push(directory);
|
|
133
|
+
if((await metadata(path.join(directory,'package.json')))?.isFile()){found=directory;break;}
|
|
134
|
+
const parent=path.dirname(directory);if(parent===directory)break;directory=parent;
|
|
135
|
+
}
|
|
136
|
+
for(const directory of visited)packageRoots.set(directory,found);
|
|
137
|
+
return found;
|
|
138
|
+
};
|
|
114
139
|
for (const file of dependencies) {
|
|
115
140
|
await addParents(file);
|
|
116
141
|
// Package export/browser maps may choose an absent file anywhere inside the
|
|
117
142
|
// package. Directory names, including absence, participate in invalidation.
|
|
118
|
-
|
|
119
|
-
while (true) {
|
|
120
|
-
try { if ((await lstat(path.join(directory, 'package.json'))).isFile()) { trees.add(directory); break; } }
|
|
121
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
122
|
-
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
123
|
-
}
|
|
143
|
+
const tree=await packageRoot(file);if(tree)trees.add(tree);
|
|
124
144
|
}
|
|
125
145
|
for (const [importer, specifier] of imports) {
|
|
126
146
|
if (typeof specifier !== 'string' || /^(?:node:|[./\\]|[A-Za-z]:)/.test(specifier)) continue;
|
|
@@ -129,7 +149,7 @@ async function snapshot(root, dependencies, { started = Infinity, imports = [],
|
|
|
129
149
|
let directory = path.dirname(importer);
|
|
130
150
|
while (true) {
|
|
131
151
|
const candidate = path.join(directory, 'node_modules', name);
|
|
132
|
-
try { const info = await
|
|
152
|
+
try { const info = await metadata(candidate); if(info){if (info.isSymbolicLink()) throw Error('linked-package'); if (info.isDirectory()) trees.add(candidate);} }
|
|
133
153
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
134
154
|
const parent = path.dirname(directory); if (parent === directory) break; directory = parent;
|
|
135
155
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Only these framework-owned, regenerable caches participate in the common
|
|
2
|
+
// budget. Runtime data, page/image indexes, invalidations and app files do not.
|
|
3
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
4
|
+
import {lstat,open,opendir,realpath,rename,unlink} from 'node:fs/promises';
|
|
5
|
+
import {constants} from 'node:fs';
|
|
6
|
+
import {hostname} from 'node:os';
|
|
7
|
+
import {randomUUID} from 'node:crypto';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {withBuildLock} from './build-lock.mjs';
|
|
10
|
+
import {captureStorageDirectory,verifyStorageDirectories,sameStoragePath} from '../runtime/storage-path.mjs';
|
|
11
|
+
|
|
12
|
+
const MiB=1024*1024,DEFAULT=256*MiB,MAX_SCAN=32768;
|
|
13
|
+
const storage=new AsyncLocalStorage();
|
|
14
|
+
const namespaces={
|
|
15
|
+
'transforms-v1':/^[a-f0-9]{64}\.json$/,
|
|
16
|
+
'transforms-dev-v1':/^[a-f0-9]{64}\.json$/,
|
|
17
|
+
'runtime-bundle-v2':/^[a-f0-9]{64}\.json$/,
|
|
18
|
+
'loaders-v2':/^[a-f0-9]{64}\.json$/,
|
|
19
|
+
'compilation-graphs-v1':/^[a-f0-9]{64}\.json$/,
|
|
20
|
+
'compilation-graphs-v2':/^[a-f0-9]{64}\.(?:graph|blob)$/,
|
|
21
|
+
'fonts-v1':/^[a-f0-9]{64}\.bin$/,
|
|
22
|
+
'precompress-v1':/^[a-f0-9]{64}\.(?:gz|br)$/,
|
|
23
|
+
'dev-compiler-v8':null,
|
|
24
|
+
'node-compile-v1':null,
|
|
25
|
+
};
|
|
26
|
+
export function validateCompilationCache(value={}){
|
|
27
|
+
if(!value||typeof value!=='object'||![Object.prototype,null].includes(Object.getPrototypeOf(value))||Reflect.ownKeys(value).some(key=>key!=='maxDiskSize'))throw new TypeError('compilationCache accepts only maxDiskSize, in bytes.');
|
|
28
|
+
const maxDiskSize=value.maxDiskSize===undefined?DEFAULT:value.maxDiskSize;
|
|
29
|
+
if(!Number.isSafeInteger(maxDiskSize)||maxDiskSize<0||maxDiskSize>4*1024*MiB)throw new TypeError('compilationCache.maxDiskSize must be an integer from 0 to 4294967296 bytes.');
|
|
30
|
+
return {maxDiskSize};
|
|
31
|
+
}
|
|
32
|
+
export function configureCompilationStorage(policy){const context=storage.getStore();if(context)context.policy=validateCompilationCache(policy);}
|
|
33
|
+
async function realDirectory(directory){const info=await lstat(directory).catch(()=>null);return info?.isDirectory()&&!info.isSymbolicLink();}
|
|
34
|
+
const sameFile=(left,right)=>left.isFile()&&!left.isSymbolicLink()&&left.ino===right.ino&&left.dev===right.dev&&left.size===right.size&&left.mtimeMs===right.mtimeMs;
|
|
35
|
+
async function unchangedFile(file,snapshot,guards){
|
|
36
|
+
if(!await verifyStorageDirectories(guards))return false;
|
|
37
|
+
const current=await lstat(file).catch(()=>null);
|
|
38
|
+
return !!current&&sameFile(current,snapshot)&&sameStoragePath(await realpath(file),file)&&await verifyStorageDirectories(guards);
|
|
39
|
+
}
|
|
40
|
+
async function boundedJson(file,guards){
|
|
41
|
+
let descriptor;
|
|
42
|
+
try{
|
|
43
|
+
if(guards&&!await verifyStorageDirectories(guards))return null;
|
|
44
|
+
const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>4096)return null;
|
|
45
|
+
descriptor=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
|
|
46
|
+
if(!sameFile(await descriptor.stat(),info))return null;
|
|
47
|
+
const contents=await descriptor.readFile('utf8');
|
|
48
|
+
if(guards&&!await unchangedFile(file,info,guards))return null;
|
|
49
|
+
return JSON.parse(contents);
|
|
50
|
+
}catch{return null;}finally{await descriptor?.close().catch(()=>{});}
|
|
51
|
+
}
|
|
52
|
+
function alive(pid){try{process.kill(pid,0);return true;}catch(error){return error.code!=='ESRCH';}}
|
|
53
|
+
async function pinned(directory,{clean=false,guards}={}){
|
|
54
|
+
let scanned=0,active=false;
|
|
55
|
+
if(!await verifyStorageDirectories(guards))return {active:true,changed:true};
|
|
56
|
+
try{
|
|
57
|
+
for await(const entry of await opendir(directory)){
|
|
58
|
+
if(++scanned>8192)return {active:true,changed:false};
|
|
59
|
+
const name=entry.name;
|
|
60
|
+
if(!/^\.lease-\d+-[a-f0-9-]{36}\.json$/.test(name))continue;
|
|
61
|
+
const file=path.join(directory,name),info=await lstat(file).catch(()=>null),owner=await boundedJson(file,guards);
|
|
62
|
+
if(!owner||owner.host!==hostname()||!Number.isSafeInteger(owner.pid)||owner.pid<1||String(owner.pid)!==name.split('-')[1]||alive(owner.pid)){active=true;continue;}
|
|
63
|
+
if(clean&&info&&await unchangedFile(file,info,guards))await unlink(file).catch(()=>{});
|
|
64
|
+
}
|
|
65
|
+
}catch{return {active:true,changed:true};}
|
|
66
|
+
return {active,changed:!await verifyStorageDirectories(guards)};
|
|
67
|
+
}
|
|
68
|
+
async function storedPolicy(root){return validateCompilationCache(await boundedJson(path.join(root,'.prnext-cache','compilation-policy.json'))||{});}
|
|
69
|
+
async function savePolicy(root,policy){
|
|
70
|
+
const directory=path.join(root,'.prnext-cache'),guards=[await captureStorageDirectory(root),await captureStorageDirectory(directory)];
|
|
71
|
+
if(!await verifyStorageDirectories(guards))return;
|
|
72
|
+
const temporary=path.join(directory,`.policy-${randomUUID()}.tmp`);
|
|
73
|
+
let temporaryInfo;
|
|
74
|
+
try{
|
|
75
|
+
if(!await verifyStorageDirectories(guards))return;
|
|
76
|
+
const file=await open(temporary,'wx',0o600);try{await file.writeFile(JSON.stringify(policy));temporaryInfo=await file.stat();}finally{await file.close();}
|
|
77
|
+
if(await unchangedFile(temporary,temporaryInfo,guards))await rename(temporary,path.join(directory,'compilation-policy.json'));
|
|
78
|
+
}catch{/* Optional cache metadata cannot fail compilation. */}
|
|
79
|
+
finally{if(temporaryInfo&&await unchangedFile(temporary,temporaryInfo,guards).catch(()=>false))await unlink(temporary).catch(()=>{});}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Stats never execute application config. The budget is from the last build. */
|
|
83
|
+
export async function compilationStorageStats(root,{maxDiskSize}={}){
|
|
84
|
+
root=await realpath(root);const base=path.join(root,'.prnext-cache');
|
|
85
|
+
let policy;
|
|
86
|
+
if(maxDiskSize!==undefined)policy=validateCompilationCache({maxDiskSize});
|
|
87
|
+
else try{policy=await storedPolicy(root);}catch{policy=validateCompilationCache();}
|
|
88
|
+
const report={version:1,root,maxDiskSize:policy.maxDiskSize,bytes:0,files:0,scanComplete:true,categories:[],excluded:['runtime data and SQLite','page and image caches','unknown files and directories']};
|
|
89
|
+
if(!await realDirectory(base))return report;
|
|
90
|
+
const baseGuards=[await captureStorageDirectory(root),await captureStorageDirectory(base)];
|
|
91
|
+
if(!await verifyStorageDirectories(baseGuards)){report.scanComplete=false;return report;}
|
|
92
|
+
let scanned=0;
|
|
93
|
+
for(const [name,pattern] of Object.entries(namespaces)){
|
|
94
|
+
const directory=path.join(base,name);if(!await realDirectory(directory))continue;
|
|
95
|
+
const guards=[...baseGuards,await captureStorageDirectory(directory)],treeGuards=[...guards];
|
|
96
|
+
const lease=await pinned(directory,{guards});
|
|
97
|
+
if(lease.changed){report.scanComplete=false;break;}
|
|
98
|
+
const category={name,bytes:0,files:0,pinned:lease.active,entries:[],guards};
|
|
99
|
+
async function collect(folder,parentGuards,depth=0){
|
|
100
|
+
if(depth>3){report.scanComplete=false;return;}
|
|
101
|
+
if(!await verifyStorageDirectories(parentGuards)){report.scanComplete=false;return;}
|
|
102
|
+
try{
|
|
103
|
+
for await(const entry of await opendir(folder)){
|
|
104
|
+
if(++scanned>MAX_SCAN){report.scanComplete=false;return;}
|
|
105
|
+
const filename=path.join(folder,entry.name),info=await lstat(filename).catch(()=>null);
|
|
106
|
+
if(!info||info.isSymbolicLink())continue;
|
|
107
|
+
if(!pattern&&info.isDirectory()&&!entry.name.startsWith('.')){
|
|
108
|
+
const snapshot=await captureStorageDirectory(filename);treeGuards.push(snapshot);
|
|
109
|
+
await collect(filename,[...parentGuards,snapshot],depth+1);
|
|
110
|
+
}else if(info.isFile()){
|
|
111
|
+
const temporary=!!pattern&&/^[a-f0-9]{64}\.(?:json|graph|blob|bin|gz|br)\.[a-f0-9-]{36}\.tmp$/.test(entry.name);
|
|
112
|
+
if(!(pattern?pattern.test(entry.name)||temporary:!entry.name.startsWith('.')))continue;
|
|
113
|
+
category.entries.push({filename,size:info.size,time:info.mtimeMs,ino:info.ino,dev:info.dev,temporary,pinned:temporary&&Date.now()-info.mtimeMs<3600000,guards:parentGuards});category.bytes+=info.size;category.files++;
|
|
114
|
+
}
|
|
115
|
+
if(!report.scanComplete)return;
|
|
116
|
+
}
|
|
117
|
+
}catch{report.scanComplete=false;}
|
|
118
|
+
if(!await verifyStorageDirectories(parentGuards))report.scanComplete=false;
|
|
119
|
+
}
|
|
120
|
+
await collect(directory,guards);
|
|
121
|
+
if(!await verifyStorageDirectories(treeGuards))report.scanComplete=false;
|
|
122
|
+
report.categories.push(category);report.bytes+=category.bytes;report.files+=category.files;
|
|
123
|
+
if(!report.scanComplete)break;
|
|
124
|
+
}
|
|
125
|
+
return report;
|
|
126
|
+
}
|
|
127
|
+
function publicReport(report){return {...report,categories:report.categories.map(({entries,guards,...category})=>category)};}
|
|
128
|
+
export async function inspectCompilationStorage(root,options){return publicReport(await compilationStorageStats(root,options));}
|
|
129
|
+
|
|
130
|
+
// Called only under the existing interprocess publisher lock. Long-lived dev
|
|
131
|
+
// compilers retain their cache until shutdown; managed V8 workers pin theirs.
|
|
132
|
+
export async function pruneCompilationStorage(root,{maxDiskSize,dryRun=false,locked=false}={}){
|
|
133
|
+
if(!locked)return withBuildLock(root,canonical=>pruneCompilationStorage(canonical,{maxDiskSize,dryRun,locked:true}));
|
|
134
|
+
const report=await compilationStorageStats(root,{maxDiskSize});
|
|
135
|
+
let dev=false;try{await lstat(path.join(report.root,'.prnext-dev.lock'));dev=true;}catch(error){if(error.code!=='ENOENT')dev=true;}
|
|
136
|
+
const result={...publicReport(report),dryRun,beforeBytes:report.bytes,afterBytes:report.bytes,removedBytes:0,removedFiles:0,deferred:dev||!report.scanComplete,reason:dev?'Development is running; stop it to reclaim compilation caches.':!report.scanComplete?'The scan limit was reached; no files were removed.':undefined};
|
|
137
|
+
if(result.deferred)return result;
|
|
138
|
+
const units=[];
|
|
139
|
+
for(const category of report.categories){
|
|
140
|
+
if(category.pinned)continue;
|
|
141
|
+
const lease=await pinned(path.join(report.root,'.prnext-cache',category.name),{clean:!dryRun,guards:category.guards});
|
|
142
|
+
if(lease.changed){result.scanComplete=false;result.deferred=true;result.reason='A compiler cache directory changed; no further files were removed.';return result;}
|
|
143
|
+
if(lease.active){result.categories.find(value=>value.name===category.name).pinned=true;continue;}
|
|
144
|
+
// Graph records share content-addressed blobs. Evict this bounded namespace
|
|
145
|
+
// together so a global trim cannot strand metadata or remove shared bytes.
|
|
146
|
+
if(category.name==='compilation-graphs-v2'&&category.entries.length){
|
|
147
|
+
const graphs=category.entries.filter(entry=>!entry.temporary);if(graphs.length)units.push(graphs);
|
|
148
|
+
for(const entry of category.entries)if(entry.temporary&&!entry.pinned)units.push([entry]);
|
|
149
|
+
}
|
|
150
|
+
else for(const entry of category.entries)if(!entry.pinned)units.push([entry]);
|
|
151
|
+
}
|
|
152
|
+
units.sort((a,b)=>Math.max(...a.map(e=>e.time))-Math.max(...b.map(e=>e.time)));
|
|
153
|
+
for(const entries of units){
|
|
154
|
+
if(result.afterBytes<=report.maxDiskSize&&!entries.some(entry=>entry.temporary))continue;
|
|
155
|
+
for(const entry of entries){
|
|
156
|
+
try{
|
|
157
|
+
// Do not unlink an entry replaced after this snapshot (or a symlink).
|
|
158
|
+
if(!await verifyStorageDirectories(entry.guards)){result.scanComplete=false;result.deferred=true;result.reason='A compiler cache directory changed; no further files were removed.';return result;}
|
|
159
|
+
const current=await lstat(entry.filename);
|
|
160
|
+
if(!current.isFile()||current.isSymbolicLink()||current.ino!==entry.ino||current.dev!==entry.dev||current.size!==entry.size||current.mtimeMs!==entry.time)continue;
|
|
161
|
+
if(!sameStoragePath(await realpath(entry.filename),entry.filename)||!await verifyStorageDirectories(entry.guards)){result.scanComplete=false;result.deferred=true;result.reason='A compiler cache directory changed; no further files were removed.';return result;}
|
|
162
|
+
if(!dryRun)await unlink(entry.filename);
|
|
163
|
+
result.afterBytes-=entry.size;result.removedBytes+=entry.size;result.removedFiles++;
|
|
164
|
+
}catch{/* Busy/changed files remain visible and retryable next time. */}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
result.overBudget=result.afterBytes>report.maxDiskSize;
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function withCompilationStorage(root,run){
|
|
172
|
+
if(process.env.PRNEXT_BUILD_CACHE==='0')return run();
|
|
173
|
+
let policy;try{policy=await storedPolicy(root);}catch{policy=validateCompilationCache();}
|
|
174
|
+
const context={policy};
|
|
175
|
+
return storage.run(context,async()=>{
|
|
176
|
+
let value;
|
|
177
|
+
try{value=await run();return value;}
|
|
178
|
+
finally{
|
|
179
|
+
await savePolicy(root,context.policy);
|
|
180
|
+
try{
|
|
181
|
+
// A live dev session pins compiler state. Avoid a full disk scan after
|
|
182
|
+
// every keystroke; shutdown applies the stored policy once it drains.
|
|
183
|
+
const dev=await lstat(path.join(root,'.prnext-dev.lock')).catch(()=>null);
|
|
184
|
+
const report=dev?{version:1,...context.policy,deferred:true,reason:'Development is running; maintenance runs after shutdown.'}:await pruneCompilationStorage(root,{...context.policy,locked:true});
|
|
185
|
+
if(value&&typeof value==='object')value.compilationStorage=report;
|
|
186
|
+
}
|
|
187
|
+
catch{/* Retention failure must not hide a build error. */}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
package/build/compiler.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {withSourceMaps,configureSourceMaps,inlineSourceMap} from './source-maps.
|
|
|
16
16
|
import { optimizeImportsPlugins } from './optimize-imports.mjs';
|
|
17
17
|
import {recordBuildAnalysis} from './analyze.mjs';
|
|
18
18
|
import {recordDevDependencies,developmentDependencies,developmentStylePlans,developmentStyleDependencies,developmentModules} from './dev-dependencies.mjs';
|
|
19
|
-
import {normalizeReactCompilerConfig,
|
|
19
|
+
import {normalizeReactCompilerConfig,reactCompilerVersion} from './react-compiler.mjs';
|
|
20
20
|
import {withReactCompilerReporting,configureReactCompilerReporting,withReactCompilerGraph,recordReactCompilerGraph} from './react-compiler-report.mjs';
|
|
21
21
|
import {withCompilationGraphCacheReporting,cachedGraphBuild} from './compilation-cache.mjs';
|
|
22
22
|
export {reactCompilerReport} from './react-compiler-report.mjs';
|
|
@@ -27,7 +27,7 @@ const compilation = new AsyncLocalStorage();
|
|
|
27
27
|
export const withCompilation = callback => withCompilationGraphCacheReporting(()=>withReactCompilerReporting(()=>withSourceMaps(()=>compilation.run({ targets: new Map(), loaded: new Map() }, callback))));
|
|
28
28
|
export function configureCompiler(options) {
|
|
29
29
|
Object.assign(compilation.getStore() || {}, options);configureSourceMaps(options.dev || options.config?.productionBrowserSourceMaps);
|
|
30
|
-
configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:
|
|
30
|
+
configureReactCompilerReporting({...options,config:{reactCompiler:options.config?.reactCompiler ? normalizeReactCompilerConfig(options.config.reactCompiler) : false},compilerVersion:reactCompilerVersion(options.config?.reactCompiler,options.root)});
|
|
31
31
|
}
|
|
32
32
|
export const buildAnalysisRecords=()=>compilation.getStore()?.analysis;
|
|
33
33
|
export const buildAnalysisRoutes=()=>compilation.getStore()?.analysisRoutes;
|
|
@@ -165,7 +165,7 @@ async function compileGraph(options,role) {
|
|
|
165
165
|
// no hooks or runtime output and leaves the opt-out graph exactly as before.
|
|
166
166
|
if ((options.plugins || []).some(plugin => framework.test(plugin.name)) && state?.config?.reactCompiler) {
|
|
167
167
|
const settings=normalizeReactCompilerConfig(state.config.reactCompiler);
|
|
168
|
-
const fingerprint=createHash('sha256').update(JSON.stringify([
|
|
168
|
+
const fingerprint=createHash('sha256').update(JSON.stringify([reactCompilerVersion(settings,state.root),settings])).digest('hex').slice(0,24);
|
|
169
169
|
options={...options,plugins:[...options.plugins,{name:'prnext-react-compiler-'+fingerprint,setup(){}}]};
|
|
170
170
|
}
|
|
171
171
|
if ((options.plugins || []).some(plugin => framework.test(plugin.name))) options = { ...options, plugins: optimizeImportsPlugins(options.plugins, state?.config?.experimental?.optimizePackageImports) };
|
package/build/config.mjs
CHANGED
|
@@ -25,6 +25,8 @@ import {validatePrerenderConfig} from './prerender-config.mjs';
|
|
|
25
25
|
import {validateHtmlOnly} from './html-only-config.mjs';
|
|
26
26
|
import { validateProxyBodySize } from './proxy-body-config.mjs';
|
|
27
27
|
import { normalizeReactCompilerConfig } from './react-compiler.mjs';
|
|
28
|
+
import {validateCompilationCache} from './compilation-storage.mjs';
|
|
29
|
+
import {validateServerConfig} from './server-config.mjs';
|
|
28
30
|
|
|
29
31
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
32
|
const configName = /^(?:next|prnext)\.config\.(?:js|mjs|cjs|ts)$/;
|
|
@@ -37,6 +39,8 @@ supported.add('workerMemory');
|
|
|
37
39
|
supported.add('workerRetirement');
|
|
38
40
|
supported.add('workerPreload');
|
|
39
41
|
supported.add('deploymentRetention');
|
|
42
|
+
supported.add('compilationCache');
|
|
43
|
+
supported.add('server');
|
|
40
44
|
supported.add('prerender');
|
|
41
45
|
supported.add('htmlOnly');
|
|
42
46
|
supported.add('cssEngine');
|
|
@@ -107,6 +111,7 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
107
111
|
config.workerRetirement = validateWorkerRetirement(config.workerRetirement);
|
|
108
112
|
config.workerPreload=validateWorkerPreload(config.workerPreload);
|
|
109
113
|
config.deploymentRetention=validateDeploymentRetention(config.deploymentRetention);
|
|
114
|
+
config.compilationCache=validateCompilationCache(config.compilationCache);
|
|
110
115
|
config.prerender=validatePrerenderConfig(config.prerender);
|
|
111
116
|
config.htmlOnly=validateHtmlOnly(config.htmlOnly);
|
|
112
117
|
config.cssEngine ??= 'auto';
|
|
@@ -127,6 +132,7 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
127
132
|
}
|
|
128
133
|
if (config.webpack !== undefined && typeof config.webpack !== 'function') throw new TypeError('webpack must be a configuration function');
|
|
129
134
|
config.turbopack = validateTurbopack(config.turbopack);
|
|
135
|
+
config.server = validateServerConfig(config.server);
|
|
130
136
|
config.i18n = validateI18n(config.i18n);
|
|
131
137
|
if (config.i18n && config.output === 'export') throw new Error("i18n and output: 'export' cannot be combined; use explicit locale routes for static exports");
|
|
132
138
|
validateStandaloneConfig(config);
|
|
@@ -164,13 +170,28 @@ export function validateProjectConfig(input, filename = 'project config') {
|
|
|
164
170
|
if (config.sassOptions !== undefined && (!config.sassOptions || typeof config.sassOptions !== 'object' || Array.isArray(config.sassOptions))) throw new Error(`${filename}: sassOptions must be an object.`);
|
|
165
171
|
if (!config.experimental || typeof config.experimental !== 'object' || Array.isArray(config.experimental)) throw new Error(`${filename}: experimental must be an object.`);
|
|
166
172
|
for (const key of Object.keys(config.experimental)) {
|
|
167
|
-
if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize', 'lazyDynamicImports'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
173
|
+
if (!['nextScriptWorkers', 'serverActions', 'optimizePackageImports', 'proxyClientMaxBodySize', 'lazyDynamicImports', 'streamingRequestBodies', 'nativePprShell', 'parallelBuilds'].includes(key)) throw new Error(`${filename}: experimental.${key} is not supported by PRNext. It cannot be silently ignored.`);
|
|
168
174
|
}
|
|
169
175
|
const nextScriptWorkers = config.experimental.nextScriptWorkers === undefined ? false : config.experimental.nextScriptWorkers;
|
|
170
176
|
if (typeof nextScriptWorkers !== 'boolean') throw new Error(`${filename}: experimental.nextScriptWorkers must be a boolean.`);
|
|
171
177
|
const lazyDynamicImports=config.experimental.lazyDynamicImports===undefined?false:config.experimental.lazyDynamicImports;
|
|
172
178
|
if(typeof lazyDynamicImports!=='boolean')throw new Error(`${filename}: experimental.lazyDynamicImports must be a boolean.`);
|
|
173
|
-
|
|
179
|
+
const streamingRequestBodies = config.experimental.streamingRequestBodies === undefined ? false : config.experimental.streamingRequestBodies;
|
|
180
|
+
const nativePprShell = config.experimental.nativePprShell === undefined ? false : config.experimental.nativePprShell;
|
|
181
|
+
for (const [key, value] of Object.entries({streamingRequestBodies, nativePprShell})) {
|
|
182
|
+
if (typeof value !== 'boolean') throw new Error(`${filename}: experimental.${key} must be a boolean.`);
|
|
183
|
+
}
|
|
184
|
+
if (nativePprShell && !config.cacheComponents) throw new Error(`${filename}: experimental.nativePprShell requires cacheComponents: true.`);
|
|
185
|
+
let parallelBuilds = config.experimental.parallelBuilds === undefined ? false : config.experimental.parallelBuilds;
|
|
186
|
+
if (parallelBuilds !== false) {
|
|
187
|
+
if (parallelBuilds === true) parallelBuilds = {};
|
|
188
|
+
if (!parallelBuilds || typeof parallelBuilds !== 'object' || Array.isArray(parallelBuilds)) throw new Error(`${filename}: experimental.parallelBuilds must be a boolean or an object.`);
|
|
189
|
+
for (const key of Object.keys(parallelBuilds)) if (!['maxWorkers', 'maxMemoryMb'].includes(key)) throw new Error(`${filename}: unknown parallelBuilds option ${key}.`);
|
|
190
|
+
parallelBuilds = {maxWorkers: parallelBuilds.maxWorkers === undefined ? 4 : parallelBuilds.maxWorkers, maxMemoryMb: parallelBuilds.maxMemoryMb === undefined ? 1024 : parallelBuilds.maxMemoryMb};
|
|
191
|
+
if (!Number.isInteger(parallelBuilds.maxWorkers) || parallelBuilds.maxWorkers < 1 || parallelBuilds.maxWorkers > 8) throw new Error(`${filename}: parallelBuilds.maxWorkers must be between 1 and 8.`);
|
|
192
|
+
if (!Number.isInteger(parallelBuilds.maxMemoryMb) || parallelBuilds.maxMemoryMb < 256 || parallelBuilds.maxMemoryMb > 8192) throw new Error(`${filename}: parallelBuilds.maxMemoryMb must be between 256 and 8192.`);
|
|
193
|
+
}
|
|
194
|
+
config.experimental = { nextScriptWorkers, lazyDynamicImports, streamingRequestBodies, nativePprShell, parallelBuilds, serverActions: validateServerActions(config.experimental.serverActions), optimizePackageImports: validateOptimizePackageImports(config.experimental.optimizePackageImports), proxyClientMaxBodySize: validateProxyBodySize(config.experimental.proxyClientMaxBodySize) };
|
|
174
195
|
for (const key of ['basePath', 'assetPrefix']) {
|
|
175
196
|
if (typeof config[key] !== 'string' || Buffer.byteLength(config[key]) > 4096) throw new Error(`${filename}: ${key} must be a string of at most 4096 bytes.`);
|
|
176
197
|
}
|
package/build/create.mjs
CHANGED
|
@@ -129,7 +129,7 @@ h1 { font-size: clamp(2.5rem, 8vw, 4rem); letter-spacing: -.04em; }
|
|
|
129
129
|
p { line-height: 1.6; }
|
|
130
130
|
code { font-size: .95em; }
|
|
131
131
|
`],
|
|
132
|
-
['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
|
|
132
|
+
['.gitignore', 'node_modules/\n.prnext/\n.prnext-types/\n.prnext-dev/\n.prnext-cache/\n.prnext-gc/\n.env*\n!.env.example\n*.tsbuildinfo\n'],
|
|
133
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
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
135
|
]);
|
package/build/css-modules.mjs
CHANGED
|
@@ -138,6 +138,10 @@ export function createCssModules({ projectRoot, stage, assetBase = '/_prnext/ass
|
|
|
138
138
|
|
|
139
139
|
return {
|
|
140
140
|
compile:get,
|
|
141
|
+
async parallelSafe() {
|
|
142
|
+
eligibility ??= cacheEligible();
|
|
143
|
+
return typeof sassOptions?.additionalData !== 'function' && await eligibility;
|
|
144
|
+
},
|
|
141
145
|
async mappings(){return Promise.all([...cache].map(async([file,result])=>({file,mapping:(await result).mapping,watchFiles:(await result).watchFiles})));},
|
|
142
146
|
plugin(browser, { emitStyles = () => true } = {}) {
|
|
143
147
|
return {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// Build-private provenance, never Action arguments, source text or encryption
|
|
4
|
+
// material. Unknown/external owners deliberately disable selective refresh.
|
|
5
|
+
export function developmentActionMetadata(root, actions) {
|
|
6
|
+
const records = [], identities = new Set(); let bytes = 0;
|
|
7
|
+
for (const action of actions) {
|
|
8
|
+
const file = path.relative(root, action.file).replaceAll(path.sep, '/');
|
|
9
|
+
const runtimes = [...(action.runtimes?.size ? action.runtimes : ['nodejs'])].sort();
|
|
10
|
+
const record = { id: action.id, file, binding: action.binding, runtimes };
|
|
11
|
+
const identity = file + '\0' + action.binding;
|
|
12
|
+
if (!/^[a-f0-9]{64}$/.test(action.id) || !file || file.startsWith('../') || path.isAbsolute(file) || /[:?\0]/.test(file) || file.startsWith('node_modules/') ||
|
|
13
|
+
typeof action.binding !== 'string' || !action.binding || action.binding.length > 1024 || identities.has(identity) ||
|
|
14
|
+
runtimes.some(runtime => !['nodejs', 'edge'].includes(runtime)) || records.length >= 10000 ||
|
|
15
|
+
(bytes += Buffer.byteLength(JSON.stringify(record))) > 1024 * 1024)
|
|
16
|
+
return { version: 1, complete: false, actions: [] };
|
|
17
|
+
identities.add(identity); records.push(record);
|
|
18
|
+
}
|
|
19
|
+
records.sort((a, b) => a.file.localeCompare(b.file) || a.binding.localeCompare(b.binding));
|
|
20
|
+
return { version: 1, complete: true, actions: records };
|
|
21
|
+
}
|
package/build/font-cache.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { open, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
|
|
3
|
+
import {constants} from 'node:fs';
|
|
4
|
+
import {privateCacheDirectory} from './persistent-cache.mjs';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
|
|
5
7
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
@@ -11,12 +13,14 @@ let writes = Promise.resolve();
|
|
|
11
13
|
*/
|
|
12
14
|
export async function cachedFontResource(root, url, maximum, accept, download) {
|
|
13
15
|
if (!root) return download();
|
|
14
|
-
const directory =
|
|
16
|
+
const directory = await privateCacheDirectory(root,'fonts-v1');
|
|
17
|
+
if(!directory)return download();
|
|
15
18
|
const file = path.join(directory, hash(JSON.stringify([url, accept])) + '.bin');
|
|
16
19
|
try {
|
|
17
20
|
const metadata = await lstat(file);
|
|
18
|
-
if (metadata.isFile() && metadata.size > 65 && metadata.size <= maximum + 65) {
|
|
19
|
-
const
|
|
21
|
+
if (metadata.isFile() && !metadata.isSymbolicLink() && metadata.size > 65 && metadata.size <= maximum + 65) {
|
|
22
|
+
const handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW||0));
|
|
23
|
+
let stored;try{const info=await handle.stat();if(!info.isFile()||info.size>maximum+65)throw Object.assign(Error('Font cache changed'),{code:'ENOENT'});stored=await handle.readFile();}finally{await handle.close();}
|
|
20
24
|
const bytes = stored.subarray(65);
|
|
21
25
|
if (stored[64] === 10 && hash(bytes) === stored.toString('ascii', 0, 64)) return bytes;
|
|
22
26
|
}
|
|
@@ -24,10 +28,9 @@ export async function cachedFontResource(root, url, maximum, accept, download) {
|
|
|
24
28
|
const bytes = await download();
|
|
25
29
|
if (!bytes.length || bytes.length > maximum) throw new Error('Font resource exceeds its cache input limit');
|
|
26
30
|
const task = writes.catch(() => {}).then(async () => {
|
|
27
|
-
await mkdir(directory, { recursive: true });
|
|
28
31
|
const temporary = file + '.' + randomUUID() + '.tmp';
|
|
29
32
|
try {
|
|
30
|
-
await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx' });
|
|
33
|
+
await writeFile(temporary, Buffer.concat([Buffer.from(hash(bytes) + '\n'), bytes]), { flag: 'wx',mode:0o600 });
|
|
31
34
|
await rename(temporary, file);
|
|
32
35
|
} finally { await rm(temporary, { force: true }); }
|
|
33
36
|
const entries = [];
|
package/build/font-files.mjs
CHANGED
|
@@ -183,7 +183,7 @@ async function google(name, options, context) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function fallbackMetrics(faces, fallbackName) {
|
|
186
|
-
const { create } = await import('fontkit');
|
|
186
|
+
const { create } = await import('../vendor/fontkit/index.mjs');
|
|
187
187
|
// Prefer an upright face near the normal text weight for a shared fallback.
|
|
188
188
|
const candidates = [...faces].sort((a, b) => Number(a.style === 'italic') - Number(b.style === 'italic') || Math.abs((parseFloat(a.weight) || 400) - 400) - Math.abs((parseFloat(b.weight) || 400) - 400));
|
|
189
189
|
for (const face of candidates) {
|