@thomas.f/prnext 0.1.18-alpha → 0.1.20-alpha

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 (162) hide show
  1. package/README.md +2 -0
  2. package/build/actions.mjs +26 -14
  3. package/build/app.mjs +90 -101
  4. package/build/build-lock.mjs +2 -71
  5. package/build/build-retention.mjs +119 -0
  6. package/build/bundle-runtime.mjs +9 -8
  7. package/build/cache-command.mjs +31 -0
  8. package/build/check.mjs +41 -3
  9. package/build/compilation-cache.mjs +32 -12
  10. package/build/compilation-storage.mjs +190 -0
  11. package/build/compiler.mjs +8 -7
  12. package/build/config.mjs +25 -2
  13. package/build/create.mjs +1 -1
  14. package/build/css-modules.mjs +4 -0
  15. package/build/dev-action-graph.mjs +21 -0
  16. package/build/dev-css-assets.mjs +173 -0
  17. package/build/dev-css-update.mjs +14 -12
  18. package/build/dev-lazy-build.mjs +77 -0
  19. package/build/dynamic.mjs +13 -6
  20. package/build/font-cache.mjs +9 -6
  21. package/build/font-files.mjs +1 -1
  22. package/build/incremental.mjs +112 -18
  23. package/build/index.mjs +61 -15
  24. package/build/lazy-dynamic-imports.mjs +137 -0
  25. package/build/migrate.mjs +4 -4
  26. package/build/migration-audit.mjs +19 -31
  27. package/build/migration-imports.mjs +245 -0
  28. package/build/migration-scenarios-runner.mjs +73 -0
  29. package/build/migration-scenarios.mjs +72 -0
  30. package/build/native-discovery.mjs +65 -5
  31. package/build/native-packages.mjs +2 -0
  32. package/build/native-ppr.mjs +32 -0
  33. package/build/npm-packages.mjs +1 -0
  34. package/build/og.mjs +121 -1
  35. package/build/parallel-plan.mjs +55 -0
  36. package/build/persistent-cache.mjs +29 -24
  37. package/build/production-builder.mjs +1 -1
  38. package/build/production-check.mjs +34 -0
  39. package/build/react-compiler-report.mjs +31 -8
  40. package/build/react-compiler.mjs +101 -10
  41. package/build/server-config.mjs +35 -0
  42. package/build/source-analysis.mjs +54 -4
  43. package/build/standalone.mjs +21 -3
  44. package/build/static-adapter.mjs +7 -3
  45. package/build/transform.mjs +38 -6
  46. package/build/typescript-dev.mjs +3 -1
  47. package/build/webpack-graph.mjs +13 -3
  48. package/cli.mjs +24 -6
  49. package/compat/app-dynamic.cjs +12 -1
  50. package/compat/app-layout-context.cjs +7 -3
  51. package/compat/cache-handlers.cjs +3 -1
  52. package/compat/flight-delta.cjs +50 -13
  53. package/compat/index.d.ts +24 -1
  54. package/compat/link.cjs +2 -2
  55. package/compat/navigation-authorization.cjs +8 -1
  56. package/docs/ai/compatibility.md +187 -7
  57. package/docs/ai/deployment.md +141 -0
  58. package/docs/ai/quickstart.md +5 -0
  59. package/docs/ai/routing.md +11 -0
  60. package/docs/ai/troubleshooting.md +79 -0
  61. package/package.json +26 -11
  62. package/release/fontkit.mjs +31 -0
  63. package/release/verify.mjs +3 -0
  64. package/runtime/api.mjs +5 -2
  65. package/runtime/app-client.mjs +254 -53
  66. package/runtime/app-partial.mjs +2 -10
  67. package/runtime/app-prefetch.mjs +2 -2
  68. package/runtime/app-render.mjs +11 -7
  69. package/runtime/app-stream-channel.mjs +3 -2
  70. package/runtime/app-styles.mjs +22 -4
  71. package/runtime/atomic-file.mjs +27 -0
  72. package/runtime/cache-components-rsc.mjs +3 -1
  73. package/runtime/compile-cache-retention.mjs +53 -0
  74. package/runtime/compile-cache.mjs +28 -1
  75. package/runtime/dev-client.mjs +13 -5
  76. package/runtime/dev-file-changes.mjs +123 -8
  77. package/runtime/dev-gateway.mjs +3 -2
  78. package/runtime/dev-lazy-imports.mjs +121 -0
  79. package/runtime/dev-lazy-staging.mjs +14 -0
  80. package/runtime/dev-route-selection.mjs +64 -0
  81. package/runtime/dev-runtime-pool.mjs +3 -2
  82. package/runtime/dev-runtime-spare.mjs +1 -1
  83. package/runtime/dev-session-retention.mjs +154 -0
  84. package/runtime/dev-style-retention.mjs +15 -0
  85. package/runtime/development.mjs +86 -26
  86. package/runtime/env.mjs +9 -0
  87. package/runtime/flight-delta.mjs +59 -9
  88. package/runtime/fs.mjs +15 -8
  89. package/runtime/lazy-import-client.mjs +38 -0
  90. package/runtime/native-ppr.mjs +35 -0
  91. package/runtime/og/NOTICE.txt +1 -1
  92. package/runtime/og/index.node.mjs +76 -31216
  93. package/runtime/og/upstream.json +115 -0
  94. package/runtime/persistent/client.mjs +4 -2
  95. package/runtime/persistent/daemon.mjs +3 -11
  96. package/runtime/persistent/health.mjs +58 -0
  97. package/runtime/project-lock.mjs +71 -0
  98. package/runtime/request-transport.mjs +112 -32
  99. package/runtime/storage-path.mjs +36 -0
  100. package/runtime/trace.mjs +44 -4
  101. package/runtime/worker.mjs +9 -4
  102. package/vendor/fontkit/NOTICE.txt +5 -0
  103. package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
  104. package/vendor/fontkit/index.mjs +3 -0
  105. package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
  106. package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
  107. package/vendor/fontkit/licenses/brotli/readme.md +61 -0
  108. package/vendor/fontkit/licenses/clone/LICENSE +18 -0
  109. package/vendor/fontkit/licenses/dfa/README.md +83 -0
  110. package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
  111. package/vendor/fontkit/licenses/fontkit/README.md +266 -0
  112. package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
  113. package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
  114. package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
  115. package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
  116. package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
  117. package/vendor/fontkit/upstream.json +215 -0
  118. package/vendor/nft/LICENSE +7 -0
  119. package/vendor/nft/README.md +16 -0
  120. package/vendor/nft/out/analyze.d.ts +8 -0
  121. package/vendor/nft/out/analyze.js +1216 -0
  122. package/vendor/nft/out/analyze.js.map +1 -0
  123. package/vendor/nft/out/cli.d.ts +2 -0
  124. package/vendor/nft/out/cli.js +94 -0
  125. package/vendor/nft/out/cli.js.map +1 -0
  126. package/vendor/nft/out/fs.d.ts +22 -0
  127. package/vendor/nft/out/fs.js +106 -0
  128. package/vendor/nft/out/fs.js.map +1 -0
  129. package/vendor/nft/out/index.d.ts +4 -0
  130. package/vendor/nft/out/index.js +25 -0
  131. package/vendor/nft/out/index.js.map +1 -0
  132. package/vendor/nft/out/node-file-trace.d.ts +41 -0
  133. package/vendor/nft/out/node-file-trace.js +380 -0
  134. package/vendor/nft/out/node-file-trace.js.map +1 -0
  135. package/vendor/nft/out/resolve-dependency.d.ts +6 -0
  136. package/vendor/nft/out/resolve-dependency.js +427 -0
  137. package/vendor/nft/out/resolve-dependency.js.map +1 -0
  138. package/vendor/nft/out/types.d.ts +66 -0
  139. package/vendor/nft/out/types.js +2 -0
  140. package/vendor/nft/out/types.js.map +1 -0
  141. package/vendor/nft/out/utils/ast-helpers.js +49 -0
  142. package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
  143. package/vendor/nft/out/utils/binary-locators.js +88 -0
  144. package/vendor/nft/out/utils/binary-locators.js.map +1 -0
  145. package/vendor/nft/out/utils/get-package-base.js +30 -0
  146. package/vendor/nft/out/utils/get-package-base.js.map +1 -0
  147. package/vendor/nft/out/utils/interop-require.js +24 -0
  148. package/vendor/nft/out/utils/interop-require.js.map +1 -0
  149. package/vendor/nft/out/utils/node-version.js +10 -0
  150. package/vendor/nft/out/utils/node-version.js.map +1 -0
  151. package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
  152. package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
  153. package/vendor/nft/out/utils/special-cases.js +347 -0
  154. package/vendor/nft/out/utils/special-cases.js.map +1 -0
  155. package/vendor/nft/out/utils/static-eval.js +579 -0
  156. package/vendor/nft/out/utils/static-eval.js.map +1 -0
  157. package/vendor/nft/out/utils/types.js +2 -0
  158. package/vendor/nft/out/utils/types.js.map +1 -0
  159. package/vendor/nft/out/utils/wrappers.js +659 -0
  160. package/vendor/nft/out/utils/wrappers.js.map +1 -0
  161. package/vendor/nft/package.json +4 -0
  162. package/vendor/nft/upstream.json +51 -0
