@reticlehq/init 3.2.0 → 3.4.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.
Files changed (93) hide show
  1. package/dist/detect/capabilities.d.ts +4 -1
  2. package/dist/detect/capabilities.js +45 -9
  3. package/dist/detect/detect.d.ts +58 -4
  4. package/dist/detect/detect.js +117 -7
  5. package/dist/detect/dev-script.d.ts +26 -2
  6. package/dist/detect/dev-script.js +80 -7
  7. package/dist/detect/non-js-project.d.ts +1 -1
  8. package/dist/detect/non-js-project.js +6 -1
  9. package/dist/detect/workspace-apps.d.ts +21 -0
  10. package/dist/detect/workspace-apps.js +111 -12
  11. package/dist/detect/workspace-redirect.d.ts +2 -1
  12. package/dist/detect/workspace-redirect.js +28 -2
  13. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  14. package/dist/diagnose/containerised-dev-server.js +36 -4
  15. package/dist/diagnose/csp-check.d.ts +12 -6
  16. package/dist/diagnose/csp-check.js +61 -13
  17. package/dist/diagnose/csp-doctor.d.ts +33 -2
  18. package/dist/diagnose/csp-doctor.js +103 -6
  19. package/dist/diagnose/csp-step.d.ts +27 -0
  20. package/dist/diagnose/csp-step.js +86 -0
  21. package/dist/diagnose/desktop-doctor.d.ts +1 -1
  22. package/dist/diagnose/desktop-doctor.js +8 -2
  23. package/dist/diagnose/install-hint.d.ts +9 -1
  24. package/dist/diagnose/install-hint.js +17 -7
  25. package/dist/diagnose/install-retries.d.ts +18 -2
  26. package/dist/diagnose/install-retries.js +50 -3
  27. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  28. package/dist/diagnose/terminal-wrap.js +63 -0
  29. package/dist/host.d.ts +6 -0
  30. package/dist/host.js +3 -0
  31. package/dist/index.d.ts +8 -3
  32. package/dist/index.js +8 -3
  33. package/dist/no-package-json.d.ts +10 -0
  34. package/dist/no-package-json.js +121 -0
  35. package/dist/node-io.d.ts +9 -5
  36. package/dist/node-io.js +25 -7
  37. package/dist/patch/angular.d.ts +68 -0
  38. package/dist/patch/angular.js +216 -0
  39. package/dist/patch/astro-patch.d.ts +48 -13
  40. package/dist/patch/astro-patch.js +151 -43
  41. package/dist/patch/cra.d.ts +0 -2
  42. package/dist/patch/cra.js +0 -2
  43. package/dist/patch/electron-patch.d.ts +10 -1
  44. package/dist/patch/electron-patch.js +79 -27
  45. package/dist/patch/electron-vite-patch.js +1 -1
  46. package/dist/patch/nuxt-patch.d.ts +1 -1
  47. package/dist/patch/nuxt-patch.js +2 -2
  48. package/dist/patch/nuxt-snippets.d.ts +44 -0
  49. package/dist/patch/nuxt-snippets.js +135 -0
  50. package/dist/patch/remix.d.ts +24 -0
  51. package/dist/patch/remix.js +59 -0
  52. package/dist/patch/snippets.d.ts +58 -57
  53. package/dist/patch/snippets.js +181 -202
  54. package/dist/patch/static-page.d.ts +39 -0
  55. package/dist/patch/static-page.js +102 -0
  56. package/dist/patch/tanstack-start.d.ts +36 -8
  57. package/dist/patch/tanstack-start.js +118 -29
  58. package/dist/patch/vite-config.d.ts +5 -0
  59. package/dist/patch/vite-config.js +8 -0
  60. package/dist/patch/vite-env-types.d.ts +9 -0
  61. package/dist/patch/vite-env-types.js +19 -0
  62. package/dist/patch/vite-owning-config.d.ts +47 -0
  63. package/dist/patch/vite-owning-config.js +104 -19
  64. package/dist/plan/connect-steps.d.ts +10 -3
  65. package/dist/plan/connect-steps.js +22 -3
  66. package/dist/plan/framework-adapter.d.ts +5 -6
  67. package/dist/plan/framework-adapter.js +105 -11
  68. package/dist/plan/notices.js +4 -3
  69. package/dist/plan/plan-angular.d.ts +6 -0
  70. package/dist/plan/plan-angular.js +89 -0
  71. package/dist/plan/plan-electron-vite.d.ts +6 -3
  72. package/dist/plan/plan-electron-vite.js +39 -8
  73. package/dist/plan/plan-framework.d.ts +9 -47
  74. package/dist/plan/plan-framework.js +114 -323
  75. package/dist/plan/plan-types.d.ts +47 -2
  76. package/dist/plan/plan-vite.d.ts +67 -0
  77. package/dist/plan/plan-vite.js +256 -0
  78. package/dist/plan/plan.js +155 -69
  79. package/dist/plan/port-steps.d.ts +45 -0
  80. package/dist/plan/port-steps.js +119 -0
  81. package/dist/plan/preflight.d.ts +17 -3
  82. package/dist/plan/preflight.js +53 -23
  83. package/dist/plan/stop-hook-step.d.ts +4 -0
  84. package/dist/plan/stop-hook-step.js +49 -0
  85. package/dist/project/agent-rules.js +2 -0
  86. package/dist/register/mcp-clients.d.ts +13 -0
  87. package/dist/register/mcp-clients.js +22 -0
  88. package/dist/register/mcp.d.ts +26 -5
  89. package/dist/register/mcp.js +64 -8
  90. package/dist/run-types.d.ts +10 -0
  91. package/dist/run.js +132 -78
  92. package/package.json +3 -4
  93. package/NOTICE +0 -5
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The connect snippet, written INTO a static page rather than printed beside it.
3
+ *
4
+ * A page with no package.json has no build, so the pasted `<script type="module">` is the whole
5
+ * install — proven end to end on a page served by `python3 -m http.server`. `init` used to print it
6
+ * and exit 1 with `no_package_json`, which made the one path that works read as a failure, and left
7
+ * the step to a human who then had to be told where to put it.
8
+ *
9
+ * Two things differ from the printed recipe, both because the file is the deployable artifact:
10
+ *
11
+ * - It runs only on a loopback host. A static file has no development build to strip it, so the
12
+ * host is the only line between this snippet and a published page; guarded, a published copy
13
+ * does not even fetch the SDK. The printed bundled-app recipe deliberately does NOT do this
14
+ * (a hosts-file dev alias defeats it, see html-no-build.test.ts) — that recipe has NODE_ENV.
15
+ * - It is marked, so a re-run recognises its own block and never writes a second one.
16
+ * - It carries no pairing token. The token used to be a literal in the page, so committing or
17
+ * publishing index.html published it; it now sits in a module beside the page that `init`
18
+ * gitignores, which the block imports and any static server serves. Absent — a teammate's
19
+ * checkout, a published copy — the connect goes without one, and the bridge says why it refused.
20
+ */
21
+ /** The page a static site serves at its root, and the one file `init` will write the snippet into. */
22
+ export declare const HTML_INDEX_PATH = "index.html";
23
+ /** The gitignored module beside the page that holds this machine's pairing token. */
24
+ export declare const STATIC_TOKEN_MODULE = "reticle.local.js";
25
+ /** Opens the block `init` writes; a re-run looks for it. */
26
+ export declare const STATIC_SNIPPET_MARKER = "<!-- reticle:connect (dev only, written by reticle init) -->";
27
+ /** Whether this page already carries a Reticle connect. */
28
+ export declare function hasStaticSnippet(html: string): boolean;
29
+ /** The token module's content. Rewritten on every run, so a re-minted token is picked up. */
30
+ export declare function staticTokenModule(pairingToken: string): string;
31
+ /** The files to write so the token module exists and git ignores it; only what actually changes. */
32
+ export declare function staticTokenFiles(pairingToken: string | undefined, readFile: (path: string) => string | null): Record<string, string>;
33
+ /**
34
+ * The page with the snippet added, or null when it already connects.
35
+ *
36
+ * Before `</body>` so the SDK starts on a parsed document; before `</head>` for a page with no body
37
+ * tag, and at the end for a fragment with neither.
38
+ */
39
+ export declare function withStaticSnippet(html: string, connectArgLiteral: string): string | null;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The connect snippet, written INTO a static page rather than printed beside it.
3
+ *
4
+ * A page with no package.json has no build, so the pasted `<script type="module">` is the whole
5
+ * install — proven end to end on a page served by `python3 -m http.server`. `init` used to print it
6
+ * and exit 1 with `no_package_json`, which made the one path that works read as a failure, and left
7
+ * the step to a human who then had to be told where to put it.
8
+ *
9
+ * Two things differ from the printed recipe, both because the file is the deployable artifact:
10
+ *
11
+ * - It runs only on a loopback host. A static file has no development build to strip it, so the
12
+ * host is the only line between this snippet and a published page; guarded, a published copy
13
+ * does not even fetch the SDK. The printed bundled-app recipe deliberately does NOT do this
14
+ * (a hosts-file dev alias defeats it, see html-no-build.test.ts) — that recipe has NODE_ENV.
15
+ * - It is marked, so a re-run recognises its own block and never writes a second one.
16
+ * - It carries no pairing token. The token used to be a literal in the page, so committing or
17
+ * publishing index.html published it; it now sits in a module beside the page that `init`
18
+ * gitignores, which the block imports and any static server serves. Absent — a teammate's
19
+ * checkout, a published copy — the connect goes without one, and the bridge says why it refused.
20
+ */
21
+ import { CDN_SDK_URL } from './snippets.js';
22
+ /** The page a static site serves at its root, and the one file `init` will write the snippet into. */
23
+ export const HTML_INDEX_PATH = 'index.html';
24
+ /** The gitignored module beside the page that holds this machine's pairing token. */
25
+ export const STATIC_TOKEN_MODULE = 'reticle.local.js';
26
+ const GITIGNORE_PATH = '.gitignore';
27
+ /** Opens the block `init` writes; a re-run looks for it. */
28
+ export const STATIC_SNIPPET_MARKER = '<!-- reticle:connect (dev only, written by reticle init) -->';
29
+ const STATIC_SNIPPET_END = '<!-- /reticle:connect -->';
30
+ /** Hostnames a developer's own machine serves a page on. */
31
+ const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]'];
32
+ /**
33
+ * Signs that the page already connects, whoever put the snippet there — the printed recipe pasted
34
+ * by hand counts as much as our own block.
35
+ */
36
+ const ALREADY_CONNECTS = [STATIC_SNIPPET_MARKER, '@reticlehq/browser'];
37
+ const BODY_CLOSE = /<\/body>/i;
38
+ const HEAD_CLOSE = /<\/head>/i;
39
+ /** Whether this page already carries a Reticle connect. */
40
+ export function hasStaticSnippet(html) {
41
+ return ALREADY_CONNECTS.some((sign) => html.includes(sign));
42
+ }
43
+ function block(connectArgLiteral) {
44
+ const hosts = LOOPBACK_HOSTS.map((h) => `'${h}'`).join(', ');
45
+ const arg = '' === connectArgLiteral
46
+ ? '{ token: local.token }'
47
+ : `{ ...${connectArgLiteral}, token: local.token }`;
48
+ return ` ${STATIC_SNIPPET_MARKER}
49
+ <script type="module">
50
+ if ([${hosts}].includes(location.hostname)) {
51
+ Promise.all([
52
+ import('${CDN_SDK_URL}'),
53
+ import('./${STATIC_TOKEN_MODULE}').catch(() => ({})),
54
+ ]).then(([{ reticle }, local]) => reticle.connect(${arg}));
55
+ }
56
+ </script>
57
+ ${STATIC_SNIPPET_END}
58
+ `;
59
+ }
60
+ /** The token module's content. Rewritten on every run, so a re-minted token is picked up. */
61
+ export function staticTokenModule(pairingToken) {
62
+ return (`// This machine's Reticle pairing token, written by \`reticle init\`. Gitignored: never commit\n` +
63
+ `// or publish it — each machine runs \`reticle init\` for its own.\n` +
64
+ `export const token = '${pairingToken}';\n`);
65
+ }
66
+ /** The files to write so the token module exists and git ignores it; only what actually changes. */
67
+ export function staticTokenFiles(pairingToken, readFile) {
68
+ if (pairingToken === undefined || 0 === pairingToken.length)
69
+ return {};
70
+ const files = {};
71
+ const module = staticTokenModule(pairingToken);
72
+ if (readFile(STATIC_TOKEN_MODULE) !== module)
73
+ files[STATIC_TOKEN_MODULE] = module;
74
+ const ignore = readFile(GITIGNORE_PATH) ?? '';
75
+ const covered = ignore
76
+ .split('\n')
77
+ .some((l) => [STATIC_TOKEN_MODULE, `/${STATIC_TOKEN_MODULE}`].includes(l.trim()));
78
+ if (!covered) {
79
+ files[GITIGNORE_PATH] =
80
+ `${'' === ignore ? '' : ignore.replace(/\n*$/, '\n')}${STATIC_TOKEN_MODULE}\n`;
81
+ }
82
+ return files;
83
+ }
84
+ /**
85
+ * The page with the snippet added, or null when it already connects.
86
+ *
87
+ * Before `</body>` so the SDK starts on a parsed document; before `</head>` for a page with no body
88
+ * tag, and at the end for a fragment with neither.
89
+ */
90
+ export function withStaticSnippet(html, connectArgLiteral) {
91
+ if (hasStaticSnippet(html))
92
+ return null;
93
+ const snippet = block(connectArgLiteral);
94
+ const anchor = BODY_CLOSE.exec(html) ?? HEAD_CLOSE.exec(html);
95
+ if (null === anchor)
96
+ return `${html.trimEnd()}\n${snippet}`;
97
+ // At the start of the closing tag's own line when it sits alone on one, so its indentation stays
98
+ // with it rather than moving onto the first line of the snippet.
99
+ const lineStart = html.lastIndexOf('\n', anchor.index - 1) + 1;
100
+ const at = '' === html.slice(lineStart, anchor.index).trim() ? lineStart : anchor.index;
101
+ return `${html.slice(0, at)}${snippet}${html.slice(at)}`;
102
+ }
@@ -1,17 +1,45 @@
1
1
  /**
2
- * The TanStack Start connect recipe.
2
+ * The TanStack Start connect: a client-only component, rendered in the document the root route SSRs.
3
3
  *
4
- * Its own module because Start is the one framework whose document module `init` deliberately
5
- * refuses to write, so the recipe and the reasoning behind that refusal are a unit.
4
+ * Its own module because Start is the one framework whose document module is ALSO the server
5
+ * render, so what may be written into it — and what must stay out — is a unit.
6
6
  */
