@reticlehq/init 2.14.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/LICENSE +105 -0
- package/NOTICE +5 -0
- package/README.md +29 -0
- package/dist/agent-rules.d.ts +116 -0
- package/dist/agent-rules.js +279 -0
- package/dist/app-choice.d.ts +29 -0
- package/dist/app-choice.js +39 -0
- package/dist/astro-host.d.ts +21 -0
- package/dist/astro-host.js +27 -0
- package/dist/astro-patch.d.ts +33 -0
- package/dist/astro-patch.js +103 -0
- package/dist/brace-scan.d.ts +24 -0
- package/dist/brace-scan.js +46 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.js +167 -0
- package/dist/closing-hint.d.ts +35 -0
- package/dist/closing-hint.js +122 -0
- package/dist/codex-toml.d.ts +10 -0
- package/dist/codex-toml.js +144 -0
- package/dist/connect-steps.d.ts +55 -0
- package/dist/connect-steps.js +92 -0
- package/dist/cra.d.ts +82 -0
- package/dist/cra.js +183 -0
- package/dist/csp-check.d.ts +49 -0
- package/dist/csp-check.js +168 -0
- package/dist/csp-doctor.d.ts +32 -0
- package/dist/csp-doctor.js +62 -0
- package/dist/desktop-doctor.d.ts +46 -0
- package/dist/desktop-doctor.js +201 -0
- package/dist/detect.d.ts +225 -0
- package/dist/detect.js +334 -0
- package/dist/dev-script.d.ts +51 -0
- package/dist/dev-script.js +66 -0
- package/dist/dev-server-ports.d.ts +20 -0
- package/dist/dev-server-ports.js +45 -0
- package/dist/electron-patch.d.ts +11 -0
- package/dist/electron-patch.js +70 -0
- package/dist/electron-vite-patch.d.ts +12 -0
- package/dist/electron-vite-patch.js +100 -0
- package/dist/existing-config.d.ts +31 -0
- package/dist/existing-config.js +63 -0
- package/dist/format-generated.d.ts +18 -0
- package/dist/format-generated.js +48 -0
- package/dist/framework-adapter.d.ts +60 -0
- package/dist/framework-adapter.js +138 -0
- package/dist/host.d.ts +41 -0
- package/dist/host.js +21 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/init-failure.d.ts +19 -0
- package/dist/init-failure.js +18 -0
- package/dist/init-opt-out.d.ts +17 -0
- package/dist/init-opt-out.js +30 -0
- package/dist/install-hint.d.ts +9 -0
- package/dist/install-hint.js +41 -0
- package/dist/install-retries.d.ts +22 -0
- package/dist/install-retries.js +65 -0
- package/dist/install-source-config.d.ts +18 -0
- package/dist/install-source-config.js +34 -0
- package/dist/lockfiles.d.ts +14 -0
- package/dist/lockfiles.js +49 -0
- package/dist/mcp-clients.d.ts +94 -0
- package/dist/mcp-clients.js +327 -0
- package/dist/mcp-registered.d.ts +12 -0
- package/dist/mcp-registered.js +14 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +129 -0
- package/dist/next-patch.d.ts +49 -0
- package/dist/next-patch.js +237 -0
- package/dist/node-io.d.ts +22 -0
- package/dist/node-io.js +150 -0
- package/dist/non-js-project.d.ts +44 -0
- package/dist/non-js-project.js +137 -0
- package/dist/nuxt-patch.d.ts +18 -0
- package/dist/nuxt-patch.js +33 -0
- package/dist/patch-kind.d.ts +20 -0
- package/dist/patch-kind.js +10 -0
- package/dist/plan-electron-vite.d.ts +16 -0
- package/dist/plan-electron-vite.js +72 -0
- package/dist/plan-framework.d.ts +122 -0
- package/dist/plan-framework.js +773 -0
- package/dist/plan.d.ts +312 -0
- package/dist/plan.js +650 -0
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/preflight.d.ts +30 -0
- package/dist/preflight.js +52 -0
- package/dist/project-id.d.ts +12 -0
- package/dist/project-id.js +25 -0
- package/dist/refresh-rules.d.ts +40 -0
- package/dist/refresh-rules.js +51 -0
- package/dist/remember-project.d.ts +24 -0
- package/dist/remember-project.js +40 -0
- package/dist/run.d.ts +122 -0
- package/dist/run.js +762 -0
- package/dist/slash-command.d.ts +27 -0
- package/dist/slash-command.js +132 -0
- package/dist/snippets.d.ts +267 -0
- package/dist/snippets.js +898 -0
- package/dist/tanstack-start.d.ts +18 -0
- package/dist/tanstack-start.js +57 -0
- package/dist/version.d.ts +18 -0
- package/dist/version.js +23 -0
- package/dist/vite-config.d.ts +18 -0
- package/dist/vite-config.js +111 -0
- package/dist/vite-owning-config.d.ts +25 -0
- package/dist/vite-owning-config.js +219 -0
- package/dist/windows-quote.d.ts +38 -0
- package/dist/windows-quote.js +70 -0
- package/dist/workspace-apps.d.ts +40 -0
- package/dist/workspace-apps.js +188 -0
- package/dist/workspace-redirect.d.ts +22 -0
- package/dist/workspace-redirect.js +97 -0
- package/package.json +53 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TanStack Start connect recipe.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than another entry in `snippets.ts`, which was at the 1000-line backstop:
|
|
5
|
+
* Start is the one framework whose document module `init` deliberately refuses to write, so the
|
|
6
|
+
* recipe and the reasoning behind that refusal are a unit.
|
|
7
|
+
*/
|
|
8
|
+
/** TanStack Start's document module — the file that SSRs `<html>`. */
|
|
9
|
+
export declare const TANSTACK_START_ROOT_PATH = "src/routes/__root.tsx";
|
|
10
|
+
/**
|
|
11
|
+
* The TanStack Start recipe, printed rather than written.
|
|
12
|
+
*
|
|
13
|
+
* `__root.tsx` is the document. A static import of the SDK on that module SSRs and 500s, so `init`
|
|
14
|
+
* does not write one. The connect has to be a client-only dynamic import inside `useEffect`, with
|
|
15
|
+
* the pairing token the plugin inlines as `__RETICLE_TOKEN__`. Same judgement React Router already
|
|
16
|
+
* makes about `app/entry.client.tsx`: a half-written document is worse than a documented manual step.
|
|
17
|
+
*/
|
|
18
|
+
export declare function tanstackStartManual(port: number | undefined, projectId?: string, rootPath?: string): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TanStack Start connect recipe.
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than another entry in `snippets.ts`, which was at the 1000-line backstop:
|
|
5
|
+
* Start is the one framework whose document module `init` deliberately refuses to write, so the
|
|
6
|
+
* recipe and the reasoning behind that refusal are a unit.
|
|
7
|
+
*/
|
|
8
|
+
import { UiLibrary } from './detect.js';
|
|
9
|
+
import { connectArg, sdkImport, UNVERIFIED_TANSTACK_START_NOTE } from './snippets.js';
|
|
10
|
+
/** TanStack Start's document module — the file that SSRs `<html>`. */
|
|
11
|
+
export const TANSTACK_START_ROOT_PATH = 'src/routes/__root.tsx';
|
|
12
|
+
/**
|
|
13
|
+
* The TanStack Start recipe, printed rather than written.
|
|
14
|
+
*
|
|
15
|
+
* `__root.tsx` is the document. A static import of the SDK on that module SSRs and 500s, so `init`
|
|
16
|
+
* does not write one. The connect has to be a client-only dynamic import inside `useEffect`, with
|
|
17
|
+
* the pairing token the plugin inlines as `__RETICLE_TOKEN__`. Same judgement React Router already
|
|
18
|
+
* makes about `app/entry.client.tsx`: a half-written document is worse than a documented manual step.
|
|
19
|
+
*/
|
|
20
|
+
export function tanstackStartManual(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
|
|
21
|
+
const sdk = sdkImport(UiLibrary.REACT);
|
|
22
|
+
const base = connectArg(port, projectId);
|
|
23
|
+
const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
|
|
24
|
+
const installLine = sdk.usesInstall
|
|
25
|
+
? ' install();'
|
|
26
|
+
: ' // No React adapter here: the sensor has no install() to call.';
|
|
27
|
+
const imports = sdk.usesInstall ? 'reticle, install' : 'reticle';
|
|
28
|
+
return `TanStack Start SSRs <html> from ${rootPath} (HeadContent / Scripts) and never sends Vite's
|
|
29
|
+
index.html, so the plugin's connect injection never fires. Keep the plugin anyway, with
|
|
30
|
+
reticle({ inject: false }): only the injection half is inapplicable, and the stamping half is
|
|
31
|
+
what puts data-reticle-source on the JSX. Dropping the plugin because one of its two jobs did
|
|
32
|
+
not apply is how Remix lost file:line.
|
|
33
|
+
|
|
34
|
+
Connect from a CLIENT-ONLY effect in ${rootPath}. A static SDK import on that module SSRs and
|
|
35
|
+
500s. Do not guard on window.location.hostname === 'localhost' — import.meta.env.DEV is the
|
|
36
|
+
correct guard, and it does not care what host you develop on.
|
|
37
|
+
|
|
38
|
+
Add this useEffect to the App component (import useEffect from react if it is not already there):
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (import.meta.env.DEV) {
|
|
42
|
+
const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
|
|
43
|
+
void import('${sdk.specifier}').then(({ ${imports} }) => {
|
|
44
|
+
${installLine}
|
|
45
|
+
reticle.connect({
|
|
46
|
+
${fields}...(token.length > 0 ? { token } : {}),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
The plugin inlines __RETICLE_TOKEN__ when Vite resolves its config. Start the daemon BEFORE the
|
|
53
|
+
dev server so that file exists; a server that started first froze an empty token and the page
|
|
54
|
+
looks like it "won't dial". Restart after init, and after the daemon is up.
|
|
55
|
+
|
|
56
|
+
${UNVERIFIED_TANSTACK_START_NOTE}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release this scaffolder belongs to, read from its own package.json at startup.
|
|
3
|
+
*
|
|
4
|
+
* Read here rather than injected because it is a MODULE constant in three generators
|
|
5
|
+
* (`snippets.ts`, `agent-rules.ts`, `mcp.ts`) — the CDN snippet pins `@reticlehq/browser@<this>`
|
|
6
|
+
* and cannot be built without it. Every published `@reticlehq/*` package shares one version, and
|
|
7
|
+
* `@reticlehq/server` depends on this one by exact version, so what a user resolves here is by
|
|
8
|
+
* construction the version of the server that invoked it. `version-lockstep.test.ts` in the server
|
|
9
|
+
* fails the fast gate if the two manifests ever disagree.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RETICLE_VERSION: string;
|
|
12
|
+
/**
|
|
13
|
+
* The published npm package that carries the `reticle` bin — the thing this scaffolder tells users
|
|
14
|
+
* and agents to run. NOT this package's own name: `@reticlehq/init` has no bin, and a self-update
|
|
15
|
+
* or an `npx` line pointing at it installs a library nobody can execute. Pinned as a constant and
|
|
16
|
+
* checked against `@reticlehq/server`'s manifest by `version-lockstep.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RETICLE_NPM_PACKAGE = "@reticlehq/server";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// NOTE: a RUNTIME path, not an import — `tsc` does not check it and a file move will not rewrite
|
|
3
|
+
// it. One level up because this file sits at the package src root: from `src/` and from `dist/`
|
|
4
|
+
// alike, `../package.json` is the package manifest.
|
|
5
|
+
const _pkg = createRequire(import.meta.url)('../package.json');
|
|
6
|
+
/**
|
|
7
|
+
* The release this scaffolder belongs to, read from its own package.json at startup.
|
|
8
|
+
*
|
|
9
|
+
* Read here rather than injected because it is a MODULE constant in three generators
|
|
10
|
+
* (`snippets.ts`, `agent-rules.ts`, `mcp.ts`) — the CDN snippet pins `@reticlehq/browser@<this>`
|
|
11
|
+
* and cannot be built without it. Every published `@reticlehq/*` package shares one version, and
|
|
12
|
+
* `@reticlehq/server` depends on this one by exact version, so what a user resolves here is by
|
|
13
|
+
* construction the version of the server that invoked it. `version-lockstep.test.ts` in the server
|
|
14
|
+
* fails the fast gate if the two manifests ever disagree.
|
|
15
|
+
*/
|
|
16
|
+
export const RETICLE_VERSION = _pkg.version;
|
|
17
|
+
/**
|
|
18
|
+
* The published npm package that carries the `reticle` bin — the thing this scaffolder tells users
|
|
19
|
+
* and agents to run. NOT this package's own name: `@reticlehq/init` has no bin, and a self-update
|
|
20
|
+
* or an `npx` line pointing at it installs a library nobody can execute. Pinned as a constant and
|
|
21
|
+
* checked against `@reticlehq/server`'s manifest by `version-lockstep.test.ts`.
|
|
22
|
+
*/
|
|
23
|
+
export const RETICLE_NPM_PACKAGE = '@reticlehq/server';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, conservative patcher for a Vite config: add the `@reticlehq/vite-plugin` import and drop
|
|
3
|
+
* `reticle` into the `plugins` array. Only handles the obvious, common shape — anything ambiguous
|
|
4
|
+
* bails to a `manual` result so we never half-edit a build config (a broken config is worse than a
|
|
5
|
+
* documented manual step).
|
|
6
|
+
*/
|
|
7
|
+
import { PatchKind, type SourcePatch } from './patch-kind.js';
|
|
8
|
+
export declare const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
9
|
+
/** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
|
|
10
|
+
export declare const VitePatchKind: {
|
|
11
|
+
readonly APPLY: "apply";
|
|
12
|
+
readonly ALREADY: "already";
|
|
13
|
+
readonly MANUAL: "manual";
|
|
14
|
+
};
|
|
15
|
+
export type VitePatchKind = PatchKind;
|
|
16
|
+
type VitePatch = SourcePatch;
|
|
17
|
+
export declare function patchViteConfig(source: string, port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): VitePatch;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, conservative patcher for a Vite config: add the `@reticlehq/vite-plugin` import and drop
|
|
3
|
+
* `reticle` into the `plugins` array. Only handles the obvious, common shape — anything ambiguous
|
|
4
|
+
* bails to a `manual` result so we never half-edit a build config (a broken config is worse than a
|
|
5
|
+
* documented manual step).
|
|
6
|
+
*/
|
|
7
|
+
import { PatchKind } from './patch-kind.js';
|
|
8
|
+
export const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
9
|
+
const RETICLE_MARKER = '@reticlehq/vite-plugin';
|
|
10
|
+
/**
|
|
11
|
+
* The `reticle(...)` call — the bridge port so the injected connect targets it.
|
|
12
|
+
*
|
|
13
|
+
* **`captureNetworkBodies` is OPT-IN.** It was written in by default, and the argument for that was
|
|
14
|
+
* a good one: without a body, a write that answers 2xx grades `unknown / outcome_unread`, because a
|
|
15
|
+
* 200 describes the transport and not the result. Measured on a real payments UI — a refund posted
|
|
16
|
+
* rupees into a paise field, the server answered 200 having refunded a hundredth of it, the page
|
|
17
|
+
* rendered the amount the user had typed, and every DOM-level check passed.
|
|
18
|
+
*
|
|
19
|
+
* That argument justifies the CAPABILITY. It does not justify the default, and the distinction is
|
|
20
|
+
* what #705 is about. A healthcare workspace proxying authenticated API traffic through Vite ran
|
|
21
|
+
* `init`, and on the first drive their login tokens and patient payloads were in the daemon's
|
|
22
|
+
* buffer. The old comment here conceded the exact gap — "the credential classes are redacted ... but
|
|
23
|
+
* an address or an email is not, and nothing here should decide that for someone silently" — and
|
|
24
|
+
* then decided it for them, because writing the line into their config IS deciding. `init` is run
|
|
25
|
+
* unattended by an agent; the person who knows the data is sensitive is not in the room.
|
|
26
|
+
*
|
|
27
|
+
* So the default is off and the ways in are all deliberate:
|
|
28
|
+
* - `reticle init --capture-bodies` writes the line, for someone who has decided
|
|
29
|
+
* - `VITE_RETICLE_CAPTURE_BODIES=1` turns it on for ONE dev-server run, no config edit
|
|
30
|
+
* - adding the option by hand, which is what the tools tell you to do
|
|
31
|
+
*
|
|
32
|
+
* Nothing goes quiet in exchange. Every tool that needs a body already says so when it is missing
|
|
33
|
+
* and names the option — see honesty/uncaptured-bodies.ts, honesty/verified.ts, predicate-eval.ts
|
|
34
|
+
* and reconcile-tools.ts — so the capability is discovered at the moment it is wanted, by the person
|
|
35
|
+
* who wanted it, instead of being switched on before anyone has asked.
|
|
36
|
+
*/
|
|
37
|
+
function reticlePluginCall(opts) {
|
|
38
|
+
const options = [
|
|
39
|
+
...(opts.port === undefined ? [] : [`port: ${String(opts.port)}`]),
|
|
40
|
+
...(false === opts.inject ? ['inject: false'] : []),
|
|
41
|
+
...(opts.captureBodies ? ['captureNetworkBodies: true'] : []),
|
|
42
|
+
...(opts.sourceMapping ? [] : ['sourceMapping: false']),
|
|
43
|
+
];
|
|
44
|
+
return 0 === options.length ? 'reticle()' : `reticle({ ${options.join(', ')} })`;
|
|
45
|
+
}
|
|
46
|
+
/** Matches the start of a `plugins: [` array literal. */
|
|
47
|
+
const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
|
|
48
|
+
/** Matches an ES import statement (used to place our import after the last one). */
|
|
49
|
+
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
50
|
+
/**
|
|
51
|
+
* The opening `{` of the exported config object — `defineConfig({`, or a bare `export default {`.
|
|
52
|
+
* Used only when there is no `plugins` array to extend: the object is right there, so adding the
|
|
53
|
+
* key is the same edit as extending the array, and bailing sent a user whose config merely set
|
|
54
|
+
* `server.port` to a manual paste for a change we can make correctly.
|
|
55
|
+
*
|
|
56
|
+
* A config built by a call (`defineConfig(buildOptions())`) has no literal to extend and still
|
|
57
|
+
* bails — the rule stays "only edit a shape we can see whole".
|
|
58
|
+
*/
|
|
59
|
+
const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
|
|
60
|
+
/** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
|
|
61
|
+
export const VitePatchKind = PatchKind;
|
|
62
|
+
const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
|
|
63
|
+
function insertImport(source) {
|
|
64
|
+
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
65
|
+
const last = matches[matches.length - 1];
|
|
66
|
+
if (last?.index === undefined) {
|
|
67
|
+
return `${VITE_IMPORT}\n${source}`;
|
|
68
|
+
}
|
|
69
|
+
const end = last.index + last[0].length;
|
|
70
|
+
return `${source.slice(0, end)}\n${VITE_IMPORT}${source.slice(end)}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Insert right after the opening `[` of the plugins array, spaced the way the surrounding line is.
|
|
74
|
+
*
|
|
75
|
+
* A multi-line array puts a newline next, and `[reticle(), \n` leaves trailing whitespace — exactly
|
|
76
|
+
* what a formatter rewrites, turning a one-line install into a diff against the user's own style. A
|
|
77
|
+
* single-line array needs the space, or the result reads `[reticle(),react()]`.
|
|
78
|
+
*/
|
|
79
|
+
function insertPlugin(source, call) {
|
|
80
|
+
return source.replace(PLUGINS_ARRAY, (match, _g, offset) => {
|
|
81
|
+
const next = source[offset + match.length] ?? '';
|
|
82
|
+
const separator = '' === next || /\s/.test(next) ? '' : ' ';
|
|
83
|
+
return `${match}${call},${separator}`;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
|
|
88
|
+
* the object it lands in: a multi-line object gets its own indented line, a one-liner stays inline.
|
|
89
|
+
*/
|
|
90
|
+
function insertPluginsKey(source, call) {
|
|
91
|
+
return source.replace(CONFIG_OBJECT, (_match, prefix, offset) => {
|
|
92
|
+
const rest = source.slice(offset + _match.length);
|
|
93
|
+
const multiline = /^\s*\n/.test(rest);
|
|
94
|
+
const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
|
|
95
|
+
const key = `plugins: [${call}],`;
|
|
96
|
+
return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function patchViteConfig(source, port, captureBodies = false, inject = true, sourceMapping = true) {
|
|
100
|
+
if (source.includes(RETICLE_MARKER)) {
|
|
101
|
+
return { kind: VitePatchKind.ALREADY };
|
|
102
|
+
}
|
|
103
|
+
const call = reticlePluginCall({ port, captureBodies, inject, sourceMapping });
|
|
104
|
+
if (PLUGINS_ARRAY.test(source)) {
|
|
105
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, call)) };
|
|
106
|
+
}
|
|
107
|
+
if (CONFIG_OBJECT.test(source)) {
|
|
108
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, call)) };
|
|
109
|
+
}
|
|
110
|
+
return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
|
|
111
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patching a build config that owns its own Vite instance.
|
|
3
|
+
*
|
|
4
|
+
* Astro and Nuxt are the same problem twice: each runs Vite itself and renders its own HTML, so the
|
|
5
|
+
* Vite plugin never loads and nothing sits in the page's path to inject the pairing token. So the
|
|
6
|
+
* token has to be inlined by the config, the dep cache has to be warmed for the dynamic SDK import,
|
|
7
|
+
* and the daemon's journal has to be kept out of the dev-server watcher — three edits to one `vite:`
|
|
8
|
+
* block, merged into whatever the app already put there.
|
|
9
|
+
*
|
|
10
|
+
* Lifted out of `astro-patch.ts` unchanged when Nuxt needed the identical patch. The two frameworks
|
|
11
|
+
* differ in exactly two ways, and both are parameters here: the config-factory call to extend
|
|
12
|
+
* (`defineConfig` / `defineNuxtConfig`), and which SDK package to pre-bundle.
|
|
13
|
+
*
|
|
14
|
+
* Every branch bails to `manual` (the printed recipe) on a shape it does not fully recognise.
|
|
15
|
+
* Half-editing somebody's build config is worse than a documented manual step.
|
|
16
|
+
*/
|
|
17
|
+
import { type SourcePatch } from './patch-kind.js';
|
|
18
|
+
/** What differs between one Vite-owning framework and the next. */
|
|
19
|
+
export interface ViteOwningConfig {
|
|
20
|
+
/** The config-factory call whose object literal our `vite:` block goes into. */
|
|
21
|
+
readonly defineCall: RegExp;
|
|
22
|
+
/** The SDK package this framework's connect imports, declared so Vite pre-bundles it. */
|
|
23
|
+
readonly sdkSpecifier: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function patchViteOwningConfig(source: string, config: ViteOwningConfig): SourcePatch;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patching a build config that owns its own Vite instance.
|
|
3
|
+
*
|
|
4
|
+
* Astro and Nuxt are the same problem twice: each runs Vite itself and renders its own HTML, so the
|
|
5
|
+
* Vite plugin never loads and nothing sits in the page's path to inject the pairing token. So the
|
|
6
|
+
* token has to be inlined by the config, the dep cache has to be warmed for the dynamic SDK import,
|
|
7
|
+
* and the daemon's journal has to be kept out of the dev-server watcher — three edits to one `vite:`
|
|
8
|
+
* block, merged into whatever the app already put there.
|
|
9
|
+
*
|
|
10
|
+
* Lifted out of `astro-patch.ts` unchanged when Nuxt needed the identical patch. The two frameworks
|
|
11
|
+
* differ in exactly two ways, and both are parameters here: the config-factory call to extend
|
|
12
|
+
* (`defineConfig` / `defineNuxtConfig`), and which SDK package to pre-bundle.
|
|
13
|
+
*
|
|
14
|
+
* Every branch bails to `manual` (the printed recipe) on a shape it does not fully recognise.
|
|
15
|
+
* Half-editing somebody's build config is worse than a documented manual step.
|
|
16
|
+
*/
|
|
17
|
+
import { ReticleDir } from '@reticlehq/core';
|
|
18
|
+
import { blockAfter } from './brace-scan.js';
|
|
19
|
+
import { PatchKind } from './patch-kind.js';
|
|
20
|
+
/** Present in a patched config AND in a hand-followed recipe — so both count as already wired. */
|
|
21
|
+
const CONFIG_MARKER = '__RETICLE_TOKEN__';
|
|
22
|
+
/**
|
|
23
|
+
* The SDK, declared so Vite pre-bundles it BEFORE the first page load.
|
|
24
|
+
*
|
|
25
|
+
* The connect does `await import('@reticlehq/react')`. Undeclared, Vite meets that import mid-load,
|
|
26
|
+
* pre-bundles it, and the hashed `/node_modules/.vite/deps/@reticlehq_react.js?v=…` URL the browser
|
|
27
|
+
* already requested stops existing — the import rejects with "Failed to fetch dynamically imported
|
|
28
|
+
* module", connect() never runs, and the page looks entirely normal. Measured on astro-nanostores,
|
|
29
|
+
* intermittent on whether the dep cache was warm. The Vite plugin has declared the SDK for this
|
|
30
|
+
* exact reason since the bug was first found on React; a hand-patched config never got it.
|
|
31
|
+
*/
|
|
32
|
+
function sdkInclude(config) {
|
|
33
|
+
return `'${config.sdkSpecifier}'`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The daemon's journal directory, kept out of the dev server's watcher.
|
|
37
|
+
*
|
|
38
|
+
* The daemon writes `.reticle/` into the project root and rewrites `ambient.json` atomically
|
|
39
|
+
* (`.tmp` + rename) for as long as a session is live. These dev servers are Vite watching that root,
|
|
40
|
+
* and `.reticle/` is not in Vite's default ignore list — so every journal write read as a project
|
|
41
|
+
* file changing and Vite answered with a full page reload. That closes a loop with no exit: the page
|
|
42
|
+
* loads, the SDK connects and streams events, the daemon journals them, Vite reloads the page, the
|
|
43
|
+
* SDK reconnects. It ran several times a second for as long as the dev server was up, and the
|
|
44
|
+
* symptom looked nothing like the cause — stale refs, actions dying mid-flight, and a log full of
|
|
45
|
+
* connect/disconnect pairs that read as a flapping SDK.
|
|
46
|
+
*
|
|
47
|
+
* The Vite plugin sets the same ignore in its `config` hook. These frameworks never load that
|
|
48
|
+
* plugin, so they need their own.
|
|
49
|
+
*
|
|
50
|
+
* A RegExp, not a glob. chokidar dropped glob support in v4 and Vite 7+ ships v4/v5, where a
|
|
51
|
+
* double-star pattern is accepted and matches nothing — the ignore would be visibly present in the
|
|
52
|
+
* config and do nothing at all. Measured against the chokidar this repo resolves.
|
|
53
|
+
*/
|
|
54
|
+
const WATCH_IGNORE_LITERAL = `/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/`;
|
|
55
|
+
/**
|
|
56
|
+
* An `include:` the app already declared — ours joins that array instead of adding a second key.
|
|
57
|
+
* Deliberately not anchored to a newline: `optimizeDeps: { include: ['x'] }` on one line is the
|
|
58
|
+
* common way to write it, and requiring a line break made the merge miss it and duplicate the key.
|
|
59
|
+
*/
|
|
60
|
+
const EXISTING_INCLUDE = /(\s*include\s*:\s*\[)/;
|
|
61
|
+
/** A `vite:` key whose value is an object LITERAL — the one shape we can merge into safely. */
|
|
62
|
+
const VITE_OBJECT_KEY = /(^\s*vite\s*:\s*\{)/m;
|
|
63
|
+
/** Any `vite:` key at all. One that is not an object literal (a spread, an imported config) is ours to refuse. */
|
|
64
|
+
const ANY_VITE_KEY = /^\s*vite\s*:/m;
|
|
65
|
+
const HELPER = `
|
|
66
|
+
function reticleToken() {
|
|
67
|
+
const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
|
|
68
|
+
try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
const HELPER_IMPORTS = `import { readFileSync } from 'node:fs';
|
|
72
|
+
import { homedir } from 'node:os';
|
|
73
|
+
import { join } from 'node:path';
|
|
74
|
+
`;
|
|
75
|
+
/**
|
|
76
|
+
* The keys we add inside `vite:`, each with what it must contain.
|
|
77
|
+
*
|
|
78
|
+
* Kept as a list rather than one blob because a config that already sets one of these must be merged
|
|
79
|
+
* INTO, not duplicated: the real astro-nanostores config has `build: { chunkSizeWarningLimit }`, and
|
|
80
|
+
* inserting our own `build` beside it gave the object two — where the last one wins, so
|
|
81
|
+
* `target: 'es2022'` was silently discarded while init reported success. Losing that target is not
|
|
82
|
+
* cosmetic: Astro's default down-levels the modern SDK bundle and dies on a destructuring transform.
|
|
83
|
+
*/
|
|
84
|
+
function viteKeys(config) {
|
|
85
|
+
return [
|
|
86
|
+
{ key: 'build', inner: `\n target: 'es2022',` },
|
|
87
|
+
{
|
|
88
|
+
key: 'optimizeDeps',
|
|
89
|
+
inner: `\n include: [${sdkInclude(config)}],\n esbuildOptions: { target: 'es2022' },`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'define',
|
|
93
|
+
inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
|
|
94
|
+
},
|
|
95
|
+
// Merged in first, so a `server: { port }` the app already set keeps its port. An app that
|
|
96
|
+
// already sets `server.watch` itself is the one shape this loses to — the inner `watch` key would
|
|
97
|
+
// be duplicated and the app's would win — which is the same nested-merge ceiling `build` has.
|
|
98
|
+
{ key: 'server', inner: `\n watch: { ignored: [${WATCH_IGNORE_LITERAL}] },` },
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
/** Whole-key form, for a `vite:` block that does not have the key at all. */
|
|
102
|
+
function wholeKeys(config) {
|
|
103
|
+
return {
|
|
104
|
+
build: `\n build: { target: 'es2022' },`,
|
|
105
|
+
optimizeDeps: `\n optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },`,
|
|
106
|
+
define: `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`,
|
|
107
|
+
server: `\n server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Add our keys to an existing `vite: { ... }` block, merging into any the app already set.
|
|
112
|
+
*
|
|
113
|
+
* Returns null when a colliding key is not an object literal (`build: sharedConfig`) — there is no
|
|
114
|
+
* brace to merge into, and duplicating or replacing it would corrupt someone's build.
|
|
115
|
+
*/
|
|
116
|
+
function mergeIntoViteBlock(source, braceAt, config) {
|
|
117
|
+
let out = source;
|
|
118
|
+
const whole = wholeKeys(config);
|
|
119
|
+
for (const { key, inner } of viteKeys(config)) {
|
|
120
|
+
// Scoped to the vite block by searching from its opening brace: a `build:` under `markdown:` or
|
|
121
|
+
// at the top level is somebody else's key and must not be touched.
|
|
122
|
+
const existing = new RegExp(`(\\n\\s*${key}\\s*:\\s*)([\\{A-Za-z_])`).exec(out.slice(braceAt));
|
|
123
|
+
if (existing?.index === undefined) {
|
|
124
|
+
out = `${out.slice(0, braceAt)}${whole[key] ?? ''}${out.slice(braceAt)}`;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// The character after the colon decides: `{` is a literal we can open; anything else is a
|
|
128
|
+
// reference we must not touch.
|
|
129
|
+
if (existing[2] !== '{')
|
|
130
|
+
return null;
|
|
131
|
+
const at = braceAt + existing.index + existing[0].length; // just past the `{`
|
|
132
|
+
// An `include:` the app already wrote is an ARRAY, and a second `include:` key in the same
|
|
133
|
+
// object literal is not a merge — the last one wins and one of the two silently disappears.
|
|
134
|
+
// That is how `build.target` was lost while init still reported success. Join their array.
|
|
135
|
+
// Scoped to THIS block's braces: an `include:` under a later key is somebody else's array, and
|
|
136
|
+
// appending the SDK to it would both miss the target and edit a config we were not asked to.
|
|
137
|
+
const own = 'optimizeDeps' === key ? EXISTING_INCLUDE.exec(blockAfter(out, at)) : null;
|
|
138
|
+
if (own?.index === undefined) {
|
|
139
|
+
out = insertAt(out, at, inner);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Their array first, then the rest of our keys at the block's start — inserting at the LOWER
|
|
143
|
+
// offset second keeps the first offset valid.
|
|
144
|
+
out = insertAt(out, at + own.index + own[0].length, `${sdkInclude(config)}, `);
|
|
145
|
+
out = insertAt(out, at, withoutInclude(inner, config));
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function insertAt(source, at, text) {
|
|
150
|
+
return `${source.slice(0, at)}${text}${source.slice(at)}`;
|
|
151
|
+
}
|
|
152
|
+
/** The optimizeDeps inner minus our `include:` line, for when the app already has one to join. */
|
|
153
|
+
function withoutInclude(inner, config) {
|
|
154
|
+
return inner.replace(`\n include: [${sdkInclude(config)}],`, '');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The `vite:` block these frameworks need. `build.target` is raised because Astro's default
|
|
158
|
+
* down-levels the modern SDK bundle and dies on a destructuring transform; `__RETICLE_ROOT__` is
|
|
159
|
+
* defined because without it every source pointer comes back as an absolute path from the machine
|
|
160
|
+
* that ran `init`.
|
|
161
|
+
*/
|
|
162
|
+
function viteBlock(config) {
|
|
163
|
+
return ` vite: {
|
|
164
|
+
build: { target: 'es2022' },
|
|
165
|
+
optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },
|
|
166
|
+
define: {
|
|
167
|
+
__RETICLE_TOKEN__: JSON.stringify(reticleToken()),
|
|
168
|
+
__RETICLE_ROOT__: JSON.stringify(process.cwd()),
|
|
169
|
+
},
|
|
170
|
+
server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
|
|
171
|
+
},
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
export function patchViteOwningConfig(source, config) {
|
|
175
|
+
if (source.includes(CONFIG_MARKER))
|
|
176
|
+
return { kind: PatchKind.ALREADY };
|
|
177
|
+
// A `vite: { ... }` block is an object literal, so our keys can go straight after the brace and
|
|
178
|
+
// whatever is already in there is untouched. Refusing outright was the only genuine install defect
|
|
179
|
+
// left in the gate: the config bailed while the LAYOUT patch applied anyway, leaving an app with a
|
|
180
|
+
// connect snippet, no inlined token and no raised build target — which cannot connect, reported as
|
|
181
|
+
// one OK step and one warning.
|
|
182
|
+
const viteObject = VITE_OBJECT_KEY.exec(source);
|
|
183
|
+
if (viteObject?.index !== undefined) {
|
|
184
|
+
const at = viteObject.index + viteObject[0].length;
|
|
185
|
+
const merged = mergeIntoViteBlock(source, at, config);
|
|
186
|
+
if (merged !== null) {
|
|
187
|
+
return {
|
|
188
|
+
kind: PatchKind.APPLY,
|
|
189
|
+
code: `${HELPER_IMPORTS}${merged.trimStart()}\n${HELPER}`.trimEnd() + '\n',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
kind: PatchKind.MANUAL,
|
|
194
|
+
reason: 'this config sets a `vite` key Reticle needs (build / optimizeDeps / define) to something ' +
|
|
195
|
+
'other than an object literal — merging into it is your call, not a text edit Reticle should make',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (ANY_VITE_KEY.test(source)) {
|
|
199
|
+
// `vite: sharedConfig` — not an object literal, so there is no brace to merge into and guessing
|
|
200
|
+
// would corrupt a build config. The recipe is the honest answer here.
|
|
201
|
+
return {
|
|
202
|
+
kind: PatchKind.MANUAL,
|
|
203
|
+
reason: 'this config sets `vite:` to something other than an object literal — merging into it is your call, not a text edit Reticle should make',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const opening = config.defineCall.exec(source);
|
|
207
|
+
if (opening?.index === undefined) {
|
|
208
|
+
return {
|
|
209
|
+
kind: PatchKind.MANUAL,
|
|
210
|
+
reason: "couldn't find a config call to extend",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const at = opening.index + opening[0].length;
|
|
214
|
+
const withBlock = `${source.slice(0, at)}\n${viteBlock(config)}${source.slice(at)}`;
|
|
215
|
+
return {
|
|
216
|
+
kind: PatchKind.APPLY,
|
|
217
|
+
code: `${HELPER_IMPORTS}${withBlock.trimStart()}\n${HELPER}`.trimEnd() + '\n',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote one argument for a Windows command line. Pure, and deliberately NOT platform-gated.
|
|
3
|
+
*
|
|
4
|
+
* The rule used to live inside a `process.platform === WINDOWS` branch in node-io.ts, which meant
|
|
5
|
+
* the Windows path could not be exercised on a mac or in CI's Linux job — so it stayed wrong. A
|
|
6
|
+
* pure function is most of the fix: it can be asserted anywhere, and it is.
|
|
7
|
+
*
|
|
8
|
+
* ## What was wrong
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Two independent holes, both reachable from an ordinary directory path:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Backslashes were not escaped.** `C:\Users\ada\My Projects\` became
|
|
17
|
+
* `"C:\Users\ada\My Projects\"`, whose trailing `\"` reads as an ESCAPED quote — the quoted
|
|
18
|
+
* region never closes and the rest of the line is re-parsed by cmd.exe. CodeQL flagged this one
|
|
19
|
+
* (`js/incomplete-sanitization`, high).
|
|
20
|
+
* 2. **The predicate only looked for whitespace and quotes.** An argument like `foo&whoami` has
|
|
21
|
+
* neither, so it was handed to cmd.exe completely unquoted. Correct backslash escaping does
|
|
22
|
+
* nothing for that; it needs the metacharacters in the trigger.
|
|
23
|
+
*
|
|
24
|
+
* ## The rule implemented here
|
|
25
|
+
*
|
|
26
|
+
* `CommandLineToArgvW`: a run of backslashes is literal unless it precedes a `"`, in which case the
|
|
27
|
+
* run is doubled and the quote escaped; a run at the very end sits before the closing quote we add,
|
|
28
|
+
* so it is doubled too. Correctly-terminated quotes also neutralise cmd.exe's metacharacters, which
|
|
29
|
+
* it does not interpret inside a quoted region.
|
|
30
|
+
*
|
|
31
|
+
* ## Known limit, stated rather than papered over
|
|
32
|
+
*
|
|
33
|
+
* `%VAR%` is expanded by cmd.exe even inside quotes, and no quoting prevents it — only avoiding
|
|
34
|
+
* `shell: true` does, which Windows needs so `pnpm.cmd`/`npx.cmd` resolve (see `shellOpt`). That is
|
|
35
|
+
* variable substitution, not command execution, and every caller here passes paths and package
|
|
36
|
+
* names rather than user prose.
|
|
37
|
+
*/
|
|
38
|
+
export declare function windowsShellArg(arg: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote one argument for a Windows command line. Pure, and deliberately NOT platform-gated.
|
|
3
|
+
*
|
|
4
|
+
* The rule used to live inside a `process.platform === WINDOWS` branch in node-io.ts, which meant
|
|
5
|
+
* the Windows path could not be exercised on a mac or in CI's Linux job — so it stayed wrong. A
|
|
6
|
+
* pure function is most of the fix: it can be asserted anywhere, and it is.
|
|
7
|
+
*
|
|
8
|
+
* ## What was wrong
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Two independent holes, both reachable from an ordinary directory path:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Backslashes were not escaped.** `C:\Users\ada\My Projects\` became
|
|
17
|
+
* `"C:\Users\ada\My Projects\"`, whose trailing `\"` reads as an ESCAPED quote — the quoted
|
|
18
|
+
* region never closes and the rest of the line is re-parsed by cmd.exe. CodeQL flagged this one
|
|
19
|
+
* (`js/incomplete-sanitization`, high).
|
|
20
|
+
* 2. **The predicate only looked for whitespace and quotes.** An argument like `foo&whoami` has
|
|
21
|
+
* neither, so it was handed to cmd.exe completely unquoted. Correct backslash escaping does
|
|
22
|
+
* nothing for that; it needs the metacharacters in the trigger.
|
|
23
|
+
*
|
|
24
|
+
* ## The rule implemented here
|
|
25
|
+
*
|
|
26
|
+
* `CommandLineToArgvW`: a run of backslashes is literal unless it precedes a `"`, in which case the
|
|
27
|
+
* run is doubled and the quote escaped; a run at the very end sits before the closing quote we add,
|
|
28
|
+
* so it is doubled too. Correctly-terminated quotes also neutralise cmd.exe's metacharacters, which
|
|
29
|
+
* it does not interpret inside a quoted region.
|
|
30
|
+
*
|
|
31
|
+
* ## Known limit, stated rather than papered over
|
|
32
|
+
*
|
|
33
|
+
* `%VAR%` is expanded by cmd.exe even inside quotes, and no quoting prevents it — only avoiding
|
|
34
|
+
* `shell: true` does, which Windows needs so `pnpm.cmd`/`npx.cmd` resolve (see `shellOpt`). That is
|
|
35
|
+
* variable substitution, not command execution, and every caller here passes paths and package
|
|
36
|
+
* names rather than user prose.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Characters that make cmd.exe do something other than pass the text along.
|
|
40
|
+
*
|
|
41
|
+
* Whitespace and `"` split or delimit; the rest are the shell's control operators. An argument
|
|
42
|
+
* containing any of them must be quoted, not just one containing a space.
|
|
43
|
+
*/
|
|
44
|
+
const NEEDS_QUOTING = /[\s"&|<>^()!%,;=]/;
|
|
45
|
+
export function windowsShellArg(arg) {
|
|
46
|
+
// An empty argument must still occupy a slot in argv. Unquoted it disappears entirely, silently
|
|
47
|
+
// shifting every argument after it by one.
|
|
48
|
+
if (0 === arg.length)
|
|
49
|
+
return '""';
|
|
50
|
+
if (!NEEDS_QUOTING.test(arg))
|
|
51
|
+
return arg;
|
|
52
|
+
let out = '"';
|
|
53
|
+
let backslashes = 0;
|
|
54
|
+
for (const ch of arg) {
|
|
55
|
+
if ('\\' === ch) {
|
|
56
|
+
backslashes += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if ('"' === ch) {
|
|
60
|
+
// 2n+1 backslashes → n literal backslashes and an escaped quote.
|
|
61
|
+
out += '\\'.repeat(2 * backslashes + 1) + '"';
|
|
62
|
+
backslashes = 0;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out += '\\'.repeat(backslashes) + ch;
|
|
66
|
+
backslashes = 0;
|
|
67
|
+
}
|
|
68
|
+
// A trailing run sits immediately before the closing quote, so it must be doubled or it escapes it.
|
|
69
|
+
return `${out}${'\\'.repeat(2 * backslashes)}"`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { InitIo } from './run.js';
|
|
2
|
+
/**
|
|
3
|
+
* The file names that say what a directory IS, shared by everything that has to ask.
|
|
4
|
+
*
|
|
5
|
+
* They lived in run.ts and are read from here now because `findWorkspaceApps` moved and needs them —
|
|
6
|
+
* one definition, imported in both directions of the question ("is this an app?" and "which app?").
|
|
7
|
+
*/
|
|
8
|
+
export declare const PACKAGE_JSON = "package.json";
|
|
9
|
+
export declare const VITE_CONFIG_CANDIDATES: string[];
|
|
10
|
+
export declare const ELECTRON_VITE_CONFIG_CANDIDATES: string[];
|
|
11
|
+
export declare const NEXT_CONFIG_CANDIDATES: string[];
|
|
12
|
+
interface WorkspaceSources {
|
|
13
|
+
/** Raw contents of pnpm-workspace.yaml, when present. */
|
|
14
|
+
pnpmWorkspace?: string;
|
|
15
|
+
/** The `workspaces` field of package.json — array form or the yarn/npm object form. */
|
|
16
|
+
pkgWorkspaces?: unknown;
|
|
17
|
+
/** Directory names at the repo root, used when nothing is declared. */
|
|
18
|
+
topLevelDirs?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function workspaceParents(sources: WorkspaceSources): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Which directories under this workspace are runnable apps.
|
|
23
|
+
*
|
|
24
|
+
* Moved here from run.ts, which was over the 1000-line cap and had carried a note saying it was at
|
|
25
|
+
* its cohesion limit. `library-path-boundary.test.ts` had already written down that this function
|
|
26
|
+
* "squats in init/ for historical reasons" — it is a general-purpose question about a repository's
|
|
27
|
+
* shape, next to `workspaceParents`, which answers the other half of it.
|
|
28
|
+
*/
|
|
29
|
+
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
30
|
+
export declare const APP_DEPS: readonly ["next", "vite", "electron-vite"];
|
|
31
|
+
/**
|
|
32
|
+
* App directories under a workspace root.
|
|
33
|
+
*
|
|
34
|
+
* Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
|
|
35
|
+
* directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
|
|
36
|
+
* would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
|
|
37
|
+
* it knows it is in a workspace; this is the matching walk DOWN.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
|
|
40
|
+
export {};
|