@@ -0,0 +1,115 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "@vercel/og",
4
+ "inputs": {
5
+ "sourceSha256": "3269eb15ca135a0eb7e07ddf8af0b35711d6e75c56b84c6a6727c5626d59409c",
6
+ "packages": {
7
+ "@vercel/og": {
8
+ "version": "1.0.3",
9
+ "integrity": "sha512-/iYJ34PDu4lty1TXdL86qHEw8viveHTllG1PWLuJX8tSFfqKL7kyVJeUORJmQ250bHB0h+alcalXUlza3c/NSA=="
10
+ },
11
+ "fflate": {
12
+ "version": "0.7.5",
13
+ "integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g=="
14
+ }
15
+ },
16
+ "components": [
17
+ {
18
+ "name": "@vercel/og",
19
+ "version": "1.0.3"
20
+ },
21
+ {
22
+ "name": "@resvg/resvg-wasm",
23
+ "version": "2.4.1"
24
+ },
25
+ {
26
+ "name": "@shuding/opentype.js",
27
+ "version": "1.4.0-beta.0"
28
+ },
29
+ {
30
+ "name": "base64-js",
31
+ "version": "0.0.8"
32
+ },
33
+ {
34
+ "name": "camelize",
35
+ "version": "1.0.1"
36
+ },
37
+ {
38
+ "name": "color-name",
39
+ "version": "1.1.4"
40
+ },
41
+ {
42
+ "name": "css-background-parser",
43
+ "version": "0.1.0"
44
+ },
45
+ {
46
+ "name": "css-box-shadow",
47
+ "version": "1.0.0-3"
48
+ },
49
+ {
50
+ "name": "css-color-keywords",
51
+ "version": "1.0.0"
52
+ },
53
+ {
54
+ "name": "css-gradient-parser",
55
+ "version": "0.0.17"
56
+ },
57
+ {
58
+ "name": "css-to-react-native",
59
+ "version": "3.2.0"
60
+ },
61
+ {
62
+ "name": "escape-html",
63
+ "version": "1.0.3"
64
+ },
65
+ {
66
+ "name": "fflate",
67
+ "version": "0.7.5"
68
+ },
69
+ {
70
+ "name": "harfbuzzjs",
71
+ "version": "0.10.0"
72
+ },
73
+ {
74
+ "name": "hex-rgb",
75
+ "version": "4.3.0"
76
+ },
77
+ {
78
+ "name": "linebreak",
79
+ "version": "1.1.0"
80
+ },
81
+ {
82
+ "name": "parse-css-color",
83
+ "version": "0.2.1"
84
+ },
85
+ {
86
+ "name": "postcss-value-parser",
87
+ "version": "4.2.0"
88
+ },
89
+ {
90
+ "name": "satori",
91
+ "version": "0.33.5"
92
+ },
93
+ {
94
+ "name": "tiny-inflate",
95
+ "version": "1.0.3"
96
+ },
97
+ {
98
+ "name": "unicode-trie",
99
+ "version": "2.0.0"
100
+ }
101
+ ]
102
+ },
103
+ "files": {
104
+ "Geist-Regular.ttf": "bde046ddd9f20be35b0bd56cc79eb752b967fb6661a3fe76cb067bb09f871d76",
105
+ "hb.wasm": "d2ab975172d6103e7624e8f7f03fea2316eca6fe562e1b80fc7f7058b283a6d0",
106
+ "index.node.mjs": "4fae6c6a5e3b6514b6a303bb2f9f5c945114aed0549724191a8ef127238e075b",
107
+ "LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
108
+ "LICENSE.fflate": "62249119bb7df6e54300f37a37145c26a6d4420410aac8881a119c5b427cf751",
109
+ "LICENSE.Geist": "c683bfbcc7e087f5d37a54ef628f10387c451a83ddc459b151403a164ac46c90",
110
+ "LICENSE.harfbuzzjs": "c85596583d1b7e4cfb4f21165ce0988aabd72e80a8c3a2ab6aaeedc1965e8288",
111
+ "LICENSE.satori": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
112
+ "NOTICE.txt": "bcc9a8f06053c81502b72a618ca004794027ec85ae94d4bbe81890f7dcdb8ec8",
113
+ "resvg.wasm": "9c1f8f54b842fcda3b72b06eba94a20b907958c443d73178da32b21ccb57b9bb"
114
+ }
115
+ }
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import path from 'node:path';
5
5
  import { initializeHome, rpc, delay, environment, validName, validHealthPath } from './common.mjs';
