@wular/pnext 0.0.24 → 0.1.1

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.
Files changed (54) hide show
  1. package/bin/pnext +1 -1
  2. package/package.json +2 -2
  3. package/reference/data/bench.json +398 -223
  4. package/reference/getting-started.md +1 -1
  5. package/reference/navigation.md +2 -2
  6. package/src/api/client-navigation.ts +7 -5
  7. package/src/cli/adapters/vercel-warm.ts +120 -28
  8. package/src/cli/build.ts +56 -15
  9. package/src/cli/create.ts +1 -1
  10. package/src/cli/dev.ts +2 -1
  11. package/src/cli/index.ts +146 -10
  12. package/src/cli/migrate/report.ts +1 -1
  13. package/src/cli/serve/pipeline.ts +23 -6
  14. package/src/cli/serve/ui.ts +1 -1
  15. package/src/cli/start.ts +2 -1
  16. package/src/cli/typegen.ts +5 -2
  17. package/src/client/build.ts +217 -58
  18. package/src/client/chunk-fold.ts +7 -1
  19. package/src/client/entry.ts +44 -7
  20. package/src/client/router/runtime.ts +1099 -236
  21. package/src/client/router/types.ts +15 -0
  22. package/src/compat/actions/client-plugin.ts +10 -0
  23. package/src/compat/actions/rewrite.ts +1 -0
  24. package/src/compat/bundler/optimize-package-imports.ts +375 -28
  25. package/src/compat/client/navigation-scroll.ts +29 -3
  26. package/src/compat/client/optimistic-routing.ts +2 -2
  27. package/src/compat/client/segment-cache-policy.ts +4 -18
  28. package/src/compat/client/segment-cache.ts +104 -0
  29. package/src/compat/client/segment-prefetch.ts +6 -6
  30. package/src/compat/next/config-loader.ts +6 -4
  31. package/src/compat/next/navigation.ts +16 -2
  32. package/src/compat/pages/router.ts +102 -9
  33. package/src/compat/protocol.ts +10 -1
  34. package/src/compat/register/actions.ts +2 -0
  35. package/src/compat/register/bundler.ts +6 -6
  36. package/src/compat/register/routing.ts +3 -4
  37. package/src/compat/tsconfig-defaults.ts +3 -2
  38. package/src/config.ts +3 -3
  39. package/src/css/postcss.ts +450 -17
  40. package/src/dev/client-actions.ts +2 -0
  41. package/src/dev/server.ts +87 -14
  42. package/src/render/island-context.ts +3 -0
  43. package/src/render/ppr.ts +14 -0
  44. package/src/render/renderer.ts +273 -43
  45. package/src/request/context.ts +32 -11
  46. package/src/resolve/scan-facts.ts +23 -4
  47. package/src/routing/proxy.ts +29 -25
  48. package/src/routing/routes.ts +1 -1
  49. package/src/runtime/loader.ts +47 -1
  50. package/src/runtime/modules.ts +546 -65
  51. package/src/runtime/vendor-build.ts +109 -22
  52. package/src/runtime/vendor.ts +7 -3
  53. package/src/utils/ansi.ts +3 -1
  54. package/src/utils/fs.ts +17 -3
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { builtinModules, createRequire } from 'node:module'
13
- import { existsSync, readFileSync, statSync } from 'node:fs'
13
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
14
14
  import { copyFile, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
15
15
  import path from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
@@ -104,6 +104,8 @@ export function clearServerRuntimeCaches() {
104
104
  clearVendorPipeline()
105
105
  clearVendorNativeCaches()
106
106
  vendorEntryResolutions.clear()
107
+ canonicalVendorEntries.clear()
108
+ nearestManifestDirs.clear()
107
109
  clearVendorPackageResolutions()
108
110
  vendorLayerRecords.clear()
109
111
  vendorSharedEntries.clear()
@@ -209,23 +211,40 @@ function resolveVendorEntry(
209
211
  resolveDir: string,
210
212
  conditionTarget: ServerBundleTarget,
211
213
  ) {
212
- if (path.isAbsolute(specifier)) return path.resolve(specifier)
213
214
  // Every demand builds a plan before dedup can see it, so this resolve runs
214
215
  // once per DEMAND, not once per artifact — and the same specifier arrives
215
216
  // from hundreds of importers. The answer is pure in its inputs.
216
217
  const key = `${config.root}\0${conditionTarget}\0${resolveDir}\0${specifier}`
217
218
  if (vendorEntryResolutions.has(key)) return vendorEntryResolutions.get(key)
218
- const entry = resolveVendorPackageSpecifier(
219
- config.root,
220
- path.join(resolveDir, 'pnext-resolve.ts'),
221
- specifier,
222
- [...serverBundleConditions(conditionTarget), 'import'],
219
+ const entry = canonicalVendorEntry(
220
+ path.isAbsolute(specifier)
221
+ ? path.resolve(specifier)
222
+ : resolveVendorPackageSpecifier(
223
+ config.root,
224
+ path.join(resolveDir, 'pnext-resolve.ts'),
225
+ specifier,
226
+ [...serverBundleConditions(conditionTarget), 'import'],
227
+ ),
223
228
  )
224
229
  vendorEntryResolutions.set(key, entry)
225
230
  return entry
226
231
  }
227
232
 
233
+ function canonicalVendorEntry(entry: string | undefined) {
234
+ if (!entry) return entry
235
+ if (canonicalVendorEntries.has(entry)) return canonicalVendorEntries.get(entry)!
236
+ let canonical = entry
237
+ try {
238
+ canonical = realpathSync.native(entry)
239
+ } catch {
240
+ // Keep the unresolved path when the file disappeared between resolution and canonicalization.
241
+ }
242
+ canonicalVendorEntries.set(entry, canonical)
243
+ return canonical
244
+ }
245
+
228
246
  const vendorEntryResolutions = new Map<string, string | undefined>()
247
+ const canonicalVendorEntries = new Map<string, string>()
229
248
 
230
249
  function vendorBuildPlan(
231
250
  config: ResolvedConfig,
@@ -940,10 +959,10 @@ async function preplanDependencyDecision(
940
959
  resolveDir: string,
941
960
  ): Promise<OnResolveResult | undefined> {
942
961
  const depName = packageNameFromSpecifier(specifier)
943
- if (!depName || !isPackageSpecifier(specifier)) return undefined
944
- if (builtinModules.includes(depName) || isBunBuiltin(depName)) return undefined
945
- if (getExternalPackagePolicy().external(depName)) return undefined
946
- if (getExternalPackagePolicy().transpile(depName)) return undefined
962
+ if (!isVendorModuleSpecifier(specifier)) return undefined
963
+ if (depName && (builtinModules.includes(depName) || isBunBuiltin(depName))) return undefined
964
+ if (depName && getExternalPackagePolicy().external(depName)) return undefined
965
+ if (depName && getExternalPackagePolicy().transpile(depName)) return undefined
947
966
  const entry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget)
948
967
  if (!entry || /\.[cm]?tsx?$/.test(entry) || !isEsmModuleEntry(entry)) return undefined
949
968
  const plan = vendorBuildPlan(config, specifier, target, resolveDir, conditionTarget)
@@ -1251,7 +1270,16 @@ async function buildVendorGroupWithPlugins(
1251
1270
  reactServerLayer: target === 'server',
1252
1271
  }),
1253
1272
  vendorGroupSiblingPlugin(siblings),
1254
- externalPackageDependencyPlugin(config, packageName, target, conditionTarget, context, preplan),
1273
+ externalPackageDependencyPlugin(
1274
+ config,
1275
+ packageName,
1276
+ target,
1277
+ conditionTarget,
1278
+ resolveDir,
1279
+ entryPoints.map(entry => entry.in),
1280
+ context,
1281
+ preplan,
1282
+ ),
1255
1283
  externalBuiltinBuildPlugin(),
1256
1284
  ])
1257
1285
  if (trace) trace.plugins = groupPlugins.map(plugin => plugin.name)
@@ -1720,15 +1748,32 @@ function vendorPackageShape(request: VendorNativeRequest): VendorPackageShape {
1720
1748
  }
1721
1749
 
1722
1750
  function nearestManifestDir(from: string) {
1751
+ if (nearestManifestDirs.has(from)) return nearestManifestDirs.get(from)
1723
1752
  let dir = from
1724
1753
  while (true) {
1725
- if (existsSync(path.join(dir, 'package.json'))) return dir
1754
+ const manifest = path.join(dir, 'package.json')
1755
+ if (existsSync(manifest)) {
1756
+ try {
1757
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: unknown }
1758
+ if (typeof parsed.name === 'string') {
1759
+ nearestManifestDirs.set(from, dir)
1760
+ return dir
1761
+ }
1762
+ } catch {
1763
+ // Malformed manifests cannot establish a package root; continue walking.
1764
+ }
1765
+ }
1726
1766
  const parent = path.dirname(dir)
1727
- if (parent === dir) return undefined
1767
+ if (parent === dir) {
1768
+ nearestManifestDirs.set(from, undefined)
1769
+ return undefined
1770
+ }
1728
1771
  dir = parent
1729
1772
  }
1730
1773
  }
