@wular/pnext 0.1.7 → 0.1.9

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 (172) hide show
  1. package/dist/server/_chunks/{aliases-j47a10ce.js → aliases-fhv4dt06.js} +7 -5
  2. package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-09kx9q6r.js} +17 -13
  3. package/dist/server/_chunks/{boot-af0mv3dy.js → boot-8aj7efqk.js} +13 -11
  4. package/dist/server/_chunks/{build-spy3dxp4.js → build-jda155c9.js} +7 -5
  5. package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-qrkpfsk1.js} +59 -75
  6. package/dist/server/_chunks/{build-t9h0hec3.js → build-wspv5h60.js} +1004 -1191
  7. package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
  8. package/dist/server/_chunks/{config-n2z7yxps.js → config-7h84p6q8.js} +6 -4
  9. package/dist/server/_chunks/{config-1yzwexq6.js → config-d4hqyygy.js} +12 -3
  10. package/dist/server/_chunks/{create-meec9hs6.js → create-skj8wzt5.js} +4 -3
  11. package/dist/server/_chunks/{dev-fjmqg6en.js → dev-pennd36n.js} +2 -2
  12. package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-d8vb1d21.js} +5 -4
  13. package/dist/server/_chunks/{index-eeaanp2e.js → index-0qt2vapb.js} +3 -3
  14. package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
  15. package/dist/server/_chunks/{index-9ych9ma8.js → index-0wm2p3ks.js} +2 -2
  16. package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
  17. package/dist/server/_chunks/{index-2dv4y4wd.js → index-1y0cesgn.js} +16 -10
  18. package/dist/server/_chunks/{index-ytjvpbm2.js → index-26gwrm07.js} +3 -3
  19. package/dist/server/_chunks/{index-8k7pfejn.js → index-3r6qpe8a.js} +7 -7
  20. package/dist/server/_chunks/{index-rn8dyr1f.js → index-42fef2rv.js} +8 -48
  21. package/dist/server/_chunks/{index-ncnes8c6.js → index-47gx7trn.js} +2 -2
  22. package/dist/server/_chunks/{index-20ppawv7.js → index-4qsce64w.js} +3 -3
  23. package/dist/server/_chunks/{index-1011cscr.js → index-4vf121ws.js} +61 -29
  24. package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
  25. package/dist/server/_chunks/{index-nyrba2kr.js → index-53fnn598.js} +11 -11
  26. package/dist/server/_chunks/{index-4hgjrqh7.js → index-6tb725m8.js} +2 -2
  27. package/dist/server/_chunks/{index-3xbr4eyv.js → index-912z4mfx.js} +3 -3
  28. package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
  29. package/dist/server/_chunks/{index-jepjvcrf.js → index-aarf1brn.js} +1 -1
  30. package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
  31. package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
  32. package/dist/server/_chunks/{index-sv8m6789.js → index-cqcwwxfp.js} +5 -5
  33. package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
  34. package/dist/server/_chunks/{index-vvmmknm1.js → index-da0fphj0.js} +19 -15
  35. package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
  36. package/dist/server/_chunks/{index-z84ab1w0.js → index-e176wk6x.js} +3 -3
  37. package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
  38. package/dist/server/_chunks/index-fpghsgb4.js +352 -0
  39. package/dist/server/_chunks/{index-jx7bqvw1.js → index-hr63fvzn.js} +1 -1
  40. package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
  41. package/dist/server/_chunks/index-jbj7ehef.js +50 -0
  42. package/dist/server/_chunks/{index-egykcqg3.js → index-jexsy9a8.js} +1 -1
  43. package/dist/server/_chunks/{index-fkp000p1.js → index-jjppzw0s.js} +2 -2
  44. package/dist/server/_chunks/index-jt8e1pje.js +12 -0
  45. package/dist/server/_chunks/{index-ggk2j965.js → index-kpdph69z.js} +56 -50
  46. package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
  47. package/dist/server/_chunks/{index-c0g2wf3z.js → index-md5jchzy.js} +1 -1
  48. package/dist/server/_chunks/{index-tvqkq6e6.js → index-n814hcky.js} +1 -1
  49. package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
  50. package/dist/server/_chunks/{index-4q2gbfwk.js → index-qjrh7w2g.js} +2 -2
  51. package/dist/server/_chunks/{index-3b3at80h.js → index-s61bx2aj.js} +6 -3
  52. package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
  53. package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
  54. package/dist/server/_chunks/{index-p77b4kcx.js → index-veycp46e.js} +1 -1
  55. package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
  56. package/dist/server/_chunks/{index-kq8kpzye.js → index-vqs3rebe.js} +3 -3
  57. package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
  58. package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
  59. package/dist/server/_chunks/{index-y2jr2n72.js → index-xrsg0041.js} +4 -4
  60. package/dist/server/_chunks/{index-k1g3epw0.js → index-xs3411qh.js} +10 -10
  61. package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
  62. package/dist/server/_chunks/{index-wpfhfbs4.js → index-y71a76dn.js} +4 -4
  63. package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
  64. package/dist/server/_chunks/{index-hkdfj58q.js → index-y9bygr95.js} +1 -1
  65. package/dist/server/_chunks/{index-3tf1xjsp.js → index-yghskk8w.js} +11 -11
  66. package/dist/server/_chunks/{index-5gyr4emz.js → index-yzcpr3yw.js} +4 -4
  67. package/dist/server/_chunks/{index-x93hv0sc.js → index-zedjt25v.js} +23 -10
  68. package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
  69. package/dist/server/_chunks/{loader-7yw0j98z.js → loader-w1e0n5sm.js} +12 -10
  70. package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-730j14cm.js} +14 -12
  71. package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-yjdnwbhm.js} +25 -21
  72. package/dist/server/_chunks/{production-jar9ztm3.js → production-1xe9b7wd.js} +14 -3
  73. package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-9r9e0jvn.js} +7 -5
  74. package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-pnevsbfd.js} +12 -10
  75. package/dist/server/_chunks/{router-k1reahfn.js → router-rzge0vz8.js} +10 -7
  76. package/dist/server/_chunks/{routes-2s277fc6.js → routes-0x8wjvcj.js} +12 -10
  77. package/dist/server/_chunks/{run-pvd8bs1t.js → run-tk90r3ac.js} +5 -4
  78. package/dist/server/_chunks/{server-mt4a994c.js → server-gev78ejc.js} +38 -34
  79. package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-qe9wd6ne.js} +5 -4
  80. package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-0yw8sry8.js} +15 -13
  81. package/dist/server/cli/index.js +12 -15
  82. package/dist/server/cli/start.js +35 -14
  83. package/dist/server/compat/next/cache.js +5 -5
  84. package/dist/server/compat/next/config-headers.js +8 -6
  85. package/dist/server/compat/next/config-loader.js +1 -1
  86. package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +5 -5
  87. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +5 -5
  88. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +5 -5
  89. package/dist/server/compat/next/dynamic.js +3 -14
  90. package/dist/server/compat/next/font/cache.js +6 -4
  91. package/dist/server/compat/next/font/local.js +7 -5
  92. package/dist/server/compat/next/font/runtime.js +7 -5
  93. package/dist/server/compat/next/headers.js +4 -4
  94. package/dist/server/compat/next/image/config.js +2 -2
  95. package/dist/server/compat/next/image/optimizer.js +3 -3
  96. package/dist/server/compat/next/image/static-metadata.js +4 -4
  97. package/dist/server/compat/next/legacy-request-apis.js +3 -3
  98. package/dist/server/compat/next/link-transform.js +4 -3
  99. package/dist/server/compat/next/navigation.js +5 -4
  100. package/dist/server/compat/next/redirects.js +8 -6
  101. package/dist/server/compat/next/resource-hints.js +2 -2
  102. package/dist/server/compat/next/rewrites.js +7 -5
  103. package/dist/server/compat/next/root-params.js +3 -3
  104. package/dist/server/compat/next/route-state.js +2 -2
  105. package/dist/server/compat/next/script.js +2 -2
  106. package/dist/server/compat/next/server.js +5 -5
  107. package/dist/server/generation.json +1 -1
  108. package/package.json +1 -1
  109. package/reference/compat.md +1 -1
  110. package/reference/config.md +2 -2
  111. package/reference/dev.md +1 -1
  112. package/reference/getting-started.md +1 -1
  113. package/reference/rendering.md +4 -4
  114. package/src/api/dynamic.tsx +84 -4
  115. package/src/cli/adapters/vercel-warm.ts +40 -29
  116. package/src/cli/adapters/vercel.ts +101 -1265
  117. package/src/cli/analyze.ts +3 -2
  118. package/src/cli/build/cache.ts +6 -6
  119. package/src/cli/build.ts +33 -33
  120. package/src/cli/index.ts +4 -6
  121. package/src/cli/serve/pipeline.ts +16 -10
  122. package/src/cli/serve/standalone-worker.ts +8 -0
  123. package/src/cli/serve/standalone.ts +727 -0
  124. package/src/cli/start.ts +29 -8
  125. package/src/client/build.ts +1 -1
  126. package/src/client/reference-stub.ts +2 -34
  127. package/src/compat/actions/extensions.ts +3 -2
  128. package/src/compat/bundler/require-context.ts +2 -1
  129. package/src/compat/bundler/webpack-loaders.ts +3 -2
  130. package/src/compat/bundler/worker.ts +2 -2
  131. package/src/compat/css/inline-css.ts +1 -1
  132. package/src/compat/export/build-complete.ts +1 -1
  133. package/src/compat/export/emit.ts +4 -4
  134. package/src/compat/export/standalone.ts +30 -65
  135. package/src/compat/image-optimizer/optimize.ts +1 -1
  136. package/src/compat/lifecycle/instrumentation-client.ts +4 -3
  137. package/src/compat/metadata-artifacts.ts +1 -1
  138. package/src/compat/next/config-loader.ts +3 -0
  139. package/src/compat/next/dynamic.tsx +10 -23
  140. package/src/compat/next/font/cache.ts +2 -1
  141. package/src/compat/next/font/runtime.ts +1 -1
  142. package/src/compat/register/image.ts +1 -1
  143. package/src/compat/typecheck/check.ts +6 -4
  144. package/src/config.ts +28 -5
  145. package/src/css/assets.ts +1 -1
  146. package/src/css/build.ts +16 -9
  147. package/src/dev/client-chunk-store.ts +2 -1
  148. package/src/dev/restart/client-key.ts +2 -1
  149. package/src/dev/restart/global-css.ts +2 -1
  150. package/src/dev/restart/route-bundle-key.ts +2 -1
  151. package/src/dev/restart/route-facts.ts +2 -1
  152. package/src/dev/server.ts +8 -7
  153. package/src/out-paths.ts +10 -0
  154. package/src/render/renderer.ts +1 -1
  155. package/src/resolve/dynamic.ts +4 -0
  156. package/src/resolve/engine.ts +2 -12
  157. package/src/resolve/scan-facts.ts +185 -1
  158. package/src/routing/metadata-files.ts +1 -1
  159. package/src/runtime/loader.ts +5 -3
  160. package/src/runtime/module-cache.ts +2 -1
  161. package/src/runtime/modules.ts +50 -16
  162. package/src/runtime/prebundle.ts +23 -3
  163. package/src/runtime/production.ts +105 -40
  164. package/src/runtime/release-graph.ts +21 -13
  165. package/src/runtime/vendor-build.ts +45 -23
  166. package/src/types.ts +2 -3
  167. package/src/utils/directive.ts +40 -0
  168. package/src/utils/fs.ts +7 -2
  169. package/src/utils/resource-query.ts +12 -0
  170. package/dist/server/_chunks/index-tqn2qnnn.js +0 -241
  171. package/dist/server/_chunks/index-y36a0ybw.js +0 -57
  172. package/src/cli/serve/entry.ts +0 -52
