@wular/pnext 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/reference/compat.md +2 -2
- package/reference/getting-started.md +2 -2
- package/src/cli/adapters/vercel.ts +27 -0
- package/src/cli/analyze.ts +8 -7
- package/src/cli/boot/trace.ts +9 -3
- package/src/cli/build/cache.ts +324 -0
- package/src/cli/build.ts +42 -18
- package/src/cli/create.ts +4 -4
- package/src/cli/dev.ts +23 -0
- package/src/cli/migrate/package-json.ts +3 -2
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/migrate/run.ts +2 -2
- package/src/cli/migrate/scan.ts +128 -1
- package/src/cli/serve/pipeline.ts +42 -42
- package/src/cli/start.ts +20 -20
- package/src/client/build.ts +64 -35
- package/src/client/entry.ts +228 -16
- package/src/client/router/page-slot.ts +81 -0
- package/src/client/router/runtime.ts +107 -112
- package/src/compat/bundler/cjs-exports.ts +26 -20
- package/src/compat/client/css-order.ts +1 -1
- package/src/compat/client/navigation-scroll.ts +36 -40
- package/src/compat/css/modules.ts +6 -2
- package/src/compat/metadata.ts +0 -9
- package/src/compat/next/font/cache.ts +10 -22
- package/src/compat/next/font/runtime.ts +20 -0
- package/src/compat/next/legacy-request-apis.ts +41 -5
- package/src/compat/next/script.tsx +16 -24
- package/src/compat/react/client-lite.ts +1 -0
- package/src/compat/react/client.ts +11 -0
- package/src/compat/react/dom-client.ts +11 -2
- package/src/compat/react/dom-react-server.ts +4 -0
- package/src/compat/react/dom.ts +11 -2
- package/src/compat/react/internals.ts +65 -0
- package/src/compat/react/parity.ts +47 -0
- package/src/compat/react/preact.ts +31 -1
- package/src/compat/react/react-server.ts +7 -0
- package/src/compat/react/server.ts +16 -2
- package/src/compat/react/style-values.ts +68 -0
- package/src/compat/register/proxy.ts +3 -1
- package/src/compat/register/render.ts +14 -3
- package/src/compat/register/routing.ts +57 -7
- package/src/compat/register/typed-routes.ts +0 -2
- package/src/compat/tsconfig-defaults.ts +4 -0
- package/src/compat/typecheck/check.ts +26 -71
- package/src/compat/typecheck/worker.ts +3 -2
- package/src/compat/validation/validate.ts +2 -0
- package/src/compat-bootstrap.ts +4 -0
- package/src/css/build.ts +153 -14
- package/src/css/postcss.ts +32 -3
- package/src/dev/restart/client-key.ts +1 -1
- package/src/dev/restart/global-css.ts +12 -94
- package/src/dev/restart/record.ts +106 -0
- package/src/dev/restart/route-facts.ts +11 -90
- package/src/dev/server.ts +208 -41
- package/src/extensions.ts +69 -3
- package/src/render/hooks.ts +1 -1
- package/src/render/metadata.ts +89 -17
- package/src/render/renderer.ts +325 -138
- package/src/render/resource-hints.ts +62 -9
- package/src/render/slots.tsx +2 -1
- package/src/resolve/scan-facts.ts +13 -5
- package/src/routing/forwarded.ts +20 -7
- package/src/routing/handler.ts +6 -2
- package/src/routing/href.ts +11 -1
- package/src/routing/metadata-files.ts +16 -3
- package/src/routing/proxy.ts +6 -3
- package/src/routing/routes.ts +253 -34
- package/src/runtime/fetch-host.ts +42 -0
- package/src/runtime/fingerprint.ts +132 -0
- package/src/runtime/loader.ts +7 -5
- package/src/runtime/module-cache.ts +3 -36
- package/src/runtime/modules.ts +333 -77
- package/src/runtime/vendor-build.ts +34 -5
- package/src/runtime/vendor.ts +1 -18
- package/src/types.ts +6 -0
- package/src/utils/asset-hash.ts +22 -0
- package/src/utils/code.ts +41 -0
- package/src/utils/html.ts +4 -1
- package/src/utils/registry.ts +70 -0
- package/src/utils/serialize.ts +66 -7
package/src/runtime/modules.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { noteModuleGeneration, noteModuleImported } from './module-generations'
|
|
|
19
19
|
import {
|
|
20
20
|
rewriteServerSource,
|
|
21
21
|
serverBundleTargetForRuntime,
|
|
22
|
+
serverBundleConditions,
|
|
22
23
|
serverBundleRequireConditions,
|
|
23
24
|
type ServerBundleTarget,
|
|
24
25
|
} from './loader'
|
|
@@ -350,6 +351,59 @@ function graphEdges(config: ResolvedConfig, file: string, source: string) {
|
|
|
350
351
|
return { imports, packages, client: hasUseClientDirective(source) }
|
|
351
352
|
}
|
|
352
353
|
|
|
354
|
+
/**
|
|
355
|
+
* No build ever waits for another build. Compiling a module needs its imports' artifact PATHS, which
|
|
356
|
+
* are a pure function of the source graph - never their bytes - so a compile registers what it
|
|
357
|
+
* reaches and moves on. That removes the deadlock class outright: real app graphs are cyclic, and any
|
|
358
|
+
* scheme where one build awaits another lets two of them each hold what the other needs, which stalls
|
|
359
|
+
* the request with no error until the socket idles out.
|
|
360
|
+
*
|
|
361
|
+
* The one real requirement is temporal and belongs at the boundary: no href may reach `import()`
|
|
362
|
+
* before everything reachable from it is on disk. An entry therefore waits for quiescence, not for a
|
|
363
|
+
* closure it can compute: with parents no longer written after their children, an artifact already on
|
|
364
|
+
* disk says nothing about its imports, so "what this entry reached" is not a set anyone can name.
|
|
365
|
+
*
|
|
366
|
+
* The wait runs off an append-only log rather than the in-flight map, so a build that starts AND
|
|
367
|
+
* settles between two looks is still waited on - and its failure still reaches the caller instead of
|
|
368
|
+
* disappearing with the map entry.
|
|
369
|
+
*
|
|
370
|
+
* Waiting is global; FAILING is not. An entry only rethrows for builds its own walk started or
|
|
371
|
+
* joined, so one request's broken module cannot 500 an unrelated request that merely overlapped it.
|
|
372
|
+
*/
|
|
373
|
+
const buildLog: Promise<string>[] = []
|
|
374
|
+
const walkBuilds = new WeakMap<Set<string>, Set<Promise<string>>>()
|
|
375
|
+
let drainsActive = 0
|
|
376
|
+
|
|
377
|
+
/** The builds this walk owns - keyed on the walk's own `visited` set, which already identifies it. */
|
|
378
|
+
function ownedBuilds(visited: Set<string>) {
|
|
379
|
+
let owned = walkBuilds.get(visited)
|
|
380
|
+
if (!owned) {
|
|
381
|
+
owned = new Set()
|
|
382
|
+
walkBuilds.set(visited, owned)
|
|
383
|
+
}
|
|
384
|
+
return owned
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function registerBuild(build: Promise<string>) {
|
|
388
|
+
buildLog.push(build)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function drainModuleBuilds(visited: Set<string>) {
|
|
392
|
+
drainsActive += 1
|
|
393
|
+
try {
|
|
394
|
+
for (let index = 0; index < buildLog.length;) {
|
|
395
|
+
const pending = buildLog.slice(index)
|
|
396
|
+
index = buildLog.length
|
|
397
|
+
await Promise.allSettled(pending)
|
|
398
|
+
}
|
|
399
|
+
await Promise.all([...(walkBuilds.get(visited) ?? [])])
|
|
400
|
+
} finally {
|
|
401
|
+
drainsActive -= 1
|
|
402
|
+
// Nothing in flight and nobody mid-drain: the log has served its purpose and can go.
|
|
403
|
+
if (drainsActive === 0 && moduleBuilds.size === 0) buildLog.length = 0
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
353
407
|
function withBuildSlot<T>(task: () => Promise<T>): Promise<T> {
|
|
354
408
|
return new Promise<T>((resolve, reject) => {
|
|
355
409
|
const run = () => {
|
|
@@ -395,21 +449,27 @@ export async function devServerModuleHref(
|
|
|
395
449
|
const reactServerLayer =
|
|
396
450
|
moduleOptions.reactServerLayer ?? (conditionTarget === 'server' || conditionTarget === 'edge')
|
|
397
451
|
const release = devModuleGraph(config).hold()
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
452
|
+
const visited = new Set<string>()
|
|
453
|
+
try {
|
|
454
|
+
const compiled = await writeDevModule(config, file, visited, {
|
|
455
|
+
aliases: {
|
|
456
|
+
...coreAliases(config, 'server'),
|
|
457
|
+
...(reactServerLayer ? getImportAliasExtensions().reactServerLayerAliases(config) : {}),
|
|
458
|
+
},
|
|
459
|
+
profile: reactCompatEnabled(config) ? 'compat' : 'server',
|
|
460
|
+
conditionTarget,
|
|
461
|
+
reactServerLayer,
|
|
462
|
+
externalLoadTarget:
|
|
463
|
+
moduleOptions.externalLoadTarget ?? externalLoadTargetForConditionTarget(conditionTarget),
|
|
464
|
+
stubClientImports: reactCompatEnabled(config),
|
|
465
|
+
rewriteExternalServerImports: true,
|
|
466
|
+
bundleExternalPackages: reactCompatEnabled(config),
|
|
467
|
+
})
|
|
468
|
+
await drainModuleBuilds(visited)
|
|
469
|
+
return pathToFileHref(compiled)
|
|
470
|
+
} finally {
|
|
471
|
+
release()
|
|
472
|
+
}
|
|
413
473
|
}
|
|
414
474
|
|
|
415
475
|
export async function devClientModuleHref(
|
|
@@ -422,13 +482,19 @@ export async function devClientModuleHref(
|
|
|
422
482
|
const throughPackage = await packageClientModuleHref(config, file, conditionTarget)
|
|
423
483
|
if (throughPackage) return throughPackage
|
|
424
484
|
const release = devModuleGraph(config).hold()
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
485
|
+
const visited = new Set<string>()
|
|
486
|
+
try {
|
|
487
|
+
const compiled = await writeDevModule(
|
|
488
|
+
config,
|
|
489
|
+
file,
|
|
490
|
+
visited,
|
|
491
|
+
clientLayerOptions(config, conditionTarget),
|
|
492
|
+
)
|
|
493
|
+
await drainModuleBuilds(visited)
|
|
494
|
+
return pathToFileHref(compiled)
|
|
495
|
+
} finally {
|
|
496
|
+
release()
|
|
497
|
+
}
|
|
432
498
|
}
|
|
433
499
|
|
|
434
500
|
/**
|
|
@@ -1056,8 +1122,14 @@ async function writeDevModule(
|
|
|
1056
1122
|
// the life of the process.
|
|
1057
1123
|
if (devArtifactUsable(outFile)) return outFile
|
|
1058
1124
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1125
|
+
// Already being built: its artifact path is all this caller needs, and the entry's drain is what
|
|
1126
|
+
// waits for the bytes. Awaiting it here is what used to deadlock two walks meeting a cycle. The
|
|
1127
|
+
// walk still OWNS it - its graph names that artifact, so its failure is this entry's failure.
|
|
1128
|
+
const inFlight = moduleBuilds.get(outFile)
|
|
1129
|
+
if (inFlight) {
|
|
1130
|
+
ownedBuilds(visited).add(inFlight)
|
|
1131
|
+
return outFile
|
|
1132
|
+
}
|
|
1061
1133
|
|
|
1062
1134
|
visited.add(visitedKey)
|
|
1063
1135
|
const started = moduleStats ? performance.now() : 0
|
|
@@ -1073,15 +1145,16 @@ async function writeDevModule(
|
|
|
1073
1145
|
}
|
|
1074
1146
|
return result
|
|
1075
1147
|
})
|
|
1076
|
-
.catch(error => {
|
|
1077
|
-
moduleBuilds.delete(outFile)
|
|
1078
|
-
throw error
|
|
1079
|
-
})
|
|
1080
1148
|
.finally(() => {
|
|
1081
|
-
|
|
1149
|
+
// Identity-guarded: a rebuild may already have claimed this path.
|
|
1150
|
+
if (moduleBuilds.get(outFile) === next) moduleBuilds.delete(outFile)
|
|
1082
1151
|
})
|
|
1152
|
+
// Only a drain consumes the rejection; this keeps it from surfacing as an unhandled one meanwhile.
|
|
1153
|
+
next.catch(() => undefined)
|
|
1154
|
+
registerBuild(next)
|
|
1155
|
+
ownedBuilds(visited).add(next)
|
|
1083
1156
|
moduleBuilds.set(outFile, next)
|
|
1084
|
-
return
|
|
1157
|
+
return outFile
|
|
1085
1158
|
}
|
|
1086
1159
|
|
|
1087
1160
|
async function writeDevModuleUncached(
|
|
@@ -1160,15 +1233,16 @@ async function writeDevModuleUncached(
|
|
|
1160
1233
|
!hasUseClientDirective(shakenSource)
|
|
1161
1234
|
? await writeClientReferenceModules(shakenSource, config, file, effectiveOptions.aliases)
|
|
1162
1235
|
: { specifiers: new Map<string, string>(), files: new Set<string>() }
|
|
1163
|
-
// Compiling this module only needs its imports' *paths*, so it runs while the
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1236
|
+
// Compiling this module only needs its imports' *paths*, so it runs while the targets themselves
|
|
1237
|
+
// compile. Registering the edges is all this awaits; drainModuleBuilds is what holds an href back
|
|
1238
|
+
// until they are on disk.
|
|
1166
1239
|
const compiled = buildDevModuleSource(
|
|
1167
1240
|
shakenSource,
|
|
1168
1241
|
config,
|
|
1169
1242
|
file,
|
|
1170
1243
|
clientReferences.specifiers,
|
|
1171
1244
|
effectiveOptions,
|
|
1245
|
+
visited,
|
|
1172
1246
|
)
|
|
1173
1247
|
// The real rejection is reported below; this only keeps it from surfacing as
|
|
1174
1248
|
// an unhandled rejection while the imports compile.
|
|
@@ -1290,8 +1364,7 @@ async function writeShakenDynamicModule(
|
|
|
1290
1364
|
if (devArtifactUsable(outFile)) return outFile
|
|
1291
1365
|
|
|
1292
1366
|
const key = `shake\0${outFile}`
|
|
1293
|
-
|
|
1294
|
-
if (existing) return existing
|
|
1367
|
+
if (moduleBuilds.has(key)) return outFile
|
|
1295
1368
|
|
|
1296
1369
|
const next = writeShakenDynamicModuleUncached(
|
|
1297
1370
|
config,
|
|
@@ -1300,16 +1373,13 @@ async function writeShakenDynamicModule(
|
|
|
1300
1373
|
visited,
|
|
1301
1374
|
options,
|
|
1302
1375
|
outFile,
|
|
1303
|
-
)
|
|
1304
|
-
.
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
.finally(() => {
|
|
1309
|
-
moduleBuilds.delete(key)
|
|
1310
|
-
})
|
|
1376
|
+
).finally(() => {
|
|
1377
|
+
if (moduleBuilds.get(key) === next) moduleBuilds.delete(key)
|
|
1378
|
+
})
|
|
1379
|
+
next.catch(() => undefined)
|
|
1380
|
+
registerBuild(next)
|
|
1311
1381
|
moduleBuilds.set(key, next)
|
|
1312
|
-
return
|
|
1382
|
+
return outFile
|
|
1313
1383
|
}
|
|
1314
1384
|
|
|
1315
1385
|
async function writeShakenDynamicModuleUncached(
|
|
@@ -1336,32 +1406,37 @@ async function writeShakenDynamicModuleUncached(
|
|
|
1336
1406
|
.map(({ target }) => writeDevModule(config, target, visited, options)),
|
|
1337
1407
|
)
|
|
1338
1408
|
const externalBundles = await externalBundleSpecifiers(targetSource, config, targetFile, options)
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1409
|
+
const shake = () =>
|
|
1410
|
+
withBuildSlot(() =>
|
|
1411
|
+
build({
|
|
1412
|
+
stdin: {
|
|
1413
|
+
contents: shakenEntrySource(usedExports),
|
|
1414
|
+
loader: 'js',
|
|
1415
|
+
resolveDir: path.dirname(targetFile),
|
|
1416
|
+
sourcefile: `${targetFile}.pnext-shake`,
|
|
1417
|
+
},
|
|
1418
|
+
bundle: true,
|
|
1419
|
+
write: false,
|
|
1420
|
+
format: 'esm',
|
|
1421
|
+
platform: 'neutral',
|
|
1422
|
+
target: 'es2022',
|
|
1423
|
+
jsx: 'automatic',
|
|
1424
|
+
jsxImportSource: 'preact',
|
|
1425
|
+
packages: 'external',
|
|
1426
|
+
logLevel: 'silent',
|
|
1427
|
+
...serverDefineOptions(),
|
|
1428
|
+
plugins: [
|
|
1429
|
+
shakeEntryPlugin(targetFile, targetSource),
|
|
1430
|
+
serverAssetPlugin(config),
|
|
1431
|
+
...getBundlerExtensions().serverEsbuildPlugins(config),
|
|
1432
|
+
devModuleResolvePlugin(config, targetFile, new Map(), externalBundles, options, visited),
|
|
1433
|
+
],
|
|
1434
|
+
}),
|
|
1435
|
+
)
|
|
1436
|
+
// The facade inlines the target outright, so its package imports are always late ones. Same shape
|
|
1437
|
+
// as buildDevModuleSource: vendor off the slot, then shake again, to a fixpoint.
|
|
1438
|
+
let result = await shake()
|
|
1439
|
+
while (await vendorLateExternalPackages(config, options, externalBundles)) result = await shake()
|
|
1365
1440
|
const output = result.outputFiles[0]
|
|
1366
1441
|
if (!output) throw new Error(`Failed to tree-shake ${targetFile}`)
|
|
1367
1442
|
await writeCompiledFile(outFile, addExtensionlessDynamicImportGlobAliases(output.text))
|
|
@@ -1460,8 +1535,48 @@ async function buildDevModuleSource(
|
|
|
1460
1535
|
file: string,
|
|
1461
1536
|
clientReferenceSpecifiers: Map<string, string>,
|
|
1462
1537
|
options: DevModuleOptions,
|
|
1538
|
+
visited: Set<string>,
|
|
1463
1539
|
) {
|
|
1464
1540
|
const externalBundles = await externalBundleSpecifiers(source, config, file, options)
|
|
1541
|
+
let compiled = await compileDevModuleSource(
|
|
1542
|
+
source,
|
|
1543
|
+
config,
|
|
1544
|
+
file,
|
|
1545
|
+
clientReferenceSpecifiers,
|
|
1546
|
+
externalBundles,
|
|
1547
|
+
options,
|
|
1548
|
+
visited,
|
|
1549
|
+
)
|
|
1550
|
+
// Vendoring for a specifier only an INLINED module asked for happens here - between compiles, with
|
|
1551
|
+
// no build slot held - and the module is compiled again with the map filled in. Doing it from the
|
|
1552
|
+
// resolve callback instead would hold a slot for the length of a vendor build.
|
|
1553
|
+
//
|
|
1554
|
+
// To a FIXPOINT, not once: vendoring a specifier changes what the next compile inlines, so round N
|
|
1555
|
+
// can expose a package round N-1 never saw. It terminates because every round records at least one
|
|
1556
|
+
// new key in `externalBundles` and a recorded specifier is never noted again.
|
|
1557
|
+
while (await vendorLateExternalPackages(config, options, externalBundles)) {
|
|
1558
|
+
compiled = await compileDevModuleSource(
|
|
1559
|
+
source,
|
|
1560
|
+
config,
|
|
1561
|
+
file,
|
|
1562
|
+
clientReferenceSpecifiers,
|
|
1563
|
+
externalBundles,
|
|
1564
|
+
options,
|
|
1565
|
+
visited,
|
|
1566
|
+
)
|
|
1567
|
+
}
|
|
1568
|
+
return compiled
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function compileDevModuleSource(
|
|
1572
|
+
source: string,
|
|
1573
|
+
config: ResolvedConfig,
|
|
1574
|
+
file: string,
|
|
1575
|
+
clientReferenceSpecifiers: Map<string, string>,
|
|
1576
|
+
externalBundles: Map<string, string>,
|
|
1577
|
+
options: DevModuleOptions,
|
|
1578
|
+
visited: Set<string>,
|
|
1579
|
+
) {
|
|
1465
1580
|
const transformed = await transformDevModuleSource(
|
|
1466
1581
|
source,
|
|
1467
1582
|
config,
|
|
@@ -1469,6 +1584,7 @@ async function buildDevModuleSource(
|
|
|
1469
1584
|
clientReferenceSpecifiers,
|
|
1470
1585
|
externalBundles,
|
|
1471
1586
|
options,
|
|
1587
|
+
visited,
|
|
1472
1588
|
)
|
|
1473
1589
|
if (transformed !== undefined) return transformed
|
|
1474
1590
|
if (moduleStats) esbuildFallbacks.add(`${devModuleProfile(options)}:${file}`)
|
|
@@ -1497,7 +1613,14 @@ async function buildDevModuleSource(
|
|
|
1497
1613
|
serverAssetPlugin(config),
|
|
1498
1614
|
...getBundlerExtensions().serverEsbuildPlugins(config),
|
|
1499
1615
|
serverClientReferencePlugin(config, options),
|
|
1500
|
-
devModuleResolvePlugin(
|
|
1616
|
+
devModuleResolvePlugin(
|
|
1617
|
+
config,
|
|
1618
|
+
file,
|
|
1619
|
+
clientReferenceSpecifiers,
|
|
1620
|
+
externalBundles,
|
|
1621
|
+
options,
|
|
1622
|
+
visited,
|
|
1623
|
+
),
|
|
1501
1624
|
],
|
|
1502
1625
|
}),
|
|
1503
1626
|
)
|
|
@@ -1522,6 +1645,7 @@ async function transformDevModuleSource(
|
|
|
1522
1645
|
clientReferenceSpecifiers: Map<string, string>,
|
|
1523
1646
|
externalBundles: Map<string, string>,
|
|
1524
1647
|
options: DevModuleOptions,
|
|
1648
|
+
visited: Set<string>,
|
|
1525
1649
|
): Promise<string | undefined> {
|
|
1526
1650
|
// Kill switch: puts every module back on the esbuild build below, for
|
|
1527
1651
|
// bisecting a suspected transform-path difference.
|
|
@@ -1542,6 +1666,7 @@ async function transformDevModuleSource(
|
|
|
1542
1666
|
clientReferenceSpecifiers,
|
|
1543
1667
|
externalBundles,
|
|
1544
1668
|
options,
|
|
1669
|
+
visited,
|
|
1545
1670
|
)
|
|
1546
1671
|
if (target === undefined) return undefined
|
|
1547
1672
|
if (target !== found.value)
|
|
@@ -1563,6 +1688,7 @@ async function devModuleSpecifierTarget(
|
|
|
1563
1688
|
clientReferenceSpecifiers: Map<string, string>,
|
|
1564
1689
|
externalBundles: Map<string, string>,
|
|
1565
1690
|
options: DevModuleOptions,
|
|
1691
|
+
visited: Set<string>,
|
|
1566
1692
|
): Promise<string | undefined> {
|
|
1567
1693
|
// Same claim `serverAssetPlugin` makes, and in the same order: it is the first
|
|
1568
1694
|
// onResolve in the build below.
|
|
@@ -1583,6 +1709,7 @@ async function devModuleSpecifierTarget(
|
|
|
1583
1709
|
options,
|
|
1584
1710
|
kind,
|
|
1585
1711
|
file,
|
|
1712
|
+
visited,
|
|
1586
1713
|
)
|
|
1587
1714
|
// A non-external result names a namespace the bundler loads inline
|
|
1588
1715
|
// (`pnext-module-external` facades) or a bare require-alias path.
|
|
@@ -1633,6 +1760,7 @@ function devModuleResolvePlugin(
|
|
|
1633
1760
|
clientReferenceSpecifiers: Map<string, string>,
|
|
1634
1761
|
externalBundles: Map<string, string>,
|
|
1635
1762
|
options: DevModuleOptions,
|
|
1763
|
+
visited: Set<string>,
|
|
1636
1764
|
): Plugin {
|
|
1637
1765
|
return {
|
|
1638
1766
|
name: 'pnext-dev-module-resolve',
|
|
@@ -1648,6 +1776,7 @@ function devModuleResolvePlugin(
|
|
|
1648
1776
|
options,
|
|
1649
1777
|
args.kind,
|
|
1650
1778
|
args.importer,
|
|
1779
|
+
visited,
|
|
1651
1780
|
),
|
|
1652
1781
|
)
|
|
1653
1782
|
},
|
|
@@ -1719,6 +1848,7 @@ async function resolveDevModuleSpecifier(
|
|
|
1719
1848
|
options: DevModuleOptions,
|
|
1720
1849
|
kind: string,
|
|
1721
1850
|
importer: string,
|
|
1851
|
+
visited: Set<string>,
|
|
1722
1852
|
): Promise<OnResolveResult> {
|
|
1723
1853
|
// An already-resolved `file://` href is a compiled module on disk; pass it straight through as
|
|
1724
1854
|
// external instead of re-resolving it. But a file:// href naming a RAW 'use client' source
|
|
@@ -1790,6 +1920,12 @@ async function resolveDevModuleSpecifier(
|
|
|
1790
1920
|
}
|
|
1791
1921
|
|
|
1792
1922
|
const externalBundle = externalBundles.get(sourcePath)
|
|
1923
|
+
// A miss here is a specifier the pre-compile scan could not have seen (its importer was inlined).
|
|
1924
|
+
// Note it and fall through to the bare-specifier path for THIS compile; the caller vendors it off
|
|
1925
|
+
// the build slot and compiles once more with the map filled in.
|
|
1926
|
+
if (externalBundle === undefined) {
|
|
1927
|
+
noteLateExternalPackage(sourcePath, config, file, importer, options, externalBundles)
|
|
1928
|
+
}
|
|
1793
1929
|
if (externalBundle && kind === 'require-call') {
|
|
1794
1930
|
const required = resolvePackageSpecifier(
|
|
1795
1931
|
config.root,
|
|
@@ -1852,12 +1988,12 @@ async function resolveDevModuleSpecifier(
|
|
|
1852
1988
|
}
|
|
1853
1989
|
const externalTarget =
|
|
1854
1990
|
resolvedExternalLoad ?? externalPackageImportTarget(config.root, file, sourcePath)
|
|
1991
|
+
if (externalTarget) return { path: `${pathToFileHref(externalTarget)}${hash}`, external: true }
|
|
1992
|
+
if (options.rewriteExternalServerImports) {
|
|
1993
|
+
return { path: runtimeServerImportTarget(specifier), external: true }
|
|
1994
|
+
}
|
|
1855
1995
|
return {
|
|
1856
|
-
path:
|
|
1857
|
-
? `${pathToFileHref(externalTarget)}${hash}`
|
|
1858
|
-
: options.rewriteExternalServerImports
|
|
1859
|
-
? runtimeServerImportTarget(specifier)
|
|
1860
|
-
: specifier,
|
|
1996
|
+
path: `${offLayerPackageTarget(config, file, sourcePath, kind, options)}${hash}`,
|
|
1861
1997
|
external: true,
|
|
1862
1998
|
}
|
|
1863
1999
|
}
|
|
@@ -1909,7 +2045,7 @@ async function resolveDevModuleSpecifier(
|
|
|
1909
2045
|
// pointing at the raw source instead would load a SECOND copy of the compat runtime graph and split its
|
|
1910
2046
|
// module state. writeDevModule dedupes in-flight builds per artifact path, so a module reached from
|
|
1911
2047
|
// several bundled importers is compiled once.
|
|
1912
|
-
await writeDevModule(config, resolved,
|
|
2048
|
+
await writeDevModule(config, resolved, visited, options)
|
|
1913
2049
|
}
|
|
1914
2050
|
return {
|
|
1915
2051
|
path: `${pathToFileHref(await devModulePath(config, resolved, options))}${hash}`,
|
|
@@ -1917,6 +2053,53 @@ async function resolveDevModuleSpecifier(
|
|
|
1917
2053
|
}
|
|
1918
2054
|
}
|
|
1919
2055
|
|
|
2056
|
+
/**
|
|
2057
|
+
* THE LAYER RULE. An artifact whose imports the dev PROCESS resolves - anything not rewritten to a
|
|
2058
|
+
* runtime server target - must not carry a bare package specifier, because the process runs under
|
|
2059
|
+
* `--conditions=react-server` and the client-SSR (and pages-SSR) layers are not that layer: `react`
|
|
2060
|
+
* resolves to React 18's `react.shared-subset`, which throws on import, and a package with a
|
|
2061
|
+
* `react-server` export condition hands back its RSC entry.
|
|
2062
|
+
*
|
|
2063
|
+
* Vendoring is the primary answer and has already run for everything eligible (it also rewrites the
|
|
2064
|
+
* package's OWN `react` to this layer's shim). This is the floor beneath it, for the specifiers
|
|
2065
|
+
* vendoring declines - a `serverExternalPackages` opt-out, a core app with react compat off: resolve
|
|
2066
|
+
* the specifier HERE, under this layer's own conditions, and emit that absolute path. Costs nothing
|
|
2067
|
+
* at run time and adds no bytes - it is the same file, named instead of re-resolved.
|
|
2068
|
+
*
|
|
2069
|
+
* A specifier that resolves under NO condition set is left bare: it is unresolvable from this
|
|
2070
|
+
* importer, so the run time reports a resolve error rather than the wrong layer's module.
|
|
2071
|
+
*/
|
|
2072
|
+
function offLayerPackageTarget(
|
|
2073
|
+
config: ResolvedConfig,
|
|
2074
|
+
file: string,
|
|
2075
|
+
specifier: string,
|
|
2076
|
+
kind: string,
|
|
2077
|
+
options: DevModuleOptions,
|
|
2078
|
+
) {
|
|
2079
|
+
if (!isPackageSpecifier(specifier)) return specifier
|
|
2080
|
+
const isRequire = kind === 'require-call'
|
|
2081
|
+
const resolved =
|
|
2082
|
+
resolvePackageSpecifier(
|
|
2083
|
+
config.root,
|
|
2084
|
+
file,
|
|
2085
|
+
specifier,
|
|
2086
|
+
isRequire
|
|
2087
|
+
? serverBundleRequireConditions(options.conditionTarget)
|
|
2088
|
+
: serverBundleConditions(options.conditionTarget),
|
|
2089
|
+
) ??
|
|
2090
|
+
resolvePackageSpecifier(
|
|
2091
|
+
config.root,
|
|
2092
|
+
file,
|
|
2093
|
+
specifier,
|
|
2094
|
+
isRequire
|
|
2095
|
+
? serverBundleConditions(options.conditionTarget)
|
|
2096
|
+
: serverBundleRequireConditions(options.conditionTarget),
|
|
2097
|
+
)
|
|
2098
|
+
if (!resolved) return specifier
|
|
2099
|
+
// `require()` takes a path; only an ESM import takes a file href.
|
|
2100
|
+
return isRequire ? resolved : pathToFileHref(resolved)
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1920
2103
|
function serverRequireAlias(specifier: string, alias: string, options: DevModuleOptions) {
|
|
1921
2104
|
if (options.conditionTarget === 'client') return alias
|
|
1922
2105
|
if (specifier === 'next/navigation' && alias.endsWith(`${path.sep}navigation.ts`)) {
|
|
@@ -2075,6 +2258,79 @@ async function externalBundleSpecifiers(
|
|
|
2075
2258
|
return bundles
|
|
2076
2259
|
}
|
|
2077
2260
|
|
|
2261
|
+
// Specifiers one compile asked for and the pre-compile scan had not vendored, keyed on that compile's
|
|
2262
|
+
// own `externalBundles` map - already threaded everywhere they are recorded and drained, so nothing
|
|
2263
|
+
// new rides through the resolve chain. Value is the importer the specifier resolves from.
|
|
2264
|
+
const lateExternalPackages = new WeakMap<Map<string, string>, Map<string, string>>()
|
|
2265
|
+
|
|
2266
|
+
/**
|
|
2267
|
+
* Record a package specifier `externalBundleSpecifiers` never saw. That scan reads the compiled
|
|
2268
|
+
* module's OWN source, so a specifier reaching this build from a module esbuild INLINED instead (a
|
|
2269
|
+
* require()d ESM sibling, a bundled compat importer) is missing from the map, and left alone it
|
|
2270
|
+
* survives verbatim into the artifact - where Bun resolves it from the real node_modules under the
|
|
2271
|
+
* server process's own `react-server` condition, and a client-only subtree (MUI -> emotion -> react)
|
|
2272
|
+
* reaches React 18's shared-subset entry and throws.
|
|
2273
|
+
*
|
|
2274
|
+
* Synchronous, and deliberately so. This runs inside esbuild's onResolve, which holds a build slot:
|
|
2275
|
+
* awaiting a vendor bundle here parks one of the few slots for however long that build takes and
|
|
2276
|
+
* heads off every queued module compile behind it. Nothing on this path may await a vendor build, a
|
|
2277
|
+
* module href, or a drain - `vendorLateExternalPackages` does the waiting, between compiles, with no
|
|
2278
|
+
* slot held.
|
|
2279
|
+
*/
|
|
2280
|
+
function noteLateExternalPackage(
|
|
2281
|
+
specifier: string,
|
|
2282
|
+
config: ResolvedConfig,
|
|
2283
|
+
file: string,
|
|
2284
|
+
importer: string,
|
|
2285
|
+
options: DevModuleOptions,
|
|
2286
|
+
externalBundles: Map<string, string>,
|
|
2287
|
+
) {
|
|
2288
|
+
if (!options.bundleExternalPackages) return
|
|
2289
|
+
if (isServerIgnoredAssetSpecifier(specifier)) return
|
|
2290
|
+
const fromFile =
|
|
2291
|
+
importer && !importer.startsWith('<') && path.isAbsolute(importer) ? importer : file
|
|
2292
|
+
if (!shouldBundleExternalPackage(specifier, config, fromFile, options)) return
|
|
2293
|
+
let pending = lateExternalPackages.get(externalBundles)
|
|
2294
|
+
if (!pending) {
|
|
2295
|
+
pending = new Map<string, string>()
|
|
2296
|
+
lateExternalPackages.set(externalBundles, pending)
|
|
2297
|
+
}
|
|
2298
|
+
pending.set(specifier, fromFile)
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Vendor what the compile just recorded and fold it into its map, so recompiling rewrites those
|
|
2303
|
+
* specifiers. False when there was nothing to do, which is the overwhelming majority of modules.
|
|
2304
|
+
*
|
|
2305
|
+
* Failures propagate, exactly as they do from the pre-compile scan: the only other outcome is
|
|
2306
|
+
* emitting the bare specifier, which is the runtime `react-server` resolution this seam exists to
|
|
2307
|
+
* prevent - so a swallowed vendor error would silently reproduce the bug it is fixing.
|
|
2308
|
+
*/
|
|
2309
|
+
async function vendorLateExternalPackages(
|
|
2310
|
+
config: ResolvedConfig,
|
|
2311
|
+
options: DevModuleOptions,
|
|
2312
|
+
externalBundles: Map<string, string>,
|
|
2313
|
+
) {
|
|
2314
|
+
const pending = lateExternalPackages.get(externalBundles)
|
|
2315
|
+
if (!pending || pending.size === 0) return false
|
|
2316
|
+
lateExternalPackages.delete(externalBundles)
|
|
2317
|
+
await Promise.all(
|
|
2318
|
+
[...pending].map(async ([specifier, fromFile]) => {
|
|
2319
|
+
externalBundles.set(
|
|
2320
|
+
specifier,
|
|
2321
|
+
await externalServerPackageHref(
|
|
2322
|
+
config,
|
|
2323
|
+
specifier,
|
|
2324
|
+
options.profile === 'client' ? 'client' : 'server',
|
|
2325
|
+
path.dirname(fromFile),
|
|
2326
|
+
options.conditionTarget,
|
|
2327
|
+
),
|
|
2328
|
+
)
|
|
2329
|
+
}),
|
|
2330
|
+
)
|
|
2331
|
+
return true
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2078
2334
|
function shouldBundleExternalPackage(
|
|
2079
2335
|
specifier: string,
|
|
2080
2336
|
config: ResolvedConfig,
|
|
@@ -26,7 +26,13 @@ import {
|
|
|
26
26
|
type ServerEsbuildPluginOptions,
|
|
27
27
|
} from '../extensions'
|
|
28
28
|
import { pathToFileHref, type CompatAliasTarget, type ResolvedConfig } from '../config'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
cacheRoot,
|
|
31
|
+
devArtifactUsable,
|
|
32
|
+
devSourceIdentity,
|
|
33
|
+
noteDevArtifactWritten,
|
|
34
|
+
} from './module-cache'
|
|
35
|
+
import { frameworkFingerprint } from './fingerprint'
|
|
30
36
|
import {
|
|
31
37
|
getExternalPackagePolicy,
|
|
32
38
|
isEsmModuleEntry,
|
|
@@ -169,9 +175,21 @@ function vendorArtifactKey(
|
|
|
169
175
|
conditionTarget: ServerBundleTarget,
|
|
170
176
|
source: string,
|
|
171
177
|
) {
|
|
172
|
-
return `${vendorPortablePath(config, config.outPath)}\0${target}\0${conditionTarget}\0${source}`
|
|
178
|
+
return `${vendorGeneration(config)}\0${vendorPortablePath(config, config.outPath)}\0${target}\0${conditionTarget}\0${source}`
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
/**
|
|
182
|
+
* pnext's generation, in every name the vendor cache derives. A vendor bundle is pnext's OWN output
|
|
183
|
+
* - its CJS interop, its aliases, its defines - so one generation's bundle must never be handed to
|
|
184
|
+
* the next, exactly as the module cache already refuses one. The same holds for the verdict stores
|
|
185
|
+
* below, which decide that a demand needs no build at all: a verdict is framework logic, and a
|
|
186
|
+
* stale one routes today's demand onto yesterday's bytes.
|
|
187
|
+
*
|
|
188
|
+
* The fingerprint is content over pnext's shipped tree, memoized per process and recorded next to
|
|
189
|
+
* the module cache, so an app whose framework did not move keeps every warm hit for free.
|
|
190
|
+
*/
|
|
191
|
+
const vendorGeneration = (config: ResolvedConfig) => frameworkFingerprint(cacheRoot(config.outPath))
|
|
192
|
+
|
|
175
193
|
const vendorPortablePath = (config: ResolvedConfig, file: string) =>
|
|
176
194
|
devSourceIdentity(file, config.workspaceRoot)
|
|
177
195
|
|
|
@@ -331,7 +349,18 @@ function vendorLayerKey(target: CompatAliasTarget, conditionTarget: ServerBundle
|
|
|
331
349
|
}
|
|
332
350
|
|
|
333
351
|
function vendorSharedFile(config: ResolvedConfig) {
|
|
334
|
-
return
|
|
352
|
+
return vendorStoreFile(config, 'layer-shared')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A vendor verdict store, named for the generation whose logic produced it. */
|
|
356
|
+
function vendorStoreFile(config: ResolvedConfig, name: string) {
|
|
357
|
+
return path.join(
|
|
358
|
+
config.outPath,
|
|
359
|
+
'cache',
|
|
360
|
+
'server',
|
|
361
|
+
'vendor',
|
|
362
|
+
`${name}.${vendorGeneration(config)}.json`,
|
|
363
|
+
)
|
|
335
364
|
}
|
|
336
365
|
|
|
337
366
|
/** Verdicts from earlier runs, so reuse lands on the first demand of this one. */
|
|
@@ -459,7 +488,7 @@ const vendorReuseLoaded = new Set<string>()
|
|
|
459
488
|
const vendorReuseProbes = new Map<string, Promise<boolean>>()
|
|
460
489
|
|
|
461
490
|
function vendorReuseFile(config: ResolvedConfig) {
|
|
462
|
-
return
|
|
491
|
+
return vendorStoreFile(config, 'reuse-verdicts')
|
|
463
492
|
}
|
|
464
493
|
|
|
465
494
|
function vendorReuseFor(config: ResolvedConfig) {
|
|
@@ -754,7 +783,7 @@ function schedulePreplanReport() {
|
|
|
754
783
|
}
|
|
755
784
|
|
|
756
785
|
function vendorPreplanFile(config: ResolvedConfig) {
|
|
757
|
-
return
|
|
786
|
+
return vendorStoreFile(config, 'preplan-verdicts')
|
|
758
787
|
}
|
|
759
788
|
|
|
760
789
|
function vendorPreplanFor(config: ResolvedConfig) {
|