7
+ import { type SourcePatch } from './patch-kind.js';
7
8
  /** TanStack Start's document module — the file that SSRs `<html>`. */
8
9
  export declare const TANSTACK_START_ROOT_PATH = "src/routes/__root.tsx";
9
10
  /**
10
- * The TanStack Start recipe, printed rather than written.
11
+ * Where the connect component goes: beside the `routes/` directory, never inside it — a file under
12
+ * `routes/` is a route to Start's file-based router.
13
+ */
14
+ export declare function tanstackStartConnectPath(rootPath?: string): string;
15
+ /**
16
+ * The connect component.
17
+ *
18
+ * A dynamic SDK import inside `useEffect`, because a static import on anything the root document
19
+ * imports is evaluated during the server render and 500s the page. Effects never run on the server.
20
+ * The pairing token is the one the Vite plugin inlines as `__RETICLE_TOKEN__`, declared here so the
21
+ * file passes the scaffold's strict `tsc` — the recipe this replaced used the name undeclared and
22
+ * failed with `TS2304: Cannot find name '__RETICLE_TOKEN__'`.
23
+ *
24
+ * It also loads the app's `src/reticle-dev.ts` after connecting. Start is written `inject: false`,
25
+ * and the plugin's injected connect is the only other thing that imports that file, so without this
26
+ * line every store and capability registered there silently never ran.
27
+ */
28
+ export declare function tanstackStartConnectFile(port: number | undefined, projectId?: string, rootPath?: string): string;
29
+ /**
30
+ * Render the component in the root document, just before `<Scripts />`.
31
+ *
32
+ * `<Scripts />` is the anchor because every Start root has to render it — it is how the client
33
+ * bundle reaches the page — whatever the component around it is called (`RootDocument` as the
34
+ * route's `shellComponent` in the current template; `RootComponent` in older ones). A root without
35
+ * one is a shape we do not recognise, and a half-edited document is worse than a documented step.
36
+ */
37
+ export declare function patchTanstackStartRoot(source: string, rootPath: string): SourcePatch;
38
+ /**
39
+ * The recipe, for a root `patchTanstackStartRoot` would not edit.
11
40
  *
12
- * `__root.tsx` is the document. A static import of the SDK on that module SSRs and 500s, so `init`
13
- * does not write one. The connect has to be a client-only dynamic import inside `useEffect`, with
14
- * the pairing token the plugin inlines as `__RETICLE_TOKEN__`. Same judgement React Router already
15
- * makes about `app/entry.client.tsx`: a half-written document is worse than a documented manual step.
41
+ * The same two edits it makes, stated as edits: create the component, render it before
42
+ * `<Scripts />`. It used to say "add this useEffect to the App component", naming a component the
43
+ * current template does not have, and its snippet used `__RETICLE_TOKEN__` undeclared.
16
44
  */