6
6
  import { resolveNativeBinary } from '../../native/resolve.mjs';
7
+ import {nativeHealthPath} from './health.mjs';
7
8
 
8
9
  export const persistentHelp = `Persistent apps (ordinary prn start stays in the foreground):
9
10
  prn pstart [directory] [options] Start a built app in the background
@@ -18,7 +19,8 @@ export const persistentHelp = `Persistent apps (ordinary prn start stays in the
18
19
 
19
20
  pstart options: --name NAME --port 3000 --hostname 127.0.0.1 --workers 1
20
21
  --profile balanced|speed|memory|classic --inspect
21
- --health-path / --health-timeout 15000 --drain-timeout 30000 (milliseconds)
22
+ --health-path /api/health (optional application probe after the native readiness check)
23
+ --health-timeout 15000 --drain-timeout 30000 (milliseconds)
22
24
  --rollback-window 15000 (post-switch monitoring; 0 disables, maximum 120000 ms)
23
25
  prestart/pstop/plogs default to the app in the current directory.
24
26
  plogs options: --lines 100 --follow. App environment comes from its .env files.
@@ -157,7 +159,7 @@ export async function persistentCommand(command, argv) {
157
159
  if (command === 'pstart') {
158
160
  const root = await realpath(path.resolve(target || '.'));
159
161
  const name = validName(options.name || path.basename(root).replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^[^a-zA-Z0-9]+/, '').slice(0, 64));
160
- request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || '/', healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000, rollbackWindow: options['rollback-window'] ?? 15000 } };
162
+ request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || nativeHealthPath, healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000, rollbackWindow: options['rollback-window'] ?? 15000 } };
161
163
  } else request = { command: ({ prestart: 'restart', pstop: 'stop', pdelete: 'delete' })[command], name: target, all: !!options.all, root: target || options.all ? undefined : await realpath(process.cwd()) };
162
164
  await ensure(connection);
163
165
  const report = await rpc(connection, request);
@@ -1,5 +1,4 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { request } from 'node:http';
3
2
  import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
4
3
  import { randomUUID, createHash } from 'node:crypto';
5
4
  import path from 'node:path';
@@ -7,6 +6,8 @@ import { readBuildDirectory } from '../build-directory.mjs';
7
6
  import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
8
7
  import { RotatingLog } from './logger.mjs';
9
8
  import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
9
+ import {health} from './health.mjs';
10
+ import {productionEnvironment} from '../env.mjs';
10
11
 
11
12
  const home = process.argv[2];
12
13
  const registry = path.join(home, 'apps.json');
@@ -78,15 +79,6 @@ async function snapshot(app) {
78
79
  return { relative, buildId: manifest.buildId || null };
79
80
  } catch (error) { await removeSnapshot(app, relative); throw error; }
80
81
  }