1731
1774
 
1775
+ const nearestManifestDirs = new Map<string, string | undefined>()
1776
+
1732
1777
  /**
1733
1778
  * Decide what each external reference in the emitted code means - the work the resolve callbacks used
1734
1779
  * to do, off the bundler's critical path. Undefined when a reference cannot be expressed as a rewrite
@@ -1899,8 +1944,11 @@ async function preplanVendorRefUncached(
1899
1944
  )
1900
1945
  return asset ? { kind: 'inline', file: asset } : undefined
1901
1946
  }
1902
- if (!depName || !isPackageSpecifier(specifier)) return undefined
1903
- if (getExternalPackagePolicy().external(depName)) {
1947
+ if (sameVendorPackageFile(specifier, importerFile)) {
1948
+ return { kind: 'inline', file: canonicalVendorEntry(specifier)! }
1949
+ }
1950
+ if (!isVendorModuleSpecifier(specifier)) return undefined
1951
+ if (depName && getExternalPackagePolicy().external(depName)) {
1904
1952
  const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions)
1905
1953
  const fromRoot = resolveVendorPackageSpecifier(
1906
1954
  config.root,
@@ -1990,11 +2038,14 @@ async function decideVendorRefUncached(
1990
2038
  )
1991
2039
  return asset ? { kind: 'inline', file: asset } : undefined
1992
2040
  }
1993
- if (!depName || !isPackageSpecifier(specifier)) return undefined
2041
+ if (sameVendorPackageFile(specifier, importerFile)) {
2042
+ return { kind: 'inline', file: canonicalVendorEntry(specifier)! }
2043
+ }
2044
+ if (!isVendorModuleSpecifier(specifier)) return undefined
1994
2045
 
1995
2046
  // B7: a serverExternalPackage stays external, pinned to the importing
1996
2047
  // package's own copy when that differs from the hoisted root version.
1997
- if (getExternalPackagePolicy().external(depName)) {
2048
+ if (depName && getExternalPackagePolicy().external(depName)) {
1998
2049
  const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions)
1999
2050
  const fromRoot = resolveVendorPackageSpecifier(
2000
2051
  config.root,
@@ -2334,7 +2385,16 @@ async function bundleExternalPackageImpl(
2334
2385
  // bundles are the SSR pass and keep the full-hooks shim).
2335
2386
  reactServerLayer: target === 'server',
2336
2387
  }),
2337
- externalPackageDependencyPlugin(config, specifier, target, conditionTarget, context, preplan),
2388
+ externalPackageDependencyPlugin(
2389
+ config,
2390
+ specifier,
2391
+ target,
2392
+ conditionTarget,
2393
+ resolveDir,
2394
+ nativeEntry ? [nativeEntry] : [],
2395
+ context,
2396
+ preplan,
2397
+ ),
2338
2398
  externalBuiltinBuildPlugin(),
2339
2399
  ])
2340
2400
  if (trace) trace.plugins = vendorPlugins.map(plugin => plugin.name)
@@ -2410,16 +2470,32 @@ function externalPackageDependencyPlugin(
2410
2470
  entrySpecifier: string,
2411
2471
  target: CompatAliasTarget,
2412
2472
  conditionTarget: ServerBundleTarget,
2473
+ resolveDir: string,
2474
+ ownEntries: string[],
2413
2475
  context?: { nestedExternalFiles: string[] },
2414
2476
  preplan = false,
2415
2477
  ): Plugin {
2416
2478
  const entryPackageName = packageNameFromSpecifier(entrySpecifier)
2479
+ const ownPackageRoots = new Set(
2480
+ [...ownEntries, resolveVendorEntry(config, entrySpecifier, resolveDir, conditionTarget)]
2481
+ .filter((entry): entry is string => entry !== undefined)
2482
+ .map(entry => nearestManifestDir(path.dirname(canonicalVendorEntry(entry)!)))
2483
+ .filter((root): root is string => root !== undefined),
2484
+ )
2417
2485
  return {
2418
2486
  name: 'pnext-external-package-dependencies',
2419
2487
  setup(build) {
2420
- build.onResolve({ filter: /^[^./].*/ }, async args => {
2421
- if (!isPackageSpecifier(args.path)) return undefined
2422
- if (packageNameFromSpecifier(args.path) === entryPackageName) return undefined
2488
+ build.onResolve({ filter: /^(?:\/|[^./]).*/ }, async args => {
2489
+ if (!isVendorModuleSpecifier(args.path)) return undefined
2490
+ if (args.kind === 'entry-point') return undefined
2491
+ const ownRoot = path.isAbsolute(args.path)
2492
+ ? nearestManifestDir(path.dirname(canonicalVendorEntry(args.path)!))
2493
+ : undefined
2494
+ if (ownRoot && ownPackageRoots.has(ownRoot)) return undefined
2495
+ if (sameVendorPackageFile(args.path, args.importer)) return undefined
2496
+ if (entryPackageName && packageNameFromSpecifier(args.path) === entryPackageName) {
2497
+ return undefined
2498
+ }
2423
2499
  if (preplan && args.kind !== 'require-call') {
2424
2500
  const planned = await preplanDependencyDecision(
2425
2501
  config,
@@ -2627,6 +2703,17 @@ function isPackageSpecifier(specifier: string) {
2627
2703
  return Boolean(packageName && !builtinModules.includes(packageName))
2628
2704
  }
2629
2705
 
2706
+ function isVendorModuleSpecifier(specifier: string) {
2707
+ return path.isAbsolute(specifier) || isPackageSpecifier(specifier)
2708
+ }
2709
+
2710
+ function sameVendorPackageFile(specifier: string, importer: string) {
2711
+ if (!path.isAbsolute(specifier) || !path.isAbsolute(importer)) return false
2712
+ const entryRoot = nearestManifestDir(path.dirname(canonicalVendorEntry(specifier)!))
2713
+ const importerRoot = nearestManifestDir(path.dirname(canonicalVendorEntry(importer)!))
2714
+ return entryRoot !== undefined && entryRoot === importerRoot
2715
+ }
2716
+
2630
2717
  // Trace copies of nested transitive externals (a version distinct from the
2631
2718
  // hoisted root copy) written alongside the vendor bundle as `.js`, so file
2632
2719
  // tracing / server-bundle scans observe the bundled dependency the way
@@ -1028,14 +1028,14 @@ export async function cjsEntryMayHaveNamedExports(entry: string) {
1028
1028
  * full cache wipe recovers from. A new key sidesteps both: the old facade stays on disk for whoever still
1029
1029
  * imports it, and every new compile names the new one.
1030
1030
  */
1031
- const FACADE_GENERATOR = 'v2'
1031
+ const FACADE_GENERATOR = 'v3'
1032
1032
 
1033
1033
  /** Marker written beside a bundle whose export set turned out to be default-only. */
1034
1034
  const NO_FACADE_SUFFIX = `.no-facade-${FACADE_GENERATOR}`
1035
1035
 
1036
1036
  /** The facade this generator writes for `bundleFile`. */
1037
1037
  export function cjsFacadePath(bundleFile: string) {
1038
- return `${bundleFile.replace(/\.mjs$/, '')}.facade-${FACADE_GENERATOR}.mjs`
1038
+ return path.join(`${bundleFile.replace(/\.mjs$/, '')}.facade-${FACADE_GENERATOR}`, 'index.mjs')
1039
1039
  }
1040
1040
 
1041
1041
  /** Where this generator records "no named exports" for `bundleFile`. */
@@ -1120,11 +1120,15 @@ async function writeCjsNamedExportFacade(bundleFile: string, importHref: string)
1120
1120
  // off it threw at facade eval. Its names live on the bundle's own exports
1121
1121
  // instead — the same precedence `executedExportNames` derived them with.
1122
1122
  const source =
1123
- `import ${binding}, * as ${namespace} from ${JSON.stringify(`./${path.basename(bundleFile)}`)};\n` +
1123
+ `import ${binding}, * as ${namespace} from ${JSON.stringify(`../${path.basename(bundleFile)}`)};\n` +
1124
1124
  `export default ${binding};\n` +
1125
1125
  `const ${holder} = ${binding} ?? ${namespace};\n` +
1126
1126
  names.map(name => `export const ${name} = ${holder}[${JSON.stringify(name)}];`).join('\n') +
1127
1127
  '\n'
1128
+ // Bun 1.4 caches a resolved directory's module names. The bundle was just
1129
+ // imported above, so a new sibling facade is invisible to a later import;
1130
+ // a content-specific child directory has not been resolved yet.
1131
+ await mkdir(path.dirname(file), { recursive: true })
1128
1132
  const ok = await facadeRebundles(file, source)
1129
1133
  heavyProfRow({
1130
1134
  k: 'facade',
package/src/utils/ansi.ts CHANGED
@@ -1,5 +1,7 @@
1
+ // NO_COLOR (https://no-color.org): any non-empty value disables all styling.
1
2
  function color(open: string, close: string) {
2
- return (value: string) => (process.stdout.isTTY ? `${open}${value}${close}` : value)
3
+ return (value: string) =>
4
+ process.stdout.isTTY && !process.env.NO_COLOR ? `${open}${value}${close}` : value
3
5
  }
4
6
 
5
7
  /** Mid-gray for subordinate text. Readable on both light and dark terminals. */
package/src/utils/fs.ts CHANGED
@@ -9,9 +9,23 @@ import { fileURLToPath } from 'node:url'
9
9
  // second rename hits ENOENT once the first moved it away. Losing that race is harmless - both wrote the same
10
10
  // content-keyed bytes - so a rename whose temp is gone only fails if the destination is missing too.
11
11
  let atomicWriteSequence = 0
12
- export async function writeFileAtomic(file: string, contents: string) {
13
- const temp = `${file}.${process.pid.toString(36)}.${(++atomicWriteSequence).toString(36)}.tmp`
14
- await writeFile(temp, contents)
12
+ export async function writeFileAtomic(file: string, contents: string, tempDirectory?: string) {
13
+ const suffix = `${process.pid.toString(36)}.${(++atomicWriteSequence).toString(36)}.tmp`
14
+ let temp = tempDirectory
15
+ ? path.join(
16
+ tempDirectory,
17
+ `.${path.basename(path.dirname(file))}-${path.basename(file)}.${suffix}`,
18
+ )
19
+ : `${file}.${suffix}`
20
+ try {
21
+ await writeFile(temp, contents)
22
+ } catch (error) {
23
+ // An unwritable tempDirectory (read-only parent of a containerized app root)
24
+ // must not fail the write: fall back to staging beside the destination.
25
+ if (!tempDirectory) throw error
26
+ temp = `${file}.${suffix}`
27
+ await writeFile(temp, contents)
28
+ }
15
29
  try {
16
30
  await rename(temp, file)
17
31
  } catch (error) {