17
45
  export declare function tanstackStartManual(port: number | undefined, projectId?: string, rootPath?: string): string;
@@ -1,52 +1,141 @@
1
1
  /**
2
- * The TanStack Start connect recipe.
2
+ * The TanStack Start connect: a client-only component, rendered in the document the root route SSRs.
3
3
  *
4
- * Its own module because Start is the one framework whose document module `init` deliberately
5
- * refuses to write, so the recipe and the reasoning behind that refusal are a unit.
4
+ * Its own module because Start is the one framework whose document module is ALSO the server
5
+ * render, so what may be written into it — and what must stay out — is a unit.
6
6
  */
7
+ import { posix } from 'node:path';
7
8
  import { UiLibrary } from '../detect/detect.js';
8
- import { connectArg, sdkImport, UNVERIFIED_TANSTACK_START_NOTE } from './snippets.js';
9
+ import { connectArg, devModuleSpecifier, sdkImport, UNVERIFIED_TANSTACK_START_NOTE, } from './snippets.js';
10
+ import { PatchKind } from './patch-kind.js';
11
+ import { VITE_ENV_DEV_DECLARATION } from './vite-env-types.js';
9
12
  /** TanStack Start's document module — the file that SSRs `<html>`. */
10
13
  export const TANSTACK_START_ROOT_PATH = 'src/routes/__root.tsx';
