@wular/pnext 0.0.2 → 0.0.4

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 (64) hide show
  1. package/README.md +76 -20
  2. package/package.json +3 -2
  3. package/reference/data/bench.json +513 -0
  4. package/reference/performance.md +75 -48
  5. package/src/api/router/runtime.ts +60 -21
  6. package/src/cache/context.ts +4 -1
  7. package/src/cli/build.ts +37 -5
  8. package/src/cli/dev.ts +6 -0
  9. package/src/cli/index.ts +17 -3
  10. package/src/cli/request-pipeline.ts +1340 -0
  11. package/src/cli/server-entry.ts +180 -0
  12. package/src/cli/start.ts +39 -1311
  13. package/src/client/build.ts +59 -20
  14. package/src/client/chunk-fold.ts +40 -0
  15. package/src/client/compat-surface.ts +175 -0
  16. package/src/client/entry.ts +67 -52
  17. package/src/compat/actions/action-client.ts +8 -1
  18. package/src/compat/actions/action-dispatch.ts +11 -1
  19. package/src/compat/actions/discovery.ts +23 -6
  20. package/src/compat/bundler/optimize-package-imports.ts +5 -1
  21. package/src/compat/bundler/worker.ts +2 -1
  22. package/src/compat/client/errors/bare-boundary.ts +32 -0
  23. package/src/compat/client/errors/error-boundary.ts +1 -15
  24. package/src/compat/client/errors/primitive-throw.ts +16 -0
  25. package/src/compat/client/link-status.ts +1 -1
  26. package/src/compat/css/lightningcss.ts +2 -1
  27. package/src/compat/css/modules.ts +4 -3
  28. package/src/compat/lifecycle/instrumentation-client.ts +1 -1
  29. package/src/compat/lifecycle/instrumentation.ts +5 -2
  30. package/src/compat/next/config-loader.ts +33 -5
  31. package/src/compat/next/dynamic.tsx +9 -5
  32. package/src/compat/next/link-validation-transform.ts +5 -1
  33. package/src/compat/next/link.tsx +51 -58
  34. package/src/compat/pages/client-plugin.ts +2 -1
  35. package/src/compat/react/action-state.ts +159 -0
  36. package/src/compat/react/client-lite.ts +74 -0
  37. package/src/compat/react/hooks-extra.ts +92 -0
  38. package/src/compat/react/parity.ts +128 -0
  39. package/src/compat/react/preact.ts +33 -420
  40. package/src/compat/react/server-inserted-html.ts +14 -7
  41. package/src/compat/react/use.ts +72 -0
  42. package/src/compat/register/actions.ts +27 -7
  43. package/src/compat/register/segment.ts +16 -6
  44. package/src/config.ts +15 -1
  45. package/src/css/build.ts +13 -2
  46. package/src/dev/imports.ts +34 -5
  47. package/src/dev/module-cache.ts +19 -0
  48. package/src/dev/module-transform.ts +7 -1
  49. package/src/dev/server.ts +91 -22
  50. package/src/dynamic/source.ts +36 -27
  51. package/src/ppr.ts +5 -4
  52. package/src/proxy.ts +5 -1
  53. package/src/render/island-context.ts +21 -3
  54. package/src/render/renderer.ts +102 -26
  55. package/src/resolve/engine.ts +12 -2
  56. package/src/resolve/scan-facts.ts +239 -1
  57. package/src/routing/href.ts +4 -5
  58. package/src/routing/routes.ts +26 -41
  59. package/src/runtime/server.ts +8 -4
  60. package/src/runtime/vendor.ts +1 -1
  61. package/src/typegen.ts +3 -3
  62. package/src/utils/esbuild.ts +58 -0
  63. package/src/utils/fs.ts +14 -2
  64. package/src/utils/native-require.ts +28 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Prebundled production server entry: `pnext build` bundles the framework's server graph into one JS