81
- function health(generation, config, timeout) {
82
- return new Promise((resolve, reject) => {
83
- const req = request({ host: '127.0.0.1', port: generation.port, path: config.healthPath, method: 'GET', headers: { host: `${config.hostname.includes(':') ? '[' + config.hostname + ']' : config.hostname}:${config.port}` }, signal: AbortSignal.timeout(timeout) }, res => {
84
- const ok = res.statusCode >= 200 && res.statusCode < 400;
85
- res.destroy(); ok ? resolve() : reject(new Error(`Health check ${config.healthPath} returned HTTP ${res.statusCode}`));
86
- });
87
- req.once('error', reject); req.end();
88
- });
89
- }
90
82
  async function stopGeneration(app, generation, drain = true) {
91
83
  if (generation.stopping) return generation.stopping;
92
84
  generation.expected = true;
@@ -123,7 +115,7 @@ async function launch(app) {
123
115
  const build = await snapshot(app);
124
116
  const child = spawn(config.binary, ['start', config.root, '--build-dir', build.relative, '--hostname', '127.0.0.1', '--port', '0', '--node', config.node, '--workers', String(config.workers), '--profile', config.profile, '--shutdown-on-stdin-eof'], {
125
117
  cwd: config.root, windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],
126
- env: { ...environment(), NODE_ENV: 'production', ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
118
+ env: { ...productionEnvironment(config.root,{env:{...environment(),NODE_ENV:'production'}}), ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
127
119
  });
128
120
  child.stdin.on('error', () => {});
129
121
  const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
@@ -0,0 +1,58 @@
1
+ import {request} from 'node:http';
2
+
3
+ export const nativeHealthPath = '/_prnext/health';
4
+ const nativeLimit = 4096, applicationLimit = 65536;
5
+
6
+ // Connect directly to the private generation. A redirect, partial response or
7
+ // open-ended body cannot certify that a process is ready to receive traffic.
8
+ function probe(generation, config, healthPath, timeout, native) {
9
+ return new Promise((resolve, reject) => {
10
+ let finished = false, response;
11
+ const finish = error => {
12
+ if (finished) return;
13
+ finished = true;
14
+ if (error) { response?.destroy(); req.destroy(); reject(error); }
15
+ else resolve();
16
+ };
17
+ const req = request({host:'127.0.0.1', port:generation.port, path:healthPath,
18
+ method:'GET', headers:{host:`${config.hostname.includes(':')?'['+config.hostname+']':config.hostname}:${config.port}`, accept:'application/json', 'accept-encoding':'identity', connection:'close'},
19
+ signal:AbortSignal.timeout(Math.max(1, timeout))}, res => {
20
+ response = res;
21
+ if (res.statusCode !== 200) return finish(new Error(`Health check ${healthPath} returned HTTP ${res.statusCode}`));
22
+ if (res.headers['content-encoding'] && res.headers['content-encoding'] !== 'identity') return finish(new Error(`Health check ${healthPath} returned an encoded body`));
23
+ const limit = native ? nativeLimit : applicationLimit;
24
+ const declared = res.headers['content-length'];
25
+ if (declared !== undefined && (!/^\d+$/.test(declared) || Number(declared) > limit)) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
26
+ const chunks = []; let size = 0;
27
+ res.on('data', chunk => {
28
+ size += chunk.length;
29
+ if (size > limit) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
30
+ if (native) chunks.push(chunk);
31
+ });
32
+ res.once('error', finish);
33
+ res.once('aborted', () => finish(new Error(`Health check ${healthPath} response was incomplete`)));
34
+ res.once('end', () => {
35
+ if (!res.complete) return finish(new Error(`Health check ${healthPath} response was incomplete`));
36
+ if (native) {
37
+ if (!/^application\/json(?:\s*;|$)/i.test(res.headers['content-type'] || '')) return finish(new Error(`Health check ${healthPath} must return application/json`));
38
+ let body;
39
+ try { body = JSON.parse(Buffer.concat(chunks, size).toString('utf8')); } catch { return finish(new Error(`Health check ${healthPath} returned invalid JSON`)); }
40
+ if (!body || Array.isArray(body) || body.schemaVersion !== 1 || body.status !== 'ready') return finish(new Error(`Health check ${healthPath} did not report ready`));
41
+ if (body.buildId !== undefined && generation.buildId && body.buildId !== generation.buildId) return finish(new Error(`Health check ${healthPath} returned a different build`));
42
+ }
43
+ finish();
44
+ });
45
+ });
46
+ req.once('error', finish); req.end();
47
+ });
48
+ }
49
+
50
+ export async function health(generation, config, timeout) {
51
+ const deadline = Date.now() + timeout;
52
+ await probe(generation, config, nativeHealthPath, timeout, true);
53
+ if (config.healthPath !== nativeHealthPath) {
54
+ const remaining = deadline - Date.now();
55
+ if (remaining <= 0) throw new Error('Application health check exceeded its deadline');
56
+ await probe(generation, config, config.healthPath, remaining, false);
57
+ }
58
+ }
@@ -0,0 +1,71 @@
1
+ import { mkdir, writeFile, rename, rm, realpath, lstat, open } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { hostname } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const LOCK = '.prnext-build.lock';
7
+ const busy = message => Object.assign(new Error(message), { code: 'PRNEXT_BUILD_LOCKED' });
8
+ function alive(pid) {
9
+ try { process.kill(pid, 0); return true; }
10
+ catch (error) { return error.code !== 'ESRCH'; }
11
+ }
12
+ async function owner(directory) {
13
+ const info = await lstat(directory);
14
+ if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
15
+ const file = await open(path.join(directory, 'owner.json'), 'r');
16
+ try {
17
+ if ((await file.stat()).size > 4096) throw new Error('oversized owner');
18
+ const value = JSON.parse(await file.readFile('utf8'));
19
+ if (!Number.isSafeInteger(value.pid) || value.pid < 1 || typeof value.host !== 'string' || !/^[a-f0-9-]{36}$/.test(value.token)) throw new Error('invalid owner');
20
+ return value;
21
+ } finally { await file.close(); }
22
+ }
23
+
24
+ /** One publisher per canonical project, covering React and direct static imports. */
25
+ export async function withBuildLock(directory, action, {development = false} = {}) {
26
+ const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
27
+ const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
28
+ const prepared = path.join(root, `.prnext-lock-${identity.token}`);
29
+ await mkdir(prepared, { mode: 0o700 });
30
+ try {
31
+ await writeFile(path.join(prepared, 'owner.json'), JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
32
+ for (let attempt = 0; ; attempt++) {
33
+ try { await rename(prepared, lock); break; }
34
+ catch (error) {
35
+ if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
36
+ let previous;
37
+ try { previous = await owner(lock); }
38
+ catch (cause) {
39
+ if (cause.code === 'ENOENT' && attempt < 8) continue;
40
+ throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
41
+ }
42
+ if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
43
+ // Exactly one contender can recover this generation. Never delete a lock
44
+ // merely because it is old: long builds and suspended processes are valid.
45
+ let claim;
46
+ try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
47
+ catch (cause) {
48
+ if (cause.code === 'ENOENT' && attempt < 8) continue;
49
+ throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
50
+ }
51
+ const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
52
+ try {
53
+ await claim.writeFile(JSON.stringify(identity)); await claim.close(); claim = null;
54
+ const current = await owner(lock);
55
+ if (current.token !== previous.token) {
56
+ // Do not unlink through this path: the new owner can release and
57
+ // another generation can appear between our read and the unlink.
58
+ throw busy('The build lock changed during recovery. Retry the build.');
59
+ }
60
+ await rename(lock, retired);
61
+ await rm(retired, { recursive: true });
62
+ } finally { await claim?.close(); }
63
+ }
64
+ }
65
+ try { return await action(root); }
66
+ finally {
67
+ // Ownership must still match before releasing a lock.
68
+ if ((await owner(lock)).token === identity.token) await rm(lock, { recursive: true });
69
+ }
70
+ } finally { await rm(prepared, { recursive: true, force: true }); }
71
+ }
@@ -1,41 +1,121 @@
1
1
  const MAX_HEADER = 256 * 1024;
2
2
  const MAX_BODY = 32 * 1024 * 1024;
3
+ export const REQUEST_BODY_CHUNK = 64 * 1024;
3
4
  const EMPTY_BODY = Buffer.alloc(0);
5
+ export const requestBodyState = Symbol('PRNext request body state');
4
6
 
5
- // Awaiting the consumer preserves socket backpressure. At most one body is
6
- // assembled, and binary bytes never pass through JSON strings or base64.
7
- export async function* requestFrames(source, { authenticate } = {}) {
8
- let fragments = [], size = 0, input, body, offset = 0;
9
- for await (const chunk of source) {
10
- let cursor = 0;
11
- while (cursor < chunk.length) {
12
- if (body) {
13
- const length = Math.min(body.length - offset, chunk.length - cursor);
14
- chunk.copy(body, offset, cursor, cursor + length);
15
- cursor += length; offset += length;
16
- if (offset !== body.length) continue;
17
- input.body = body; body = undefined; offset = 0;
18
- yield input; input = undefined;
19
- continue;
7
+ // One owner reads a lane. Streaming bodies borrow this reader until their
8
+ // invocation finishes; body bytes are never parsed as request metadata.
9
+ function laneReader(source) {
10
+ const iterator = source[Symbol.asyncIterator]?.() || source[Symbol.iterator]();
11
+ let chunk = EMPTY_BODY, offset = 0, ended = false;
12
+ async function available() {
13
+ while (offset === chunk.length && !ended) {
14
+ const next = await iterator.next(); ended = !!next.done;
15
+ chunk = ended ? EMPTY_BODY : next.value; offset = 0;
16
+ if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
17
+ }
18
+ return !ended;
19
+ }
20
+ return {
21
+ async line(limit) {
22
+ const parts = []; let size = 0;
23
+ while (await available()) {
24
+ const end = chunk.indexOf(10, offset), stop = end < 0 ? chunk.length : end;
25
+ const part = chunk.subarray(offset, stop); size += part.length;
26
+ if (size > limit) throw new Error('Request transport header exceeds limit');
27
+ parts.push(part); offset = end < 0 ? stop : end + 1;
28
+ if (end >= 0) return (parts.length === 1 ? parts[0] : Buffer.concat(parts, size)).toString();
29
+ }
30
+ if (size) throw new Error('Truncated request transport header');
31
+ return undefined;
32
+ },
33
+ async exact(length) {
34
+ if (length === 0) return EMPTY_BODY;
35
+ const result = Buffer.allocUnsafe(length); let written = 0;
36
+ while (written < length) {
37
+ if (!await available()) throw new Error('Truncated request transport body');
38
+ const count = Math.min(length - written, chunk.length - offset);
39
+ chunk.copy(result, written, offset, offset + count); written += count; offset += count;
20
40
  }
21
- const end = chunk.indexOf(10, cursor);
22
- const part = chunk.subarray(cursor, end < 0 ? chunk.length : end);
23
- size += part.length;
24
- if (size > (authenticate ? 256 : MAX_HEADER)) throw new Error('Request transport header exceeds limit');
25
- fragments.push(part);
26
- cursor = end < 0 ? chunk.length : end + 1;
27
- if (end < 0) continue;
28
- const line = fragments.length === 1 ? part.toString() : Buffer.concat(fragments, size).toString();
29
- fragments = []; size = 0;
30
- if (authenticate) { authenticate(line); authenticate = undefined; continue; }
31
- if (!line.trim()) continue;
32
- input = JSON.parse(line);
33
- if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid request frame');
34
- if (input.bodyLength === undefined) { yield input; input = undefined; continue; }
41
+ return result;
42
+ },
43
+ destroy() { source.destroy?.(); },
44
+ };
45
+ }
46
+
47
+ function streamingBody(reader, input, credit) {
48
+ const descriptor = input.bodyStream;
49
+ if (!descriptor || descriptor.version !== 1 || Object.keys(descriptor).some(key => !['version', 'length', 'maxBytes'].includes(key)) ||
50
+ descriptor.maxBytes !== MAX_BODY || !(descriptor.length === null || Number.isSafeInteger(descriptor.length) && descriptor.length >= 0 && descriptor.length <= MAX_BODY) ||
51
+ input.bodyLength !== undefined || input.body !== undefined || !Number.isSafeInteger(input.id) || input.id < 1 || ['GET', 'HEAD'].includes(input.method)) {
52
+ throw new Error('Invalid streaming request descriptor');
53
+ }
54
+ let controller, complete = false, total = 0, cancelled = false;
55
+ const state = { get complete() { return complete; }, get bytes() { return total; }, cancel() {
56
+ if (complete || cancelled) return;
57
+ cancelled = true;
58
+ try { controller?.error(new Error('Request body transport cancelled')); } catch {}
59
+ reader.destroy();
60
+ } };
61
+ const body = new ReadableStream({
62
+ start(value) { controller = value; },
63
+ async pull(value) {
64
+ try {
65
+ // A single explicit credit authorizes one frame. No speculative read
66
+ // consumes an upload while the application is not reading.
67
+ await credit({ id: input.id, type: 'bodycredit', length: REQUEST_BODY_CHUNK });
68
+ const header = await reader.exact(4), length = header.readUInt32BE();
69
+ if (length === 0xffffffff) {
70
+ const code = (await reader.exact(1))[0];
71
+ const kinds = { 1: ['PRNEXT_BODY_LIMIT', 413], 2: ['PRNEXT_BODY_TIMEOUT', 408], 3: ['PRNEXT_BODY_READ', 400], 4: ['PRNEXT_BODY_LENGTH', 400] };
72
+ if (!kinds[code]) throw new Error('Invalid request body error frame');
73
+ throw Object.assign(new Error('Request body transport failed'), { code: kinds[code][0], statusCode: kinds[code][1] });
74
+ }
75
+ if (length === 0) {
76
+ if (descriptor.length !== null && total !== descriptor.length) throw new Error('Request body length mismatch');
77
+ complete = true; value.close(); return;
78
+ }
79
+ if (length > REQUEST_BODY_CHUNK || total + length > MAX_BODY || descriptor.length !== null && total + length > descriptor.length) throw new Error('Request body exceeds its transport budget');
80
+ const bytes = await reader.exact(length); total += bytes.length; value.enqueue(bytes);
81
+ } catch (error) {
82
+ if (cancelled) return;
83
+ value.error(error);
84
+ if (!/^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error.code || '')) reader.destroy();
85
+ }
86
+ },
87
+ async cancel() { cancelled = true; await credit({ id: input.id, type: 'bodycancel' }); },
88
+ }, { highWaterMark: 0 });
89
+ Object.defineProperty(input, requestBodyState, { value: state });
90
+ return body;
91
+ }
92
+
93
+ export async function* requestFrames(source, { authenticate, streamBodies = false, credit } = {}) {
94
+ const reader = laneReader(source);
95
+ if (authenticate) {
96
+ const token = await reader.line(256);
97
+ if (token === undefined) throw new Error('Truncated request authentication');
98
+ authenticate(token);
99
+ }
100
+ for (;;) {
101
+ const line = await reader.line(MAX_HEADER);
102
+ if (line === undefined) return;
103
+ if (!line.trim()) continue;
104
+ const input = JSON.parse(line);
105
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid request frame');
106
+ if (input.bodyStream !== undefined) {
107
+ if (!authenticate || !streamBodies || typeof credit !== 'function') throw new Error('Streaming request protocol was not negotiated');
108
+ input.body = streamingBody(reader, input, credit);
109
+ try { yield input; }
110
+ finally { input[requestBodyState].cancel(); }
111
+ // Streaming upload lanes are single-use: an early response cannot turn
112
+ // unread upload bytes into another request. The Node host stays warm.
113
+ return;
114
+ }
115
+ if (input.bodyLength !== undefined) {
35
116
  if (!Number.isSafeInteger(input.bodyLength) || input.bodyLength < 0 || input.bodyLength > MAX_BODY) throw new Error('Invalid request body length');
36
- if (input.bodyLength === 0) { input.body = EMPTY_BODY; yield input; input = undefined; }
37
- else body = Buffer.allocUnsafe(input.bodyLength);
117
+ input.body = await reader.exact(input.bodyLength);
38
118
  }
119
+ yield input;
39
120
  }
40
- if (size || body || authenticate) throw new Error('Truncated request transport frame');
41
121
  }
@@ -0,0 +1,36 @@
1
+ import {lstat, realpath} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const normalized = filename => process.platform === 'win32' ? path.resolve(filename).toLowerCase() : path.resolve(filename);
5
+ export const sameStoragePath = (left, right) => normalized(left) === normalized(right);
6
+ const ordinary = info => info?.isDirectory() && !info.isSymbolicLink();
7
+ const identity = (left, right) => left.dev === right.dev && left.ino === right.ino && left.birthtimeMs === right.birthtimeMs;
8
+
9
+ /** Callers supply canonical roots and explicit descendants, never glob results. */
10
+ export async function captureStorageDirectory(directory) {
11
+ try {
12
+ directory = path.resolve(directory);
13
+ const info = await lstat(directory);
14
+ if (!ordinary(info) || !sameStoragePath(await realpath(directory), directory)) return null;
15
+ const current = await lstat(directory);
16
+ if (!ordinary(current) || !identity(info, current)) return null;
17
+ return {directory, dev: info.dev, ino: info.ino, birthtimeMs: info.birthtimeMs};
18
+ } catch { return null; }
19
+ }
20
+
21
+ /** Recheck ancestors around each filesystem operation. Node does not expose an
22
+ * atomic openat/unlinkat boundary: these guards reject observed replacements,
23
+ * but are not isolation from a malicious process with the same filesystem rights. */
24
+ export async function verifyStorageDirectories(snapshots) {
25
+ if (!Array.isArray(snapshots) || !snapshots.length) return false;
26
+ for (const snapshot of snapshots) {
27
+ if (!snapshot) return false;
28
+ try {
29
+ const info = await lstat(snapshot.directory);
30
+ if (!ordinary(info) || !identity(info, snapshot) || !sameStoragePath(await realpath(snapshot.directory), snapshot.directory)) return false;
31
+ const current = await lstat(snapshot.directory);
32
+ if (!ordinary(current) || !identity(current, snapshot)) return false;
33
+ } catch { return false; }
34
+ }
35
+ return true;
36
+ }
package/runtime/trace.mjs CHANGED
@@ -2,6 +2,7 @@ import {isControlError} from './navigation.mjs';
2
2
  import {AsyncLocalStorage} from 'node:async_hooks';
3
3
  import {randomBytes} from 'node:crypto';
4
4
  import {createRequire} from 'node:module';
5
+ import {channel} from 'node:diagnostics_channel';
5
6
  const scope=new AsyncLocalStorage(),rawFetch=globalThis.fetch;
6
7
  let api;
7
8
  try{api=createRequire(import.meta.url)('@opentelemetry/api');}catch{}
@@ -9,6 +10,17 @@ const endpoint=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||(process.env.OTEL
9
10
  const protocol=process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL||process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
10
11
  const enabled=process.env.OTEL_SDK_DISABLED!=='true'&&endpoint&&(!protocol||protocol==='http/json');
11
12
  const queue=[];let timer,flushing;
13
+ const diagnostics=channel('prnext.telemetry');
14
+ const counters={droppedSpans:0,rejectedSpans:0,exportAttempts:0,exportFailures:0,failedBatches:0,exportedSpans:0};
15
+ let lastWarning=-Infinity;
16
+ export function telemetryDiagnostics(){return {enabled:!!enabled,queuedSpans:queue.length,...counters};}
17
+ function notify(reason){
18
+ const snapshot=telemetryDiagnostics();
19
+ // This local channel and fixed-field warning never pass through the exporter.
20
+ // No collector URL, headers, exception or request attributes are included.
21
+ if(diagnostics.hasSubscribers)diagnostics.publish({reason,...snapshot});
22
+ if(reason!=='exported'&&Date.now()-lastWarning>=10000){lastWarning=Date.now();try{console.error('[prnext] telemetry '+JSON.stringify({reason,...snapshot}));}catch{}}
23
+ }
12
24
  const epoch=BigInt(Date.now())*1000000n-process.hrtime.bigint();
13
25
  const nanos=()=>String(epoch+process.hrtime.bigint());
14
26
  export function parseTraceparent(value){
@@ -23,9 +35,25 @@ export function propagatedFetch(input,init){
23
35
  return [input,{...init,headers}];
24
36
  }
25
37
  function enqueue(span){
26
- if(!enabled||!span.flags||queue.length>=1024)return;
38
+ if(!enabled||!span.flags)return;
39
+ if(queue.length>=1024){counters.droppedSpans++;notify('queue_full');return;}
27
40
  queue.push(span);if(!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();
28
41
  }
42
+ async function collectorResult(response,count){
43
+ const reader=response.body?.getReader();if(!reader)return {rejected:0,warning:false};
44
+ const chunks=[];let size=0;
45
+ try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>16384)throw Error('collector response too large');chunks.push(value);}}
46
+ catch(error){await reader.cancel().catch(()=>{});throw error;}finally{reader.releaseLock();}
47
+ const body=size?JSON.parse(Buffer.concat(chunks,size).toString('utf8')):{};
48
+ if(!body||typeof body!=='object'||Array.isArray(body))throw Error('invalid collector response');
49
+ const partial=body.partialSuccess??body.partial_success;if(partial==null)return {rejected:0,warning:false};
50
+ if(typeof partial!=='object'||Array.isArray(partial))throw Error('invalid collector partial success');
51
+ const raw=partial.rejectedSpans??partial.rejected_spans??0;
52
+ if(typeof raw!=='number'&&!(typeof raw==='string'&&/^\d{1,20}$/.test(raw)))throw Error('invalid collector rejection count');
53
+ const rejected=Number(raw);
54
+ if(!Number.isSafeInteger(rejected)||rejected<0||rejected>count)throw Error('invalid collector rejection count');
55
+ return {rejected,warning:typeof(partial.errorMessage??partial.error_message)==='string'&&!!(partial.errorMessage??partial.error_message)};
56
+ }
29
57
  export async function flushTraces(){
30
58
  if(flushing)return flushing;
31
59
  if(!queue.length)return;
@@ -36,15 +64,27 @@ export async function flushTraces(){
36
64
  const spans=queue.splice(0,128);
37
65
  flushing=(async()=>{
38
66
  const body=JSON.stringify({resourceSpans:[{resource:{attributes:[{key:'service.name',value:{stringValue:process.env.OTEL_SERVICE_NAME||'prnext'}}]},scopeSpans:[{scope:{name:'prnext.runtime'},spans}]}]});
67
+ let exported=false;
39
68
  for(let attempt=0;attempt<3;attempt++){
40
- let retry=false;
69
+ let retry=false,receivedSuccess=false,accepted;
70
+ counters.exportAttempts++;
41
71
  try{
42
72
  const response=await rawFetch(endpoint,{method:'POST',redirect:'error',headers,body,signal:AbortSignal.timeout(1000)});
43
- retry=[429,502,503,504].includes(response.status);await response.body?.cancel();
44
- }catch{retry=true;} // Telemetry failure never changes an application response.
73
+ receivedSuccess=response.ok;
74
+ if(receivedSuccess)accepted=await collectorResult(response,spans.length);
75
+ else{retry=[429,502,503,504].includes(response.status);await response.body?.cancel();}
76
+ }catch{retry=!receivedSuccess;} // Never replay a successful/partial acknowledgement.
77
+ if(accepted){
78
+ exported=true;counters.exportedSpans+=spans.length-accepted.rejected;
79
+ if(accepted.rejected){counters.rejectedSpans+=accepted.rejected;counters.droppedSpans+=accepted.rejected;counters.exportFailures++;counters.failedBatches++;notify('collector_rejected');}
80
+ else notify(accepted.warning?'collector_warning':'exported');
81
+ break;
82
+ }
83
+ counters.exportFailures++;
45
84
  if(!retry)break;
46
85
  if(attempt<2)await new Promise(resolve=>setTimeout(resolve,100*2**attempt).unref());
47
86
  }
87
+ if(!exported){counters.failedBatches++;counters.droppedSpans+=spans.length;notify('export_failed');}
48
88
  })().finally(()=>{flushing=undefined;if(queue.length&&!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();});
49
89
  return flushing;
50
90
  }
@@ -142,6 +142,9 @@ async function handleRequest(input, output, signalController, responseComplete,
142
142
  ? { ...manifest.middleware, id: '__prnext_middleware', kind: 'middleware' }
143
143
  : routes.get(input.routeId);
144
144
  if (!route) { await output(input.id, { status: 404, body: 'Not Found' }, input); return; }
145
+ if (input.bodyStream && (route.router !== 'app' || route.kind !== 'api' || input.renderMode || !manifest.config?.streamingRequestBodies)) {
146
+ throw new Error('Streaming request bodies require an enabled App Route Handler');
147
+ }
145
148
  const sourceModules=!production&&route.router==='app'&&route.kind==='page'?advancedRoutingSources({...input,basePath:manifest.config?.basePath||''},manifest.routes):[];
146
149
  const deferredSources=sourceModules.length?manifest.routes.filter(candidate=>candidate.devDeferred&&sourceModules.includes(candidate.module)).map(candidate=>candidate.pattern):[];
147
150
  if(route.devDeferred||deferredSources.length){
@@ -197,7 +200,9 @@ async function handleRequest(input, output, signalController, responseComplete,
197
200
  // The application's scope was aborted above, but the lane is still valid
198
201
  // for its one error/redirect response. Do not mistake that cleanup signal
199
202
  // for a client disconnect while writing the replacement headers.
200
- await output(input?.id ?? null, navigationResponse(error) || errorResponse(error, production), { ...input, production });
203
+ const ingressFailure = /^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error?.code || '')
204
+ ? { status: error.statusCode, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 'Request body transport failed' } : undefined;
205
+ await output(input?.id ?? null, ingressFailure || navigationResponse(error) || errorResponse(error, production), { ...input, production });
201
206
  responseComplete?.();
202
207
  } finally {
203
208
  if (finalizers.size) {
@@ -261,7 +266,7 @@ if (token && laneLimit > 1) {
261
266
  const candidate = Buffer.from(line);
262
267
  if (candidate.length !== expected.length || !timingSafeEqual(candidate, expected)) throw new Error('Worker authentication failed');
263
268
  clearTimeout(authTimer); socket.write('ready\n');
264
- } })) {
269
+ }, streamBodies: true, credit: frame => new Promise((resolve, reject) => socket.write(JSON.stringify(frame) + '\n', error => error ? reject(error) : resolve())) })) {
265
270
  if (invocations.size >= laneLimit) throw new Error('Worker invocation capacity exceeded');
266
271
  controller = new AbortController();
267
272
  const active = controller;
@@ -339,10 +344,10 @@ if (token && laneLimit > 1) {
339
344
  });
340
345
  server.listen(0, '127.0.0.1', () => {
341
346
  if(!production&&typeof globalThis.__PRNEXT_DEV_WORKER_READY__==='function'){
342
- globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true});
347
+ globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true,requestBodyStream:1});
343
348
  delete globalThis.__PRNEXT_DEV_WORKER_READY__;
344
349
  }
345
- writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true }) + '\n');
350
+ writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true, requestBodyStream: 1 }) + '\n');
346
351
  heartbeat = setInterval(() => {
347
352
  // Coalesce the idle transition with the existing heartbeat. Normal
348
353
  // request completion adds no stdout messages, and short cache bursts
@@ -0,0 +1,5 @@
1
+ Fontkit 2.0.4 Node create() bundle, generated by scripts/prepare-fontkit.mjs.
2
+ Upstream: https://github.com/foliojs/fontkit
3
+ The original code is bundled/minified without patches. Source copyright notices, dependency licenses, README license declarations and exact versions/integrities are retained alongside it.
4
+ fontkit, dfa and brotli declare MIT in their package metadata/README and publish no separate license file. brotli also contains Google Apache-2.0 source notices, retained in SOURCE-NOTICES.txt; the Apache-2.0 text is included with @swc/helpers.
5
+ Use upstream.json to audit/update this bundled dependency when security fixes are released.