@thomas.f/prnext 0.1.16-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/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- 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 +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -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 +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- 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-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- 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 +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- 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/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- 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 +100 -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/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -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 +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- 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 +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -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 +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- 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 +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -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 +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
package/build/styles.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { build as bundle } from 'esbuild';
|
|
2
2
|
import { access, readFile, readdir, writeFile, rm } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import {transform as transformCss, browserslistToTargets} from 'lightningcss';
|
|
5
|
+
import browserslist from 'browserslist';
|
|
4
6
|
import { randomUUID } from 'node:crypto';
|
|
5
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
8
|
import path from 'node:path';
|
|
@@ -37,8 +39,20 @@ async function loadConfiguration(root) {
|
|
|
37
39
|
return /\.(?:js|mjs|cjs)$/.test(filename) ? importConfiguration(filename) : JSON.parse(await readFile(filename, 'utf8'));
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
export const resolveStandardBrowserTargets = root => browserslist(undefined,{path:path.resolve(root)});
|
|
43
|
+
|
|
44
|
+
async function createProcessor(root, engine, browserTargets) {
|
|
41
45
|
const config = await loadConfiguration(root);
|
|
46
|
+
if (config === undefined && engine !== 'postcss') {
|
|
47
|
+
const targets=browserslistToTargets(browserTargets ? await browserTargets() : resolveStandardBrowserTargets(root));
|
|
48
|
+
return {async process(css,options){
|
|
49
|
+
const result=transformCss({filename:options.from,code:Buffer.from(css),targets,minify:false,sourceMap:!!options.map,
|
|
50
|
+
...(options.map?.prev ? {inputSourceMap:JSON.stringify(options.map.prev)} : {}),drafts:{customMedia:true}});
|
|
51
|
+
// Preserve source origins for Sass URL rebasing and the ICSS extraction
|
|
52
|
+
// below. Custom PostCSS configurations always retain their own pipeline.
|
|
53
|
+
return postcss([]).process(result.code,{...options,map:result.map?{prev:JSON.parse(result.map.toString()),inline:false,annotation:false}:false});
|
|
54
|
+
}};
|
|
55
|
+
}
|
|
42
56
|
if (config === undefined) return postcss([flexbugs(), presetEnv({ autoprefixer: { flexbox: 'no-2009' }, stage: 3, features: { 'custom-properties': false } })]);
|
|
43
57
|
if (!config || typeof config !== 'object' || Array.isArray(config) || !config.plugins || typeof config.plugins !== 'object') throw new Error('PostCSS configuration must export an object with a plugins object or array; configuration functions are not supported.');
|
|
44
58
|
const entries = Array.isArray(config.plugins)
|
|
@@ -122,7 +136,7 @@ function packageImporter(require) {
|
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/** Build-only transformations, shared and deduplicated across all three React graphs. */
|
|
125
|
-
export function createStyleProcessor({ projectRoot, sassOptions = {} }) {
|
|
139
|
+
export function createStyleProcessor({ projectRoot, sassOptions = {}, cssEngine = 'auto', browserTargets }) {
|
|
126
140
|
const root = path.resolve(projectRoot);
|
|
127
141
|
const require = createRequire(path.join(root, 'package.json'));
|
|
128
142
|
const cache = new Map();
|
|
@@ -156,7 +170,7 @@ export function createStyleProcessor({ projectRoot, sassOptions = {} }) {
|
|
|
156
170
|
sourceMap = compiled.sourceMap;
|
|
157
171
|
for (const url of compiled.loadedUrls) if (url.protocol === 'file:') dependencies.add(fileURLToPath(url));
|
|
158
172
|
}
|
|
159
|
-
processor ??= createProcessor(root);
|
|
173
|
+
processor ??= createProcessor(root,cssEngine,browserTargets);
|
|
160
174
|
const processed = await (await processor).process(css, { from: file, to: file, map: sourceMap ? { prev: sourceMap, inline: false, annotation: false } : false });
|
|
161
175
|
for (const message of processed.messages) if (message.type === 'dependency' && message.file) dependencies.add(path.resolve(root, message.file));
|
|
162
176
|
rebaseReferences(processed.root, file);
|
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import {sourceMapsEnabled} from './source-maps.mjs';
|
|
3
|
+
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
4
|
+
import {buildCacheIdentity,privateCacheDirectory,readCacheRecord,writeCacheRecord} from './persistent-cache.mjs';
|
|
3
5
|
|
|
4
6
|
const entries = new Map();
|
|
5
7
|
const byteLimit = 2 * 1024 * 1024, entryLimit = 256, itemLimit = 128 * 1024;
|
|
6
8
|
let bytes = 0, hits = 0, misses = 0;
|
|
7
|
-
|
|
9
|
+
let diskHits=0,diskWrites=0;
|
|
10
|
+
const persistent=new AsyncLocalStorage();
|
|
11
|
+
export const transformCacheStats = () => ({entries:entries.size,bytes,hits,misses,diskHits,diskWrites});
|
|
12
|
+
export async function withPersistentTransforms(root,{enabled=true,dev=false}={},run){
|
|
13
|
+
if(!enabled||process.env.PRNEXT_BUILD_CACHE==='0')return run();
|
|
14
|
+
let directory,key,record;
|
|
15
|
+
try{directory=await privateCacheDirectory(root,dev?'transforms-dev-v1':'transforms-v1');key=directory&&await buildCacheIdentity(root);record=key&&await readCacheRecord(directory,key);}catch{return run();}
|
|
16
|
+
if(!directory||!key)return run();
|
|
17
|
+
const scope={key,values:new Map(),bytes:0,dirty:false};
|
|
18
|
+
for(const item of Array.isArray(record?.entries)?record.entries.slice(-4096):[]){
|
|
19
|
+
if(!Array.isArray(item)||item.length!==2||!/^[a-f0-9]{64}$/.test(item[0])||typeof item[1]!=='string')continue;
|
|
20
|
+
const size=Buffer.byteLength(item[1]);if(size>128*1024||scope.bytes+size>8*1024*1024)continue;
|
|
21
|
+
scope.values.set(item[0],item[1]);scope.bytes+=size;
|
|
22
|
+
}
|
|
23
|
+
const result=await persistent.run(scope,run);
|
|
24
|
+
if(scope.dirty&&await writeCacheRecord(directory,key,{entries:[...scope.values]}))diskWrites++;
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function identity(kind,source,settings){return createHash('sha256').update(persistent.getStore()?.key||'memory').update(JSON.stringify([kind,settings,sourceMapsEnabled()])).update('\0').update(source).digest('hex');}
|
|
28
|
+
function diskValue(key){const scope=persistent.getStore();if(!scope?.values.has(key))return;const value=scope.values.get(key);scope.values.delete(key);scope.values.set(key,value);diskHits++;hits++;return value;}
|
|
29
|
+
function remember(key,output){
|
|
30
|
+
const scope=persistent.getStore();if(!scope||typeof output!=='string')return;
|
|
31
|
+
const size=Buffer.byteLength(output);if(size>128*1024)return;
|
|
32
|
+
const previous=scope.values.get(key);if(previous!==undefined){scope.bytes-=Buffer.byteLength(previous);scope.values.delete(key);}
|
|
33
|
+
while(scope.values.size&&(scope.values.size>=4096||scope.bytes+size>8*1024*1024)){const [old,value]=scope.values.entries().next().value;scope.values.delete(old);scope.bytes-=Buffer.byteLength(value);}
|
|
34
|
+
scope.values.set(key,output);scope.bytes+=size;scope.dirty=true;
|
|
35
|
+
}
|
|
8
36
|
|
|
9
37
|
// Only pure string transformations belong here. No AST, input source, plugin
|
|
10
38
|
// closure or mutable build metadata survives in the cache. Include all options
|
|
11
39
|
// affecting output in settings, including build identity and target mode.
|
|
12
40
|
export function cachedTransform(kind, source, settings, transform) {
|
|
13
|
-
const key =
|
|
41
|
+
const key = identity(kind,source,settings);
|
|
14
42
|
const found = entries.get(key);
|
|
15
43
|
if (found) {
|
|
16
44
|
hits++; entries.delete(key); entries.set(key,found);
|
|
45
|
+
if(persistent.getStore()&&!persistent.getStore().values.has(key))remember(key,found.output);
|
|
17
46
|
return found.output;
|
|
18
47
|
}
|
|
48
|
+
const saved=diskValue(key);if(saved!==undefined)return saved;
|
|
19
49
|
misses++;
|
|
20
50
|
const output = transform();
|
|
51
|
+
remember(key,output);
|
|
21
52
|
// UTF-16 is an upper bound for the retained string payload. Metadata adds
|
|
22
53
|
// engine overhead; the entry count also bounds that overhead.
|
|
23
54
|
const size = output.length * 2 + key.length * 2;
|
|
@@ -33,11 +64,13 @@ export function cachedTransform(kind, source, settings, transform) {
|
|
|
33
64
|
|
|
34
65
|
/** Async transforms share the same payload and entry budget as synchronous ones. */
|
|
35
66
|
export async function cachedAsyncTransform(kind, source, settings, transform) {
|
|
36
|
-
const key =
|
|
67
|
+
const key = identity(kind,source,settings);
|
|
37
68
|
const found = entries.get(key);
|
|
38
|
-
if (found) { hits++; entries.delete(key); entries.set(key,found); return found.output; }
|
|
69
|
+
if (found) { hits++; entries.delete(key); entries.set(key,found); if(persistent.getStore()&&!persistent.getStore().values.has(key))remember(key,found.output);return found.output; }
|
|
70
|
+
const saved=diskValue(key);if(saved!==undefined)return saved;
|
|
39
71
|
misses++;
|
|
40
72
|
const output = await transform();
|
|
73
|
+
remember(key,output);
|
|
41
74
|
const size = output.length * 2 + key.length * 2;
|
|
42
75
|
if (size <= itemLimit) {
|
|
43
76
|
if (entries.has(key)) {bytes -= entries.get(key).size;entries.delete(key);}
|
package/build/transform.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export async function rewriteServerChunks(file,chunks,publicPath){
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function parserOptions(filename) {
|
|
54
|
-
return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.tsx?$/.test(filename) ? ['typescript'] : [])] };
|
|
54
|
+
return { sourceType: 'unambiguous', sourceFilename: filename, plugins: ['jsx', ...( /\.[cm]?tsx?$/.test(filename) ? ['typescript'] : [])] };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function assertSupportedSource(source, filename) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { loadProjectConfig } from './config.mjs';
|
|
4
|
+
import { scanProject } from './scan.mjs';
|
|
5
|
+
import { writeTypedRoutes } from './typed-routes.mjs';
|
|
6
|
+
import { checkTypeScript, projectTsconfig } from './typescript.mjs';
|
|
7
|
+
import { snapshotEnvConfig } from '../runtime/env.mjs';
|
|
8
|
+
|
|
9
|
+
/** Generate/check from route discovery only; no bundler, native server or install. */
|
|
10
|
+
export async function runTypeCommand(directory, { check = false } = {}) {
|
|
11
|
+
const root = path.resolve(directory);
|
|
12
|
+
const report = { version: 1, root, command: check ? 'typecheck' : 'typegen', ok: false, typedRoutes: false,
|
|
13
|
+
tsconfig: null, routes: 0, updated: [], configUpdates: [], notes: [], errors: [] };
|
|
14
|
+
const restore = snapshotEnvConfig();
|
|
15
|
+
try {
|
|
16
|
+
const config = await loadProjectConfig(root);
|
|
17
|
+
const tsconfig = await projectTsconfig(root, config);
|
|
18
|
+
if (!tsconfig) throw new Error('Type commands require an existing tsconfig.json, or typescript.tsconfigPath pointing to an existing configuration.');
|
|
19
|
+
const resolve = createRequire(path.join(root, 'package.json'));
|
|
20
|
+
try { resolve.resolve('typescript'); }
|
|
21
|
+
catch { throw new Error('Install typescript as a dev dependency in the application before running typegen or typecheck.'); }
|
|
22
|
+
report.tsconfig = path.relative(root, tsconfig).replaceAll(path.sep, '/');
|
|
23
|
+
// Explicit type commands opt into route generation unless the application
|
|
24
|
+
// explicitly disables it. A clean checkout gets fresh declarations first.
|
|
25
|
+
report.typedRoutes = config.typedRoutes !== false;
|
|
26
|
+
const project = await scanProject(root, { basePath: config.basePath, pageExtensions: config.pageExtensions, i18n: config.i18n });
|
|
27
|
+
report.routes = project.routes.filter(route => route.kind === 'page' && !route.internal && !route.errorStatus).length;
|
|
28
|
+
const updates = await writeTypedRoutes(root, project, { ...config, typedRoutes: report.typedRoutes });
|
|
29
|
+
report.updated = (updates || []).map(file => path.relative(root, file).replaceAll(path.sep, '/')).sort();
|
|
30
|
+
report.configUpdates = report.updated.filter(file => file === report.tsconfig || file === 'prnext-env.d.ts');
|
|
31
|
+
if (report.configUpdates.length) report.notes.push('Updated the selected tsconfig roots/Next type aliases and/or prnext-env.d.ts to include generated declarations. Existing JSONC comments and inherited settings are preserved.');
|
|
32
|
+
if (!report.typedRoutes) report.notes.push('Route declarations are disabled by typedRoutes: false; application TypeScript is still checked by typecheck.');
|
|
33
|
+
if (check) {
|
|
34
|
+
await checkTypeScript(root, config, { tsconfig, force: true });
|
|
35
|
+
if (config.typescript?.ignoreBuildErrors) report.notes.push('typecheck checks errors even when typescript.ignoreBuildErrors is enabled for builds.');
|
|
36
|
+
}
|
|
37
|
+
report.ok = true;
|
|
38
|
+
} catch (error) { report.errors.push(error.message); }
|
|
39
|
+
finally { restore(); }
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {readFile,writeFile,mkdir,rename,lstat} from 'node:fs/promises';
|
|
2
|
+
import {createRequire} from 'node:module';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {resolveTypeScript} from './typescript-toolchain.mjs';
|
|
7
|
+
import {nativeTypeConfig} from './native-type-config.mjs';
|
|
8
|
+
|
|
9
|
+
const marker='// Generated by PRNext: typed route registry. Do not edit.';
|
|
10
|
+
async function changed(file,contents,updates){
|
|
11
|
+
const info=await lstat(file).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
12
|
+
if(info&&(!info.isFile()||info.isSymbolicLink()))throw Error('Typed routes cannot replace a non-regular file: '+file);
|
|
13
|
+
if(await readFile(file,'utf8').catch(()=>null)===contents)return;
|
|
14
|
+
const temporary=file+'.'+randomUUID()+'.tmp';try{await writeFile(temporary,contents,{mode:0o600,flag:'wx'});await rename(temporary,file);updates?.add(file);}finally{const {rm}=await import('node:fs/promises');await rm(temporary,{force:true});}
|
|
15
|
+
}
|
|
16
|
+
export function routeDeclarations(project,{enabled=true}={}){
|
|
17
|
+
const patterns=[...new Set(project.routes.filter(route=>route.kind==='page'&&!route.internal&&!route.errorStatus).map(route=>route.pattern))].sort();
|
|
18
|
+
if(patterns.length>10000)throw Error('Typed routes supports at most 10,000 page patterns');
|
|
19
|
+
const fields=patterns.map(pattern=>{
|
|
20
|
+
const segments=pattern.split('/');if(segments.length>33)throw Error('Typed routes supports at most 32 route segments');
|
|
21
|
+
const params=segments.flatMap(part=>{
|
|
22
|
+
const optional=/^\[\[\.\.\.([^\]]+)\]\]$/.exec(part),rest=/^\[\.\.\.([^\]]+)\]$/.exec(part),single=/^\[([^\]]+)\]$/.exec(part);
|
|
23
|
+
const match=optional||rest||single;return match?[`${JSON.stringify(match[1])}${optional?'?':''}: ${single?'string':'string[]'}`]:[];
|
|
24
|
+
});
|
|
25
|
+
return ` ${JSON.stringify(pattern)}: {${params.join('; ')}};`;
|
|
26
|
+
});
|
|
27
|
+
return marker+'\nexport {};\ndeclare global {\n namespace PRNext {\n'+(enabled?' interface RouteConfig { enabled: true }\n':'')+' interface RouteMap {\n'+fields.join('\n')+'\n }\n }\n}\n';
|
|
28
|
+
}
|
|
29
|
+
// Bind missing Next-style imports to the matching PRNext declarations. Existing
|
|
30
|
+
// aliases remain authoritative; inherited aliases retain their resolved targets.
|
|
31
|
+
async function bindTypes(filename,ts,updates){
|
|
32
|
+
const source=await readFile(filename,'utf8');
|
|
33
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(filename,{},{...ts.sys,onUnRecoverableConfigFileDiagnostic:()=>{}});
|
|
34
|
+
const paths=parsed.options.paths||{},missing=['next','next/*'].filter(name=>!Object.hasOwn(paths,name));if(!missing.length)return;
|
|
35
|
+
const ast=ts.parseJsonText(filename,source),object=ast.statements[0].expression;
|
|
36
|
+
const options=object.properties.find(p=>p.name?.text==='compilerOptions')?.initializer;
|
|
37
|
+
const ownPaths=options?.properties?.find(p=>p.name?.text==='paths')?.initializer;
|
|
38
|
+
const base=parsed.options.baseUrl||path.dirname(filename),oldBase=parsed.options.baseUrl||parsed.options.pathsBasePath||path.dirname(filename);
|
|
39
|
+
const relative=file=>{let value=path.relative(base,file).replaceAll(path.sep,'/');return value.startsWith('.')?value:'./'+value;};
|
|
40
|
+
const compat=fileURLToPath(new URL('../compat/',import.meta.url));
|
|
41
|
+
const additions=Object.fromEntries(missing.map(name=>[name,[relative(path.join(compat,name==='next'?'index.d.ts':'*.d.cts'))]]));
|
|
42
|
+
let target,values;
|
|
43
|
+
if(ownPaths){if(!ts.isObjectLiteralExpression(ownPaths))throw Error('tsconfig.compilerOptions.paths must be an object');target=ownPaths;values=additions;}
|
|
44
|
+
else{
|
|
45
|
+
const merged={...Object.fromEntries(Object.entries(paths).map(([name,values])=>[name,values.map(value=>relative(path.resolve(oldBase,value)))])),...additions};
|
|
46
|
+
if(options){if(!ts.isObjectLiteralExpression(options))throw Error('tsconfig.compilerOptions must be an object');target=options;values={paths:merged};}
|
|
47
|
+
else{target=object;values={compilerOptions:{paths:merged}};}
|
|
48
|
+
}
|
|
49
|
+
const position=target.end-1,separator=target.properties.length&&!target.properties.hasTrailingComma?',':'';
|
|
50
|
+
const text=Object.entries(values).map(([name,value])=>JSON.stringify(name)+': '+JSON.stringify(value)).join(',\n ');
|
|
51
|
+
await changed(filename,source.slice(0,position)+separator+'\n '+text+'\n'+source.slice(position),updates);
|
|
52
|
+
}
|
|
53
|
+
/** Include one generated root without replacing JSONC comments or inherited include globs. */
|
|
54
|
+
async function includeTypes(root,config,helper,updates){
|
|
55
|
+
const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');
|
|
56
|
+
const source=await readFile(filename,'utf8').catch(error=>{if(error.code==='ENOENT')return;throw error;});if(source===undefined)return;
|
|
57
|
+
const toolchain=await resolveTypeScript(root);
|
|
58
|
+
if(toolchain.native){await changed(filename,await nativeTypeConfig(filename,helper),updates);return;}
|
|
59
|
+
let ts;try{ts=createRequire(path.join(root,'package.json'))('typescript');}catch{throw Error('typedRoutes requires TypeScript installed in the application');}
|
|
60
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(filename,{noEmit:true},{...ts.sys,onUnRecoverableConfigFileDiagnostic:diagnostic=>{throw Error(ts.flattenDiagnosticMessageText(diagnostic.messageText,'\n'));}});
|
|
61
|
+
if(!parsed||parsed.errors.some(error=>error.code!==18003))throw Error('Cannot configure typed routes: invalid tsconfig');
|
|
62
|
+
if(parsed.fileNames.some(name=>path.resolve(name)===helper)){await bindTypes(filename,ts,updates);return;}
|
|
63
|
+
const ast=ts.parseJsonText(filename,source),object=ast.statements[0]?.expression;
|
|
64
|
+
if(!object||!ts.isObjectLiteralExpression(object))throw Error('Cannot add typed route declarations to tsconfig');
|
|
65
|
+
const property=object.properties.find(p=>p.name?.text==='files');
|
|
66
|
+
let edited;
|
|
67
|
+
if(property){
|
|
68
|
+
if(!ts.isArrayLiteralExpression(property.initializer))throw Error('tsconfig.files must be an array');
|
|
69
|
+
const array=property.initializer,position=array.end-1,separator=array.elements.length&&!array.elements.hasTrailingComma?',':'';
|
|
70
|
+
edited=source.slice(0,position)+separator+JSON.stringify(path.relative(path.dirname(filename),helper).replaceAll(path.sep,'/'))+source.slice(position);
|
|
71
|
+
}else{
|
|
72
|
+
// TS resolves inherited files relative to this config in parsed.raw.files.
|
|
73
|
+
const files=[...(parsed.raw.files||[]),path.relative(path.dirname(filename),helper).replaceAll(path.sep,'/')];
|
|
74
|
+
const position=object.end-1,separator=object.properties.length&&!object.properties.hasTrailingComma?',':'';
|
|
75
|
+
edited=source.slice(0,position)+separator+'\n "files": '+JSON.stringify(files)+'\n'+source.slice(position);
|
|
76
|
+
}
|
|
77
|
+
await changed(filename,edited,updates);
|
|
78
|
+
await bindTypes(filename,ts,updates);
|
|
79
|
+
}
|
|
80
|
+
export async function writeTypedRoutes(root,project,config){
|
|
81
|
+
const updates=new Set();
|
|
82
|
+
const directory=path.join(root,'.prnext-types'),file=path.join(directory,'routes.d.ts');
|
|
83
|
+
const info=await lstat(directory).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
84
|
+
if(info&&(!info.isDirectory()||info.isSymbolicLink())){if(config.typedRoutes)throw Error('.prnext-types must be a real directory');return;}
|
|
85
|
+
if(!config.typedRoutes){const existing=await readFile(file,'utf8').catch(()=>null);if(existing?.startsWith(marker))await changed(file,routeDeclarations({routes:[]},{enabled:false}),updates);return [...updates];}
|
|
86
|
+
await mkdir(directory,{recursive:true,mode:0o700});
|
|
87
|
+
const previous=await readFile(file,'utf8').catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
88
|
+
if(previous&&!previous.startsWith(marker))throw Error('Refusing to overwrite a non-generated route registry');
|
|
89
|
+
await changed(file,routeDeclarations(project),updates);
|
|
90
|
+
const helper=path.join(root,'prnext-env.d.ts'),reference='/// <reference path="./.prnext-types/routes.d.ts" />';
|
|
91
|
+
const contents=await readFile(helper,'utf8').catch(error=>{if(error.code==='ENOENT')return '';throw error;});
|
|
92
|
+
if(!contents.includes(reference))await changed(helper,reference+'\n'+contents,updates);
|
|
93
|
+
await includeTypes(root,config,helper,updates);
|
|
94
|
+
return [...updates];
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {createRequire} from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createTypeScriptWatchSystem} from './typescript-watch.mjs';
|
|
4
|
+
const [compiler,filename,cache]=process.argv.slice(2),ts=createRequire(import.meta.url)(compiler);
|
|
5
|
+
const format={getCanonicalFileName:name=>name,getCurrentDirectory:()=>path.dirname(filename),getNewLine:()=>ts.sys.newLine};
|
|
6
|
+
const send=diagnostics=>{
|
|
7
|
+
let bytes=0;const errors=[];
|
|
8
|
+
for(const diagnostic of diagnostics){if(diagnostic.category!==ts.DiagnosticCategory.Error)continue;const text=ts.formatDiagnostics([diagnostic],format);if(bytes+text.length>20000||errors.length>=40)break;bytes+=text.length;errors.push(text);}
|
|
9
|
+
if(process.connected)process.send({type:'diagnostics',errors});
|
|
10
|
+
};
|
|
11
|
+
const host=ts.createWatchCompilerHost(filename,{noEmit:true,incremental:true,...(cache?{tsBuildInfoFile:cache}:{})},createTypeScriptWatchSystem(ts,process.cwd()),ts.createSemanticDiagnosticsBuilderProgram,diagnostic=>send([diagnostic]),()=>{});
|
|
12
|
+
host.writeFile=(filename,contents)=>{if(cache&&path.resolve(filename)===path.resolve(cache))ts.sys.writeFile(filename,contents);};
|
|
13
|
+
host.afterProgramCreate=builder=>{
|
|
14
|
+
try{send(ts.getPreEmitDiagnostics(builder.getProgram()));builder.emit(undefined,host.writeFile);}catch(error){send([{category:ts.DiagnosticCategory.Error,code:0,messageText:error.message}]);}
|
|
15
|
+
};
|
|
16
|
+
const watcher=ts.createWatchProgram(host);
|
|
17
|
+
const close=()=>{watcher.close();process.exit(0);};
|
|
18
|
+
process.once('disconnect',close);process.once('SIGTERM',close);process.once('SIGINT',close);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {fork} from 'node:child_process';
|
|
2
|
+
import {access} from 'node:fs/promises';
|
|
3
|
+
import {resolveTypeScript} from './typescript-toolchain.mjs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {privateCacheDirectory} from './persistent-cache.mjs';
|
|
6
|
+
export async function startTypeChecker(root,config,notify,{fork:spawnChecker=fork,recheckDelayMs=3000}={}){
|
|
7
|
+
const noop={inactive:true,async close(){}};
|
|
8
|
+
if(config.typescript?.devChecks===false)return noop;
|
|
9
|
+
const filename=path.resolve(root,config.typescript?.tsconfigPath||'tsconfig.json');try{await access(filename);}catch{return noop;}
|
|
10
|
+
let toolchain;try{toolchain=await resolveTypeScript(root);}catch(error){console.warn('TypeScript diagnostics could not start: '+error.message);return noop;}
|
|
11
|
+
const directory=await privateCacheDirectory(root,'typescript-dev');
|
|
12
|
+
let child,closed=false,failures=0,timer,restartTimer,planned,pendingInvalidation;
|
|
13
|
+
function launch(){
|
|
14
|
+
if(closed)return;
|
|
15
|
+
const worker=toolchain.native?'./typescript-native-worker.mjs':'./typescript-dev-worker.mjs';
|
|
16
|
+
child=spawnChecker(new URL(worker,import.meta.url),[toolchain.compiler,filename,toolchain.native?'watch':directory?path.join(directory,'project.tsbuildinfo'):''],{cwd:root,execArgv:['--max-old-space-size=512'],stdio:['ignore','ignore','pipe','ipc'],windowsHide:true});
|
|
17
|
+
const current=child;
|
|
18
|
+
let details='',healthyAt=0;child.stderr.on('data',chunk=>{details=(details+chunk).slice(-2048);});
|
|
19
|
+
child.on('message',message=>{
|
|
20
|
+
if(closed||child!==current||planned===current)return;
|
|
21
|
+
if(message?.type==='diagnostics'&&Array.isArray(message.errors)){
|
|
22
|
+
// The native watcher normally handles edits incrementally. Its startup
|
|
23
|
+
// can miss an edit; retain one bounded fallback when no report arrives.
|
|
24
|
+
if(pendingInvalidation?.worker===current&&!pendingInvalidation.forced){clearTimeout(restartTimer);pendingInvalidation=undefined;}
|
|
25
|
+
healthyAt ||= Date.now();notify(message.errors.filter(value=>typeof value==='string').slice(0,40));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
child.on('error',error=>{notify(['TypeScript checker: '+error.message]);});
|
|
29
|
+
child.once('exit',()=>{
|
|
30
|
+
if(pendingInvalidation?.worker===current){clearTimeout(restartTimer);pendingInvalidation=undefined;}
|
|
31
|
+
if(closed)return;if(planned===current){planned=undefined;launch();return;}if(healthyAt&&Date.now()-healthyAt>=60000)failures=0;if(++failures>=3){notify(['TypeScript checker stopped after repeated failures. '+details]);return;}timer=setTimeout(launch,1000*failures);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
launch();return {
|
|
35
|
+
invalidate(file,event){
|
|
36
|
+
if(!toolchain.native||closed||!child?.connected)return;
|
|
37
|
+
const extension=path.extname(file);if(extension&&!/^\.(?:[cm]?[jt]sx?|json)$/i.test(extension))return;
|
|
38
|
+
const forced=event==='rename'||/\.json$/i.test(file);
|
|
39
|
+
if(!forced&&event!=='change')return;
|
|
40
|
+
clearTimeout(restartTimer);
|
|
41
|
+
const pending={worker:child,forced:forced||pendingInvalidation?.forced===true};pendingInvalidation=pending;
|
|
42
|
+
restartTimer=setTimeout(()=>{
|
|
43
|
+
if(closed||pendingInvalidation!==pending||child!==pending.worker||!child?.connected)return;
|
|
44
|
+
pendingInvalidation=undefined;planned=child;child.disconnect();
|
|
45
|
+
},pending.forced?150:recheckDelayMs);
|
|
46
|
+
},
|
|
47
|
+
async close(){closed=true;clearTimeout(timer);clearTimeout(restartTimer);const old=child;if(!old||old.exitCode!==null||old.signalCode)return;await new Promise(resolve=>{const timer=setTimeout(()=>old.kill('SIGKILL'),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});if(old.connected)old.disconnect();else old.kill();});},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {StringDecoder} from 'node:string_decoder';
|
|
3
|
+
import {mkdtemp,rm} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const [compiler,filename,mode]=process.argv.slice(2);
|
|
8
|
+
const watching=mode==='watch';
|
|
9
|
+
let closing=false,buffer='',output='',overflow=false;
|
|
10
|
+
const cache=await mkdtemp(path.join(tmpdir(),'prnext-typescript-native-'));
|
|
11
|
+
const child=spawn(process.execPath,[compiler,'--project',filename,'--noEmit','--incremental','true','--tsBuildInfoFile',path.join(cache,'project.tsbuildinfo'),'--pretty','false','--locale','en',...(watching?['--watch','--preserveWatchOutput']:[])],{
|
|
12
|
+
cwd:process.cwd(),windowsHide:true,detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],
|
|
13
|
+
// Native TS respects explicit project watch options. The polling defaults
|
|
14
|
+
// also cover editors/filesystems that silently lose native watch events.
|
|
15
|
+
env:{...process.env,TSC_WATCHFILE:process.env.TSC_WATCHFILE||'DynamicPriorityPolling',TSC_WATCHDIRECTORY:process.env.TSC_WATCHDIRECTORY||'DynamicPriorityPolling'},
|
|
16
|
+
});
|
|
17
|
+
function send(value){if(process.connected)process.send(value);}
|
|
18
|
+
function consume(chunk){
|
|
19
|
+
const text=chunk.toString('utf8').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'');
|
|
20
|
+
if(!watching){output+=text;if(output.length>4*1024*1024){overflow=true;void close();}return;}
|
|
21
|
+
buffer+=text;
|
|
22
|
+
// Consume complete lines, not arbitrary OS chunks. Start/completion markers
|
|
23
|
+
// delimit each diagnostic generation; a successful edit clears old errors.
|
|
24
|
+
let end;
|
|
25
|
+
while((end=buffer.indexOf('\n'))!==-1){
|
|
26
|
+
const line=buffer.slice(0,end).replace(/\r$/,'');buffer=buffer.slice(end+1);
|
|
27
|
+
if(/Starting (?:incremental )?compilation in watch mode|File change detected/.test(line))output='';
|
|
28
|
+
else if(/Found \d+ errors?\. Watching for file changes\./.test(line)){
|
|
29
|
+
send({type:'diagnostics',errors:/Found 0 errors/.test(line)?[]:output.trim()?[output.trim()]:['TypeScript reported errors. Run prnext typecheck for details.']});output='';
|
|
30
|
+
}else if(line.trim()&&output.length<20000)output+=(line+'\n').slice(0,20000-output.length);
|
|
31
|
+
}
|
|
32
|
+
if(buffer.length>20000)buffer=buffer.slice(-20000);
|
|
33
|
+
}
|
|
34
|
+
for(const stream of [child.stdout,child.stderr]){
|
|
35
|
+
const decoder=new StringDecoder('utf8');stream.on('data',chunk=>consume(decoder.write(chunk)));stream.on('end',()=>consume(decoder.end()));
|
|
36
|
+
}
|
|
37
|
+
child.once('error',error=>{send({type:'failure',message:error.message});process.exitCode=1;});
|
|
38
|
+
child.once('close',async(code,signal)=>{
|
|
39
|
+
// Only the directory returned by mkdtemp above is removed.
|
|
40
|
+
if(path.dirname(cache)===path.resolve(tmpdir())&&path.basename(cache).startsWith('prnext-typescript-native-'))await rm(cache,{recursive:true,force:true,maxRetries:5,retryDelay:100});
|
|
41
|
+
send({type:'complete',code:overflow?1:code??1,output:overflow?'TypeScript output exceeded 4 MiB. Run tsc directly for the complete output.':output,signal});
|
|
42
|
+
if(process.connected)process.disconnect();
|
|
43
|
+
});
|
|
44
|
+
async function close(){
|
|
45
|
+
if(closing)return;closing=true;
|
|
46
|
+
if(child.exitCode!==null||child.signalCode)return;
|
|
47
|
+
if(process.platform==='win32'){
|
|
48
|
+
// Own exactly this compiler subtree; TypeScript's official Node launcher
|
|
49
|
+
// starts its native executable as a child on Windows.
|
|
50
|
+
const killer=spawn('taskkill',['/PID',String(child.pid),'/T','/F'],{windowsHide:true,stdio:'ignore'});
|
|
51
|
+
killer.on('error',()=>child.kill('SIGKILL'));
|
|
52
|
+
}else{try{process.kill(-child.pid,'SIGTERM');}catch{child.kill('SIGTERM');}}
|
|
53
|
+
}
|
|
54
|
+
process.once('disconnect',()=>{void close();});
|
|
55
|
+
process.once('SIGTERM',()=>{void close();});process.once('SIGINT',()=>{void close();});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {fork} from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function checkNativeTypeScript(compiler,filename,root){
|
|
4
|
+
return new Promise((resolve,reject)=>{
|
|
5
|
+
const child=fork(new URL('./typescript-native-worker.mjs',import.meta.url),[compiler,filename,'check'],{cwd:root,windowsHide:true,execArgv:[],stdio:['ignore','ignore','pipe','ipc']});
|
|
6
|
+
let result,details='',timedOut=false,force;
|
|
7
|
+
const deadline=setTimeout(()=>{timedOut=true;if(child.connected)child.disconnect();force=setTimeout(()=>child.kill('SIGKILL'),5000);},120000);
|
|
8
|
+
child.stderr.on('data',chunk=>{details=(details+chunk).slice(-8192);});
|
|
9
|
+
child.on('message',message=>{if(message?.type==='complete')result=message;else if(message?.type==='failure')details=String(message.message).slice(0,8192);});
|
|
10
|
+
child.once('error',error=>{clearTimeout(deadline);clearTimeout(force);reject(error);});
|
|
11
|
+
child.once('exit',()=>{
|
|
12
|
+
clearTimeout(deadline);clearTimeout(force);
|
|
13
|
+
if(timedOut)reject(new Error('TypeScript check exceeded 120 seconds. Run tsc directly to inspect the project.'));
|
|
14
|
+
else if(result?.code===0)resolve();
|
|
15
|
+
else reject(new Error(result?.output||details||'TypeScript native checker exited before returning diagnostics.'));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {readFile,stat} from 'node:fs/promises';
|
|
2
|
+
import {createRequire} from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Select from the application's installation, never from the framework's own
|
|
6
|
+
// development dependencies. Native TypeScript deliberately lacks the old API.
|
|
7
|
+
export async function resolveTypeScript(root) {
|
|
8
|
+
const require=createRequire(path.join(root,'package.json'));
|
|
9
|
+
const manifest=require.resolve('typescript/package.json');
|
|
10
|
+
const metadata=JSON.parse(await readFile(manifest,'utf8'));
|
|
11
|
+
const major=Number(/^([0-9]+)\./.exec(metadata.version)?.[1]);
|
|
12
|
+
if(!Number.isInteger(major))throw new Error('The installed TypeScript package has no valid version.');
|
|
13
|
+
if(major<7)return {native:false,compiler:require.resolve('typescript'),version:metadata.version};
|
|
14
|
+
const entry=typeof metadata.bin==='string'?metadata.bin:metadata.bin?.tsc;
|
|
15
|
+
if(typeof entry!=='string'||!entry)throw new Error('The installed TypeScript package has no tsc command. Reinstall TypeScript with its optional native dependencies.');
|
|
16
|
+
const directory=path.dirname(manifest),bin=path.resolve(directory,entry);
|
|
17
|
+
if(!bin.startsWith(directory+path.sep)||!(await stat(bin)).isFile())throw new Error('The installed TypeScript tsc command is invalid.');
|
|
18
|
+
return {native:true,compiler:bin,version:metadata.version};
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// macOS native file notifications can miss an edit while the watch program is
|
|
4
|
+
// compiling. Use TypeScript's adaptive metadata polling for project inputs;
|
|
5
|
+
// installed dependencies keep native watchers and incur no extra polling.
|
|
6
|
+
export function createTypeScriptWatchSystem(ts,root,{platform=process.platform,env=process.env}={}){
|
|
7
|
+
const system=ts.sys;
|
|
8
|
+
if(platform!=='darwin'||env.TSC_WATCHFILE||env.TSC_NONPOLLING_WATCHER)return system;
|
|
9
|
+
const requestedRoot=path.resolve(root);
|
|
10
|
+
const physical=filename=>{try{return path.resolve(system.realpath?.(filename)||filename);}catch{return path.resolve(filename);}};
|
|
11
|
+
const physicalRoot=physical(requestedRoot),normalize=filename=>system.useCaseSensitiveFileNames?filename:filename.toLowerCase();
|
|
12
|
+
const inside=(base,filename)=>{
|
|
13
|
+
const relative=path.relative(normalize(base),normalize(filename));
|
|
14
|
+
return relative&&relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative)&&!relative.split(path.sep).includes('node_modules');
|
|
15
|
+
};
|
|
16
|
+
return {...system,watchFile(filename,callback,interval,options){
|
|
17
|
+
const absolute=path.resolve(filename);
|
|
18
|
+
if(options?.watchFile===undefined&&(inside(requestedRoot,absolute)||inside(physicalRoot,absolute))&&inside(physicalRoot,physical(absolute)))
|
|
19
|
+
options={...options,watchFile:ts.WatchFileKind.DynamicPriorityPolling};
|
|
20
|
+
return system.watchFile(filename,callback,interval,options);
|
|
21
|
+
}};
|
|
22
|
+
}
|
package/build/typescript.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import { execFile } from 'node:child_process';
|
|
4
3
|
import { promisify } from 'node:util';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import path from 'node:path';
|
|
6
|
+
import {resolveTypeScript} from './typescript-toolchain.mjs';
|
|
7
|
+
import {checkNativeTypeScript} from './typescript-native.mjs';
|
|
7
8
|
|
|
8
9
|
const execute = promisify(execFile);
|
|
9
10
|
|
|
10
11
|
export function validateTypeScript(input) {
|
|
11
12
|
if (input === undefined) return undefined;
|
|
12
13
|
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new TypeError('typescript must be an object');
|
|
13
|
-
for (const key of Object.keys(input)) if (!['ignoreBuildErrors', 'tsconfigPath'].includes(key)) throw new TypeError(`typescript.${key} is not supported`);
|
|
14
|
+
for (const key of Object.keys(input)) if (!['ignoreBuildErrors', 'tsconfigPath', 'devChecks'].includes(key)) throw new TypeError(`typescript.${key} is not supported`);
|
|
14
15
|
const { ignoreBuildErrors = false, tsconfigPath = 'tsconfig.json' } = input;
|
|
15
16
|
if (typeof ignoreBuildErrors !== 'boolean') throw new TypeError('typescript.ignoreBuildErrors must be a boolean');
|
|
16
17
|
if (typeof tsconfigPath !== 'string' || !tsconfigPath || tsconfigPath.includes('\0') || Buffer.byteLength(tsconfigPath) > 4096) throw new TypeError('typescript.tsconfigPath must be a nonempty filesystem path of at most 4096 bytes');
|
|
17
|
-
|
|
18
|
+
if(input.devChecks!==undefined&&typeof input.devChecks!=='boolean')throw new TypeError('typescript.devChecks must be a boolean');
|
|
19
|
+
return { ignoreBuildErrors, tsconfigPath, ...(input.devChecks===undefined?{}:{devChecks:input.devChecks}) };
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export async function projectTsconfig(root, config) {
|
|
@@ -32,15 +34,16 @@ export async function projectTsconfig(root, config) {
|
|
|
32
34
|
|
|
33
35
|
/** Run the checker outside the build/dev process so its graph is released on exit.
|
|
34
36
|
* Never emit files, rewrite the user's config, or install packages implicitly. */
|
|
35
|
-
export async function checkTypeScript(root, config, { dev = false, tsconfig } = {}) {
|
|
36
|
-
if (dev || !config.typescript || config.typescript.ignoreBuildErrors) return;
|
|
37
|
+
export async function checkTypeScript(root, config, { dev = false, tsconfig, force = false } = {}) {
|
|
38
|
+
if (!force && (dev || !config.typescript || config.typescript.ignoreBuildErrors)) return;
|
|
37
39
|
tsconfig ??= await projectTsconfig(root, config);
|
|
38
40
|
if (!tsconfig) throw new Error('TypeScript checking was explicitly enabled, but tsconfig.json does not exist. Create a TypeScript configuration, set typescript.tsconfigPath to an existing configuration, or explicitly set typescript.ignoreBuildErrors: true if types are checked separately.');
|
|
39
|
-
let
|
|
40
|
-
try {
|
|
41
|
+
let toolchain;
|
|
42
|
+
try { toolchain = await resolveTypeScript(root); }
|
|
41
43
|
catch (error) { throw new Error('TypeScript checking requires typescript in the project. Install it as a dev dependency, or explicitly set typescript.ignoreBuildErrors: true if a separate process checks types.', { cause: error }); }
|
|
42
44
|
try {
|
|
43
|
-
|
|
45
|
+
if(toolchain.native){await checkNativeTypeScript(toolchain.compiler,tsconfig,root);return;}
|
|
46
|
+
await execute(process.execPath, [fileURLToPath(new URL('./typescript-check.mjs', import.meta.url)), toolchain.compiler, tsconfig], {
|
|
44
47
|
cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024,
|
|
45
48
|
});
|
|
46
49
|
} catch (error) {
|