14
+ /** The component `init` writes, and renders in the root document. */
15
+ const CONNECT_COMPONENT = 'ReticleConnect';
16
+ const CONNECT_FILE_BASENAME = 'reticle-connect';
17
+ /** `<Scripts />` — Start's own client-script outlet, which every root document has to render. */
18
+ const SCRIPTS_OUTLET = /^([ \t]*)<Scripts\s*\/>/m;
11
19
  /**
12
- * The TanStack Start recipe, printed rather than written.
20
+ * Where the connect component goes: beside the `routes/` directory, never inside it — a file under
21
+ * `routes/` is a route to Start's file-based router.
22
+ */
23
+ export function tanstackStartConnectPath(rootPath = TANSTACK_START_ROOT_PATH) {
24
+ return posix.join(posix.dirname(posix.dirname(rootPath)), `${CONNECT_FILE_BASENAME}.tsx`);
25
+ }
26
+ /**
27
+ * The connect component.
28
+ *
29
+ * A dynamic SDK import inside `useEffect`, because a static import on anything the root document
30
+ * imports is evaluated during the server render and 500s the page. Effects never run on the server.
31
+ * The pairing token is the one the Vite plugin inlines as `__RETICLE_TOKEN__`, declared here so the
32
+ * file passes the scaffold's strict `tsc` — the recipe this replaced used the name undeclared and
33
+ * failed with `TS2304: Cannot find name '__RETICLE_TOKEN__'`.
13
34
  *
14
- * `__root.tsx` is the document. A static import of the SDK on that module SSRs and 500s, so `init`
15
- * does not write one. The connect has to be a client-only dynamic import inside `useEffect`, with
16
- * the pairing token the plugin inlines as `__RETICLE_TOKEN__`. Same judgement React Router already
17
- * makes about `app/entry.client.tsx`: a half-written document is worse than a documented manual step.
35
+ * It also loads the app's `src/reticle-dev.ts` after connecting. Start is written `inject: false`,
36
+ * and the plugin's injected connect is the only other thing that imports that file, so without this
37
+ * line every store and capability registered there silently never ran.
18
38
  */
