@wular/pnext 0.0.24 → 0.1.0
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/bin/pnext +1 -1
- package/package.json +2 -2
- package/reference/data/bench.json +398 -223
- package/reference/getting-started.md +1 -1
- package/src/api/client-navigation.ts +7 -5
- package/src/cli/adapters/vercel-warm.ts +120 -28
- package/src/cli/build.ts +56 -15
- package/src/cli/create.ts +1 -1
- package/src/cli/dev.ts +2 -1
- package/src/cli/index.ts +146 -10
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/serve/pipeline.ts +23 -6
- package/src/cli/serve/ui.ts +1 -1
- package/src/cli/start.ts +2 -1
- package/src/cli/typegen.ts +5 -2
- package/src/client/build.ts +217 -58
- package/src/client/chunk-fold.ts +7 -1
- package/src/client/entry.ts +44 -7
- package/src/client/router/runtime.ts +1016 -230
- package/src/client/router/types.ts +15 -0
- package/src/compat/actions/client-plugin.ts +10 -0
- package/src/compat/actions/rewrite.ts +1 -0
- package/src/compat/bundler/optimize-package-imports.ts +375 -28
- package/src/compat/client/navigation-scroll.ts +29 -3
- package/src/compat/client/optimistic-routing.ts +2 -2
- package/src/compat/client/segment-cache-policy.ts +4 -18
- package/src/compat/client/segment-cache.ts +104 -0
- package/src/compat/client/segment-prefetch.ts +6 -6
- package/src/compat/next/config-loader.ts +6 -4
- package/src/compat/next/navigation.ts +16 -2
- package/src/compat/pages/router.ts +102 -9
- package/src/compat/protocol.ts +10 -1
- package/src/compat/register/actions.ts +2 -0
- package/src/compat/register/bundler.ts +4 -6
- package/src/compat/register/routing.ts +3 -4
- package/src/compat/tsconfig-defaults.ts +3 -2
- package/src/config.ts +3 -3
- package/src/css/postcss.ts +450 -17
- package/src/dev/client-actions.ts +2 -0
- package/src/dev/server.ts +87 -14
- package/src/render/island-context.ts +3 -0
- package/src/render/ppr.ts +14 -0
- package/src/render/renderer.ts +268 -42
- package/src/request/context.ts +32 -11
- package/src/resolve/scan-facts.ts +23 -4
- package/src/routing/proxy.ts +29 -25
- package/src/routing/routes.ts +1 -1
- package/src/runtime/loader.ts +47 -1
- package/src/runtime/modules.ts +546 -65
- package/src/runtime/vendor-build.ts +109 -22
- package/src/runtime/vendor.ts +7 -3
- package/src/utils/ansi.ts +3 -1
- package/src/utils/fs.ts +17 -3
|
@@ -33,6 +33,8 @@ export interface ScanEdge {
|
|
|
33
33
|
star?: boolean
|
|
34
34
|
/** `export … from` rather than `import`. */
|
|
35
35
|
reexport?: boolean
|
|
36
|
+
/** Public export name -> name imported from the target. */
|
|
37
|
+
exportMap?: Record<string, string>
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** One module specifier literal, with the span of its quoted text. */
|
|
@@ -192,6 +194,12 @@ export function rewriteFacts(file: string, source: string): RewriteFacts {
|
|
|
192
194
|
return parsed(file, source).rewrite
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/** Every specifier literal with the syntax that determines when it is resolved. */
|
|
198
|
+
export function moduleSpecifierEdges(source: string, file = 'source'): SpecifierEdge[] {
|
|
199
|
+
const facts = rewriteFacts(file, source)
|
|
200
|
+
return facts.unreliable ? textualEdges(source) : facts.edges
|
|
201
|
+
}
|
|
202
|
+
|
|
195
203
|
function parsed(file: string, source: string, lang = langForFile(file)) {
|
|
196
204
|
const key = `${lang}\0${source.length}\0${bun.hash(source).toString()}`
|
|
197
205
|
const cached = memo.get(key)
|
|
@@ -216,10 +224,9 @@ export function rewriteSpecifierLiterals(
|
|
|
216
224
|
file: string,
|
|
217
225
|
replace: (specifier: string, kind: SpecifierEdge['kind']) => string | undefined,
|
|
218
226
|
): string {
|
|
219
|
-
const facts = rewriteFacts(file, source)
|
|
220
227
|
// A partially recovered parse has a partial record; the textual scan the
|
|
221
228
|
// folded passes replaced is the safe answer there.
|
|
222
|
-
const edges =
|
|
229
|
+
const edges = moduleSpecifierEdges(source, file)
|
|
223
230
|
const edits: { start: number; end: number; value: string }[] = []
|
|
224
231
|
for (const edge of edges) {
|
|
225
232
|
const value = replace(edge.specifier, edge.kind)
|
|
@@ -341,13 +348,25 @@ function parseFacts(file: string, source: string, lang: ParserLang) {
|
|
|
341
348
|
if (star) exportStars.push(specifier)
|
|
342
349
|
let edge = edges.get(specifier)
|
|
343
350
|
if (!edge) {
|
|
344
|
-
edge = {
|
|
351
|
+
edge = {
|
|
352
|
+
specifier,
|
|
353
|
+
exports: [],
|
|
354
|
+
index: statement.start,
|
|
355
|
+
reexport: true,
|
|
356
|
+
exportMap: {},
|
|
357
|
+
}
|
|
345
358
|
edges.set(specifier, edge)
|
|
346
359
|
imports.push(edge)
|
|
347
360
|
}
|
|
348
361
|
if (star) edge.star = true
|
|
349
362
|
else if (importKind === 'All') edge.exports.push('*')
|
|
350
|
-
else
|
|
363
|
+
else {
|
|
364
|
+
const imported = importKind === 'Default' ? 'default' : entry.importName.name
|
|
365
|
+
if (!imported) continue
|
|
366
|
+
edge.exports.push(imported)
|
|
367
|
+
const exported = exportKind === 'Default' ? 'default' : entry.exportName.name
|
|
368
|
+
if (exported) edge.exportMap![exported] = imported
|
|
369
|
+
}
|
|
351
370
|
}
|
|
352
371
|
}
|
|
353
372
|
|
package/src/routing/proxy.ts
CHANGED
|
@@ -231,10 +231,9 @@ export function createProxyRunner(
|
|
|
231
231
|
|
|
232
232
|
if (!response) return { request }
|
|
233
233
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
response = normalizeRedirectLocation(response, url, config)
|
|
234
|
+
// Redirects use the router-server envelope: relative same-origin target in
|
|
235
|
+
// both Location and body, with trailing-slash normalization when enabled.
|
|
236
|
+
response = await normalizeRedirectResponse(response, url, request.method, config)
|
|
238
237
|
|
|
239
238
|
const meta = nextResponseMeta(response)
|
|
240
239
|
if (!meta) return response
|
|
@@ -299,12 +298,13 @@ export function createProxyRunner(
|
|
|
299
298
|
return runner
|
|
300
299
|
}
|
|
301
300
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
301
|
+
// Normalize the redirect emitted by middleware into the router-server envelope.
|
|
302
|
+
async function normalizeRedirectResponse(
|
|
303
|
+
response: Response,
|
|
304
|
+
requestUrl: URL,
|
|
305
|
+
method: string,
|
|
306
|
+
config: ResolvedConfig,
|
|
307
|
+
) {
|
|
308
308
|
if (response.status < 300 || response.status >= 400) return response
|
|
309
309
|
const location = response.headers.get('location')
|
|
310
310
|
if (!location) return response
|
|
@@ -314,22 +314,26 @@ function normalizeRedirectLocation(response: Response, requestUrl: URL, config:
|
|
|
314
314
|
} catch {
|
|
315
315
|
return response
|
|
316
316
|
}
|
|
317
|
-
if (target.origin
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
response.headers.set('location', target.href)
|
|
323
|
-
return response
|
|
324
|
-
} catch {
|
|
325
|
-
const headers = new Headers(response.headers)
|
|
326
|
-
headers.set('location', target.href)
|
|
327
|
-
return new Response(response.body, {
|
|
328
|
-
status: response.status,
|
|
329
|
-
statusText: response.statusText,
|
|
330
|
-
headers,
|
|
331
|
-
})
|
|
317
|
+
if (target.origin === requestUrl.origin) {
|
|
318
|
+
if (!config.skipTrailingSlashRedirect && config.trailingSlash) {
|
|
319
|
+
target.pathname = canonicalTrailingSlashPath(target.pathname, true)
|
|
320
|
+
}
|
|
332
321
|
}
|
|
322
|
+
const destination =
|
|
323
|
+
target.origin === requestUrl.origin
|
|
324
|
+
? `${target.pathname}${target.search}${target.hash}`
|
|
325
|
+
: target.href
|
|
326
|
+
const headers = new Headers(response.headers)
|
|
327
|
+
headers.set('location', destination)
|
|
328
|
+
const body =
|
|
329
|
+
method.toUpperCase() === 'HEAD' || response.body === null ? null : await response.arrayBuffer()
|
|
330
|
+
const synthesizeDestination = method.toUpperCase() !== 'HEAD' && (!body || body.byteLength === 0)
|
|
331
|
+
if (synthesizeDestination) headers.delete('content-length')
|
|
332
|
+
return new Response(synthesizeDestination ? destination : body, {
|
|
333
|
+
status: response.status,
|
|
334
|
+
statusText: response.statusText,
|
|
335
|
+
headers,
|
|
336
|
+
})
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
// Human label for the truncation warning ("10MB", "500KB") — whole units only,
|
package/src/routing/routes.ts
CHANGED
|
@@ -2342,7 +2342,7 @@ function publishedClientExports(
|
|
|
2342
2342
|
const found: ModuleEdge[] = []
|
|
2343
2343
|
for (const edge of facts.imports) {
|
|
2344
2344
|
if (!edge.reexport) continue
|
|
2345
|
-
const map = esmReexportMap(source, edge.specifier)
|
|
2345
|
+
const map = { ...esmReexportMap(source, edge.specifier), ...edge.exportMap }
|
|
2346
2346
|
const next = edge.star
|
|
2347
2347
|
? requested.filter(name => name !== 'default')
|
|
2348
2348
|
: requested.map(name => map[name]).filter((name): name is string => Boolean(name))
|
package/src/runtime/loader.ts
CHANGED
|
@@ -131,6 +131,7 @@ const rootPathCache = new Map<string, string[]>()
|
|
|
131
131
|
const runtimeConfigLookupCache = new Map<string, RuntimeConfig | null>()
|
|
132
132
|
const firstAliasCache = new Map<string, string | null>()
|
|
133
133
|
const transformCache = new Map<string, TransformCacheEntry>()
|
|
134
|
+
const pendingTransforms = new Map<string, Promise<string>>()
|
|
134
135
|
const nodeModuleTransformCache = new Map<string, TransformCacheEntry>()
|
|
135
136
|
// Freshness of generated `.pnext-require.cjs` sidecars (see inlineModuleScopeRequires).
|
|
136
137
|
const requireSidecarCache = new Map<string, { mtimeMs: number; size: number }>()
|
|
@@ -427,6 +428,19 @@ async function transformSource(file: string) {
|
|
|
427
428
|
if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
|
|
428
429
|
return cached.code
|
|
429
430
|
}
|
|
431
|
+
const pendingKey = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}`
|
|
432
|
+
const pending = pendingTransforms.get(pendingKey)
|
|
433
|
+
if (pending) return pending
|
|
434
|
+
const started = transformSourceUncached(file, fileStat)
|
|
435
|
+
pendingTransforms.set(pendingKey, started)
|
|
436
|
+
try {
|
|
437
|
+
return await started
|
|
438
|
+
} finally {
|
|
439
|
+
if (pendingTransforms.get(pendingKey) === started) pendingTransforms.delete(pendingKey)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function transformSourceUncached(file: string, fileStat: { mtimeMs: number; size: number }) {
|
|
430
444
|
// The in-memory cache is empty on every restart, so a restarted server
|
|
431
445
|
// re-transforms the whole materialized module set on the request path.
|
|
432
446
|
const diskCache = transformCacheFile(file, fileStat)
|
|
@@ -469,11 +483,43 @@ async function transformSource(file: string) {
|
|
|
469
483
|
})
|
|
470
484
|
if (diskCache) {
|
|
471
485
|
await mkdir(path.dirname(diskCache), { recursive: true }).catch(() => undefined)
|
|
472
|
-
|
|
486
|
+
// Off the caller's path: a render never waits on the cache it is filling.
|
|
487
|
+
// Tracked so a process whose whole job IS filling it can await them.
|
|
488
|
+
const write = writeFileAtomic(diskCache, result.code).catch(() => undefined)
|
|
489
|
+
transformWrites.add(write)
|
|
490
|
+
void write.finally(() => transformWrites.delete(write))
|
|
473
491
|
}
|
|
474
492
|
return result.code
|
|
475
493
|
}
|
|
476
494
|
|
|
495
|
+
// Transform writes in flight; see prewarmServerTransforms.
|
|
496
|
+
const transformWrites = new Set<Promise<unknown>>()
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Fill the on-disk transform cache for compiled artifacts a build just wrote.
|
|
500
|
+
*
|
|
501
|
+
* Importing a materialized module transforms it first, and that transform is the last piece of the
|
|
502
|
+
* compile pipeline a built app would otherwise reach on its first served request - esbuild service
|
|
503
|
+
* child included. Running it here is pure build work on build output: no app module is evaluated,
|
|
504
|
+
* unlike the import pass the vercel adapter needs for its vendor bundles.
|
|
505
|
+
*/
|
|
506
|
+
export async function prewarmServerTransforms(root: string, files: Iterable<string>) {
|
|
507
|
+
// The load hook's own filter, so this warms exactly the files an import would
|
|
508
|
+
// route through it — never a `.css` shim or the natively-loaded vendor tree.
|
|
509
|
+
const loadable = rootFilter(root)
|
|
510
|
+
await Promise.all(
|
|
511
|
+
[...files].map(async file => {
|
|
512
|
+
if (!loadable.test(file)) return
|
|
513
|
+
const stats = await stat(file).catch(() => undefined)
|
|
514
|
+
if (!stats || !transformCacheFile(file, stats)) return
|
|
515
|
+
await transformSource(file).catch(() => undefined)
|
|
516
|
+
}),
|
|
517
|
+
)
|
|
518
|
+
// Settle the writes above, or this process exits with entries in flight and
|
|
519
|
+
// the next boot recompiles exactly what this pass was for.
|
|
520
|
+
while (transformWrites.size > 0) await Promise.all([...transformWrites])
|
|
521
|
+
}
|
|
522
|
+
|
|
477
523
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
478
524
|
const transformDiskCacheDisabled = process.env.PNEXT_TRANSFORM_DISK_CACHE === '0'
|
|
479
525
|
const materializedSegment = `${path.sep}cache${path.sep}server${path.sep}`
|