@thomas.f/prnext 0.1.16-alpha → 0.1.18-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.
- package/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Troubleshooting and live verification
|
|
2
|
+
|
|
3
|
+
[AI documentation index](../../AI.md)
|
|
4
|
+
|
|
5
|
+
## Collect a useful failure
|
|
6
|
+
|
|
7
|
+
Read the installed version, command, working directory, Node version, manager,
|
|
8
|
+
configuration and first complete diagnostic. Separate build errors, semantic
|
|
9
|
+
TypeScript errors, server exceptions, HTTP failures and browser hydration
|
|
10
|
+
errors; they have different causes. Keep secrets, cookies and authorization
|
|
11
|
+
headers out of reports.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
prnext --version
|
|
15
|
+
prnext check --json
|
|
16
|
+
prnext typecheck --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`check` does not install, compile or start the application. `typecheck` generates
|
|
20
|
+
types and runs application-installed TypeScript; it needs the application's
|
|
21
|
+
tsconfig and may update generated references. `build` then exercises loaders,
|
|
22
|
+
server/client transforms and prerendering. A browser visit checks the live
|
|
23
|
+
result. Report which layer failed instead of treating one green command as proof
|
|
24
|
+
of all layers.
|
|
25
|
+
|
|
26
|
+
## Common diagnoses
|
|
27
|
+
|
|
28
|
+
| Symptom | Check first |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Native component missing or wrong version | Optional dependencies, supported platform/Node, package lock, framework/native version family and an unintended `PRNEXT_BINARY` override. |
|
|
31
|
+
| Peer or React server/client mismatch | Exact installed React, React DOM and RSC peers; duplicate React instances and a second framework runtime. |
|
|
32
|
+
| Unknown configuration option | The installed version's accepted config; remove or replace only after understanding the intended feature. |
|
|
33
|
+
| Cannot resolve a framework import | Public supported subpath, dependency placement and the migration audit; do not fabricate a private adapter. |
|
|
34
|
+
| Hook or server-only import error | The nearest `'use client'` boundary and everything it imports; keep Node/secret code on the server. |
|
|
35
|
+
| Request API outside context | Move cookie/header access into a request; cached callbacks receive needed values as arguments. |
|
|
36
|
+
| Type errors missing from editor or build | Run `prnext typecheck`; inspect `typescript.tsconfigPath`, inherited roots and existing aliases. |
|
|
37
|
+
| A prerender needs runtime data | Decide whether the route is actually dynamic; do not insert build-time production credentials or suppress the bailout blindly. |
|
|
38
|
+
| Cached content remains stale | The affected cache layer, explicit key/tags and mutation-phase invalidation; custom handlers may delegate caching. |
|
|
39
|
+
|
|
40
|
+
Do not manually patch installed native binaries or force a different release
|
|
41
|
+
into an application. Reinstall through the existing manager and reviewed
|
|
42
|
+
lockfile if installation is the actual problem. Keep the original diagnostic
|
|
43
|
+
when reporting a platform that lacks a compatible published component.
|
|
44
|
+
|
|
45
|
+
## Hydration errors
|
|
46
|
+
|
|
47
|
+
Compare server HTML with the first client render. Typical causes include
|
|
48
|
+
`Date.now()`, random values, locale/time-zone differences, invalid HTML nesting,
|
|
49
|
+
browser-only conditions and client state initialized from storage. Pass stable
|
|
50
|
+
server values as props or move browser reads to a lifecycle. Check that the
|
|
51
|
+
browser loaded assets from the matching build. Use `suppressHydrationWarning`
|
|
52
|
+
only for a deliberately unavoidable, understood mismatch; it is not a general
|
|
53
|
+
repair. Verify the interactive result after the change.
|
|
54
|
+
|
|
55
|
+
## HMR and cache failures
|
|
56
|
+
|
|
57
|
+
Development compiles requested routes and preserves state for compatible edits;
|
|
58
|
+
config/environment, custom Document or incompatible module changes can reload
|
|
59
|
+
the document. Production prerendering and compression are not development HMR.
|
|
60
|
+
An unused route need not rebuild until requested.
|
|
61
|
+
|
|
62
|
+
Middleware and rewrites can select a cold destination after routing. PRNext
|
|
63
|
+
compiles that destination through its private development compiler and continues
|
|
64
|
+
the same request. It does not send an upload or action through middleware again.
|
|
65
|
+
Retained parallel branches are compiled together when their source is needed.
|
|
66
|
+
An error in an unrelated unopened route should not force that route into the
|
|
67
|
+
active graph. Safe mode remains available for full-graph comparisons.
|
|
68
|
+
|
|
69
|
+
For a controlled diagnosis, stop only the development process you own and try:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
prnext dev --safe
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Safe mode uses full builds and fresh workers. `PRNEXT_BUILD_CACHE=0` disables
|
|
76
|
+
PRNext's persistent compiler/build caches when set in the current process environment; use
|
|
77
|
+
the syntax appropriate to the user's shell and restore the previous setting
|
|
78
|
+
after the comparison. In-memory compiler/transform reuse is a separate layer.
|
|
79
|
+
Optional cache-storage failures should fall back to work,
|
|
80
|
+
while genuine loader errors remain build failures. Do not delete every cache or
|
|
81
|
+
lockfile as a first response, especially with another server running.
|
|
82
|
+
|
|
83
|
+
Production builds can reuse eligible compiled JavaScript graphs and CSS Module
|
|
84
|
+
outputs from the default CSS engine across processes through the private
|
|
85
|
+
`.prnext-cache` directory. The successful artifact's `compilation-cache-report.json` and the
|
|
86
|
+
programmatic build result's `compilationCache` report actual hits, misses and
|
|
87
|
+
bypasses. These are compilation diagnostics, not response-cache or latency
|
|
88
|
+
measurements. App graphs that discover actions or client boundaries, development
|
|
89
|
+
graphs, the explicit PostCSS engine and custom loader/PostCSS pipelines still
|
|
90
|
+
execute their compiler work.
|
|
91
|
+
HTML, Flight responses and private rendered data are not stored in this cache.
|
|
92
|
+
Changes to tracked dependencies, resolution candidates, options, environment
|
|
93
|
+
or framework identity invalidate eligible entries. Corrupt or unavailable cache
|
|
94
|
+
records fall back to compilation; `PRNEXT_BUILD_CACHE=0` disables their persistent reuse.
|
|
95
|
+
|
|
96
|
+
For production observations:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
prnext start --inspect
|
|
100
|
+
prnext inspect --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The inspector needs actual traffic for observed timings. Header-ready timings
|
|
104
|
+
exclude the full streaming body, and missing/dropped samples are not zero.
|
|
105
|
+
`prnext routes` describes compiled routes. Bundle size questions use
|
|
106
|
+
`prnext analyze`, after building, rather than interpreting request time as
|
|
107
|
+
compiler performance.
|
|
108
|
+
|
|
109
|
+
Initial-JS analysis includes required hydration imports and SSR-used dynamic
|
|
110
|
+
chunks. Schema 2 reports per-prerendered-path samples and the maximum size for
|
|
111
|
+
each compression metric; a route's file list can be the union of its samples.
|
|
112
|
+
Request-dependent or incomplete graphs are lower bounds. Their size budgets
|
|
113
|
+
fail as `initial-unknown`, even if that lower bound is small. Do not present it
|
|
114
|
+
as a complete measurement or a size saving. Rebuild old schema-1 baselines before
|
|
115
|
+
comparing them with schema 2. An opaque webpack graph is also marked incomplete.
|
|
116
|
+
|
|
117
|
+
For memoization questions, run `prnext compiler --json` after a successful build
|
|
118
|
+
with `reactCompiler` enabled. Inspect the affected function and its skip reason,
|
|
119
|
+
the reported graph coverage and cached-transform counts. An absent function in
|
|
120
|
+
an observed-transforms report is not proof that it was optimized or rejected.
|
|
121
|
+
Keep application behavior and hydration checks alongside this diagnostic.
|
|
122
|
+
|
|
123
|
+
## Connect the existing development MCP
|
|
124
|
+
|
|
125
|
+
Start `prnext dev` in the application, then configure a compatible assistant to
|
|
126
|
+
launch the following **stdio MCP command** with its absolute project directory:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
prnext mcp /absolute/path/to/application
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use the locally installed executable, not an unreviewed remote package. This
|
|
133
|
+
stdio process is a tool server for an MCP client, not a second application
|
|
134
|
+
server. The development endpoint binds to loopback; its private
|
|
135
|
+
`.prnext-dev/tools.json` contains a capability, so never publish or paste that
|
|
136
|
+
file. No new shell-access tool or public debug endpoint is needed.
|
|
137
|
+
|
|
138
|
+
The existing tools include `dev_snapshot`, `dev_logs`, `dev_routes`,
|
|
139
|
+
`dev_route_ensure`, `dev_cache_invalidate`, `dev_source`, `dev_ai_context` and
|
|
140
|
+
`dev_verify`. Read their returned schemas rather than inventing tool arguments.
|
|
141
|
+
|
|
142
|
+
For a fix, capture `dev_snapshot` before editing and retain its `sequence`.
|
|
143
|
+
After the edit, call `dev_verify` with that value as `sinceSequence` and the
|
|
144
|
+
pathname being checked. Keep an actual application browser open. Verification
|
|
145
|
+
requires a newer successful generation and browser evidence that it applied
|
|
146
|
+
that exact build. A missing build or browser is inconclusive; compilation,
|
|
147
|
+
TypeScript or reported browser errors fail the observation. Continue relevant
|
|
148
|
+
application tests: this development observation does not establish complete
|
|
149
|
+
functional correctness.
|
|
150
|
+
|
|
151
|
+
Local MCP works independently of managed hosting. `prnext dev --host-ai` is an
|
|
152
|
+
explicit relay option for a logged-in terminal and a compatible connected Host
|
|
153
|
+
server; it can share requested diagnostics and queued context. Do not enable
|
|
154
|
+
remote sharing or change a user's assistant configuration as a hidden side
|
|
155
|
+
effect of fixing an application.
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { build } from './build/index.mjs';
|
|
2
|
+
export {createBuilder} from './build/production-builder.mjs';
|
|
3
|
+
export {route} from './runtime/typed-route.mjs';
|
|
2
4
|
export { buildStatic } from './build/static-adapter.mjs';
|
|
3
5
|
export { readBuildOutput, validateBuildOutput } from './build/output-contract.mjs';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { packageManifest, nativePlatform, resolveNativeBinary } from './resolve.mjs';
|
|
2
|
+
|
|
3
|
+
// The package's Node engines use explicit minimum versions and major ranges.
|
|
4
|
+
// Keep this check local: a diagnostic must never fetch package metadata.
|
|
5
|
+
export function supportedNode(version, range = packageManifest.engines.node) {
|
|
6
|
+
const parsed = /^(?:v)?(\d+)\.(\d+)\.(\d+)(?:\+[^\s]+)?$/.exec(version);
|
|
7
|
+
if (!parsed) return false;
|
|
8
|
+
const actual = parsed.slice(1).map(Number);
|
|
9
|
+
return range.split('||').some(part => {
|
|
10
|
+
const match = /^\s*(\^|>=)(\d+)\.(\d+)\.(\d+)\s*$/.exec(part);
|
|
11
|
+
if (!match) return false;
|
|
12
|
+
const minimum = match.slice(2).map(Number);
|
|
13
|
+
const compared = actual[0] - minimum[0] || actual[1] - minimum[1] || actual[2] - minimum[2];
|
|
14
|
+
return compared >= 0 && (match[1] !== '^' || actual[0] === minimum[0]);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Inspect installation metadata and executable access; never execute or build it. */
|
|
19
|
+
export async function environmentDiagnostics({ version = process.version, platform = process.platform, arch = process.arch,
|
|
20
|
+
libc, env = process.env, resolve = resolveNativeBinary } = {}) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
diagnostics.push(supportedNode(version)
|
|
23
|
+
? { id: 'node', severity: 'info', message: `Node ${version} satisfies ${packageManifest.engines.node}.` }
|
|
24
|
+
: { id: 'node', severity: 'error', message: `Node ${version} is unsupported.`, action: `Use Node ${packageManifest.engines.node}.` });
|
|
25
|
+
try {
|
|
26
|
+
const target = env.PRNEXT_BINARY ? null : nativePlatform({ platform, arch, libc });
|
|
27
|
+
await resolve({ env, allowBuild: false, platform: { platform, arch, libc } });
|
|
28
|
+
diagnostics.push({ id: 'native', severity: 'info', message: `Native executable is accessible for ${platform}/${arch}${target?.libc ? `/${target.libc}` : ''}; metadata/access checked without running it.`, ...(target ? { package: target.package } : { override: true }) });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
diagnostics.push({ id: 'native', severity: 'error', message: error.message,
|
|
31
|
+
action: 'Install matching PRNext optional dependencies on this machine. For a source checkout, build the native server explicitly; check never builds it.' });
|
|
32
|
+
}
|
|
33
|
+
return diagnostics;
|
|
34
|
+
}
|
package/native/resolve.mjs
CHANGED
|
@@ -14,10 +14,15 @@ export const nativeBinaryName = (platform = process.platform) => platform === 'w
|
|
|
14
14
|
// the framework checkout, even if that consumer also has a Cargo workspace.
|
|
15
15
|
export function sourceCheckout(root = packageRoot) {
|
|
16
16
|
try {
|
|
17
|
-
|
|
17
|
+
// Package-manager portals and linked checkouts may expose a virtual path.
|
|
18
|
+
// Establish the physical package root before looking for its repository.
|
|
19
|
+
// The native resolver also expands Windows DOS aliases and canonicalizes
|
|
20
|
+
// junction target casing; the JS resolver preserves their stored spelling.
|
|
21
|
+
const canonicalRoot = realpathSync.native(root);
|
|
22
|
+
const repository = path.resolve(canonicalRoot, '../..');
|
|
18
23
|
const manifest = JSON.parse(readFileSync(path.join(repository, 'package.json'), 'utf8'));
|
|
19
24
|
if (manifest.name !== 'prnext-monorepo' || manifest.private !== true) return null;
|
|
20
|
-
if (
|
|
25
|
+
if (canonicalRoot !== realpathSync.native(path.join(repository, 'packages/prnext'))) return null;
|
|
21
26
|
readFileSync(path.join(repository, 'crates/prnext/Cargo.toml'));
|
|
22
27
|
return repository;
|
|
23
28
|
} catch { return null; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18-alpha",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
6
6
|
"homepage": "https://prnext.dev",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"registry": "https://registry.npmjs.org/"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
|
+
"AI.md",
|
|
37
|
+
"docs/ai/",
|
|
36
38
|
"cli.mjs",
|
|
37
39
|
"index.mjs",
|
|
38
40
|
"build/",
|
|
@@ -49,6 +51,14 @@
|
|
|
49
51
|
"prn": "./cli.mjs"
|
|
50
52
|
},
|
|
51
53
|
"exports": {
|
|
54
|
+
"./AI.md": "./AI.md",
|
|
55
|
+
"./docs/ai/manifest.json": "./docs/ai/manifest.json",
|
|
56
|
+
"./docs/ai/quickstart.md": "./docs/ai/quickstart.md",
|
|
57
|
+
"./docs/ai/routing.md": "./docs/ai/routing.md",
|
|
58
|
+
"./docs/ai/data.md": "./docs/ai/data.md",
|
|
59
|
+
"./docs/ai/compatibility.md": "./docs/ai/compatibility.md",
|
|
60
|
+
"./docs/ai/deployment.md": "./docs/ai/deployment.md",
|
|
61
|
+
"./docs/ai/troubleshooting.md": "./docs/ai/troubleshooting.md",
|
|
52
62
|
"./package.json": "./package.json",
|
|
53
63
|
"./og": {
|
|
54
64
|
"types": "./compat/og.d.cts",
|
|
@@ -66,6 +76,10 @@
|
|
|
66
76
|
"types": "./compat/index.d.ts",
|
|
67
77
|
"default": "./index.mjs"
|
|
68
78
|
},
|
|
79
|
+
"./route": {
|
|
80
|
+
"types": "./compat/typed-route.d.cts",
|
|
81
|
+
"default": "./runtime/typed-route.mjs"
|
|
82
|
+
},
|
|
69
83
|
"./link": "./compat/link.cjs",
|
|
70
84
|
"./head": "./compat/head.cjs",
|
|
71
85
|
"./script": {
|
|
@@ -101,6 +115,14 @@
|
|
|
101
115
|
"./headers": "./compat/headers.cjs",
|
|
102
116
|
"./server": "./compat/server.cjs",
|
|
103
117
|
"./cache": "./compat/cache.cjs",
|
|
118
|
+
"./cache/redis": {
|
|
119
|
+
"types": "./compat/redis-cache.d.cts",
|
|
120
|
+
"default": "./compat/redis-cache.cjs"
|
|
121
|
+
},
|
|
122
|
+
"./offline": {
|
|
123
|
+
"types": "./compat/offline.d.cts",
|
|
124
|
+
"default": "./compat/offline.cjs"
|
|
125
|
+
},
|
|
104
126
|
"./constants": "./compat/constants.cjs"
|
|
105
127
|
},
|
|
106
128
|
"dependencies": {
|
|
@@ -110,34 +132,49 @@
|
|
|
110
132
|
"@babel/traverse": "^7.29.0",
|
|
111
133
|
"@babel/types": "^7.29.0",
|
|
112
134
|
"@edge-runtime/vm": "5.0.0",
|
|
135
|
+
"@jridgewell/trace-mapping": "0.3.31",
|
|
113
136
|
"@vercel/nft": "1.11.0",
|
|
137
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
138
|
+
"browserslist": "4.29.0",
|
|
114
139
|
"dotenv": "16.6.1",
|
|
115
140
|
"dotenv-expand": "11.0.7",
|
|
116
141
|
"esbuild": "^0.28.2",
|
|
117
142
|
"fontkit": "2.0.4",
|
|
143
|
+
"get-tsconfig": "4.14.3",
|
|
144
|
+
"html-to-image": "1.11.13",
|
|
145
|
+
"jsonc-parser": "3.3.1",
|
|
146
|
+
"lightningcss": "1.31.0",
|
|
118
147
|
"loader-runner": "4.3.2",
|
|
148
|
+
"oxc-parser": "0.155.0",
|
|
119
149
|
"path-to-regexp": "6.3.0",
|
|
120
150
|
"picomatch": "4.0.7",
|
|
121
151
|
"postcss": "8.5.28",
|
|
122
152
|
"postcss-flexbugs-fixes": "5.0.2",
|
|
123
153
|
"postcss-preset-env": "11.5.3",
|
|
154
|
+
"postcss-selector-parser": "7.1.6",
|
|
124
155
|
"postcss-value-parser": "4.2.0",
|
|
125
156
|
"react-refresh": "0.19.0",
|
|
126
157
|
"schema-utils": "4.3.3",
|
|
127
158
|
"webpack": "5.111.1"
|
|
128
159
|
},
|
|
129
160
|
"peerDependencies": {
|
|
161
|
+
"@redis/client": "^6.3.0",
|
|
130
162
|
"react": "19.3.1",
|
|
131
163
|
"react-dom": "19.3.1",
|
|
132
164
|
"react-server-dom-webpack": "19.3.1"
|
|
133
165
|
},
|
|
166
|
+
"peerDependenciesMeta": {
|
|
167
|
+
"@redis/client": {
|
|
168
|
+
"optional": true
|
|
169
|
+
}
|
|
170
|
+
},
|
|
134
171
|
"optionalDependencies": {
|
|
135
|
-
"prnext-darwin-arm64": "0.1.
|
|
136
|
-
"prnext-darwin-x64": "0.1.
|
|
137
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
138
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
139
|
-
"@thomas.f/prnext-win32-x64-msvc": "0.1.
|
|
140
|
-
"@thomas.f/prnext-win32-arm64-msvc": "0.1.
|
|
172
|
+
"prnext-darwin-arm64": "0.1.18-alpha",
|
|
173
|
+
"prnext-darwin-x64": "0.1.18-alpha",
|
|
174
|
+
"prnext-linux-arm64-gnu": "0.1.18-alpha",
|
|
175
|
+
"prnext-linux-x64-gnu": "0.1.18-alpha",
|
|
176
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.18-alpha",
|
|
177
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.18-alpha"
|
|
141
178
|
},
|
|
142
179
|
"engines": {
|
|
143
180
|
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const expectedTopics = ['quickstart', 'routing', 'data', 'compatibility', 'deployment', 'troubleshooting'];
|
|
5
|
+
|
|
6
|
+
// These guides ship as package API. Validate their distribution and local links
|
|
7
|
+
// on every release, independently of a website or a native server installation.
|
|
8
|
+
export async function verifyAiDocumentation(root, pkg) {
|
|
9
|
+
if (!pkg.files?.includes('AI.md') || !pkg.files?.includes('docs/ai/')) {
|
|
10
|
+
throw new Error('The npm files list must include AI.md and docs/ai/.');
|
|
11
|
+
}
|
|
12
|
+
const manifest = JSON.parse(await readFile(path.join(root, 'docs/ai/manifest.json'), 'utf8'));
|
|
13
|
+
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.topics) ||
|
|
14
|
+
JSON.stringify(manifest.topics.map(topic => topic.id)) !== JSON.stringify(expectedTopics)) {
|
|
15
|
+
throw new Error('AI documentation manifest has missing, duplicate or unexpected topics.');
|
|
16
|
+
}
|
|
17
|
+
const files = ['AI.md'];
|
|
18
|
+
for (const topic of manifest.topics) {
|
|
19
|
+
if (topic.file !== `docs/ai/${topic.id}.md` || typeof topic.title !== 'string' || !topic.title.trim() || /[\r\n\x00-\x1f]/.test(topic.title)) {
|
|
20
|
+
throw new Error(`Invalid AI documentation topic: ${topic.id}`);
|
|
21
|
+
}
|
|
22
|
+
files.push(topic.file);
|
|
23
|
+
}
|
|
24
|
+
for (const file of [...files, 'docs/ai/manifest.json']) {
|
|
25
|
+
if (pkg.exports?.[`./${file}`] !== `./${file}`) throw new Error(`Missing npm documentation export: ${file}`);
|
|
26
|
+
}
|
|
27
|
+
let totalBytes = 0;
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const content = await readFile(path.join(root, file), 'utf8');
|
|
30
|
+
const bytes = Buffer.byteLength(content);
|
|
31
|
+
totalBytes += bytes;
|
|
32
|
+
if (!/^# .+/m.test(content) || bytes > (file === 'AI.md' ? 6_000 : 32_000)) {
|
|
33
|
+
throw new Error(`AI guide is missing its heading or exceeds its focused reading budget: ${file}`);
|
|
34
|
+
}
|
|
35
|
+
for (const match of content.matchAll(/\[[^\]\n]+\]\(([^)\s]+)\)/g)) {
|
|
36
|
+
const link = match[1];
|
|
37
|
+
if (link.startsWith('#') || /^https?:\/\//.test(link)) continue;
|
|
38
|
+
const target = path.resolve(root, path.dirname(file), decodeURIComponent(link.split('#')[0]));
|
|
39
|
+
const relative = path.relative(root, target);
|
|
40
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`AI guide link leaves the package: ${file}: ${link}`);
|
|
41
|
+
try { await access(target); }
|
|
42
|
+
catch { throw new Error(`Broken AI guide link: ${file}: ${link}`); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (totalBytes > 100_000) throw new Error('AI documentation exceeds the total focused reading budget.');
|
|
46
|
+
await access(path.join(root, 'runtime/ai-docs.mjs'));
|
|
47
|
+
return { topics: manifest.topics.length, files: files.length, bytes: totalBytes };
|
|
48
|
+
}
|
package/release/verify.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, access } from 'node:fs/promises';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { packageRoot, packageManifest, platforms, sourceCheckout } from '../native/resolve.mjs';
|
|
5
|
+
import { verifyAiDocumentation } from './ai-docs.mjs';
|
|
5
6
|
|
|
6
7
|
export async function verifyPackage() {
|
|
7
8
|
const pkg = packageManifest;
|
|
@@ -25,6 +26,7 @@ export async function verifyPackage() {
|
|
|
25
26
|
for (const target of Object.values(value)) await exported(target);
|
|
26
27
|
}
|
|
27
28
|
await exported(pkg.exports);
|
|
29
|
+
await verifyAiDocumentation(packageRoot, pkg);
|
|
28
30
|
await access(path.join(packageRoot, 'runtime/profiles.json'));
|
|
29
31
|
for (const name of ['index.node.mjs', 'resvg.wasm', 'hb.wasm', 'Geist-Regular.ttf', 'LICENSE', 'LICENSE.fflate', 'LICENSE.Geist', 'LICENSE.harfbuzzjs', 'LICENSE.satori', 'NOTICE.txt']) await access(path.join(packageRoot, 'runtime/og', name));
|
|
30
32
|
if (pkg.dependencies['@vercel/og'] || (await readFile(path.join(packageRoot, 'runtime/og/index.node.mjs'), 'utf8')).includes('fflate@0.7.3')) throw new Error('OG renderer still depends on vulnerable upstream fflate. Run scripts/prepare-og.mjs.');
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// A newly opened route can move this helper into a different development ESM
|
|
2
|
+
// chunk. Its action proxies still belong to the already mounted application.
|
|
3
|
+
// Production keeps the ordinary per-graph module state.
|
|
4
|
+
const state = process.env.NODE_ENV === 'development'
|
|
5
|
+
? globalThis[Symbol.for('prnext.development.server-actions')] ||= {}
|
|
6
|
+
: {};
|
|
3
7
|
|
|
4
8
|
// App bootstrap installs the official Flight client after its module loader is
|
|
5
9
|
// ready. Action proxy modules load through that same client graph afterwards.
|
|
6
10
|
export function configureServerActions({ createServerReference, callServer }) {
|
|
7
|
-
referenceFactory = createServerReference;
|
|
8
|
-
transport = callServer;
|
|
11
|
+
state.referenceFactory = createServerReference;
|
|
12
|
+
state.transport = callServer;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export function callServer(id, args) {
|
|
16
|
+
const transport = state.transport;
|
|
12
17
|
if (!transport) return Promise.reject(new Error('The PRNext Server Action transport is not initialized'));
|
|
13
18
|
return transport(id, args);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export function createServerReference(id) {
|
|
22
|
+
const referenceFactory = state.referenceFactory;
|
|
17
23
|
if (!referenceFactory) throw new Error('Server Action references must load after the PRNext Flight client');
|
|
18
24
|
return referenceFactory(id, callServer);
|
|
19
25
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { decodeReply, decodeAction, decodeFormState, createTemporaryReferenceSet } from 'react-server-dom-webpack/server.node';
|
|
2
3
|
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -126,6 +127,7 @@ export async function invokeAction(request) {
|
|
|
126
127
|
} else if (navigation?.status === 404) {
|
|
127
128
|
actionNotFound = true;
|
|
128
129
|
} else {
|
|
130
|
+
reportRequestError(error,{routeType:'action'});
|
|
129
131
|
console.error('[prnext]', error?.stack || error);
|
|
130
132
|
actionError = errorPayload(error, request.production);
|
|
131
133
|
failure = { message: error?.message || String(error), stack: error?.stack, digest: error?.digest };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs, {constants} from 'node:fs';
|
|
2
|
+
import {lstat, open, realpath, unlink} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
|
|
6
|
+
const defaultPackageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
7
|
+
const maxDocumentBytes = 256 * 1024;
|
|
8
|
+
const maxInstructionsBytes = 1024 * 1024;
|
|
9
|
+
const startMarker = '<!-- prnext-ai-docs:start -->';
|
|
10
|
+
const endMarker = '<!-- prnext-ai-docs:end -->';
|
|
11
|
+
const noFollow = constants.O_NOFOLLOW || 0;
|
|
12
|
+
|
|
13
|
+
const help = `Usage: prnext docs --ai [topic] [--json]
|
|
14
|
+
prnext docs --ai --list [--json]
|
|
15
|
+
prnext docs --ai --init [directory] [--json]
|
|
16
|
+
|
|
17
|
+
Read the AI documentation shipped with the installed PRNext package.
|
|
18
|
+
With no topic, print the short documentation index. --list shows available topics.
|
|
19
|
+
--init explicitly appends a short documentation pointer to a project's AGENTS.md.
|
|
20
|
+
Existing instructions are preserved; no files are changed by documentation reads.
|
|
21
|
+
For initialization, use a physical directory path without symbolic links or junctions.
|
|
22
|
+
No native binary, project configuration, network access, or installation is needed.
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function parseArguments(args) {
|
|
26
|
+
if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) throw new Error('Documentation arguments must be strings.');
|
|
27
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
28
|
+
if (args.every(arg => ['--ai', '--help', '-h'].includes(arg))) return {help: true};
|
|
29
|
+
throw new Error('Use prnext docs --help without other documentation options.');
|
|
30
|
+
}
|
|
31
|
+
const flags = new Set(), positional = [];
|
|
32
|
+
for (const arg of args) {
|
|
33
|
+
if (['--ai', '--list', '--json', '--init'].includes(arg)) {
|
|
34
|
+
if (flags.has(arg)) throw new Error(`Duplicate documentation option: ${arg}`);
|
|
35
|
+
flags.add(arg);
|
|
36
|
+
} else if (arg.startsWith('-')) throw new Error(`Unknown documentation option: ${arg}`);
|
|
37
|
+
else positional.push(arg);
|
|
38
|
+
}
|
|
39
|
+
if (!flags.has('--ai')) throw new Error('Use prnext docs --ai to read the installed AI documentation.');
|
|
40
|
+
if (positional.length > 1) throw new Error('Provide only one documentation topic or initialization directory.');
|
|
41
|
+
if (flags.has('--list') && (flags.has('--init') || positional.length)) throw new Error('--list cannot be combined with a topic or --init.');
|
|
42
|
+
return {list: flags.has('--list'), json: flags.has('--json'), init: flags.has('--init'), value: positional[0]};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readBounded(handle, limit, label) {
|
|
46
|
+
const info = await handle.stat();
|
|
47
|
+
if (!info.isFile()) throw new Error(`${label} must be a regular file.`);
|
|
48
|
+
if (info.size > limit) throw new Error(`${label} exceeds the ${limit}-byte limit.`);
|
|
49
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
50
|
+
let size = 0;
|
|
51
|
+
while (size < buffer.length) {
|
|
52
|
+
const {bytesRead} = await handle.read(buffer, size, buffer.length - size, size);
|
|
53
|
+
if (!bytesRead) break;
|
|
54
|
+
size += bytesRead;
|
|
55
|
+
}
|
|
56
|
+
if (size > limit) throw new Error(`${label} exceeds the ${limit}-byte limit.`);
|
|
57
|
+
return buffer.subarray(0, size);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function decodeUtf8(buffer, label) {
|
|
61
|
+
try { return new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}).decode(buffer); }
|
|
62
|
+
catch { throw new Error(`${label} must contain UTF-8 text.`); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readPackageFile(root, relative, limit = maxDocumentBytes) {
|
|
66
|
+
const file = path.resolve(root, relative);
|
|
67
|
+
const canonicalRoot = await realpath(root), canonicalFile = await realpath(file);
|
|
68
|
+
const inside = path.relative(canonicalRoot, canonicalFile);
|
|
69
|
+
if (inside.startsWith(`..${path.sep}`) || inside === '..' || path.isAbsolute(inside)) throw new Error(`Documentation file escapes the installed package: ${relative}`);
|
|
70
|
+
const handle = await open(file, constants.O_RDONLY);
|
|
71
|
+
try { return decodeUtf8(await readBounded(handle, limit, relative), relative); }
|
|
72
|
+
finally { await handle.close(); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readCatalog(root) {
|
|
76
|
+
const metadata = JSON.parse(await readPackageFile(root, 'package.json', 64 * 1024));
|
|
77
|
+
if (typeof metadata.name !== 'string' || typeof metadata.version !== 'string') throw new Error('The installed package metadata has no name or version.');
|
|
78
|
+
const manifest = JSON.parse(await readPackageFile(root, 'docs/ai/manifest.json', 64 * 1024));
|
|
79
|
+
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.topics) || manifest.topics.length > 64) throw new Error('The AI documentation manifest must use schemaVersion 1 and contain a bounded topics array.');
|
|
80
|
+
const topics = [{id: 'index', title: 'AI documentation index', file: 'AI.md'}], ids = new Set(['index']);
|
|
81
|
+
for (const topic of manifest.topics) {
|
|
82
|
+
if (!topic || !/^[a-z][a-z0-9-]*$/.test(topic.id) || ids.has(topic.id)
|
|
83
|
+
|| typeof topic.title !== 'string' || !topic.title.trim() || /[\r\n\x00-\x1f]/.test(topic.title)
|
|
84
|
+
|| topic.file !== `docs/ai/${topic.id}.md`) throw new Error('The AI documentation manifest contains an invalid or duplicate topic.');
|
|
85
|
+
ids.add(topic.id);
|
|
86
|
+
topics.push({id: topic.id, title: topic.title, file: topic.file});
|
|
87
|
+
}
|
|
88
|
+
return {name: metadata.name, version: metadata.version, topics};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function instructionBlock(newline) {
|
|
92
|
+
return [startMarker, '## PRNext documentation', '',
|
|
93
|
+
'Before changing PRNext configuration, routes, data fetching, or deployment, read the documentation shipped with the installed package:', '',
|
|
94
|
+
'`npx --no-install prnext docs --ai`', '',
|
|
95
|
+
'Use `npx --no-install prnext docs --ai --list` to find topics, then read only the relevant topic with `npx --no-install prnext docs --ai <topic>`.',
|
|
96
|
+
'With pnpm or Yarn, use `pnpm exec prnext docs --ai` or `yarn prnext docs --ai` instead. The installed package documentation is the version-specific reference; do not assume full Next.js compatibility.',
|
|
97
|
+
endMarker, ''].join(newline);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function samePath(left, right) {
|
|
101
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function checkDirectory(directory) {
|
|
105
|
+
const parts = path.relative(path.parse(directory).root, directory).split(path.sep).filter(Boolean);
|
|
106
|
+
let current = path.parse(directory).root;
|
|
107
|
+
for (const part of parts) {
|
|
108
|
+
current = path.join(current, part);
|
|
109
|
+
const info = await lstat(current);
|
|
110
|
+
if (info.isSymbolicLink()) throw new Error(`Refusing to initialize through a symbolic link or junction: ${current}`);
|
|
111
|
+
if (!info.isDirectory()) throw new Error(`Initialization directory is not a directory: ${current}`);
|
|
112
|
+
}
|
|
113
|
+
if (!samePath(await realpath(directory), directory)) throw new Error('Initialization directory resolves outside its requested path.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sameFile(left, right) {
|
|
117
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function verifyTarget(directory, file, handle) {
|
|
121
|
+
await checkDirectory(directory);
|
|
122
|
+
const actual = await lstat(file), opened = await handle.stat();
|
|
123
|
+
if (actual.isSymbolicLink() || !actual.isFile() || !sameFile(actual, opened) || opened.nlink !== 1) {
|
|
124
|
+
throw new Error('AGENTS.md changed identity or is linked; no instructions were appended.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasPointer(content) {
|
|
129
|
+
const starts = content.split(startMarker).length - 1, ends = content.split(endMarker).length - 1;
|
|
130
|
+
if (!starts && !ends) return false;
|
|
131
|
+
if (starts !== 1 || ends !== 1 || content.indexOf(startMarker) > content.indexOf(endMarker)) {
|
|
132
|
+
throw new Error('AGENTS.md contains incomplete or duplicate PRNext documentation markers; preserve and fix those markers before initializing again.');
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function initializeInstructions(directory) {
|
|
138
|
+
await checkDirectory(directory);
|
|
139
|
+
const file = path.join(directory, 'AGENTS.md'), lockFile = path.join(directory, '.prnext-ai-docs.lock');
|
|
140
|
+
let lock;
|
|
141
|
+
try { lock = await open(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, 0o600); }
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code === 'EEXIST') throw new Error(`Another initialization or an existing lock prevents changes: ${lockFile}`);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
let handle;
|
|
147
|
+
try {
|
|
148
|
+
await checkDirectory(directory);
|
|
149
|
+
let existing;
|
|
150
|
+
try { existing = await lstat(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
151
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile() || existing.nlink !== 1)) throw new Error('AGENTS.md must be a regular file with no symbolic or hard links.');
|
|
152
|
+
handle = await open(file, existing
|
|
153
|
+
? constants.O_RDWR | constants.O_APPEND | noFollow
|
|
154
|
+
: constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_EXCL | noFollow, 0o644);
|
|
155
|
+
await verifyTarget(directory, file, handle);
|
|
156
|
+
const before = await readBounded(handle, maxInstructionsBytes, 'AGENTS.md');
|
|
157
|
+
const content = decodeUtf8(before, 'AGENTS.md');
|
|
158
|
+
if (hasPointer(content)) return {path: file, changed: false};
|
|
159
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
160
|
+
const separator = !content ? '' : content.endsWith(newline + newline) ? '' : content.endsWith(newline) ? newline : newline + newline;
|
|
161
|
+
const addition = Buffer.from(separator + instructionBlock(newline));
|
|
162
|
+
if (before.length + addition.length > maxInstructionsBytes) throw new Error('Appending the PRNext pointer would exceed the AGENTS.md size limit.');
|
|
163
|
+
// Append through the verified handle: concurrent appends never get replaced by
|
|
164
|
+
// a stale full-file write. Refuse observed edits or target replacement.
|
|
165
|
+
if (!before.equals(await readBounded(handle, maxInstructionsBytes, 'AGENTS.md'))) throw new Error('AGENTS.md changed during initialization; retry without overwriting those edits.');
|
|
166
|
+
await verifyTarget(directory, file, handle);
|
|
167
|
+
await handle.writeFile(addition);
|
|
168
|
+
// Yarn PnP wraps FileHandle.sync() with an unimplemented method, even for
|
|
169
|
+
// physical project files. Its descriptor still supports callback fsync.
|
|
170
|
+
await new Promise((resolve, reject) => fs.fsync(handle.fd, error => error ? reject(error) : resolve()));
|
|
171
|
+
return {path: file, changed: true};
|
|
172
|
+
} finally {
|
|
173
|
+
await handle?.close();
|
|
174
|
+
// Only remove the lock we created, never a replacement supplied by another process.
|
|
175
|
+
try {
|
|
176
|
+
const opened = await lock.stat(), actual = await lstat(lockFile);
|
|
177
|
+
if (sameFile(opened, actual) && !actual.isSymbolicLink()) await unlink(lockFile);
|
|
178
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
179
|
+
finally { await lock.close(); }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Read bundled docs or explicitly append a project pointer. Never launches native code. */
|
|
184
|
+
export async function executeAiDocs(args, {cwd = process.cwd(), packageRoot = defaultPackageRoot} = {}) {
|
|
185
|
+
const options = parseArguments(args);
|
|
186
|
+
if (options.help) return {output: help};
|
|
187
|
+
const catalog = await readCatalog(packageRoot);
|
|
188
|
+
if (options.init) {
|
|
189
|
+
const initialized = await initializeInstructions(path.resolve(cwd, options.value || '.'));
|
|
190
|
+
const result = {name: catalog.name, version: catalog.version, initialized};
|
|
191
|
+
return {initialized, output: options.json ? JSON.stringify(result, null, 2) + '\n'
|
|
192
|
+
: `${initialized.changed ? 'Added the PRNext documentation pointer to' : 'Kept the existing PRNext documentation pointer in'} ${initialized.path}\n`};
|
|
193
|
+
}
|
|
194
|
+
if (options.list) return {output: options.json ? JSON.stringify(catalog, null, 2) + '\n'
|
|
195
|
+
: `${catalog.name} ${catalog.version}\n\n${catalog.topics.map(topic => `${topic.id.padEnd(18)} ${topic.title}`).join('\n')}\n`};
|
|
196
|
+
const id = options.value || 'index';
|
|
197
|
+
const topic = catalog.topics.find(item => item.id === id);
|
|
198
|
+
if (!topic) throw new Error(`Unknown AI documentation topic: ${id}. Use prnext docs --ai --list.`);
|
|
199
|
+
const content = await readPackageFile(packageRoot, topic.file);
|
|
200
|
+
return {output: options.json ? JSON.stringify({...catalog, topic: topic.id, content}, null, 2) + '\n'
|
|
201
|
+
: `${catalog.name} ${catalog.version} — ${topic.title}\n\n${content}${content.endsWith('\n') ? '' : '\n'}`};
|
|
202
|
+
}
|