package/src/config.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  import { existsSync, realpathSync } from 'node:fs'
2
2
  import { fileURLToPath, pathToFileURL } from 'node:url'
3
3
  import path from 'node:path'
4
+ import { outCachePath, standaloneOutSegment } from './out-paths'
4
5
  import { loadEnv } from './env'
5
6
  import { findWorkspaceRoot } from './resolve/package-json'
6
7
  import { PREFETCH_MODES, type PNextConfig } from './types'
7
8
  import { setCacheComponents } from './render/ppr'
8
9
  import { setLegacyRequestAPIs } from './request/context'
9
- import { attachRelease, locateRelease, readBuildIndex, releaseConfig } from './runtime/production'
10
+ import {
11
+ attachRelease,
12
+ buildIndexFile,
13
+ linkStandaloneOrigins,
14
+ readBuildIndex,
15
+ releaseConfig,
16
+ releaseSourceRoot,
17
+ servingReleaseDir,
18
+ } from './runtime/production'
10
19
 
11
20
  export type ResolvedConfig = Required<Pick<PNextConfig, 'outDir' | 'basePath'>> &
12
21
  Omit<PNextConfig, 'outDir' | 'basePath'> & {
@@ -16,7 +25,7 @@ export type ResolvedConfig = Required<Pick<PNextConfig, 'outDir' | 'basePath'>>
16
25
  publicDir: 'public'
17
26
  appPath: string
18
27
  publicPath: string
19
- /** Where this process writes and serves from: the out root, or `<outRoot>/dev` in dev. */
28
+ /** Where this process writes and serves from: `<outRoot>/standalone`, or `<outRoot>/dev` in dev. */
20
29
  outPath: string
21
30
  /** The out root itself (`.pnext`), identical in dev and build. */
22
31
  outRootPath: string
@@ -97,7 +106,7 @@ export async function loadConfig(
97
106
  options: { dev?: boolean; serve?: boolean; warnings?: boolean } = {},
98
107
  ): Promise<ResolvedConfig> {
99
108
  const root = canonicalRoot(path.resolve(rootInput))
100
- const released = options.serve ? locateRelease(root, DEFAULT_OUT_DIR) : undefined
109
+ const released = options.serve ? servingReleaseDir() : undefined
101
110
  if (released) return loadReleasedConfig(root, released, options)
102
111
  await loadEnv(root, options)
103
112
  const configPath = path.join(root, 'pnext.config.ts')
@@ -129,6 +138,12 @@ export async function loadConfig(
129
138
  const sourceOverrides = stripUndefined(await resolveConfigSource(config, root, options))
130
139
  const merged = { ...defaultConfig, ...config, ...sourceOverrides }
131
140
  const outRootPath = path.resolve(root, merged.outDir)
141
+ // `pnext start` reads outDir from the config, as `next start` reads distDir, then serves the release.
142
+ const release = path.join(outRootPath, standaloneOutSegment)
143
+ if (options.serve && existsSync(buildIndexFile(release))) {
144
+ // The source config above already warned; its compiled copy must not warn again.
145
+ return loadReleasedConfig(root, release, { ...options, warnings: false })
146
+ }
132
147
  // A build materializes pages into its output, so the release names them portably.
133
148
  const appPath = await resolveAppPath(
134
149
  root,
@@ -163,7 +178,7 @@ export async function loadConfig(
163
178
  // Dev owns `<outRoot>/dev` exclusively so a concurrent `pnext build` — which
164
179
  // wipes its own outputs under the out root — can never pull a running dev
165
180
  // server's cache, manifest or assets out from under it.
166
- outPath: options.dev ? path.join(outRootPath, devOutSegment) : outRootPath,
181
+ outPath: path.join(outRootPath, options.dev ? devOutSegment : standaloneOutSegment),
167
182
  outRootPath,
168
183
  typesPath: path.join(outRootPath, 'types'),
169
184
  checksPath: path.join(outRootPath, 'typecheck', 'checks'),
@@ -180,7 +195,9 @@ async function loadReleasedConfig(
180
195
  options: { dev?: boolean; serve?: boolean; warnings?: boolean },
181
196
  ): Promise<ResolvedConfig> {
182
197
  const index = readBuildIndex(outPath)
183
- const config = releaseConfig(index.config, root, outPath)
198
+ linkStandaloneOrigins(outPath)
199
+ const sourceRoot = releaseSourceRoot(outPath, index.config.outPath as string)
200
+ const config = releaseConfig(index.config, sourceRoot, outPath)
184
201
  await loadEnv(root, options)
185
202
  setLegacyRequestAPIs(
186
203
  typeof config.compat?.next === 'object' && config.compat.next.legacyRequestAPIs === true,
@@ -219,6 +236,8 @@ function validateConfig(config: PNextConfig) {
219
236
  /** Dev's private subtree under the out root. Build never touches it. */
220
237
  export const devOutSegment = 'dev'
221
238
 
239
+ export { outCachePath, standaloneOutSegment }
240
+
222
241
  // Relative-import resolution realpaths the importing file (so a materialized shim resolves `./sibling`
223
242
  // against real source), so every containment check against root/workspaceRoot compares realpaths.
224
243
  // Canonicalize the roots to match - otherwise an app served through a symlinked directory (a macOS
@@ -297,6 +316,10 @@ export function frameworkRuntimeAliasEntries(): Record<FrameworkRuntimeAlias, st
297
316
  return frameworkRuntimeAliases
298
317
  }
299
318
 
319
+ /** Packages the framework resolves at runtime by name, not by import. */
320
+ export const frameworkRuntimeSpecifiers = (): string[] =>
321
+ Object.keys(frameworkRuntimeAliasEntries())
322
+
300
323
  function resolveRuntimeModule(specifier: string) {
301
324
  return fileURLToPath(import.meta.resolve(specifier))
302
325
  }
package/src/css/assets.ts CHANGED
@@ -116,7 +116,7 @@ export async function emitFontCssStylesheet(
116
116
  baseAsset: string | undefined,
117
117
  options: { dev: boolean },
118
118
  ) {
119
- const outDir = path.join(config.outPath, options.dev ? 'cache' : 'public', 'assets')
119
+ const outDir = path.join(config.outPath, options.dev ? 'cache' : 'static', 'assets')
120
120
  await ensureDir(outDir)
121
121
  const baseFile = baseAsset ? path.join(outDir, baseAsset) : undefined
122
122
  const baseCss = baseFile && existsSync(baseFile) ? await readFile(baseFile, 'utf8') : ''
package/src/css/build.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  withAssetPrefix,
41
41
  type AssetHrefConfig,
42
42
  } from './assets'
43
+ import { outCachePath } from '../out-paths'
43
44
 
44
45
  interface Pending {
45
46
  resolve: () => void
@@ -114,14 +115,20 @@ export async function stopCssWorker(): Promise<void> {
114
115
  if (active) await Promise.resolve(active.terminate())
115
116
  }
116
117
 
118
+ // Tailwind skips the out root and caches candidates in it; dev keeps its own subtree.
119
+ function scanOutDir(config: Pick<ResolvedConfig, 'outPath' | 'outRootPath'>, dev: boolean) {
120
+ return dev ? config.outPath : config.outRootPath
121
+ }
122
+
117
123
  export async function runPostcssOffThread(
118
- config: Pick<ResolvedConfig, 'root' | 'outPath'>,
124
+ config: Pick<ResolvedConfig, 'root' | 'outPath' | 'outRootPath'>,
119
125
  cssFile: string,
120
126
  options: { dev?: boolean },
121
127
  from?: string,
122
128
  ) {
123
- const { root, outPath: outDir } = config
124
129
  const dev = options.dev ?? false
130
+ const { root } = config
131
+ const outDir = scanOutDir(config, dev)
125
132
  const sent = send(id => ({ kind: 'process', id, root, dev, outDir, cssFile, from }))
126
133
  if (sent) return sent
127
134
  return runPostcss(root, cssFile, { dev, outDir }, from)
@@ -132,7 +139,7 @@ export async function runPostcssOffThread(
132
139
  * stylesheet, so a cold Tailwind boot overlaps with the rest of dev startup.
133
140
  */
134
141
  export function warmCssWorker(
135
- config: Pick<ResolvedConfig, 'root' | 'outPath'>,
142
+ config: Pick<ResolvedConfig, 'root' | 'outPath' | 'outRootPath'>,
136
143
  options: { dev?: boolean } = {},
137
144
  ) {
138
145
  send(id => ({
@@ -140,7 +147,7 @@ export function warmCssWorker(
140
147
  id,
141
148
  root: config.root,
142
149
  dev: options.dev ?? false,
143
- outDir: config.outPath,
150
+ outDir: scanOutDir(config, options.dev ?? false),
144
151
  }))?.catch(() => {
145
152
  // A broken postcss config surfaces on the real build; warming stays quiet.
146
153
  })
@@ -265,7 +272,7 @@ function rootLayoutFile(appPath: string, missing?: Set<string>) {
265
272
  * instead of landing on the first request. No-op without a postcss config.
266
273
  */
267
274
  export function warmCssPipeline(
268
- config: Pick<ResolvedConfig, 'root' | 'outPath'>,
275
+ config: Pick<ResolvedConfig, 'root' | 'outPath' | 'outRootPath'>,
269
276
  options: CssBuildOptions = {},
270
277
  ) {
271
278
  if (!hasPostcssConfig(config.root)) return
@@ -281,8 +288,8 @@ export async function buildGlobalCss(config: ResolvedConfig, options: CssBuildOp
281
288
  }
282
289
 
283
290
  const outDir = options.dev
284
- ? path.join(config.outPath, 'cache', 'assets')
285
- : path.join(config.outPath, 'public', 'assets')
291
+ ? path.join(outCachePath(config.outPath), 'assets')
292
+ : path.join(config.outPath, 'static', 'assets')
286
293
  const outfile = path.join(outDir, 'global.css')
287
294
  await ensureDir(outDir)
288
295
 
@@ -606,8 +613,8 @@ async function writeCssChunk(
606
613
  const log = createVerboseLogger(options.verbose ?? false, 'css')
607
614
 
608
615
  const outDir = options.dev
609
- ? path.join(config.outPath, 'cache', 'assets')
610
- : path.join(config.outPath, 'public', 'assets')
616
+ ? path.join(outCachePath(config.outPath), 'assets')
617
+ : path.join(config.outPath, 'static', 'assets')
611
618
  const outfile = path.join(outDir, assetName)
612
619
  await ensureDir(outDir)
613
620
 
@@ -9,9 +9,10 @@ import { createHash } from 'node:crypto'
9
9
  import { link, mkdir, readFile, readdir, rm, stat, unlink } from 'node:fs/promises'
10
10
  import path from 'node:path'
11
11
  import { listFiles } from '../utils/fs'
12
+ import { outCachePath } from '../out-paths'
12
13
 
13
14
  export function clientChunkStoreDir(outPath: string) {
14
- return path.join(outPath, 'cache', 'client-store')
15
+ return path.join(outCachePath(outPath), 'client-store')
15
16
  }
16
17
 
17
18
  function contentHash(data: Buffer) {
@@ -23,6 +23,7 @@ import type { ResolvedConfig } from '../../config'
23
23
  import type { RouteManifestEntry } from '../../types'
24
24
  import { writeFileAtomic } from '../../utils/fs'
25
25
  import { restartCacheEnabled } from './enabled'
26
+ import { outCachePath } from '../../out-paths'
26
27
 
27
28
  /** `[file, mtimeMs, size, contentHash]` — the same record shape the module index uses. */
28
29
  type SourceEntry = [string, number, number, string]
@@ -53,7 +54,7 @@ interface KeyCache {
53
54
  const caches = new Map<string, KeyCache>()
54
55
 
55
56
  function keyCache(config: ResolvedConfig): KeyCache {
56
- const file = path.join(config.outPath, 'cache', 'client', 'keys.json')
57
+ const file = path.join(outCachePath(config.outPath), 'client', 'keys.json')
57
58
  const existing = caches.get(file)
58
59
  if (existing) return existing
59
60
  const created: KeyCache = { file, routes: readIndex(file), dirty: false, retries: 0 }
@@ -17,6 +17,7 @@ import { setGlobalCssSourceStore, type GlobalCssSourceStore } from '../../css/bu
17
17
  import { traceEnabled } from '../../utils/trace-flags'
18
18
  import { restartCacheEnabled } from './enabled'
19
19
  import { hash, lazyRecordCacheKey, revalidate, sourceEntries, type SourceEntry } from './record'
20
+ import { outCachePath } from '../../out-paths'
20
21
 
21
22
  interface RecordFile {
22
23
  version: number
@@ -34,7 +35,7 @@ export function installGlobalCssCache(config: ResolvedConfig) {
34
35
  if (!restartCacheEnabled()) return
35
36
  // eslint-disable-next-line turbo/no-undeclared-env-vars
36
37
  if (process.env.PNEXT_DEV_GLOBAL_CSS_CACHE === '0') return
37
- const dir = path.join(config.outPath, 'cache', 'global-css')
38
+ const dir = path.join(outCachePath(config.outPath), 'global-css')
38
39
  setGlobalCssSourceStore(createStore(dir, lazyRecordCacheKey(config, RECORD_VERSION)))
39
40
  }
40
41
 
@@ -25,6 +25,7 @@ import {
25
25
  type DevGraphSource,
26
26
  } from '../../runtime/module-cache'
27
27
  import { restartCacheEnabled } from './enabled'
28
+ import { outCachePath } from '../../out-paths'
28
29
 
29
30
  interface RecordFile {
30
31
  version: number
@@ -49,7 +50,7 @@ function enabled() {
49
50
  }
50
51
 
51
52
  function recordFile(config: ResolvedConfig, routeId: string) {
52
- return path.join(config.outPath, 'cache', 'route-bundles', `${hash(routeId)}.json`)
53
+ return path.join(outCachePath(config.outPath), 'route-bundles', `${hash(routeId)}.json`)
53
54
  }
54
55
 
55
56
  /**
@@ -20,6 +20,7 @@ import { setRouteFactsStore, type RouteFacts, type RouteFactsStore } from '../..
20
20
  import { traceEnabled } from '../../utils/trace-flags'
21
21
  import { restartCacheEnabled } from './enabled'
22
22
  import { hash, lazyRecordCacheKey, revalidate, sourceEntry, type SourceEntry } from './record'
23
+ import { outCachePath } from '../../out-paths'
23
24
 
24
25
  interface RecordFile {
25
26
  version: number
@@ -45,7 +46,7 @@ export function installRouteFactsCache(config: ResolvedConfig) {
45
46
  // eslint-disable-next-line turbo/no-undeclared-env-vars
46
47
  if (process.env.PNEXT_DEV_FACTS_CACHE === '0') return
47
48
  const cache: FactsCache = {
48
- dir: path.join(config.outPath, 'cache', 'route-facts'),
49
+ dir: path.join(outCachePath(config.outPath), 'route-facts'),
49
50
  key: lazyRecordCacheKey(config, RECORD_VERSION),
50
51
  written: new Set(),
51
52
  }
package/src/dev/server.ts CHANGED
@@ -121,6 +121,7 @@ import { cacheRoot, setDevWatcherFreshness } from '../runtime/module-cache'
121
121
  import { abortUpstreamFetchOnDisconnect } from '../runtime/fetch-host'
122
122
  import { markBoot } from '../cli/boot/trace'
123
123
  import { formatDuration } from '../utils/verbose'
124
+ import { outCachePath } from '../out-paths'
124
125
 
125
126
  interface DevServerOptions {
126
127
  config: ResolvedConfig
@@ -217,7 +218,7 @@ async function evictStaleClientCaches(outPath: string) {
217
218
  // eslint-disable-next-line turbo/no-undeclared-env-vars
218
219
  const keep = Number(process.env.PNEXT_DEV_CLIENT_CACHE_KEEP || 32)
219
220
  if (!Number.isFinite(keep) || keep <= 0) return
220
- const clientRoot = path.join(outPath, 'cache', 'client')
221
+ const clientRoot = path.join(outCachePath(outPath), 'client')
221
222
  try {
222
223
  const entries = await readdir(clientRoot)
223
224
  if (entries.length <= keep) return
@@ -597,7 +598,7 @@ export async function startDevServer(options: DevServerOptions) {
597
598
  .some(prefix => url.pathname.startsWith(prefix))
598
599
  ) {
599
600
  const emitted = await maybeStaticFile(
600
- path.join(config.outPath, 'public'),
601
+ path.join(config.outPath, 'static'),
601
602
  url.pathname,
602
603
  exactNames,
603
604
  )
@@ -613,7 +614,7 @@ export async function startDevServer(options: DevServerOptions) {
613
614
  )
614
615
  if (staticAssetPathname) {
615
616
  const outPublicStatic = await maybeStaticFile(
616
- path.join(config.outPath, 'public'),
617
+ path.join(config.outPath, 'static'),
617
618
  staticAssetPathname,
618
619
  exactNames,
619
620
  )
@@ -1274,7 +1275,7 @@ async function buildDevClient(config: ResolvedConfig, route: RouteManifestEntry)
1274
1275
  batchRoutes.length === 1
1275
1276
  ? cacheKeys[0]!
1276
1277
  : `batch-${clientSourceHash([...cacheKeys].sort().join('\n'))}`
1277
- const outDir = path.join(config.outPath, 'cache', 'client', cacheKey)
1278
+ const outDir = path.join(outCachePath(config.outPath), 'client', cacheKey)
1278
1279
  const outFile = path.join(outDir, `${clientEntryName(route)}.js`)
1279
1280
 
1280
1281
  // Dedup in-flight builds first. A single page view fires up to three requests
@@ -1361,7 +1362,7 @@ async function resolveDevClientChunk(config: ResolvedConfig, name: string) {
1361
1362
  const settled = clientChunks.get(name)
1362
1363
  if (settled && existsSync(settled)) return settled
1363
1364
 
1364
- const clientRoot = path.join(config.outPath, 'cache', 'client')
1365
+ const clientRoot = path.join(outCachePath(config.outPath), 'client')
1365
1366
  for (const generation of await readdir(clientRoot).catch(() => [])) {
1366
1367
  const chunksDir = path.join(clientRoot, generation, 'chunks')
1367
1368
  const file = path.join(chunksDir, name)
@@ -1500,8 +1501,8 @@ async function maybeBuiltAsset(
1500
1501
  }
1501
1502
 
1502
1503
  const outPath = config.outPath
1503
- const filePath = path.join(outPath, 'cache', pathname.replace(/^\/+/, ''))
1504
- if (!isInside(path.join(outPath, 'cache'), filePath) || !existsSync(filePath)) return null
1504
+ const filePath = path.join(outCachePath(outPath), pathname.replace(/^\/+/, ''))
1505
+ if (!isInside(path.join(outCachePath(outPath)), filePath) || !existsSync(filePath)) return null
1505
1506
  const fileStat = await stat(filePath)
1506
1507
  if (!fileStat.isFile()) return null
1507
1508
  return new Response(await readFile(filePath), {
@@ -0,0 +1,10 @@
1
+ import path from 'node:path'
2
+
3
+ /** The release a build writes under the out root: everything a production server reads. */
4
+ export const standaloneOutSegment = 'standalone'
5
+
6
+ /** The build cache beside a release (`<outRoot>/cache`); dev keeps its own. */
7
+ export function outCachePath(outPath: string) {
8
+ const owner = path.basename(outPath) === standaloneOutSegment ? path.dirname(outPath) : outPath
9
+ return path.join(owner, 'cache')
10
+ }
@@ -3403,7 +3403,7 @@ export function withDevReloadScript(html: string, request?: Request): string {
3403
3403
  }
3404
3404
 
3405
3405
  function withGlobalErrorStylesheet(html: string, config: ResolvedConfig): string {
3406
- const asset = `${config.outPath}/public/assets/${emittedAssetName(config, 'global-error.css')}`
3406
+ const asset = `${config.outPath}/static/assets/${emittedAssetName(config, 'global-error.css')}`
3407
3407
  if (!existsSync(asset)) return html
3408
3408
  const link = `<link rel="stylesheet" href="${assetHref(config, 'global-error.css')}">`
3409
3409
  if (html.includes('</head>')) return html.replace('</head>', `${link}</head>`)
@@ -224,6 +224,7 @@ export function deferredDynamicImportSpecifiers(source: string, file?: string) {
224
224
  }
225
225
 
226
226
  const facts = dynamicCallFacts(source, file)
227
+ const rendered = new Set<string>()
227
228
  for (const call of dynamicCallsFromSource(source, dynamicNames)) {
228
229
  const target = loaderImportTargetForCall(call, facts)
229
230
  if (!target) continue
@@ -239,7 +240,10 @@ export function deferredDynamicImportSpecifiers(source: string, file?: string) {
239
240
  const visible =
240
241
  /\bload\s*:\s*['"]visible['"]/.test(options) && !/\bssr\s*:\s*true\b/.test(options)
241
242
  if (ssrFalse || visible) deferred.add(target.specifier)
243
+ else rendered.add(target.specifier)
242
244
  }
245
+ // A target another call server-renders must compile for the server.
246
+ for (const specifier of rendered) deferred.delete(specifier)
243
247
  return deferred
244
248
  }
245
249
 
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import type { ResolverFactory, NapiResolveOptions } from 'oxc-resolver'
4
4
  import { loadNative } from '../utils/native-require'
5
+ import { splitResourceQuery } from '../utils/resource-query'
5
6
 
6
7
  // Lazy: the native binding costs ~1.3 MB RSS and a prod server may never resolve.
7
8
  function createResolverFactory(options: NapiResolveOptions): ResolverFactory {
@@ -95,18 +96,7 @@ export function tsConfigFileFor(root: string): string | undefined {
95
96
  return file
96
97
  }
97
98
 
98
- /**
99
- * Split a webpack resource specifier into its file request and its `?query#fragment` suffix - loader
100
- * plumbing (turbopack rules, `.wasm?module`) that takes no part in the on-disk lookup. A leading `#` is a
101
- * package-imports specifier, not a fragment.
102
- */
103
- export function splitResourceQuery(specifier: string): { path: string; query: string } {
104
- const offset = specifier.startsWith('#') ? 1 : 0
105
- const match = /[?#]/.exec(specifier.slice(offset))
106
- if (!match) return { path: specifier, query: '' }
107
- const index = offset + match.index
108
- return { path: specifier.slice(0, index), query: specifier.slice(index) }
109
- }
99
+ export { splitResourceQuery }
110
100
 
111
101
  /**
112
102
  * Resolve `specifier` from `dir`. `tsConfigRoot` opts the lookup into that
@@ -76,6 +76,8 @@ export interface RewriteFacts {
76
76
  edges: SpecifierEdge[]
77
77
  imports: ImportStatement[]
78
78
  importMetas: { start: number; end: number }[]
79
+ /** `import()` calls whose argument is not a plain string literal. */
80
+ computedImports: { start: number; end: number }[]
79
81
  /**
80
82
  * The parse recovered from errors, so the record may be incomplete — folded
81
83
  * passes fall back to their textual form rather than silently under-rewrite.
@@ -146,6 +148,7 @@ const emptyRewriteFacts: RewriteFacts = {
146
148
  edges: [],
147
149
  imports: [],
148
150
  importMetas: [],
151
+ computedImports: [],
149
152
  unreliable: false,
150
153
  }
151
154
 
@@ -373,10 +376,14 @@ function parseFacts(file: string, source: string, lang: ParserLang) {
373
376
  imports.sort((a, b) => a.index - b.index)
374
377
 
375
378
  const dynamicImports: { specifier: string; index: number }[] = []
379
+ const computedImports: { start: number; end: number }[] = []
376
380
  for (const entry of result.module.dynamicImports) {
377
381
  const raw = source.slice(entry.moduleRequest.start, entry.moduleRequest.end)
378
382
  const specifier = literalSpecifier(raw)
379
- if (specifier === undefined) continue
383
+ if (specifier === undefined) {
384
+ computedImports.push({ start: entry.start, end: entry.end })
385
+ continue
386
+ }
380
387
  dynamicImports.push({ specifier, index: entry.start })
381
388
  // The literal's own span, without the whitespace `moduleRequest` includes.
382
389
  const lead = raw.length - raw.trimStart().length
@@ -419,6 +426,7 @@ function parseFacts(file: string, source: string, lang: ParserLang) {
419
426
  edges,
420
427
  imports: importStatements,
421
428
  importMetas: result.module.importMetas.map(meta => ({ start: meta.start, end: meta.end })),
429
+ computedImports,
422
430
  unreliable: result.errors.length > 0,
423
431
  },
424
432
  }
@@ -581,6 +589,182 @@ function child(node: AstNode | undefined, key: string): AstNode | undefined {
581
589
  return isNode(value) ? value : undefined
582
590
  }
583
591
 
592
+ /** A string literal argument: its cooked value and the span of its quoted text. */
593
+ export interface LiteralSpan {
594
+ specifier: string
595
+ start: number
596
+ end: number
597
+ }
598
+
599
+ /** How a module refers to its own location (see `importMetaRefs`). */
600
+ export interface ImportMetaRefs {
601
+ /** `new URL('<literal>', import.meta.url)`. */
602
+ urls: LiteralSpan[]
603
+ /** `import.meta.resolve('<literal>')`. */
604
+ resolves: LiteralSpan[]
605
+ /** Reads `import.meta.url`/`dirname`/`filename`/`resolve`, or takes `import.meta` whole. */
606
+ readsLocation: boolean
607
+ /** Spans of each `import()` whose argument is not a string literal. */
608
+ computedImports: { start: number; end: number }[]
609
+ /** The parse recovered from errors: callers keep their conservative answer. */
610
+ unreliable: boolean
611
+ }
612
+
613
+ const noMetaRefs: ImportMetaRefs = {
614
+ urls: [],
615
+ resolves: [],
616
+ readsLocation: false,
617
+ computedImports: [],
618
+ unreliable: false,
619
+ }
620
+ const metaRefMemo = new Map<string, ImportMetaRefs>()
621
+ const locationMembers = new Set(['url', 'dirname', 'filename', 'resolve'])
622
+
623
+ /** Every reference a module makes to its own location, from the AST: never a comment or a string. */
624
+ export function importMetaRefs(file: string, source: string): ImportMetaRefs {
625
+ if (stylesheetOrData.test(file)) return noMetaRefs
626
+ if (!source.includes('import.meta') && !source.includes('import(')) return noMetaRefs
627
+ // The memoized module record answers computed imports and whether `import.meta` occurs at all;
628
+ // only a module that really uses it pays for the AST.
629
+ const record = rewriteFacts(file, source)
630
+ if (record.importMetas.length === 0)
631
+ return { ...noMetaRefs, computedImports: record.computedImports, unreliable: record.unreliable }
632
+ const lang = langForFile(file)
633
+ const key = `${lang}\0${source.length}\0${bun.hash(source).toString()}`
634
+ const cached = metaRefMemo.get(key)
635
+ if (cached) return cached
636
+ const result = parseSync(file, source, { lang })
637
+ const refs: ImportMetaRefs = {
638
+ ...noMetaRefs,
639
+ urls: [],
640
+ resolves: [],
641
+ computedImports: record.computedImports,
642
+ }
643
+ refs.unreliable = result.errors.length > 0
644
+ const isImportMeta = (node: AstNode | undefined) =>
645
+ node?.type === 'MetaProperty' &&
646
+ child(node, 'meta')?.name === 'import' &&
647
+ child(node, 'property')?.name === 'meta'
648
+ const memberName = (node: AstNode) =>
649
+ node.computed ? stringValue(child(node, 'property')) : (child(node, 'property')?.name as string)
650
+ const literal = (node: AstNode | undefined): LiteralSpan | undefined => {
651
+ if (!node) return undefined
652
+ const value =
653
+ stringValue(node) ??
654
+ (node.type === 'TemplateLiteral' && (node.expressions as unknown[]).length === 0
655
+ ? ((node.quasis as AstNode[])[0]?.value as { cooked?: string } | undefined)?.cooked
656
+ : undefined)
657
+ return value === undefined ? undefined : { specifier: value, start: node.start, end: node.end }
658
+ }
659
+ const visit = (value: unknown, parent: AstNode | undefined) => {
660
+ if (Array.isArray(value)) {
661
+ for (const item of value) visit(item, parent)
662
+ return
663
+ }
664
+ if (!isNode(value)) return
665
+ if (isImportMeta(value)) {
666
+ const member = parent?.type === 'MemberExpression' && parent.object === value
667
+ if (!member || locationMembers.has(memberName(parent) ?? '')) refs.readsLocation = true
668
+ } else if (value.type === 'NewExpression' && child(value, 'callee')?.name === 'URL') {
669
+ const [target, base] = value.arguments as AstNode[]
670
+ const span = literal(target)
671
+ if (span && base?.type === 'MemberExpression' && isImportMeta(child(base, 'object')))
672
+ if (memberName(base) === 'url') refs.urls.push(span)
673
+ } else if (value.type === 'CallExpression') {
674
+ const callee = child(value, 'callee')
675
+ if (callee?.type === 'MemberExpression' && isImportMeta(child(callee, 'object'))) {
676
+ const span = literal((value.arguments as AstNode[])[0])
677
+ if (span && memberName(callee) === 'resolve') refs.resolves.push(span)
678
+ }
679
+ }
680
+ for (const key in value) {
681
+ if (key !== 'start' && key !== 'end' && key !== 'type') visit(value[key], value)
682
+ }
683
+ }
684
+ visit(result.program, undefined)
685
+ metaRefMemo.set(key, refs)
686
+ return refs
687
+ }
688
+
689
+ /**
690
+ * Whether the module declares top-level state: a `const`/`let`/`var` initialized by `new`, or by a
691
+ * call through a plain name (not `require`, not `/* @__PURE__ *\/`), from the AST: never a line
692
+ * inside a string, and every declarator counts.
693
+ */
694
+ export function hasModuleState(file: string, source: string): boolean {
695
+ const result = parseSync(file, source, { lang: langForFile(file) })
696
+ const pureEnds = new Set(
697
+ result.comments
698
+ .filter(comment => /^\s*[#@]__PURE__\s*$/.test(comment.value))
699
+ .map(comment => comment.end),
700
+ )
701
+ const annotatedPure = (node: AstNode) => {
702
+ for (let index = node.start - 1; index >= 0; index -= 1) {
703
+ if (pureEnds.has(index + 1)) return true
704
+ if (!/\s/.test(source[index]!)) return false
705
+ }
706
+ return false
707
+ }
708
+ const plainCallee = (node: AstNode | undefined): boolean =>
709
+ node?.type === 'Identifier' ||
710
+ (node?.type === 'MemberExpression' && !node.computed && plainCallee(child(node, 'object')))
711
+ const declarations = (result.program.body as unknown as AstNode[]).flatMap(statement => {
712
+ const declaration =
713
+ statement.type === 'ExportNamedDeclaration' ? child(statement, 'declaration') : statement
714
+ return declaration?.type === 'VariableDeclaration'
715
+ ? (declaration.declarations as AstNode[])
716
+ : []
717
+ })
718
+ return declarations.some(declarator => {
719
+ const init = unwrapExpression(child(declarator, 'init'))
720
+ if (init?.type === 'NewExpression') return true
721
+ if (init?.type !== 'CallExpression') return false
722
+ const callee = child(init, 'callee')
723
+ return plainCallee(callee) && callee?.name !== 'require' && !annotatedPure(init)
724
+ })
725
+ }
726
+
727
+ /** A string literal holding an absolute path or `file:` URL, and whether it is an object key. */
728
+ export interface PathLiteral {
729
+ value: string
730
+ start: number
731
+ end: number
732
+ key: boolean
733
+ }
734
+
735
+ /** Absolute path literals in emitted code, from the AST; undefined when the parse recovered. */
736
+ export function absolutePathLiterals(file: string, source: string): PathLiteral[] | undefined {
737
+ const result = parseSync(file, source, { lang: langForFile(file) })
738
+ if (result.errors.length > 0) return undefined
739
+ const found: PathLiteral[] = []
740
+ const visit = (value: unknown, parent: AstNode | undefined) => {
741
+ if (Array.isArray(value)) {
742
+ for (const item of value) visit(item, parent)
743
+ return
744
+ }
745
+ if (!isNode(value)) return
746
+ const literal = stringValue(value)
747
+ if (literal !== undefined && (literal.startsWith('/') || literal.startsWith('file:///'))) {
748
+ const key = parent?.type === 'Property' && parent.key === value && !parent.computed
749
+ found.push({ value: literal, start: value.start, end: value.end, key })
750
+ }
751
+ for (const key in value) {
752
+ if (key !== 'start' && key !== 'end' && key !== 'type') visit(value[key], value)
753
+ }
754
+ }
755
+ visit(result.program, undefined)
756
+ return found
757
+ }
758
+
759
+ /** Line comments (`// …`), from the parse; undefined when it recovered. */
760
+ export function lineComments(file: string, source: string) {
761
+ const result = parseSync(file, source, { lang: langForFile(file) })
762
+ if (result.errors.length > 0) return undefined
763
+ return result.comments
764
+ .filter(comment => comment.type === 'Line')
765
+ .map(comment => ({ value: comment.value, start: comment.start, end: comment.end }))
766
+ }
767
+
584
768
  /**
585
769
  * Drop `type: "json"` attributes from the import declarations and `import()` calls whose target
586
770
  * `compiled` claims: the target is JS, which a native load would parse as JSON.
@@ -216,7 +216,7 @@ export function staticMetadataLink(file: StaticMetadataFile): MetadataLink {
216
216
  }
217
217
 
218
218
  export function staticMetadataOutputFile(outPath: string, file: StaticMetadataFile) {
219
- return path.join(outPath, 'public', file.outputPath)
219
+ return path.join(outPath, 'static', file.outputPath)
220
220
  }
221
221
 
222
222
  export function metadataHasOwnImages(metadata: Metadata, field: 'openGraph' | 'twitter') {