@reticlehq/init 3.3.0 → 3.5.0
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/dist/detect/capabilities.d.ts +4 -1
- package/dist/detect/capabilities.js +45 -9
- package/dist/detect/detect.d.ts +45 -2
- package/dist/detect/detect.js +76 -5
- package/dist/detect/dev-script.d.ts +26 -2
- package/dist/detect/dev-script.js +80 -7
- package/dist/detect/non-js-project.d.ts +1 -1
- package/dist/detect/non-js-project.js +6 -1
- package/dist/detect/workspace-apps.d.ts +21 -0
- package/dist/detect/workspace-apps.js +111 -12
- package/dist/detect/workspace-redirect.js +28 -2
- package/dist/diagnose/containerised-dev-server.d.ts +12 -4
- package/dist/diagnose/containerised-dev-server.js +36 -4
- package/dist/diagnose/csp-check.d.ts +12 -6
- package/dist/diagnose/csp-check.js +61 -13
- package/dist/diagnose/csp-doctor.d.ts +13 -1
- package/dist/diagnose/csp-doctor.js +2 -2
- package/dist/diagnose/csp-step.d.ts +27 -0
- package/dist/diagnose/csp-step.js +86 -0
- package/dist/diagnose/desktop-doctor.js +8 -2
- package/dist/diagnose/init-failure.d.ts +7 -0
- package/dist/diagnose/init-failure.js +7 -0
- package/dist/diagnose/install-hint.d.ts +9 -1
- package/dist/diagnose/install-hint.js +17 -7
- package/dist/diagnose/install-retries.d.ts +16 -1
- package/dist/diagnose/install-retries.js +50 -3
- package/dist/diagnose/terminal-wrap.d.ts +18 -0
- package/dist/diagnose/terminal-wrap.js +63 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/no-package-json.d.ts +10 -0
- package/dist/no-package-json.js +131 -0
- package/dist/node-io.d.ts +12 -6
- package/dist/node-io.js +33 -11
- package/dist/patch/angular.d.ts +68 -0
- package/dist/patch/angular.js +216 -0
- package/dist/patch/astro-patch.d.ts +1 -1
- package/dist/patch/astro-patch.js +2 -2
- package/dist/patch/cra.d.ts +0 -2
- package/dist/patch/cra.js +0 -2
- package/dist/patch/electron-patch.d.ts +10 -1
- package/dist/patch/electron-patch.js +79 -27
- package/dist/patch/electron-vite-patch.js +1 -1
- package/dist/patch/nuxt-patch.d.ts +1 -1
- package/dist/patch/nuxt-patch.js +2 -2
- package/dist/patch/nuxt-snippets.js +6 -6
- package/dist/patch/remix.d.ts +24 -0
- package/dist/patch/remix.js +59 -0
- package/dist/patch/snippets.d.ts +57 -9
- package/dist/patch/snippets.js +119 -28
- package/dist/patch/static-page.d.ts +39 -0
- package/dist/patch/static-page.js +102 -0
- package/dist/patch/tanstack-start.d.ts +36 -8
- package/dist/patch/tanstack-start.js +118 -29
- package/dist/patch/vite-config.d.ts +5 -0
- package/dist/patch/vite-config.js +8 -0
- package/dist/patch/vite-owning-config.d.ts +34 -0
- package/dist/patch/vite-owning-config.js +63 -5
- package/dist/plan/connect-steps.d.ts +7 -1
- package/dist/plan/connect-steps.js +18 -1
- package/dist/plan/framework-adapter.d.ts +3 -1
- package/dist/plan/framework-adapter.js +99 -6
- package/dist/plan/notices.js +4 -3
- package/dist/plan/plan-angular.d.ts +6 -0
- package/dist/plan/plan-angular.js +89 -0
- package/dist/plan/plan-electron-vite.d.ts +6 -3
- package/dist/plan/plan-electron-vite.js +39 -8
- package/dist/plan/plan-framework.d.ts +8 -46
- package/dist/plan/plan-framework.js +81 -312
- package/dist/plan/plan-types.d.ts +29 -0
- package/dist/plan/plan-vite.d.ts +67 -0
- package/dist/plan/plan-vite.js +256 -0
- package/dist/plan/plan.js +38 -34
- package/dist/plan/port-steps.d.ts +45 -0
- package/dist/plan/port-steps.js +119 -0
- package/dist/plan/preflight.d.ts +17 -3
- package/dist/plan/preflight.js +53 -23
- package/dist/register/mcp-clients.js +5 -3
- package/dist/register/mcp.d.ts +18 -5
- package/dist/register/mcp.js +48 -8
- package/dist/run-types.d.ts +8 -0
- package/dist/run.js +118 -80
- package/package.json +3 -4
- package/NOTICE +0 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Angular CLI: a dev-only connect in the browser entry, and a way for the pairing token to reach it.
|
|
3
|
+
*
|
|
4
|
+
* The connect is the part proven by hand on Angular 22, with and without SSR: a dynamic
|
|
5
|
+
* `import('@reticlehq/browser')` guarded on `ngDevMode` (see DEV_GUARD for why not `isDevMode()`),
|
|
6
|
+
* which the production build folds to `false` and drops together with the chunk it would load.
|
|
7
|
+
*
|
|
8
|
+
* The token is the hard part, and why this is not one line. The bridge refuses a hello without the
|
|
9
|
+
* machine's pairing token even on localhost, and every other stack inlines it from Node at build
|
|
10
|
+
* time — the Vite plugin's `define`, Next's `NEXT_PUBLIC_*`, CRA's gitignored `.env.development.local`.
|
|
11
|
+
* The Angular CLI has no config file that runs code, so each of those has a failure here, all
|
|
12
|
+
* measured on a scaffolded Angular 21 app before this was written:
|
|
13
|
+
*
|
|
14
|
+
* - A literal in `angular.json` (`define`, `fileReplacements`) commits a per-machine secret.
|
|
15
|
+
* - A gitignored module imported from `main.ts` is bundled by `ng build` too: with the file present
|
|
16
|
+
* the production output held a lazy chunk carrying the token behind `isDevMode()`, which the
|
|
17
|
+
* build cannot fold — and a token must not depend on a guard being written exactly right.
|
|
18
|
+
* - Making that module a dev-only `fileReplacements` target breaks `ng serve` for every teammate
|
|
19
|
+
* who has not run `init` ("path in file replacements does not exist").
|
|
20
|
+
*
|
|
21
|
+
* What is left is the serve target, which `ng build` never reads. Its `proxyConfig` is a JavaScript
|
|
22
|
+
* module that `ng serve` loads in Node, so `reticle.proxy.mjs` reads `~/.reticle/pairing-token` at
|
|
23
|
+
* request time — the same file the Vite and Next plugins read — and answers one same-origin route
|
|
24
|
+
* with it. The token is never written into the project, and a production build has no route to it.
|
|
25
|
+
*/
|
|
26
|
+
import { connectArg } from './snippets.js';
|
|
27
|
+
import { PatchKind } from './patch-kind.js';
|
|
28
|
+
/** The generated dev-server module, beside `angular.json`. */
|
|
29
|
+
export const ANGULAR_PROXY_PATH = 'reticle.proxy.mjs';
|
|
30
|
+
/** The one route that module answers, fetched by the entry's dev-only connect. */
|
|
31
|
+
export const ANGULAR_TOKEN_PATH = '/__reticle/pairing-token';
|
|
32
|
+
/** What an Angular CLI app's browser entry is when `angular.json` does not say. */
|
|
33
|
+
export const ANGULAR_DEFAULT_ENTRY = 'src/main.ts';
|
|
34
|
+
const SENSOR = '@reticlehq/browser';
|
|
35
|
+
const DEV_SERVER_BUILDER = /:dev-server$/;
|
|
36
|
+
const UNPARSEABLE_REASON = 'angular.json is not plain JSON, so it is not safe to rewrite by hand';
|
|
37
|
+
const NO_SERVE_TARGET_REASON = 'no project in angular.json has a `serve` target on a dev-server builder, so there is nowhere ' +
|
|
38
|
+
'for `ng serve` to pick up the token route';
|
|
39
|
+
const SEVERAL_APPS_REASON = 'angular.json has several projects with a `serve` target, and which one is the app is not ours ' +
|
|
40
|
+
'to guess';
|
|
41
|
+
function parseWorkspace(source) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(source);
|
|
44
|
+
if ('object' !== typeof parsed || null === parsed)
|
|
45
|
+
return undefined;
|
|
46
|
+
const root = parsed;
|
|
47
|
+
const projects = root['projects'];
|
|
48
|
+
if ('object' !== typeof projects || null === projects)
|
|
49
|
+
return undefined;
|
|
50
|
+
return { root, projects: projects };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function targetsOf(project) {
|
|
57
|
+
return project.architect ?? project.targets ?? {};
|
|
58
|
+
}
|
|
59
|
+
/** The projects `ng serve` can serve — the only ones with a page to connect. */
|
|
60
|
+
function servedProjects(projects) {
|
|
61
|
+
return Object.values(projects).filter((p) => DEV_SERVER_BUILDER.test(targetsOf(p)['serve']?.builder ?? ''));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The browser entry `angular.json` names — `browser` on the application builder (v17+), `main` on
|
|
65
|
+
* the webpack one — or the CLI's default when it cannot be read.
|
|
66
|
+
*/
|
|
67
|
+
export function angularEntry(source) {
|
|
68
|
+
const workspace = null === source ? undefined : parseWorkspace(source);
|
|
69
|
+
const [only, ...rest] = workspace === undefined ? [] : servedProjects(workspace.projects);
|
|
70
|
+
if (only === undefined || rest.length > 0)
|
|
71
|
+
return ANGULAR_DEFAULT_ENTRY;
|
|
72
|
+
const options = targetsOf(only)['build']?.options ?? {};
|
|
73
|
+
const named = options['browser'] ?? options['main'];
|
|
74
|
+
return 'string' === typeof named && named.length > 0 ? named : ANGULAR_DEFAULT_ENTRY;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Point the one app's serve target at `reticle.proxy.mjs`.
|
|
78
|
+
*
|
|
79
|
+
* An existing `proxyConfig` is the app's, and is never replaced: the step goes MANUAL naming the
|
|
80
|
+
* entry to add to it. The file is rewritten through `JSON.stringify` with the CLI's own two-space
|
|
81
|
+
* indent, which is how `ng new` writes it, so the diff is the one key.
|
|
82
|
+
*/
|
|
83
|
+
export function patchAngularJson(source) {
|
|
84
|
+
const workspace = parseWorkspace(source);
|
|
85
|
+
if (workspace === undefined)
|
|
86
|
+
return { kind: PatchKind.MANUAL, reason: UNPARSEABLE_REASON };
|
|
87
|
+
const served = servedProjects(workspace.projects);
|
|
88
|
+
const [only, ...rest] = served;
|
|
89
|
+
if (only === undefined)
|
|
90
|
+
return { kind: PatchKind.MANUAL, reason: NO_SERVE_TARGET_REASON };
|
|
91
|
+
if (rest.length > 0)
|
|
92
|
+
return { kind: PatchKind.MANUAL, reason: SEVERAL_APPS_REASON };
|
|
93
|
+
const serve = targetsOf(only)['serve'];
|
|
94
|
+
if (serve === undefined)
|
|
95
|
+
return { kind: PatchKind.MANUAL, reason: NO_SERVE_TARGET_REASON };
|
|
96
|
+
const existing = serve.options?.['proxyConfig'];
|
|
97
|
+
if (ANGULAR_PROXY_PATH === existing)
|
|
98
|
+
return { kind: PatchKind.ALREADY };
|
|
99
|
+
if (existing !== undefined) {
|
|
100
|
+
return {
|
|
101
|
+
kind: PatchKind.MANUAL,
|
|
102
|
+
reason: `the serve target already uses ${JSON.stringify(existing)} as its proxyConfig, and it is yours — ` +
|
|
103
|
+
`add the \`${ANGULAR_TOKEN_PATH}\` entry from ${ANGULAR_PROXY_PATH} to it`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
serve.options = { ...serve.options, proxyConfig: ANGULAR_PROXY_PATH };
|
|
107
|
+
return { kind: PatchKind.APPLY, code: `${JSON.stringify(workspace.root, null, 2)}\n` };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The dev-server module. Plain ESM, no dependencies, because it runs in the app's `ng serve`.
|
|
111
|
+
*
|
|
112
|
+
* Two refusals, both about who may read a token: a request that did not come over loopback (an
|
|
113
|
+
* `ng serve --host 0.0.0.0` on a shared network), and a browser request from another origin — a
|
|
114
|
+
* different localhost app — which `Sec-Fetch-Site` names on every current browser.
|
|
115
|
+
*
|
|
116
|
+
* `bypass` answers the request itself and returns `false`, so the dummy `target` is never dialled.
|
|
117
|
+
* ponytail: relies on the Vite-based dev server (Angular 17+) ignoring the 404 it sets after the
|
|
118
|
+
* response has already ended; the older webpack dev server calls `next()` instead, and if it ever
|
|
119
|
+
* matters there the upgrade is a `configure` hook instead of `bypass`.
|
|
120
|
+
*/
|
|
121
|
+
export function angularProxyFile() {
|
|
122
|
+
return `// Written by \`reticle init\`. Read by \`ng serve\` only — \`ng build\` never loads a proxy config.
|
|
123
|
+
//
|
|
124
|
+
// Hands the page this machine's Reticle pairing token, read at request time from the file the
|
|
125
|
+
// Reticle daemon keeps (~/.reticle/pairing-token). The token is per-machine and never written into
|
|
126
|
+
// this project, so nothing here needs to be kept out of version control.
|
|
127
|
+
import { readFileSync } from 'node:fs';
|
|
128
|
+
import { homedir } from 'node:os';
|
|
129
|
+
import { join } from 'node:path';
|
|
130
|
+
|
|
131
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
132
|
+
|
|
133
|
+
function pairingToken() {
|
|
134
|
+
const dir = process.env.RETICLE_PAIRING_TOKEN_DIR || join(homedir(), '.reticle');
|
|
135
|
+
try {
|
|
136
|
+
return readFileSync(join(dir, 'pairing-token'), 'utf8').trim();
|
|
137
|
+
} catch {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default {
|
|
143
|
+
'${ANGULAR_TOKEN_PATH}': {
|
|
144
|
+
target: 'http://127.0.0.1:9',
|
|
145
|
+
bypass(req, res) {
|
|
146
|
+
const site = req.headers['sec-fetch-site'];
|
|
147
|
+
const allowed =
|
|
148
|
+
LOOPBACK.has(req.socket.remoteAddress ?? '') && (site === undefined || site === 'same-origin');
|
|
149
|
+
res.writeHead(allowed ? 200 : 403, { 'content-type': 'text/plain', 'cache-control': 'no-store' });
|
|
150
|
+
res.end(allowed ? pairingToken() : '');
|
|
151
|
+
return false;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
/** Everything `connect()` is called with, as the literal's inner fields — `url` only off 4400. */
|
|
158
|
+
function connectFields(port, projectId) {
|
|
159
|
+
const literal = connectArg(port, projectId);
|
|
160
|
+
return literal.length > 0 ? `${literal.slice(1, -1).trim()}, ` : '';
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The guard the connect sits under: Angular's own `ngDevMode` global, not `isDevMode()`.
|
|
164
|
+
*
|
|
165
|
+
* `isDevMode()` is a runtime call, so `ng build` cannot fold it: the production bundle shipped the
|
|
166
|
+
* whole connect — the bridge URL included — plus ~300KB of lazy Reticle chunks that nothing
|
|
167
|
+
* would ever load. The Angular CLI defines `ngDevMode` to `false` in a production build, which lets
|
|
168
|
+
* esbuild drop the branch and, with it, the dynamic import; in dev it is undefined or an object, so
|
|
169
|
+
* this reads exactly as `isDevMode()` did there. It is the check Angular's own sources use.
|
|
170
|
+
*/
|
|
171
|
+
const DEV_GUARD = "if (typeof ngDevMode === 'undefined' || ngDevMode) {";
|
|
172
|
+
/** Declared locally so strict TypeScript accepts the global without the app importing anything. */
|
|
173
|
+
const NG_DEV_MODE_DECLARATION = 'declare const ngDevMode: unknown;';
|
|
174
|
+
/** The guard the previous version wrote, which is rewritten in place on a re-run. */
|
|
175
|
+
const LEGACY_GUARD = /^if \(isDevMode\(\)\) \{(?=\s*void Promise\.all\(\[\s*import\('@reticlehq\/browser'\))/m;
|
|
176
|
+
/** The comment the previous version wrote above it, which named the guard it no longer uses. */
|
|
177
|
+
const LEGACY_COMMENT = /^\/\/ Reticle, dev only — written by `reticle init`\. `isDevMode\(\)` is false in a production build,\n\/\/ so this never runs there\./m;
|
|
178
|
+
const HEADER = '// Reticle, dev only — written by `reticle init`. `ngDevMode` is false in a production build,\n' +
|
|
179
|
+
'// so the build drops this block.';
|
|
180
|
+
/**
|
|
181
|
+
* Add the dev-only connect to the browser entry, or ALREADY when it is there.
|
|
182
|
+
*
|
|
183
|
+
* Appended after the bootstrap rather than before it: the app must never wait on Reticle, and a
|
|
184
|
+
* connect that fails (no daemon yet) must not be able to stop Angular from starting.
|
|
185
|
+
*
|
|
186
|
+
* An entry wired by the previous version (`if (isDevMode())`) is rewritten in place, so a re-run is
|
|
187
|
+
* what takes the connect out of an existing app's production bundle.
|
|
188
|
+
*/
|
|
189
|
+
export function patchAngularMain(source, port, projectId) {
|
|
190
|
+
if (LEGACY_GUARD.test(source)) {
|
|
191
|
+
const guarded = source
|
|
192
|
+
.replace(LEGACY_COMMENT, HEADER)
|
|
193
|
+
.replace(LEGACY_GUARD, `${NG_DEV_MODE_DECLARATION}\n${DEV_GUARD}`);
|
|
194
|
+
return { kind: PatchKind.APPLY, code: guarded };
|
|
195
|
+
}
|
|
196
|
+
if (source.includes(SENSOR))
|
|
197
|
+
return { kind: PatchKind.ALREADY };
|
|
198
|
+
const declaration = source.includes(NG_DEV_MODE_DECLARATION)
|
|
199
|
+
? ''
|
|
200
|
+
: `${NG_DEV_MODE_DECLARATION}\n`;
|
|
201
|
+
const block = `
|
|
202
|
+
${HEADER} The pairing token comes from \`ng serve\` (${ANGULAR_PROXY_PATH}), so it
|
|
203
|
+
// is never in a source file or a bundle.
|
|
204
|
+
${declaration}${DEV_GUARD}
|
|
205
|
+
void Promise.all([
|
|
206
|
+
import('${SENSOR}'),
|
|
207
|
+
fetch('${ANGULAR_TOKEN_PATH}')
|
|
208
|
+
.then((res) => (res.ok ? res.text() : ''))
|
|
209
|
+
.catch(() => ''),
|
|
210
|
+
]).then(([{ reticle }, token]) => {
|
|
211
|
+
reticle.connect({ ${connectFields(port, projectId)}...(token ? { token } : {}) });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
return { kind: PatchKind.APPLY, code: `${source.trimEnd()}\n${block}` };
|
|
216
|
+
}
|
|
@@ -45,7 +45,7 @@ export declare const ASTRO_VITE_OWNING_MARKER = "reticle-vite-owning";
|
|
|
45
45
|
* have to restart the dev server.
|
|
46
46
|
*/
|
|
47
47
|
export declare const ASTRO_MISSING_TOKEN_WARNING: string;
|
|
48
|
-
export declare function patchAstroConfig(source: string): SourcePatch;
|
|
48
|
+
export declare function patchAstroConfig(source: string, viteMajor?: number | null): SourcePatch;
|
|
49
49
|
/**
|
|
50
50
|
* Import path from a layout/page to the generated connect module, always relative and posix.
|
|
51
51
|
*
|
|
@@ -99,8 +99,8 @@ const ASTRO_VITE_CONFIG = {
|
|
|
99
99
|
defineToken: false,
|
|
100
100
|
alreadyMarker: ASTRO_VITE_OWNING_MARKER,
|
|
101
101
|
};
|
|
102
|
-
export function patchAstroConfig(source) {
|
|
103
|
-
return patchViteOwningConfig(source, ASTRO_VITE_CONFIG);
|
|
102
|
+
export function patchAstroConfig(source, viteMajor = null) {
|
|
103
|
+
return patchViteOwningConfig(source, { ...ASTRO_VITE_CONFIG, viteMajor });
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Aliased imports so a layout that already pulled `readFileSync` from `node:fs` does not get a
|
package/dist/patch/cra.d.ts
CHANGED
|
@@ -21,8 +21,6 @@ export declare const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
|
|
|
21
21
|
* makes the first compile after init fail while the plan still reports `[✓]`.
|
|
22
22
|
*/
|
|
23
23
|
export declare function craDevModulePath(typescript: boolean): string;
|
|
24
|
-
/** TypeScript default path — prefer `craDevModulePath` when the project's language is known. */
|
|
25
|
-
export declare const CRA_DEV_MODULE_PATH: string;
|
|
26
24
|
export declare const CRA_ENV_PATH = ".env.development.local";
|
|
27
25
|
export declare const TOKEN_VAR = "REACT_APP_RETICLE_TOKEN";
|
|
28
26
|
/**
|
package/dist/patch/cra.js
CHANGED
|
@@ -26,8 +26,6 @@ export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
|
|
|
26
26
|
export function craDevModulePath(typescript) {
|
|
27
27
|
return typescript ? 'src/reticle-dev.ts' : 'src/reticle-dev.js';
|
|
28
28
|
}
|
|
29
|
-
/** TypeScript default path — prefer `craDevModulePath` when the project's language is known. */
|
|
30
|
-
export const CRA_DEV_MODULE_PATH = craDevModulePath(true);
|
|
31
29
|
export const CRA_ENV_PATH = '.env.development.local';
|
|
32
30
|
export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
|
|
33
31
|
/**
|
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
|
|
6
6
|
* constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
|
|
7
7
|
* (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
|
|
8
|
+
*
|
|
9
|
+
* BOTH ARE DEV-ONLY, as the adapter's README says they must be. They used to be written unguarded
|
|
10
|
+
* — a static import in preload, a static import and call in main — so `electron-vite build` and
|
|
11
|
+
* `electron-forge package` bundled the IPC shim (which patches `ipcRenderer.invoke` and exposes
|
|
12
|
+
* `__reticleIpc`) and the screenshot IPC handler into the shipped app. Each is now behind a guard
|
|
13
|
+
* the bundler replaces at build time, so a production build folds the branch away and carries
|
|
14
|
+
* neither. Measured on electron-vite: absent from a production build, present in
|
|
15
|
+
* `--mode development`, which is the build the renderer's `desktop: true` also instruments.
|
|
16
|
+
* A line an older `init` wrote is rewritten in place on a re-run.
|
|
8
17
|
*/
|
|
9
18
|
import { type SourcePatch } from './patch-kind.js';
|
|
10
|
-
export declare function patchElectronPreload(source: string, path: string): SourcePatch;
|
|
19
|
+
export declare function patchElectronPreload(source: string, path: string, viteTypes?: boolean): SourcePatch;
|
|
11
20
|
export declare function patchElectronMain(source: string, path?: string): SourcePatch;
|
|
@@ -5,48 +5,97 @@
|
|
|
5
5
|
* Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
|
|
6
6
|
* constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
|
|
7
7
|
* (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
|
|
8
|
+
*
|
|
9
|
+
* BOTH ARE DEV-ONLY, as the adapter's README says they must be. They used to be written unguarded
|
|
10
|
+
* — a static import in preload, a static import and call in main — so `electron-vite build` and
|
|
11
|
+
* `electron-forge package` bundled the IPC shim (which patches `ipcRenderer.invoke` and exposes
|
|
12
|
+
* `__reticleIpc`) and the screenshot IPC handler into the shipped app. Each is now behind a guard
|
|
13
|
+
* the bundler replaces at build time, so a production build folds the branch away and carries
|
|
14
|
+
* neither. Measured on electron-vite: absent from a production build, present in
|
|
15
|
+
* `--mode development`, which is the build the renderer's `desktop: true` also instruments.
|
|
16
|
+
* A line an older `init` wrote is rewritten in place on a re-run.
|
|
8
17
|
*/
|
|
9
18
|
import { matchingParenEnd } from './brace-scan.js';
|
|
10
19
|
import { CAPTURE_REQUIRE, ELECTRON_CAPTURE_FIX, PRELOAD_REQUIRE, } from '../diagnose/desktop-doctor.js';
|
|
11
20
|
import { PatchKind } from './patch-kind.js';
|
|
12
21
|
const ESM_PRELOAD_EXT = /\.(?:ts|mts|mjs)$/i;
|
|
13
22
|
const WINDOW_DECL = /const\s+(\w+)\s*=\s*new\s+BrowserWindow\s*\(/g;
|
|
14
|
-
|
|
15
|
-
const
|
|
23
|
+
/** Replaced by Vite in every bundled main and preload, so a production build folds it to false. */
|
|
24
|
+
const BUILD_MODE_GUARD = "import.meta.env.MODE !== 'production'";
|
|
25
|
+
/**
|
|
26
|
+
* Forge's own dev-server define (`MAIN_WINDOW_VITE_DEV_SERVER_URL` in its template): a URL under
|
|
27
|
+
* `electron-forge start`, undefined in a package, and declared by the template — so it folds AND
|
|
28
|
+
* typechecks, where Forge's `tsconfig` has no types for `import.meta.env`.
|
|
29
|
+
*/
|
|
30
|
+
const FORGE_DEV_SERVER_DEFINE = /\b([A-Z][A-Z0-9_]*_VITE_DEV_SERVER_URL)\b/;
|
|
31
|
+
/** An unbundled CommonJS main has nothing to fold, so it asks at runtime. */
|
|
32
|
+
const PACKAGED_GUARD = "!require('electron').app.isPackaged";
|
|
33
|
+
/** The unguarded lines an older `init` wrote, rewritten in place on a re-run. */
|
|
34
|
+
const LEGACY_PRELOAD_IMPORT = new RegExp(`^import '${PRELOAD_REQUIRE}'\\n`, 'm');
|
|
35
|
+
const LEGACY_MAIN_IMPORT = new RegExp(`^(?:import \\{ installReticleCapture \\} from '${CAPTURE_REQUIRE}'|const \\{ installReticleCapture \\} = require\\('${CAPTURE_REQUIRE}'\\);)\\n`, 'm');
|
|
36
|
+
const LEGACY_MAIN_CALL = /^([ \t]*)installReticleCapture\((\w+)\)[ \t]*;?[ \t]*$/m;
|
|
16
37
|
function isEsmPath(path) {
|
|
17
38
|
return ESM_PRELOAD_EXT.test(path);
|
|
18
39
|
}
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The shim line, first in the file.
|
|
42
|
+
*
|
|
43
|
+
* A DYNAMIC import behind the build mode, for a bundled preload. Not a `require`: the electron-vite
|
|
44
|
+
* template lints with `no-require-imports`, and a clean install must not fail the project's own lint.
|
|
45
|
+
* Not the static import either, which no guard can remove. The cost is that the shim installs a
|
|
46
|
+
* microtask after the preload body runs rather than before it — still before the page's own
|
|
47
|
+
* scripts, and every exposed API that calls `ipcRenderer.invoke` at call time (electron-toolkit's
|
|
48
|
+
* included) is observed; one that captured the function itself (`invoke: ipcRenderer.invoke`) is
|
|
49
|
+
* not. Verified end to end on electron-vite: IPC observed and screenshots saved in dev, and no
|
|
50
|
+
* Reticle code in a production build.
|
|
51
|
+
*
|
|
52
|
+
* `viteTypes` is for Electron Forge, whose preload is SANDBOXED by Electron's default and whose
|
|
53
|
+
* tsconfig has no `import.meta.env` types. The same guarded line works there because Forge builds its
|
|
54
|
+
* preload with `codeSplitting: false` and the real build mode: the dynamic import is inlined into the
|
|
55
|
+
* one file a sandbox can load under `electron-forge start`, and folds away under `package`. Measured
|
|
56
|
+
* on a scaffolded Forge 8 app — Forge used to get the static import instead, which no guard removes,
|
|
57
|
+
* so the packaged app shipped the shim. The reference line supplies the types, so `tsc` stays green.
|
|
58
|
+
*/
|
|
59
|
+
const VITE_CLIENT_TYPES = '/// <reference types="vite/client" />\n';
|
|
60
|
+
function preloadLine(path, viteTypes) {
|
|
61
|
+
if (!isEsmPath(path))
|
|
62
|
+
return `require('${PRELOAD_REQUIRE}');\n`;
|
|
63
|
+
return ((viteTypes ? VITE_CLIENT_TYPES : '') +
|
|
64
|
+
`if (${BUILD_MODE_GUARD}) {\n` +
|
|
65
|
+
` // Reticle IPC observation, dev only — written by \`reticle init\`. A production build drops it.\n` +
|
|
66
|
+
` void import('${PRELOAD_REQUIRE}').catch(() => undefined)\n` +
|
|
67
|
+
`}\n`);
|
|
21
68
|
}
|
|
22
|
-
export function patchElectronPreload(source, path) {
|
|
69
|
+
export function patchElectronPreload(source, path, viteTypes = false) {
|
|
70
|
+
if (isEsmPath(path) && LEGACY_PRELOAD_IMPORT.test(source)) {
|
|
71
|
+
return {
|
|
72
|
+
kind: PatchKind.APPLY,
|
|
73
|
+
code: `${preloadLine(path, viteTypes)}${source.replace(LEGACY_PRELOAD_IMPORT, '')}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
23
76
|
if (source.includes(PRELOAD_REQUIRE)) {
|
|
24
77
|
return { kind: PatchKind.ALREADY };
|
|
25
78
|
}
|
|
26
|
-
return { kind: PatchKind.APPLY, code: `${preloadLine(path)}${source}` };
|
|
79
|
+
return { kind: PatchKind.APPLY, code: `${preloadLine(path, viteTypes)}${source}` };
|
|
27
80
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const line = `import { installReticleCapture } from '${CAPTURE_REQUIRE}'`;
|
|
33
|
-
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
34
|
-
const last = matches[matches.length - 1];
|
|
35
|
-
if (last?.index === undefined)
|
|
36
|
-
return `${line}\n${source}`;
|
|
37
|
-
const end = last.index + last[0].length;
|
|
38
|
-
return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
|
|
81
|
+
/** The guarded capture call for this main file. Nothing to import at the top: it loads itself. */
|
|
82
|
+
function captureCall(source, path, windowName) {
|
|
83
|
+
if (!isEsmPath(path)) {
|
|
84
|
+
return `if (${PACKAGED_GUARD}) require('${CAPTURE_REQUIRE}').installReticleCapture(${windowName})`;
|
|
39
85
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const last = matches[matches.length - 1];
|
|
43
|
-
if (last?.index === undefined)
|
|
44
|
-
return `${line}\n${source}`;
|
|
45
|
-
const endOfLine = source.indexOf('\n', last.index);
|
|
46
|
-
const end = endOfLine < 0 ? source.length : endOfLine;
|
|
47
|
-
return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
|
|
86
|
+
const guard = FORGE_DEV_SERVER_DEFINE.exec(source)?.[1] ?? BUILD_MODE_GUARD;
|
|
87
|
+
return `if (${guard}) void import('${CAPTURE_REQUIRE}').then(({ installReticleCapture }) => installReticleCapture(${windowName}))`;
|
|
48
88
|
}
|
|
49
89
|
export function patchElectronMain(source, path = 'electron/main.cjs') {
|
|
90
|
+
const legacyCall = LEGACY_MAIN_CALL.exec(source);
|
|
91
|
+
if (LEGACY_MAIN_IMPORT.test(source) && legacyCall?.[2] !== undefined) {
|
|
92
|
+
const withoutImport = source.replace(LEGACY_MAIN_IMPORT, '');
|
|
93
|
+
const indent = legacyCall[1] ?? '';
|
|
94
|
+
return {
|
|
95
|
+
kind: PatchKind.APPLY,
|
|
96
|
+
code: withoutImport.replace(LEGACY_MAIN_CALL, `${indent}${captureCall(withoutImport, path, legacyCall[2])}`),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
50
99
|
if (source.includes(CAPTURE_REQUIRE)) {
|
|
51
100
|
return { kind: PatchKind.ALREADY };
|
|
52
101
|
}
|
|
@@ -65,6 +114,9 @@ export function patchElectronMain(source, path = 'electron/main.cjs') {
|
|
|
65
114
|
insertAt += 1;
|
|
66
115
|
const indentMatch = /^[ \t]*/.exec(source.slice(source.lastIndexOf('\n', matches[0].index) + 1));
|
|
67
116
|
const indent = indentMatch?.[0] ?? ' ';
|
|
68
|
-
const
|
|
69
|
-
return {
|
|
117
|
+
const call = captureCall(source, path, name);
|
|
118
|
+
return {
|
|
119
|
+
kind: PatchKind.APPLY,
|
|
120
|
+
code: `${source.slice(0, insertAt)}\n${indent}${call}${source.slice(insertAt)}`,
|
|
121
|
+
};
|
|
70
122
|
}
|
|
@@ -19,7 +19,7 @@ const RENDERER_KEY = /renderer\s*:\s*/;
|
|
|
19
19
|
const NO_RENDERER_REASON = "couldn't find a `renderer` key to extend";
|
|
20
20
|
const RENDERER_NOT_LITERAL_REASON = 'this config sets `renderer:` to something other than an object literal — merging into it is your call, not a text edit Reticle should make';
|
|
21
21
|
const WRONG_BLOCK_REASON = 'reticle() is in this file but not in the `renderer` block — the plugin belongs where there is a document, and leaving it in `main` or `preload` would report success for an app that cannot connect';
|
|
22
|
-
const MISSING_DESKTOP_REASON = 'reticle() is already in the `renderer` block but without `desktop: true` — a packaged renderer
|
|
22
|
+
const MISSING_DESKTOP_REASON = 'reticle() is already in the `renderer` block but without `desktop: true` — a packaged renderer has no dev server and reports production, so without it the SDK refuses to start there and the packaged app never connects';
|
|
23
23
|
function reticlePluginCall(port) {
|
|
24
24
|
const options = [
|
|
25
25
|
'desktop: true',
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
import type { SourcePatch } from './patch-kind.js';
|
|
16
16
|
/** The config filenames Nuxt itself accepts, in the order `init` should prefer them. */
|
|
17
17
|
export declare const NUXT_CONFIG_CANDIDATES: string[];
|
|
18
|
-
export declare function patchNuxtConfig(source: string): SourcePatch;
|
|
18
|
+
export declare function patchNuxtConfig(source: string, viteMajor?: number | null): SourcePatch;
|
package/dist/patch/nuxt-patch.js
CHANGED
|
@@ -28,6 +28,6 @@ const NUXT_VITE_CONFIG = {
|
|
|
28
28
|
defineCall: /defineNuxtConfig\s*\(\s*\{/,
|
|
29
29
|
sdkSpecifier: sdkImport(UiLibrary.VUE).specifier,
|
|
30
30
|
};
|
|
31
|
-
export function patchNuxtConfig(source) {
|
|
32
|
-
return patchViteOwningConfig(source, NUXT_VITE_CONFIG);
|
|
31
|
+
export function patchNuxtConfig(source, viteMajor = null) {
|
|
32
|
+
return patchViteOwningConfig(source, { ...NUXT_VITE_CONFIG, viteMajor });
|
|
33
33
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 1000-line cap): its path, the plugin file itself, the notice, and the manual fallback.
|
|
4
4
|
*/
|
|
5
5
|
import { ReticleDir } from '@reticlehq/core';
|
|
6
|
-
import { connectArg, registerCapabilitiesCall } from './snippets.js';
|
|
6
|
+
import { connectArg, registerCapabilitiesCall, STATE_DOCS_URL } from './snippets.js';
|
|
7
7
|
/** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
|
|
8
8
|
const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
|
|
9
9
|
/**
|
|
@@ -53,9 +53,9 @@ export default defineNuxtPlugin(() => {
|
|
|
53
53
|
...(root.length > 0 ? { root } : {}),
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
// What the agent can drive without guessing. Add a store here too —
|
|
57
|
-
// piniaStore(
|
|
58
|
-
// rendered. See
|
|
56
|
+
// What the agent can drive without guessing. Add a store here too — for Pinia,
|
|
57
|
+
// registerStore('<key>', piniaStore(<useYourStore>())) — and the agent can check what the app
|
|
58
|
+
// BELIEVES, not just what it rendered. See ${STATE_DOCS_URL}
|
|
59
59
|
${registerCapabilitiesCall(testids, ' ')}
|
|
60
60
|
});
|
|
61
61
|
});
|
|
@@ -130,6 +130,6 @@ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which i
|
|
|
130
130
|
|
|
131
131
|
The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
|
|
132
132
|
source file:line all work in Vue. What you do not get is React component identity, which is the only
|
|
133
|
-
thing the React adapter adds.
|
|
134
|
-
if something does not work, please open an issue.`;
|
|
133
|
+
thing the React adapter adds. The install gate scaffolds a Nuxt app on every release and requires a session, so this wiring
|
|
134
|
+
is proven; if something does not work, please open an issue.`;
|
|
135
135
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remix v2's client entry, and the one honest answer for the classic compiler.
|
|
3
|
+
*
|
|
4
|
+
* Remix on Vite is React Router framework mode under its old name: it SSRs its own HTML, so the
|
|
5
|
+
* Vite plugin's `index.html` injection never reaches the page, and the connect has to come from
|
|
6
|
+
* `app/entry.client.tsx` — the same line, the same file, verified by hand to connect.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The client entry `init` writes when a Remix app has none.
|
|
10
|
+
*
|
|
11
|
+
* Like React Router's, `app/entry.client.tsx` is an OVERRIDE of a default Remix supplies — it only
|
|
12
|
+
* exists once somebody runs `remix reveal`. So the written file has to hydrate as well as connect:
|
|
13
|
+
* one holding only our import would replace the default with an app that renders nothing. This is
|
|
14
|
+
* Remix v2's own default entry with one line added.
|
|
15
|
+
*/
|
|
16
|
+
export declare function remixEntryFile(): string;
|
|
17
|
+
/**
|
|
18
|
+
* The classic compiler has no Vite, so nothing serves `/@reticle-connect` and nothing stamps source.
|
|
19
|
+
*
|
|
20
|
+
* Said plainly and left as a ⚠ rather than papered over: writing the entry line here would import a
|
|
21
|
+
* module no dev server serves, and the app would log a failed import on every page load instead of
|
|
22
|
+
* connecting.
|
|
23
|
+
*/
|
|
24
|
+
export declare const REMIX_CLASSIC_MANUAL = "This is Remix on the classic compiler (remix.config.js, no Vite), and Reticle cannot wire it:\nthe connect module and the source stamping both come from @reticlehq/vite-plugin, and this build\nnever runs Vite.\n\n \u2022 Recommended: move to Remix's Vite plugin (https://remix.run/docs/en/main/guides/vite), or on to\n React Router v7+ framework mode, then re-run `reticle init` \u2014 both are wired automatically.\n \u2022 Or connect by hand from app/entry.client.tsx, dev-only, through the React kit init installed:\n\n if (process.env.NODE_ENV === 'development') {\n void import('@reticlehq/react').then(({ reticle, install }) => {\n install();\n reticle.connect({ token: '<the contents of ~/.reticle/pairing-token>' });\n });\n }\n\n The token is per-machine: keep it out of version control.";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remix v2's client entry, and the one honest answer for the classic compiler.
|
|
3
|
+
*
|
|
4
|
+
* Remix on Vite is React Router framework mode under its old name: it SSRs its own HTML, so the
|
|
5
|
+
* Vite plugin's `index.html` injection never reaches the page, and the connect has to come from
|
|
6
|
+
* `app/entry.client.tsx` — the same line, the same file, verified by hand to connect.
|
|
7
|
+
*/
|
|
8
|
+
import { REACT_ROUTER_CONNECT_LINE } from './snippets.js';
|
|
9
|
+
/**
|
|
10
|
+
* The client entry `init` writes when a Remix app has none.
|
|
11
|
+
*
|
|
12
|
+
* Like React Router's, `app/entry.client.tsx` is an OVERRIDE of a default Remix supplies — it only
|
|
13
|
+
* exists once somebody runs `remix reveal`. So the written file has to hydrate as well as connect:
|
|
14
|
+
* one holding only our import would replace the default with an app that renders nothing. This is
|
|
15
|
+
* Remix v2's own default entry with one line added.
|
|
16
|
+
*/
|
|
17
|
+
export function remixEntryFile() {
|
|
18
|
+
return `import { RemixBrowser } from '@remix-run/react';
|
|
19
|
+
import { startTransition, StrictMode } from 'react';
|
|
20
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
21
|
+
|
|
22
|
+
// Dev-only: Remix renders HTML through its own request handler, so the Vite plugin's index.html
|
|
23
|
+
// injection never fires and the connect script never reaches the page. The plugin is still doing
|
|
24
|
+
// the other half of the job — it stamps data-reticle-source, which puts file:line on every verdict.
|
|
25
|
+
${REACT_ROUTER_CONNECT_LINE}
|
|
26
|
+
|
|
27
|
+
startTransition(() => {
|
|
28
|
+
hydrateRoot(
|
|
29
|
+
document,
|
|
30
|
+
<StrictMode>
|
|
31
|
+
<RemixBrowser />
|
|
32
|
+
</StrictMode>,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The classic compiler has no Vite, so nothing serves `/@reticle-connect` and nothing stamps source.
|
|
39
|
+
*
|
|
40
|
+
* Said plainly and left as a ⚠ rather than papered over: writing the entry line here would import a
|
|
41
|
+
* module no dev server serves, and the app would log a failed import on every page load instead of
|
|
42
|
+
* connecting.
|
|
43
|
+
*/
|
|
44
|
+
export const REMIX_CLASSIC_MANUAL = `This is Remix on the classic compiler (remix.config.js, no Vite), and Reticle cannot wire it:
|
|
45
|
+
the connect module and the source stamping both come from @reticlehq/vite-plugin, and this build
|
|
46
|
+
never runs Vite.
|
|
47
|
+
|
|
48
|
+
• Recommended: move to Remix's Vite plugin (https://remix.run/docs/en/main/guides/vite), or on to
|
|
49
|
+
React Router v7+ framework mode, then re-run \`reticle init\` — both are wired automatically.
|
|
50
|
+
• Or connect by hand from app/entry.client.tsx, dev-only, through the React kit init installed:
|
|
51
|
+
|
|
52
|
+
if (process.env.NODE_ENV === 'development') {
|
|
53
|
+
void import('@reticlehq/react').then(({ reticle, install }) => {
|
|
54
|
+
install();
|
|
55
|
+
reticle.connect({ token: '<the contents of ~/.reticle/pairing-token>' });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
The token is per-machine: keep it out of version control.`;
|