@wular/pnext 0.1.4 → 0.1.5
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/config/ts/react.json +2 -1
- package/package.json +1 -1
- package/reference/navigation.md +3 -1
- package/src/api/client-navigation.ts +53 -4
- package/src/api/link.tsx +6 -7
- package/src/cli/adapters/vercel-warm.ts +6 -1
- package/src/cli/adapters/vercel.ts +1 -1
- package/src/cli/build/cache.ts +8 -3
- package/src/cli/build.ts +3 -2
- package/src/cli/serve/pipeline.ts +11 -3
- package/src/cli/typegen.ts +65 -3
- package/src/client/build.ts +10 -7
- package/src/client/entry.ts +50 -29
- package/src/client/reference-stub.ts +1 -1
- package/src/client/router/hub.ts +13 -11
- package/src/client/router/page-slot.ts +58 -0
- package/src/client/router/runtime.ts +377 -388
- package/src/compat/bundler/symlink-imports.ts +1 -1
- package/src/compat/client/errors/bare-boundary.ts +1 -1
- package/src/compat/client/errors/error-boundary.ts +1 -1
- package/src/compat/client/errors/global-error.ts +40 -79
- package/src/compat/client/errors/install.ts +1 -1
- package/src/compat/client/errors/lazy.ts +7 -0
- package/src/compat/client/errors/primitive-throw.ts +36 -53
- package/src/compat/client/segment-cache-policy.ts +5 -22
- package/src/compat/client/segment-cache.ts +7 -26
- package/src/compat/css/modules.ts +1 -7
- package/src/compat/css/sass.ts +1 -7
- package/src/compat/react/parity.ts +1 -1
- package/src/compat/register/bundler.ts +9 -1
- package/src/compat/tsconfig-defaults.ts +4 -7
- package/src/compat-bootstrap.ts +15 -0
- package/src/css/build.ts +18 -15
- package/src/dev/server.ts +2 -2
- package/src/index.ts +2 -0
- package/src/render/renderer.ts +209 -24
- package/src/render/resource-hints.ts +4 -1
- package/src/resolve/imports.ts +13 -8
- package/src/routing/routes.ts +32 -18
- package/src/runtime/loader.ts +29 -10
- package/src/runtime/modules.ts +28 -7
- package/src/runtime/vendor-build.ts +2 -2
- package/src/types.ts +2 -0
- package/src/utils/fs.ts +122 -0
package/src/routing/routes.ts
CHANGED
|
@@ -181,6 +181,7 @@ interface ModuleEdge {
|
|
|
181
181
|
|
|
182
182
|
interface ScanContext {
|
|
183
183
|
root: string
|
|
184
|
+
workspaceRoot?: string
|
|
184
185
|
appPath: string
|
|
185
186
|
sources: Map<string, string>
|
|
186
187
|
edges: Map<string, ModuleEdge[]>
|
|
@@ -198,11 +199,16 @@ interface ScanContext {
|
|
|
198
199
|
globalCssImports(): Set<string>
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
function createScanContext(
|
|
202
|
+
function createScanContext(
|
|
203
|
+
appPath: string,
|
|
204
|
+
globalCss?: () => Set<string>,
|
|
205
|
+
workspaceRoot?: string,
|
|
206
|
+
): ScanContext {
|
|
202
207
|
const root = rootFromAppPath(appPath)
|
|
203
208
|
let memo: Set<string> | undefined
|
|
204
209
|
return {
|
|
205
210
|
root,
|
|
211
|
+
workspaceRoot,
|
|
206
212
|
appPath,
|
|
207
213
|
sources: new Map(),
|
|
208
214
|
edges: new Map(),
|
|
@@ -213,7 +219,8 @@ function createScanContext(appPath: string, globalCss?: () => Set<string>): Scan
|
|
|
213
219
|
usesNavigation: new Map(),
|
|
214
220
|
entryReasons: new Map(),
|
|
215
221
|
globalCssImports:
|
|
216
|
-
globalCss ??
|
|
222
|
+
globalCss ??
|
|
223
|
+
(() => (memo ??= new Set(globalCssSourcesForPaths(root, appPath, workspaceRoot)))),
|
|
217
224
|
}
|
|
218
225
|
}
|
|
219
226
|
|
|
@@ -375,14 +382,21 @@ function withDeferredFacts(
|
|
|
375
382
|
return entry
|
|
376
383
|
}
|
|
377
384
|
|
|
378
|
-
export async function scanRoutes(
|
|
385
|
+
export async function scanRoutes(
|
|
386
|
+
appPath: string,
|
|
387
|
+
workspaceRoot?: string,
|
|
388
|
+
): Promise<RouteManifestEntry[]> {
|
|
379
389
|
const files = await listFiles(appPath)
|
|
380
|
-
return withDirCache(() => buildRouteTable(appPath, files))
|
|
390
|
+
return withDirCache(() => buildRouteTable(appPath, files, workspaceRoot))
|
|
381
391
|
}
|
|
382
392
|
|
|
383
393
|
/** The path pass: file paths in, route table out — no file content is read. */
|
|
384
|
-
function buildRouteTable(
|
|
385
|
-
|
|
394
|
+
function buildRouteTable(
|
|
395
|
+
appPath: string,
|
|
396
|
+
files: string[],
|
|
397
|
+
workspaceRoot?: string,
|
|
398
|
+
): RouteManifestEntry[] {
|
|
399
|
+
const context = createScanContext(appPath, undefined, workspaceRoot)
|
|
386
400
|
const routes: RouteManifestEntry[] = []
|
|
387
401
|
const slotPageFiles: { file: string; relative: string }[] = []
|
|
388
402
|
const defaultPageFiles: { file: string; relative: string }[] = []
|
|
@@ -1893,7 +1907,7 @@ function defaultReexportsClientModule(
|
|
|
1893
1907
|
if (importBinding?.[1]) specifiers.push(importBinding[1])
|
|
1894
1908
|
}
|
|
1895
1909
|
for (const specifier of specifiers) {
|
|
1896
|
-
const resolved = resolveModuleEdge(context
|
|
1910
|
+
const resolved = resolveModuleEdge(context, file, specifier)
|
|
1897
1911
|
if (!resolved || !fileExists(context, resolved)) continue
|
|
1898
1912
|
if (isClientModule(context, resolved)) return true
|
|
1899
1913
|
if (defaultReexportsClientModule(context, resolved, visited)) return true
|
|
@@ -2126,7 +2140,7 @@ function readScanSource(context: ScanContext, file: string, visited = new Set<st
|
|
|
2126
2140
|
const parts = [source]
|
|
2127
2141
|
for (const edge of scanFacts(file, source).imports) {
|
|
2128
2142
|
if (!edge.reexport || !edge.specifier.startsWith('.')) continue
|
|
2129
|
-
const resolved = resolveModuleEdge(context
|
|
2143
|
+
const resolved = resolveModuleEdge(context, file, edge.specifier)
|
|
2130
2144
|
if (!resolved || !existsSync(resolved)) continue
|
|
2131
2145
|
parts.push(readScanSource(context, resolved, visited))
|
|
2132
2146
|
}
|
|
@@ -2274,12 +2288,12 @@ function moduleEdges(context: ScanContext, file: string) {
|
|
|
2274
2288
|
// `next` package, plain resolveImport finds the real package's directive-less re-export stub and the
|
|
2275
2289
|
// 'use client' boundary of the compat module is silently missed, so the route never gets its client
|
|
2276
2290
|
// reference or entry.
|
|
2277
|
-
function resolveModuleEdge(
|
|
2291
|
+
function resolveModuleEdge(context: ScanContext, file: string, specifier: string) {
|
|
2278
2292
|
return (
|
|
2279
2293
|
resolveModuleAlias(specifier) ??
|
|
2280
|
-
resolveImport(root, file, specifier) ??
|
|
2281
|
-
getBundlerExtensions().resolveRouteDependency(root, file, specifier) ??
|
|
2282
|
-
getCssExtensions().resolveCssDependency(root, file, specifier)
|
|
2294
|
+
resolveImport(context.root, file, specifier, context.workspaceRoot) ??
|
|
2295
|
+
getBundlerExtensions().resolveRouteDependency(context.root, file, specifier) ??
|
|
2296
|
+
getCssExtensions().resolveCssDependency(context.root, file, specifier)
|
|
2283
2297
|
)
|
|
2284
2298
|
}
|
|
2285
2299
|
|
|
@@ -2288,7 +2302,7 @@ function publishedClientImports(context: ScanContext, file: string) {
|
|
|
2288
2302
|
const source = readSource(context, file)
|
|
2289
2303
|
const found: ModuleEdge[] = []
|
|
2290
2304
|
for (const edge of scanFacts(file, source).imports) {
|
|
2291
|
-
if (resolveModuleEdge(context
|
|
2305
|
+
if (resolveModuleEdge(context, file, edge.specifier)) continue
|
|
2292
2306
|
found.push(...publishedClientEdges(context, file, edge.specifier, edge.exports))
|
|
2293
2307
|
}
|
|
2294
2308
|
return found
|
|
@@ -2316,7 +2330,7 @@ function resolvePublishedEdge(
|
|
|
2316
2330
|
kind: 'import' | 'require',
|
|
2317
2331
|
) {
|
|
2318
2332
|
return (
|
|
2319
|
-
resolveModuleEdge(context
|
|
2333
|
+
resolveModuleEdge(context, file, specifier) ??
|
|
2320
2334
|
resolvePackageSpecifier(context.root, file, specifier, [kind, 'default'])
|
|
2321
2335
|
)
|
|
2322
2336
|
}
|
|
@@ -2376,7 +2390,7 @@ function moduleEdgesFromSource(context: ScanContext, file: string, source: strin
|
|
|
2376
2390
|
const facts = scanFacts(file, source)
|
|
2377
2391
|
|
|
2378
2392
|
for (const edge of facts.imports) {
|
|
2379
|
-
const resolved = resolveModuleEdge(context
|
|
2393
|
+
const resolved = resolveModuleEdge(context, file, edge.specifier)
|
|
2380
2394
|
if (!resolved) continue
|
|
2381
2395
|
ordered.push({
|
|
2382
2396
|
file: resolved,
|
|
@@ -2389,7 +2403,7 @@ function moduleEdgesFromSource(context: ScanContext, file: string, source: strin
|
|
|
2389
2403
|
ordered.sort((a, b) => a.index - b.index)
|
|
2390
2404
|
const imports: ModuleEdge[] = ordered.map(({ index: _index, ...edge }) => edge)
|
|
2391
2405
|
|
|
2392
|
-
for (const imported of dynamicImportEdgesFromSource(context
|
|
2406
|
+
for (const imported of dynamicImportEdgesFromSource(context, file, source, dynamicNames))
|
|
2393
2407
|
imports.push(imported)
|
|
2394
2408
|
|
|
2395
2409
|
return imports
|
|
@@ -2488,7 +2502,7 @@ function assertSupportedDynamicCalls(file: string, source: string, dynamicNames:
|
|
|
2488
2502
|
}
|
|
2489
2503
|
|
|
2490
2504
|
function dynamicImportEdgesFromSource(
|
|
2491
|
-
|
|
2505
|
+
context: ScanContext,
|
|
2492
2506
|
file: string,
|
|
2493
2507
|
source: string,
|
|
2494
2508
|
dynamicNames: Set<string>,
|
|
@@ -2499,7 +2513,7 @@ function dynamicImportEdgesFromSource(
|
|
|
2499
2513
|
const optionObjects = dynamicOptionObjects(source)
|
|
2500
2514
|
for (const fact of dynamicCallFacts(source, file)) {
|
|
2501
2515
|
if (!dynamicNames.has(fact.name)) continue
|
|
2502
|
-
const resolved = resolveModuleEdge(
|
|
2516
|
+
const resolved = resolveModuleEdge(context, file, fact.specifier)
|
|
2503
2517
|
if (!resolved) continue
|
|
2504
2518
|
imports.push({
|
|
2505
2519
|
file: resolved,
|
package/src/runtime/loader.ts
CHANGED
|
@@ -187,6 +187,7 @@ export function registerServerRuntime(config: ResolvedConfig, sourceFiles: strin
|
|
|
187
187
|
|
|
188
188
|
const aliases = aliasesForConfig(config)
|
|
189
189
|
const roots = [
|
|
190
|
+
...rootPaths(config.root),
|
|
190
191
|
...rootPaths(config.appPath),
|
|
191
192
|
...rootPaths(path.join(config.outPath, 'cache', 'server')),
|
|
192
193
|
// The pnext src root, so the framework's own source transforms too.
|
|
@@ -219,7 +220,7 @@ export function registerServerRuntime(config: ResolvedConfig, sourceFiles: strin
|
|
|
219
220
|
}
|
|
220
221
|
registerResolvePlugin()
|
|
221
222
|
registerNodeModulesLoadPlugin()
|
|
222
|
-
for (const root of roots) registerLoadPlugin(root)
|
|
223
|
+
for (const root of roots) registerLoadPlugin(root, rootPaths(config.root).includes(root))
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
/**
|
|
@@ -236,7 +237,12 @@ export function installServerRuntimePlugins(): void {
|
|
|
236
237
|
pluginsDeferred = false
|
|
237
238
|
registerResolvePlugin()
|
|
238
239
|
registerNodeModulesLoadPlugin()
|
|
239
|
-
for (const root of deferredPluginRoots)
|
|
240
|
+
for (const root of deferredPluginRoots) {
|
|
241
|
+
const projectRoot = [...runtimeConfigs.values()].some(config =>
|
|
242
|
+
rootPaths(config.root).includes(root),
|
|
243
|
+
)
|
|
244
|
+
registerLoadPlugin(root, projectRoot)
|
|
245
|
+
}
|
|
240
246
|
deferredPluginRoots.clear()
|
|
241
247
|
}
|
|
242
248
|
|
|
@@ -302,7 +308,7 @@ function registerResolvePlugin() {
|
|
|
302
308
|
})
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
function registerLoadPlugin(root: string) {
|
|
311
|
+
function registerLoadPlugin(root: string, projectRoot = false) {
|
|
306
312
|
if (registeredLoadRoots.has(root)) return
|
|
307
313
|
registeredLoadRoots.add(root)
|
|
308
314
|
if (traceEnabled('server')) {
|
|
@@ -312,7 +318,7 @@ function registerLoadPlugin(root: string) {
|
|
|
312
318
|
Bun.plugin({
|
|
313
319
|
name: `pnext-server-runtime-load-${hashRoot(root)}`,
|
|
314
320
|
setup(plugin) {
|
|
315
|
-
plugin.onLoad({ filter: rootFilter(root) }, async ({ path: file }) => ({
|
|
321
|
+
plugin.onLoad({ filter: rootFilter(root, projectRoot) }, async ({ path: file }) => ({
|
|
316
322
|
contents: await transformSource(file),
|
|
317
323
|
loader: 'js',
|
|
318
324
|
}))
|
|
@@ -594,12 +600,17 @@ async function inlineModuleScopeRequires(
|
|
|
594
600
|
function requireCallTarget(root: string, file: string, specifier: string): string | undefined {
|
|
595
601
|
if (specifier.startsWith('file://')) {
|
|
596
602
|
try {
|
|
597
|
-
return resolveImport(
|
|
603
|
+
return resolveImport(
|
|
604
|
+
root,
|
|
605
|
+
file,
|
|
606
|
+
fileURLToPath(specifier),
|
|
607
|
+
runtimeConfigForFile(file)?.resolved.workspaceRoot,
|
|
608
|
+
)
|
|
598
609
|
} catch {
|
|
599
610
|
return undefined
|
|
600
611
|
}
|
|
601
612
|
}
|
|
602
|
-
return resolveImport(root, file, specifier)
|
|
613
|
+
return resolveImport(root, file, specifier, runtimeConfigForFile(file)?.resolved.workspaceRoot)
|
|
603
614
|
}
|
|
604
615
|
|
|
605
616
|
// Compile an app module to a sibling `.pnext-require.cjs` (CommonJS), reusing a
|
|
@@ -751,7 +762,8 @@ export function rewriteServerSource(
|
|
|
751
762
|
const rewritten = sourceHasDynamicImport(source)
|
|
752
763
|
? rewriteDynamicCallTargets(
|
|
753
764
|
rewriteLiteralDynamicCalls(source, file),
|
|
754
|
-
specifier =>
|
|
765
|
+
specifier =>
|
|
766
|
+
resolveImport(root, file, specifier, runtimeConfigForFile(file)?.resolved.workspaceRoot),
|
|
755
767
|
file,
|
|
756
768
|
)
|
|
757
769
|
: source
|
|
@@ -867,12 +879,13 @@ function rootPaths(root: string) {
|
|
|
867
879
|
return resolved
|
|
868
880
|
}
|
|
869
881
|
|
|
870
|
-
function rootFilter(root: string) {
|
|
882
|
+
function rootFilter(root: string, projectRoot = false) {
|
|
871
883
|
// Base source extensions plus any extra loadable extensions registered by
|
|
872
884
|
// compat (e.g. mdx/md via pageExtensions). Without these, `.mdx` modules skip
|
|
873
885
|
// the transform load hook and resolve as raw assets (default export = path).
|
|
874
886
|
const extras = currentLoadExtras().map(escapeRegex)
|
|
875
|
-
|
|
887
|
+
// Project-wide coverage handles JSX/TS; plain JS tools and build output keep their own loaders.
|
|
888
|
+
const exts = [projectRoot ? 'tsx?|jsx' : '[jt]sx?', ...extras].join('|')
|
|
876
889
|
// Exclude `next.config.{js,cjs,mjs,ts}`: it is loaded for its exported value (often CommonJS
|
|
877
890
|
// `module.exports = ...`), not as part of the app's server module graph. Routing it through the
|
|
878
891
|
// ES-module source transform drops the CJS exports, silently disabling next.config
|
|
@@ -884,8 +897,14 @@ function rootFilter(root: string) {
|
|
|
884
897
|
const vendorGuard = vendorLoadsNatively()
|
|
885
898
|
? `${root.endsWith(`${path.sep}cache${path.sep}server`) ? '(?!\\/vendor\\/)' : ''}(?!.*\\/cache\\/server\\/vendor\\/)`
|
|
886
899
|
: ''
|
|
900
|
+
// Project-wide roots include precompiled server output. Keep its chunks native: transforming
|
|
901
|
+
// them loads compiler libraries and starts an esbuild service on the first production request.
|
|
902
|
+
const serverGuard = [...runtimeConfigs.values()]
|
|
903
|
+
.flatMap(runtime => rootPaths(path.join(runtime.resolved.outPath, 'server')))
|
|
904
|
+
.map(serverRoot => `(?!${escapeRegex(serverRoot)}(?:/|$))`)
|
|
905
|
+
.join('')
|
|
887
906
|
return new RegExp(
|
|
888
|
-
`^${escapeRegex(root)}${vendorGuard}(?!.*\\/node_modules\\/)(?!\\/next\\.config\\.(?:js|cjs|mjs|ts)$)(?:/.*)?\\.(?:${exts})$`,
|
|
907
|
+
`^${serverGuard}${escapeRegex(root)}${vendorGuard}(?!.*\\/node_modules\\/)(?!\\/next\\.config\\.(?:js|cjs|mjs|ts)$)(?:/.*)?\\.(?:${exts})$`,
|
|
889
908
|
)
|
|
890
909
|
}
|
|
891
910
|
|
package/src/runtime/modules.ts
CHANGED
|
@@ -1420,6 +1420,24 @@ export const compiledSpecifiersManifestSuffix = '.pnext-specifiers.json'
|
|
|
1420
1420
|
const compiledScriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/
|
|
1421
1421
|
|
|
1422
1422
|
async function writeCompiledFile(file: string, contents: string) {
|
|
1423
|
+
const profileRoot = compiledArtifactProfileRoot(file)
|
|
1424
|
+
if (profileRoot && contents.includes('file://')) {
|
|
1425
|
+
const cacheRoot = path.dirname(profileRoot)
|
|
1426
|
+
const edits: { start: number; end: number; value: string }[] = []
|
|
1427
|
+
for (const found of outputSpecifiers(contents)) {
|
|
1428
|
+
const { sourcePath, hash } = splitHash(found.value)
|
|
1429
|
+
if (!sourcePath.startsWith('file://')) continue
|
|
1430
|
+
const target = fileURLToPath(sourcePath)
|
|
1431
|
+
if (!isInside(cacheRoot, target)) continue
|
|
1432
|
+
const relative = toPosixPath(path.relative(path.dirname(file), target))
|
|
1433
|
+
edits.push({
|
|
1434
|
+
start: found.start,
|
|
1435
|
+
end: found.end,
|
|
1436
|
+
value: JSON.stringify(`${relative.startsWith('.') ? relative : `./${relative}`}${hash}`),
|
|
1437
|
+
})
|
|
1438
|
+
}
|
|
1439
|
+
contents = spliceSource(contents, edits)
|
|
1440
|
+
}
|
|
1423
1441
|
await writeFileAtomic(file, contents)
|
|
1424
1442
|
noteDevArtifactWritten(file)
|
|
1425
1443
|
if (emitCompiledSpecifiersManifest && compiledScriptFilePattern.test(file)) {
|
|
@@ -1431,7 +1449,7 @@ async function writeCompiledFile(file: string, contents: string) {
|
|
|
1431
1449
|
}
|
|
1432
1450
|
|
|
1433
1451
|
/**
|
|
1434
|
-
* Whether every
|
|
1452
|
+
* Whether every module reachable from a compiled artifact is already on
|
|
1435
1453
|
* disk. Individual files publish atomically, but the graph is a set of files:
|
|
1436
1454
|
* the build's warm child can expose an importer to the parent before it has
|
|
1437
1455
|
* exposed that importer's slower sibling. A mere existsSync(importer) is
|
|
@@ -1474,10 +1492,12 @@ function missingCompiledArtifact(entry: string): string | undefined {
|
|
|
1474
1492
|
// profile directory. Static imports must be complete before evaluation.
|
|
1475
1493
|
if (kind === 'dynamic') continue
|
|
1476
1494
|
const { sourcePath } = splitHash(specifier)
|
|
1477
|
-
if (!sourcePath.startsWith('file://')) continue
|
|
1495
|
+
if (!sourcePath.startsWith('file://') && !sourcePath.startsWith('.')) continue
|
|
1478
1496
|
let target: string
|
|
1479
1497
|
try {
|
|
1480
|
-
target =
|
|
1498
|
+
target = sourcePath.startsWith('file://')
|
|
1499
|
+
? fileURLToPath(sourcePath)
|
|
1500
|
+
: path.resolve(path.dirname(file), sourcePath)
|
|
1481
1501
|
} catch {
|
|
1482
1502
|
return sourcePath
|
|
1483
1503
|
}
|
|
@@ -2433,6 +2453,7 @@ async function resolveDevModuleSpecifier(
|
|
|
2433
2453
|
config.root,
|
|
2434
2454
|
importer && !importer.startsWith('<') ? importer : file,
|
|
2435
2455
|
sourcePath,
|
|
2456
|
+
config.workspaceRoot,
|
|
2436
2457
|
)
|
|
2437
2458
|
// A relative specifier that resolved names first-party source next to its importer, so compile it
|
|
2438
2459
|
// through the normal pipeline even outside workspaceRoot (compat's own runtime files live inside
|
|
@@ -2806,7 +2827,7 @@ function shouldBundleExternalPackage(
|
|
|
2806
2827
|
if (options.aliases[specifier]) return false
|
|
2807
2828
|
if (specifier === 'server-only') return false
|
|
2808
2829
|
if (!isPackageSpecifier(specifier)) return false
|
|
2809
|
-
const resolved = resolveImport(config.root, file, specifier)
|
|
2830
|
+
const resolved = resolveImport(config.root, file, specifier, config.workspaceRoot)
|
|
2810
2831
|
const inWorkspace = resolved !== undefined && isInside(config.workspaceRoot, resolved)
|
|
2811
2832
|
// transpilePackages force-bundle; serverExternalPackages force-external (never bundled). A
|
|
2812
2833
|
// transpiled package that resolves INSIDE the workspace is first-party source, not a registry
|
|
@@ -2859,7 +2880,7 @@ function localImportTargets(
|
|
|
2859
2880
|
// compiling them as modules would fail the import scan. Css keeps its
|
|
2860
2881
|
// copy-through so compiled css module paths stay importable.
|
|
2861
2882
|
if (isServerIgnoredAssetSpecifier(sourcePath) && !isCssFile(sourcePath)) continue
|
|
2862
|
-
const resolved = resolveImport(config.root, file, sourcePath)
|
|
2883
|
+
const resolved = resolveImport(config.root, file, sourcePath, config.workspaceRoot)
|
|
2863
2884
|
// `resolveExtensions` can land an extensionless specifier on an asset
|
|
2864
2885
|
// (`import img from './image'` -> image.png), which the specifier check
|
|
2865
2886
|
// above misses — compiling the image bytes as a module fails the build.
|
|
@@ -2894,7 +2915,7 @@ async function writeClientReferenceModules(
|
|
|
2894
2915
|
for (const specifier of importSpecifiers(source, file)) {
|
|
2895
2916
|
const { sourcePath } = splitHash(specifier)
|
|
2896
2917
|
if (aliases[specifier] || aliases[sourcePath]) continue
|
|
2897
|
-
const resolved = resolveImport(config.root, file, sourcePath)
|
|
2918
|
+
const resolved = resolveImport(config.root, file, sourcePath, config.workspaceRoot)
|
|
2898
2919
|
if (!resolved || !isInside(config.workspaceRoot, resolved) || isCssFile(resolved)) continue
|
|
2899
2920
|
if (!(await fileHasUseClientDirective(resolved))) continue
|
|
2900
2921
|
|
|
@@ -3107,7 +3128,7 @@ function packageNameFromSpecifier(specifier: string) {
|
|
|
3107
3128
|
|
|
3108
3129
|
function resolveLocalImport(config: ResolvedConfig, fromFile: string, specifier: string) {
|
|
3109
3130
|
if (path.isAbsolute(specifier)) return existsSync(specifier) ? specifier : undefined
|
|
3110
|
-
return resolveImport(config.root, fromFile, specifier)
|
|
3131
|
+
return resolveImport(config.root, fromFile, specifier, config.workspaceRoot)
|
|
3111
3132
|
}
|
|
3112
3133
|
|
|
3113
3134
|
async function linkAssetContext(config: ResolvedConfig, file: string, profile: string) {
|
|
@@ -1754,8 +1754,8 @@ function nearestManifestDir(from: string) {
|
|
|
1754
1754
|
const manifest = path.join(dir, 'package.json')
|
|
1755
1755
|
if (existsSync(manifest)) {
|
|
1756
1756
|
try {
|
|
1757
|
-
const parsed = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
1758
|
-
if (typeof parsed
|
|
1757
|
+
const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
1758
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1759
1759
|
nearestManifestDirs.set(from, dir)
|
|
1760
1760
|
return dir
|
|
1761
1761
|
}
|
package/src/types.ts
CHANGED
|
@@ -658,7 +658,9 @@ export interface StaticRouteMetadata {
|
|
|
658
658
|
|
|
659
659
|
export interface BuildManifest {
|
|
660
660
|
version: 0
|
|
661
|
+
/** Source root relative to outDir on disk; resolved at load. */
|
|
661
662
|
root: string
|
|
663
|
+
/** App directory relative to root on disk; resolved at load. */
|
|
662
664
|
appDir: string
|
|
663
665
|
outDir: string
|
|
664
666
|
routes: RouteManifestEntry[]
|
package/src/utils/fs.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { mkdir, readdir, rename, rm, stat, readFile, writeFile } from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import type { BuildManifest } from '../types'
|
|
5
6
|
|
|
6
7
|
// Write-then-rename so a concurrent import never observes a truncated file; Bun caches a failed load for the
|
|
7
8
|
// life of the process. The temp name must be unique per CALL, not per process: two writers of the same output
|
|
@@ -145,6 +146,34 @@ export function toPosixPath(value: string) {
|
|
|
145
146
|
return value.split(path.sep).join('/')
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
export function cssModuleScopePath(file: string) {
|
|
150
|
+
const normalized = toPosixPath(file)
|
|
151
|
+
const appIndex = normalized.lastIndexOf('/app/')
|
|
152
|
+
if (appIndex !== -1) return normalized.slice(appIndex + 1)
|
|
153
|
+
let dir = path.dirname(file)
|
|
154
|
+
let fallback: string | undefined
|
|
155
|
+
while (true) {
|
|
156
|
+
const packageFile = path.join(dir, 'package.json')
|
|
157
|
+
if (
|
|
158
|
+
existsSync(packageFile) ||
|
|
159
|
+
existsSync(path.join(dir, 'pnext.config.ts')) ||
|
|
160
|
+
existsSync(path.join(dir, 'node_modules'))
|
|
161
|
+
) {
|
|
162
|
+
const relative = toPosixPath(path.relative(dir, file))
|
|
163
|
+
try {
|
|
164
|
+
const { name } = JSON.parse(readFileSync(packageFile, 'utf8')) as { name?: unknown }
|
|
165
|
+
if (typeof name === 'string') return `${name}/${relative}`
|
|
166
|
+
} catch {
|
|
167
|
+
// Unnamed projects still have a root-relative identity.
|
|
168
|
+
}
|
|
169
|
+
fallback ??= relative
|
|
170
|
+
}
|
|
171
|
+
const parent = path.dirname(dir)
|
|
172
|
+
if (parent === dir) return fallback ?? path.basename(file)
|
|
173
|
+
dir = parent
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
/** '^' + pnext's own running version, read from its package.json (this module lives at src/utils/). */
|
|
149
178
|
export function pnextVersionRange() {
|
|
150
179
|
const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url))
|
|
@@ -164,3 +193,96 @@ export function pnextOptionalDependencyRange(name: string) {
|
|
|
164
193
|
}
|
|
165
194
|
return pkg.optionalDependencies?.[name]
|
|
166
195
|
}
|
|
196
|
+
|
|
197
|
+
export function includeTypes(config: { include?: unknown }, files: string[], defaults: string[]) {
|
|
198
|
+
if (!('include' in config)) {
|
|
199
|
+
config.include = [...files, ...defaults]
|
|
200
|
+
return files
|
|
201
|
+
}
|
|
202
|
+
const include = config.include
|
|
203
|
+
if (!Array.isArray(include)) return []
|
|
204
|
+
const added = files.filter(file => !include.includes(file))
|
|
205
|
+
include.push(...added)
|
|
206
|
+
return added
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Relativize only source-path fields while writing; build hooks keep absolute paths. */
|
|
210
|
+
export function serializeBuildManifest(data: BuildManifest): string {
|
|
211
|
+
const { root, appDir, outDir } = data
|
|
212
|
+
const stored = {
|
|
213
|
+
...mapSourcePaths(data, file => toPosixPath(path.relative(root, file))),
|
|
214
|
+
root: toPosixPath(path.relative(outDir, root)) || '.',
|
|
215
|
+
appDir: toPosixPath(path.relative(root, appDir)),
|
|
216
|
+
outDir: '.',
|
|
217
|
+
}
|
|
218
|
+
return `${JSON.stringify(stored, null, 2)}\n`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveManifest(data: BuildManifest, outPath: string, root: string): BuildManifest {
|
|
222
|
+
if (path.isAbsolute(data.root)) return data
|
|
223
|
+
const absolute = (file: string) => path.join(root, file)
|
|
224
|
+
return {
|
|
225
|
+
...mapSourcePaths(data, absolute),
|
|
226
|
+
root,
|
|
227
|
+
appDir: absolute(data.appDir),
|
|
228
|
+
outDir: outPath,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const sourcePathFields = [
|
|
233
|
+
'file',
|
|
234
|
+
'sourceFiles',
|
|
235
|
+
'cssImports',
|
|
236
|
+
'templateFiles',
|
|
237
|
+
'forbiddenFiles',
|
|
238
|
+
'unauthorizedFiles',
|
|
239
|
+
'slotDirs',
|
|
240
|
+
'slotDir',
|
|
241
|
+
'syntheticSlotDir',
|
|
242
|
+
'childrenDefault',
|
|
243
|
+
'serverActionFile',
|
|
244
|
+
'globalErrorFile',
|
|
245
|
+
] as const
|
|
246
|
+
|
|
247
|
+
type SourcePathFields = Partial<Record<(typeof sourcePathFields)[number], string | string[]>>
|
|
248
|
+
|
|
249
|
+
function mapSourcePaths(data: BuildManifest, map: (file: string) => string): BuildManifest {
|
|
250
|
+
// Only schema path owners are visited; params and user metadata remain data.
|
|
251
|
+
function owner<T extends SourcePathFields | undefined>(value: T): T {
|
|
252
|
+
if (!value) return value
|
|
253
|
+
const mapped = { ...value }
|
|
254
|
+
for (const key of sourcePathFields) {
|
|
255
|
+
const field = value[key]
|
|
256
|
+
if (field !== undefined) mapped[key] = Array.isArray(field) ? field.map(map) : map(field)
|
|
257
|
+
}
|
|
258
|
+
return mapped
|
|
259
|
+
}
|
|
260
|
+
const mapped = owner(data)
|
|
261
|
+
mapped.routes = data.routes.map(route => ({
|
|
262
|
+
...owner(route),
|
|
263
|
+
clientReferences: route.clientReferences.map(owner),
|
|
264
|
+
...(route.interception && { interception: owner(route.interception) }),
|
|
265
|
+
}))
|
|
266
|
+
if (data.staticMetadataFiles) mapped.staticMetadataFiles = data.staticMetadataFiles.map(owner)
|
|
267
|
+
if (data.staticRouteMetadata) {
|
|
268
|
+
mapped.staticRouteMetadata = Object.fromEntries(
|
|
269
|
+
Object.entries(data.staticRouteMetadata).map(([route, entry]) => [
|
|
270
|
+
route,
|
|
271
|
+
{
|
|
272
|
+
...entry,
|
|
273
|
+
favicon: owner(entry.favicon),
|
|
274
|
+
manifest: owner(entry.manifest),
|
|
275
|
+
icons: entry.icons.map(owner),
|
|
276
|
+
appleIcons: entry.appleIcons.map(owner),
|
|
277
|
+
rootIcons: entry.rootIcons.map(owner),
|
|
278
|
+
},
|
|
279
|
+
]),
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
if (data.staticModuleMetadata) {
|
|
283
|
+
mapped.staticModuleMetadata = Object.fromEntries(
|
|
284
|
+
Object.entries(data.staticModuleMetadata).map(([file, metadata]) => [map(file), metadata]),
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
return mapped
|
|
288
|
+
}
|