19
- export function tanstackStartManual(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
39
+ export function tanstackStartConnectFile(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
20
40
  const sdk = sdkImport(UiLibrary.REACT);
21
41
  const base = connectArg(port, projectId);
22
42
  const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
23
- const installLine = sdk.usesInstall
24
- ? ' install();'
25
- : ' // No React adapter here: the sensor has no install() to call.';
26
43
  const imports = sdk.usesInstall ? 'reticle, install' : 'reticle';
44
+ const installLine = sdk.usesInstall
45
+ ? ' install();'
46
+ : ' // No React adapter here: the sensor has no install() to call.';
47
+ const devModule = devModuleSpecifier(tanstackStartConnectPath(rootPath));
48
+ return `import { useEffect } from 'react';
49
+
50
+ /**
51
+ * Dev-only: connect Reticle from the client. Start SSRs its own <html> and never sends Vite's
52
+ * index.html, so the plugin's injection cannot do it. Rendered once in the root document.
53
+ */
54
+ export function ${CONNECT_COMPONENT}() {
55
+ useEffect(() => {
56
+ if (!import.meta.env.DEV) return;
57
+ // Inlined by @reticlehq/vite-plugin. The bridge refuses a connect without it, even on localhost.
58
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
59
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
60
+ void import('${sdk.specifier}').then(({ ${imports} }) => {
61
+ ${installLine}
62
+ reticle.connect({
63
+ ${fields}...(token.length > 0 ? { token } : {}),
64
+ ...(root.length > 0 ? { root } : {}),
65
+ });
66
+ // Your stores and capabilities live here; nothing else loads this file under Start.
67
+ void import('${devModule}');
68
+ });
69
+ }, []);
70
+ return null;
71
+ }
72
+
73
+ declare const __RETICLE_TOKEN__: string | undefined;
74
+ declare const __RETICLE_ROOT__: string | undefined;
75
+
76
+ ${VITE_ENV_DEV_DECLARATION}`;
77
+ }
78
+ /** The import the root document gets, relative to where it lives. */
79
+ function connectImport(rootPath) {
80
+ const target = tanstackStartConnectPath(rootPath).replace(/\.tsx$/, '');
81
+ const rel = posix.relative(posix.dirname(rootPath), target);
82
+ return `import { ${CONNECT_COMPONENT} } from '${rel.startsWith('.') ? rel : `./${rel}`}';`;
83
+ }
84
+ /**
85
+ * Render the component in the root document, just before `<Scripts />`.
86
+ *
87
+ * `<Scripts />` is the anchor because every Start root has to render it — it is how the client
88
+ * bundle reaches the page — whatever the component around it is called (`RootDocument` as the
89
+ * route's `shellComponent` in the current template; `RootComponent` in older ones). A root without
90
+ * one is a shape we do not recognise, and a half-edited document is worse than a documented step.
91
+ */
92
+ export function patchTanstackStartRoot(source, rootPath) {
93
+ if (source.includes(CONNECT_COMPONENT))
94
+ return { kind: PatchKind.ALREADY };
95
+ if (!rootPath.endsWith('.tsx')) {
96
+ return { kind: PatchKind.MANUAL, reason: `${rootPath} is not TypeScript` };
97
+ }
98
+ const outlet = SCRIPTS_OUTLET.exec(source);
99
+ if (null === outlet) {
100
+ return { kind: PatchKind.MANUAL, reason: `no <Scripts /> found in ${rootPath}` };
101
+ }
102
+ const indent = outlet[1] ?? '';
103
+ const rendered = source.replace(SCRIPTS_OUTLET, `${indent}<${CONNECT_COMPONENT} />\n$&`);
104
+ const lines = rendered.split('\n');
105
+ let lastImport = -1;
106
+ for (const [index, line] of lines.entries()) {
107
+ if (/^import\s/.test(line))
108
+ lastImport = index;
109
+ }
110
+ lines.splice(lastImport + 1, 0, connectImport(rootPath));
111
+ return { kind: PatchKind.APPLY, code: lines.join('\n') };
112
+ }
113
+ /**
114
+ * The recipe, for a root `patchTanstackStartRoot` would not edit.
115
+ *
116
+ * The same two edits it makes, stated as edits: create the component, render it before
117
+ * `<Scripts />`. It used to say "add this useEffect to the App component", naming a component the
118
+ * current template does not have, and its snippet used `__RETICLE_TOKEN__` undeclared.
119
+ */
120
+ export function tanstackStartManual(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
121
+ const component = tanstackStartConnectFile(port, projectId, rootPath)
122
+ .split('\n')
123
+ .map((line) => (0 === line.length ? line : ` ${line}`))
124
+ .join('\n');
27
125
  return `TanStack Start SSRs <html> from ${rootPath} (HeadContent / Scripts) and never sends Vite's
28
126
  index.html, so the plugin's connect injection never fires. Keep the plugin anyway, with
29
127
  reticle({ inject: false }): only the injection half is inapplicable, and the stamping half is
30
128
  what puts data-reticle-source on the JSX. Dropping the plugin because one of its two jobs did
31
129
  not apply is how Remix lost file:line.
32
130
 
33
- Connect from a CLIENT-ONLY effect in ${rootPath}. A static SDK import on that module SSRs and
34
- 500s. Do not guard on window.location.hostname === 'localhost' — import.meta.env.DEV is the
35
- correct guard, and it does not care what host you develop on.
36
-
37
- Add this useEffect to the App component (import useEffect from react if it is not already there):
131
+ 1. Create ${tanstackStartConnectPath(rootPath)}. It connects from a CLIENT-ONLY effect: a static SDK
132
+ import on anything the root document imports SSRs and 500s.
133
+ Do not guard on window.location.hostname === 'localhost' — import.meta.env.DEV is the correct
134
+ guard, and it does not care what host you develop on.
38
135
 
39
- useEffect(() => {
40
- if (import.meta.env.DEV) {
41
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
42
- void import('${sdk.specifier}').then(({ ${imports} }) => {
43
- ${installLine}
44
- reticle.connect({
45
- ${fields}...(token.length > 0 ? { token } : {}),
46
- });
47
- });
48
- }
49
- }, []);
136
+ ${component}
137
+ 2. In ${rootPath}, add \`${connectImport(rootPath)}\` and render <${CONNECT_COMPONENT} /> in the
138
+ component that renders <html> (the route's shellComponent), just before <Scripts />.
50
139
 
51
140
  The plugin inlines __RETICLE_TOKEN__ when Vite resolves its config. Start the daemon BEFORE the
52
141
  dev server so that file exists; a server that started first froze an empty token and the page
@@ -14,5 +14,10 @@ export declare const VitePatchKind: {
14
14
  };
15
15
  export type VitePatchKind = PatchKind;
16
16
  type VitePatch = SourcePatch;
17
+ /**
18
+ * A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
19
+ * rather than patched from an empty one, because it is a file the person will open and read.
20
+ */
21
+ export declare function newViteConfig(port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): string;
17
22
  export declare function patchViteConfig(source: string, port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): VitePatch;
18
23
  export {};
@@ -98,6 +98,14 @@ function insertPluginsKey(source, call) {
98
98
  return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
99
99
  });
100
100
  }
101
+ /**
102
+ * A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
103
+ * rather than patched from an empty one, because it is a file the person will open and read.
104
+ */
105
+ export function newViteConfig(port, captureBodies = false, inject = true, sourceMapping = true) {
106
+ const call = reticlePluginCall({ port, captureBodies, inject, sourceMapping });
107
+ return `import { defineConfig } from 'vite';\n${VITE_IMPORT}\n\nexport default defineConfig({\n plugins: [${call}],\n});\n`;
108
+ }
101
109
  export function patchViteConfig(source, port, captureBodies = false, inject = true, sourceMapping = true) {
102
110
  if (source.includes(RETICLE_MARKER)) {
103
111
  return { kind: VitePatchKind.ALREADY };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The generated dev module must compile even when the host excludes `vite/client` ambient types.
3
+ * Keep the guard itself as `import.meta.env.DEV` so Vite can remove dev-only code from production.
4
+ *
5
+ * These declarations must merge with Vite's existing interfaces: DEV is mutable there, while env
6
+ * is readonly and refers to the named ImportMetaEnv interface. A triple-slash package reference
7
+ * would also require vite to be directly resolvable, which electron-vite-only projects need not have.
8
+ */
9
+ export declare const VITE_ENV_DEV_DECLARATION = "// Supply the dev flag type when the host does not include vite/client.\n// These interfaces merge with vite/client when it is already present.\ndeclare global {\n interface ImportMetaEnv {\n DEV: boolean;\n }\n interface ImportMeta {\n readonly env: ImportMetaEnv;\n }\n}\n";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The generated dev module must compile even when the host excludes `vite/client` ambient types.
3
+ * Keep the guard itself as `import.meta.env.DEV` so Vite can remove dev-only code from production.
4
+ *
5
+ * These declarations must merge with Vite's existing interfaces: DEV is mutable there, while env
6
+ * is readonly and refers to the named ImportMetaEnv interface. A triple-slash package reference
7
+ * would also require vite to be directly resolvable, which electron-vite-only projects need not have.
8
+ */
9
+ export const VITE_ENV_DEV_DECLARATION = `// Supply the dev flag type when the host does not include vite/client.
10
+ // These interfaces merge with vite/client when it is already present.
11
+ declare global {
12
+ interface ImportMetaEnv {
13
+ DEV: boolean;
14
+ }
15
+ interface ImportMeta {
16
+ readonly env: ImportMetaEnv;
17
+ }
18
+ }
19
+ `;
@@ -21,5 +21,52 @@ export interface ViteOwningConfig {
21
21
  readonly defineCall: RegExp;
22
22
  /** The SDK package this framework's connect imports, declared so Vite pre-bundles it. */
23
23
  readonly sdkSpecifier: string;
24
+ /**
25
+ * When true (default), inline the pairing token and project root via `vite.define`.
26
+ *
27
+ * Nuxt still needs this: its client plugin reads `__RETICLE_TOKEN__` from the define. Astro 7.2+
28
+ * no longer substitutes `define` into the client pipeline (#1008), so Astro sets this false and
29
+ * puts the token on a frontmatter `<meta>` instead.
30
+ */
31
+ readonly defineToken?: boolean;
32
+ /**
33
+ * Present once this patch has applied. Defaults to `__RETICLE_TOKEN__`, which is what the define
34
+ * path writes. Astro's path never writes that name into the config, so it supplies its own.
35
+ */
36
+ readonly alreadyMarker?: string;
37
+ /**
38
+ * The installed Vite's major (see `installedViteMajor`), or null when nothing on disk said.
39
+ *
40
+ * Decides whether the dep-optimizer target is written at all. Vite 7 moved the optimizer to
41
+ * rolldown and deprecated `optimizeDeps.esbuildOptions`: on Nuxt 4.5 / Vite 8 the key printed a
42
+ * deprecation warning on every dev start and build. The target exists for esbuild, which cannot
43
+ * lower some of the SDK's syntax to Vite's older default; rolldown has no such gap, so on 7+ there
44
+ * is nothing to write under either key.
45
+ */
46
+ readonly viteMajor?: number | null | undefined;
24
47
  }
25
48
  export declare function patchViteOwningConfig(source: string, config: ViteOwningConfig): SourcePatch;
49
+ /**
50
+ * The major version of the Vite this app will actually run, read from disk.
51
+ *
52
+ * Needed by the configs `init` patches for frameworks that own their Vite instance (Nuxt, Astro):
53
+ * Vite 7 moved the dep optimizer to rolldown and deprecated `optimizeDeps.esbuildOptions`, so the
54
+ * key `init` wrote printed a deprecation warning on every dev start and build there. The Vite
55
+ * plugin answers the same question for itself (`optimizerOptionsKey` in @reticlehq/vite-plugin),
56
+ * but these frameworks never load the plugin.
57
+ *
58
+ * Nuxt and Astro do not list `vite` in the app's package.json — it comes with the framework — so the
59
+ * declared range is the last resort, after the hoisted package and pnpm's store. Null when none
60
+ * says: the caller keeps the older key, as the plugin does, because a deprecation notice is a much
61
+ * smaller failure than an option the installed Vite has never heard of.
62
+ */
63
+ interface ViteReader {
64
+ readFile(relPath: string): string | null;
65
+ listDirs(relPath: string): readonly string[];
66
+ }
67
+ interface DeclaredDeps {
68
+ dependencies?: Record<string, string>;
69
+ devDependencies?: Record<string, string>;
70
+ }
71
+ export declare function installedViteMajor(io: ViteReader, pkg: DeclaredDeps): number | null;
72
+ export {};