3
+ * file (entry point IS src/cli/start.ts, so the identical `start()` runs) and `pnext start` parses
4
+ * that instead of walking hundreds of source modules - the dominant share of spawn->first-200.
5
+ */
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import type { Plugin as EsbuildPlugin } from 'esbuild';
10
+
11
+ const frameworkRoot = path.resolve(import.meta.dirname, '..', '..');
12
+
13
+ /** Stamped into the filename so an upgraded framework never runs a stale bundle. */
14
+ function frameworkVersion(): string {
15
+ try {
16
+ const pkg = readFileSync(path.join(frameworkRoot, 'package.json'), 'utf8');
17
+ return (JSON.parse(pkg) as { version?: string }).version ?? '0';
18
+ } catch {
19
+ return '0';
20
+ }
21
+ }
22
+
23
+ function serverEntryDir(outPath: string): string {
24
+ return path.join(outPath, 'server', `bundle-${frameworkVersion()}`);
25
+ }
26
+
27
+ function serverEntryFile(outPath: string): string {
28
+ return path.join(serverEntryDir(outPath), 'entry.js');
29
+ }
30
+
31
+ /**
32
+ * The prebuilt entry for a project root, or undefined when there is none to
33
+ * use (no build, a custom `outDir`, a version mismatch, or the opt-out). The
34
+ * caller then falls back to importing src/cli/start.ts directly, so a miss only
35
+ * costs speed. Deliberately cheap: one existsSync and one package.json read,
36
+ * because it runs before anything else on the start path.
37
+ */
38
+ export function prebuiltServerEntry(root = process.cwd()): string | undefined {
39
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
40
+ if (process.env.PNEXT_NO_SERVER_BUNDLE === '1') return undefined;
41
+ const file = serverEntryFile(path.resolve(root, '.pnext'));
42
+ return existsSync(file) ? pathToFileURL(file).href : undefined;
43
+ }
44
+
45
+ /**
46
+ * Bundle src/cli/start.ts under `<outPath>/server/bundle-<version>/`. Bare
47
+ * specifiers stay external (the framework's deps are already plain JS), so only
48
+ * the framework's own source is inlined. Splitting is on so the graph behind a
49
+ * dynamic import (compat, dev, build) stays in its own chunk — folding it into
50
+ * the entry would evaluate eagerly what the source graph never even reads.
51
+ * Returns the emitted entry, or undefined when bundling failed — the build must
52
+ * not fail over a start-time optimization.
53
+ */
54
+ export async function emitServerEntry(outPath: string): Promise<string | undefined> {
55
+ const { build } = await import('../utils/esbuild');
56
+ try {
57
+ await build({
58
+ entryPoints: [path.join(import.meta.dirname, 'start.ts')],
59
+ outdir: serverEntryDir(outPath),
60
+ entryNames: 'entry',
61
+ chunkNames: '[name]-[hash]',
62
+ bundle: true,
63
+ splitting: true,
64
+ format: 'esm',
65
+ platform: 'node',
66
+ target: 'esnext',
67
+ sourcemap: false,
68
+ tsconfig: path.join(frameworkRoot, 'tsconfig.json'),
69
+ logLevel: 'silent',
70
+ // esbuild's ESM `require` shim throws for anything it did not bundle; the
71
+ // framework's lazy `require('esbuild' | 'oxc-*')` facades need a real one,
72
+ // anchored at the framework so they load ITS copy, not the app's.
73
+ banner: {
74
+ js:
75
+ 'import { createRequire as __pnextCreateRequire } from "node:module";\n' +
76
+ `const require = __pnextCreateRequire(${JSON.stringify(
77
+ pathToFileURL(path.join(frameworkRoot, 'package.json')).href,
78
+ )});`,
79
+ },
80
+ plugins: [await serverEntryPlugin(outPath)],
81
+ });
82
+ return serverEntryFile(outPath);
83
+ } catch (error) {
84
+ console.warn(`pnext build: server entry bundling skipped — ${(error as Error).message}`);
85
+ return undefined;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Emit in a short-lived child process: the bundle's source strings and parse
91
+ * allocations land in the child's heap (well under the build's own peak), not
92
+ * on top of it. Falls back to in-process emission if the spawn fails.
93
+ */
94
+ export async function emitServerEntryChild(outPath: string): Promise<void> {
95
+ try {
96
+ const child = Bun.spawn([process.execPath, import.meta.filename, outPath], {
97
+ stdout: 'ignore',
98
+ stderr: 'inherit',
99
+ });
100
+ if ((await child.exited) === 0) return;
101
+ } catch {
102
+ // fall through to in-process emission
103
+ }
104
+ await emitServerEntry(outPath);
105
+ }
106
+
107
+ async function serverEntryPlugin(outPath: string): Promise<EsbuildPlugin> {
108
+ const { rewriteFacts } = await import('../resolve/scan-facts');
109
+ const { spliceSource } = await import('../dev/module-transform');
110
+ return {
111
+ name: 'pnext-server-entry',
112
+ setup(build) {
113
+ // Bare specifiers (and node builtins) stay out of the bundle. They must
114
+ // still resolve to the SAME file the framework's own source graph would
115
+ // reach: keep the specifier bare when the output directory resolves it
116
+ // identically (portable), otherwise pin the absolute path.
117
+ build.onResolve({ filter: /^[^./]/ }, args => {
118
+ if (args.path.startsWith('node:')) return { path: args.path, external: true };
119
+ const target = resolveFrom(args.path, frameworkRoot);
120
+ if (!target) return { path: args.path, external: true };
121
+ const fromOutput = resolveFrom(args.path, serverEntryDir(outPath));
122
+ return { path: fromOutput === target ? args.path : target, external: true };
123
+ });
124
+ // `import.meta` in a bundled module still means the ORIGINAL source file
125
+ // (the framework locates its own runtime shims that way). oxc's exact
126
+ // spans keep occurrences in strings and comments untouched.
127
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async args => {
128
+ const source = await Bun.file(args.path).text();
129
+ if (!source.includes('import.meta')) return undefined;
130
+ const facts = rewriteFacts(args.path, source);
131
+ if (facts.unreliable) return undefined;
132
+ const edits = facts.importMetas.flatMap(meta => {
133
+ const value = importMetaValue(source.slice(meta.end, meta.end + 16), args.path);
134
+ return value
135
+ ? [{ start: meta.start, end: meta.end + value.length, value: value.code }]
136
+ : [];
137
+ });
138
+ if (edits.length === 0) return undefined;
139
+ return { contents: spliceSource(source, edits), loader: loaderFor(args.path) };
140
+ });
141
+ },
142
+ };
143
+ }
144
+
145
+ const importMetaProperties = ['dirname', 'filename', 'resolve', 'url', 'dir'] as const;
146
+
147
+ /** The literal a bundled `import.meta.<prop>` must become, given the tail after `import.meta`. */
148
+ function importMetaValue(tail: string, file: string): { code: string; length: number } | undefined {
149
+ const property = importMetaProperties.find(name => new RegExp(`^\\.${name}\\b`).test(tail));
150
+ if (!property) return undefined;
151
+ const length = property.length + 1;
152
+ const dir = path.dirname(file);
153
+ if (property === 'dirname' || property === 'dir') return { code: JSON.stringify(dir), length };
154
+ if (property === 'filename') return { code: JSON.stringify(file), length };
155
+ if (property === 'url') return { code: JSON.stringify(pathToFileURL(file).href), length };
156
+ return {
157
+ code: `(specifier => Bun.pathToFileURL(Bun.resolveSync(specifier, ${JSON.stringify(dir)})).href)`,
158
+ length,
159
+ };
160
+ }
161
+
162
+ function loaderFor(file: string): 'ts' | 'tsx' | 'js' | 'jsx' {
163
+ if (file.endsWith('.tsx')) return 'tsx';
164
+ if (file.endsWith('.jsx')) return 'jsx';
165
+ return /\.[cm]?ts$/.test(file) ? 'ts' : 'js';
166
+ }
167
+
168
+ function resolveFrom(specifier: string, dir: string): string | undefined {
169
+ try {
170
+ return Bun.resolveSync(specifier, dir);
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ // Child-process entry for emitServerEntryChild; last so every binding above is initialized.
177
+ if (import.meta.main && process.argv[2]) {
178
+ const emitted = await emitServerEntry(process.argv[2]);
179
+ process.exit(emitted ? 0 : 1);
180
+ }