@reticlehq/init 3.9.0 → 3.10.1
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/detect.js +4 -1
- package/dist/detect/dev-script.js +24 -5
- package/dist/detect/non-js-project.js +17 -7
- package/dist/detect/workspace-apps.d.ts +1 -1
- package/dist/detect/workspace-apps.js +1 -1
- package/dist/diagnose/containerised-dev-server.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/no-package-json.js +53 -16
- package/dist/patch/angular.js +4 -0
- package/dist/patch/brace-scan.d.ts +15 -1
- package/dist/patch/brace-scan.js +133 -7
- package/dist/patch/vite-config.d.ts +12 -1
- package/dist/patch/vite-config.js +188 -20
- package/dist/plan/connect-steps.d.ts +2 -0
- package/dist/plan/connect-steps.js +9 -0
- package/dist/plan/plan-vite.js +4 -2
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +4 -3
- package/dist/run-types.d.ts +5 -0
- package/dist/run.js +5 -1
- package/package.json +2 -2
package/dist/detect/detect.js
CHANGED
|
@@ -202,7 +202,10 @@ export const FRAMEWORK_SIGNALS = {
|
|
|
202
202
|
deps: ['@tanstack/react-start', '@tanstack/start'],
|
|
203
203
|
configs: [],
|
|
204
204
|
},
|
|
205
|
-
|
|
205
|
+
/**
|
|
206
|
+
* `vite-plus` too: a Vite+ app under pnpm may declare only it, and its CLI runs Vite (#2134).
|
|
207
|
+
*/
|
|
208
|
+
[Framework.VITE]: { deps: ['vite', 'vite-plus', 'vinext'], configs: VITE_CONFIGS },
|
|
206
209
|
/** CRA has no config file at all, so the dependency is the only signal. */
|
|
207
210
|
[Framework.CRA]: { deps: ['react-scripts'], configs: [] },
|
|
208
211
|
/** Nothing identifies plain HTML; it is where the chain below ends. */
|
|
@@ -173,11 +173,29 @@ function safeRelativePath(value) {
|
|
|
173
173
|
}
|
|
174
174
|
return normal;
|
|
175
175
|
}
|
|
176
|
-
function
|
|
176
|
+
function isBin(token, bin) {
|
|
177
177
|
const normal = token.replace(/\\/g, '/');
|
|
178
|
-
return
|
|
178
|
+
return bin === normal || `${bin}.cmd` === normal || normal.endsWith(`/node_modules/.bin/${bin}`);
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
const VITE_BIN = 'vite';
|
|
181
|
+
/** Vite+'s CLI. `vp dev` starts Vite's dev server and takes Vite's own flags (#2134). */
|
|
182
|
+
const VITE_PLUS_BIN = 'vp';
|
|
183
|
+
const VITE_PLUS_DEV = 'dev';
|
|
184
|
+
/**
|
|
185
|
+
* Where Vite's own arguments start in a command segment, or undefined when the segment does not run
|
|
186
|
+
* Vite: after `vite`, or after `vp dev`. Any other `vp` command does not start a dev server.
|
|
187
|
+
*/
|
|
188
|
+
function viteArgsStart(segment, command) {
|
|
189
|
+
const token = segment[command] ?? '';
|
|
190
|
+
if (isBin(token, VITE_BIN)) {
|
|
191
|
+
return command + 1;
|
|
192
|
+
}
|
|
193
|
+
if (isBin(token, VITE_PLUS_BIN) && VITE_PLUS_DEV === segment[command + 1]) {
|
|
194
|
+
return command + 2;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
/** The direct Vite invocation (`vite` or `vp dev`) in the selected dev script, if it has one. */
|
|
181
199
|
function viteInvocation(script) {
|
|
182
200
|
const tokens = script.match(SHELL_TOKEN) ?? [];
|
|
183
201
|
let start = 0;
|
|
@@ -189,8 +207,9 @@ function viteInvocation(script) {
|
|
|
189
207
|
let command = 0;
|
|
190
208
|
while (command < segment.length && ENV_ASSIGNMENT.test(segment[command] ?? ''))
|
|
191
209
|
command += 1;
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
const argsStart = viteArgsStart(segment, command);
|
|
211
|
+
if (argsStart !== undefined) {
|
|
212
|
+
const args = segment.slice(argsStart);
|
|
194
213
|
for (let i = 0; i < args.length; i += 1) {
|
|
195
214
|
const arg = args[i] ?? '';
|
|
196
215
|
const inline = /^(?:--config|-c)=(.*)$/.exec(arg);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
|
|
16
16
|
* found anywhere.
|
|
17
17
|
*/
|
|
18
|
+
import { ReticleTool } from '@reticlehq/core';
|
|
18
19
|
/** Marker files that identify an ecosystem, and what to call it. */
|
|
19
20
|
const ECOSYSTEM_MARKERS = [
|
|
20
21
|
['requirements.txt', 'Python'],
|
|
@@ -43,13 +44,15 @@ const STREAMLIT_IMPORT = /(?:^|\n)\s*(?:import\s+streamlit(?:\s+as\s+\w+)?|from\
|
|
|
43
44
|
export function detectStreamlitProject(readFile, rootFiles) {
|
|
44
45
|
for (const file of STREAMLIT_DEPENDENCY_FILES) {
|
|
45
46
|
const source = readFile(file);
|
|
46
|
-
if (null !== source && STREAMLIT_DEPENDENCY.test(source))
|
|
47
|
+
if (null !== source && STREAMLIT_DEPENDENCY.test(source)) {
|
|
47
48
|
return true;
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
51
|
for (const file of rootFiles.filter((name) => name.endsWith('.py'))) {
|
|
50
52
|
const source = readFile(file);
|
|
51
|
-
if (null !== source && STREAMLIT_IMPORT.test(source))
|
|
53
|
+
if (null !== source && STREAMLIT_IMPORT.test(source)) {
|
|
52
54
|
return true;
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
return false;
|
|
55
58
|
}
|
|
@@ -71,10 +74,12 @@ export function streamlitSetupMessage() {
|
|
|
71
74
|
* that also holds the real app deeper down should not claim more than it knows.
|
|
72
75
|
*/
|
|
73
76
|
export function detectDjangoProject(exists, rootDirectories = []) {
|
|
74
|
-
if (!exists('manage.py'))
|
|
77
|
+
if (!exists('manage.py')) {
|
|
75
78
|
return false;
|
|
76
|
-
|
|
79
|
+
}
|
|
80
|
+
if (exists('settings.py')) {
|
|
77
81
|
return true;
|
|
82
|
+
}
|
|
78
83
|
return rootDirectories.some((dir) => exists(`${dir}/settings.py`));
|
|
79
84
|
}
|
|
80
85
|
/** The Django-specific explanation printed immediately before its generated middleware. */
|
|
@@ -92,8 +97,9 @@ export function djangoSetupMessage() {
|
|
|
92
97
|
/** The ecosystem this directory looks like, or undefined when nothing says. */
|
|
93
98
|
export function detectNonJsEcosystem(exists) {
|
|
94
99
|
for (const [marker, name] of ECOSYSTEM_MARKERS) {
|
|
95
|
-
if (exists(marker))
|
|
100
|
+
if (exists(marker)) {
|
|
96
101
|
return name;
|
|
102
|
+
}
|
|
97
103
|
}
|
|
98
104
|
return undefined;
|
|
99
105
|
}
|
|
@@ -131,8 +137,12 @@ export function noPackageJsonAnswer(exists, rootFiles = []) {
|
|
|
131
137
|
return {
|
|
132
138
|
kind: NoPackageJsonCase.STATIC_PAGE,
|
|
133
139
|
message: 'This is a static page with no build step, so there is nothing for `reticle init` to wire. ' +
|
|
134
|
-
|
|
135
|
-
'
|
|
140
|
+
// Named first because it changes no file: the page is often the file the user hands over (#1544).
|
|
141
|
+
'To verify the page without a change to it, serve the folder, then call ' +
|
|
142
|
+
`\`${ReticleTool.LEASE} { action: "acquire", url }\`. It supplies the SDK to a page that has none. ` +
|
|
143
|
+
'To connect your own browser, add the script-tag snippet below to the page while you develop, ' +
|
|
144
|
+
'then reload it. Remove the snippet before publishing: a static file has no development ' +
|
|
145
|
+
'build to keep it out of production.',
|
|
136
146
|
};
|
|
137
147
|
}
|
|
138
148
|
return {
|
|
@@ -25,7 +25,7 @@ export declare function workspaceParents(sources: WorkspaceSources): string[];
|
|
|
25
25
|
* which answers the other half of it.
|
|
26
26
|
*/
|
|
27
27
|
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
28
|
-
export declare const APP_DEPS: readonly ["next", "vite", "electron-vite"];
|
|
28
|
+
export declare const APP_DEPS: readonly ["next", "vite", "vite-plus", "electron-vite"];
|
|
29
29
|
/**
|
|
30
30
|
* Whether `dir` is itself an app with a page: what the daemon asks before wiring a folder unasked,
|
|
31
31
|
* so a library or API package it happens to run in is left alone.
|
|
@@ -102,7 +102,7 @@ export function workspaceParents(sources) {
|
|
|
102
102
|
* which answers the other half of it.
|
|
103
103
|
*/
|
|
104
104
|
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
105
|
-
export const APP_DEPS = ['next', 'vite', 'electron-vite'];
|
|
105
|
+
export const APP_DEPS = ['next', 'vite', 'vite-plus', 'electron-vite'];
|
|
106
106
|
function hasDevScript(pkgRaw) {
|
|
107
107
|
try {
|
|
108
108
|
const parsed = JSON.parse(pkgRaw);
|
|
@@ -64,7 +64,7 @@ const DEVCONTAINER = '.devcontainer/devcontainer.json';
|
|
|
64
64
|
const SEP = `["',\\s]+`;
|
|
65
65
|
const DEV_SERVER_COMMANDS = [
|
|
66
66
|
new RegExp(`\\b(?:npm|pnpm|yarn|bun)${SEP}(?:run${SEP})?dev\\b`),
|
|
67
|
-
new RegExp(`\\b(?:next|nuxt|nuxi|astro|react-router|remix|vinxi)${SEP}dev\\b`),
|
|
67
|
+
new RegExp(`\\b(?:next|nuxt|nuxi|astro|react-router|remix|vinxi|vp)${SEP}dev\\b`),
|
|
68
68
|
new RegExp(`\\bng${SEP}serve\\b`),
|
|
69
69
|
new RegExp(`\\breact-scripts${SEP}start\\b`),
|
|
70
70
|
new RegExp(`\\bwebpack${SEP}serve\\b|\\bwebpack-dev-server\\b`),
|
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,9 @@ export { refreshAgentRules } from './project/refresh-rules.js';
|
|
|
37
37
|
export { diagnoseDesktop, isDesktopProject } from './diagnose/desktop-doctor.js';
|
|
38
38
|
export { CspBasis, diagnoseObservedWebCsp, diagnoseWebCsp, resolveWebCspFindings, type CspDiagnosis, type ObservedWebDocument, } from './diagnose/csp-doctor.js';
|
|
39
39
|
export { webCspOptionsFor } from './diagnose/csp-step.js';
|
|
40
|
-
export { reticleConfigContent } from './patch/snippets.js';
|
|
40
|
+
export { connectArg, reticleConfigContent } from './patch/snippets.js';
|
|
41
|
+
/** For Reticle's static server, which adds the snippet to the page it serves (#1544). */
|
|
42
|
+
export { withStaticSnippet } from './patch/static-page.js';
|
|
41
43
|
/**
|
|
42
44
|
* MCP registration, for a caller that has no project.
|
|
43
45
|
*
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,9 @@ export { refreshAgentRules } from './project/refresh-rules.js';
|
|
|
37
37
|
export { diagnoseDesktop, isDesktopProject } from './diagnose/desktop-doctor.js';
|
|
38
38
|
export { CspBasis, diagnoseObservedWebCsp, diagnoseWebCsp, resolveWebCspFindings, } from './diagnose/csp-doctor.js';
|
|
39
39
|
export { webCspOptionsFor } from './diagnose/csp-step.js';
|
|
40
|
-
export { reticleConfigContent } from './patch/snippets.js';
|
|
40
|
+
export { connectArg, reticleConfigContent } from './patch/snippets.js';
|
|
41
|
+
/** For Reticle's static server, which adds the snippet to the page it serves (#1544). */
|
|
42
|
+
export { withStaticSnippet } from './patch/static-page.js';
|
|
41
43
|
/**
|
|
42
44
|
* MCP registration, for a caller that has no project.
|
|
43
45
|
*
|
package/dist/no-package-json.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* Django, several pages) and stops.
|
|
8
8
|
*/
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
+
import { ReticleTool } from '@reticlehq/core';
|
|
10
11
|
import { PackageManager, Framework } from './detect/detect.js';
|
|
11
12
|
import { PACKAGE_JSON } from './detect/workspace-apps.js';
|
|
12
13
|
import { detectDjangoProject, detectStreamlitProject, djangoSetupMessage, NoPackageJsonCase, noPackageJsonAnswer, streamlitSetupMessage, } from './detect/non-js-project.js';
|
|
13
14
|
import { InitFailure } from './diagnose/init-failure.js';
|
|
14
|
-
import { HTML_INDEX_PATH, STATIC_TOKEN_MODULE, staticTokenFiles, withStaticSnippet, } from './patch/static-page.js';
|
|
15
|
+
import { HTML_INDEX_PATH, hasStaticSnippet, STATIC_TOKEN_MODULE, staticTokenFiles, withStaticSnippet, } from './patch/static-page.js';
|
|
15
16
|
import { connectArg, reticleConfigContent, staticPageSnippetWithLocalToken, } from './patch/snippets.js';
|
|
16
17
|
import { djangoMiddlewareSnippet, pythonConnectArg, streamlitPageSnippet, } from './patch/python-snippets.js';
|
|
17
18
|
import { RETICLE_CONFIG_FILE } from './detect/existing-config.js';
|
|
@@ -25,6 +26,14 @@ const SNIPPET_WRITTEN = `Wrote the dev-only connect snippet into ${HTML_INDEX_PA
|
|
|
25
26
|
const TOKEN_MODULE_WRITTEN = `This machine’s pairing token is in ${STATIC_TOKEN_MODULE}, which is gitignored. Serve it beside ` +
|
|
26
27
|
'the page: the snippet imports it. Keep it out of anything you publish.';
|
|
27
28
|
const SNIPPET_ALREADY = `${HTML_INDEX_PATH} already loads the Reticle SDK — nothing to change.`;
|
|
29
|
+
/**
|
|
30
|
+
* Said when Reticle's static server serves the page, so the file stays as the user wrote it (#1544).
|
|
31
|
+
* The lease route is named first because it changes no file at all.
|
|
32
|
+
*/
|
|
33
|
+
const SNIPPET_SERVED = `${HTML_INDEX_PATH} is unchanged. Reticle's static server adds the dev-only connect snippet to ` +
|
|
34
|
+
`the page that it serves. This machine's pairing token is in ${STATIC_TOKEN_MODULE}, which is ` +
|
|
35
|
+
'gitignored. To serve the folder another way, change no file: serve it, then call ' +
|
|
36
|
+
`\`${ReticleTool.LEASE} { action: "acquire", url }\`.`;
|
|
28
37
|
/** How far up `init` looks for the app it was run inside. The same bound as the workspace walk. */
|
|
29
38
|
const MAX_ANCESTOR_DEPTH = 8;
|
|
30
39
|
const HOME_FOLDER = 'This is your home folder, not a project. Go to the folder of your app (the one with its ' +
|
|
@@ -41,14 +50,17 @@ const insideApp = (dir) => `No package.json here, but this folder is inside the
|
|
|
41
50
|
function namedDirectory(io) {
|
|
42
51
|
const home = io.homeDir();
|
|
43
52
|
let dir = io.cwd();
|
|
44
|
-
if (dir === home)
|
|
53
|
+
if (dir === home) {
|
|
45
54
|
return HOME_FOLDER;
|
|
55
|
+
}
|
|
46
56
|
for (let depth = 0; depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
47
57
|
const parent = dirname(dir);
|
|
48
|
-
if (parent === dir || parent === home)
|
|
58
|
+
if (parent === dir || parent === home) {
|
|
49
59
|
return undefined;
|
|
50
|
-
|
|
60
|
+
}
|
|
61
|
+
if (null !== io.readFile(join(parent, PACKAGE_JSON))) {
|
|
51
62
|
return insideApp(parent);
|
|
63
|
+
}
|
|
52
64
|
dir = parent;
|
|
53
65
|
}
|
|
54
66
|
return undefined;
|
|
@@ -76,24 +88,46 @@ function printedSnippet(streamlit, django, connect) {
|
|
|
76
88
|
}
|
|
77
89
|
return staticPageSnippetWithLocalToken(connect);
|
|
78
90
|
}
|
|
91
|
+
/** The line `wireStaticPage` prints, in the order of its cases. */
|
|
92
|
+
function staticPageLine(written, moved, port, served) {
|
|
93
|
+
if (null !== written) {
|
|
94
|
+
return SNIPPET_WRITTEN;
|
|
95
|
+
}
|
|
96
|
+
if (null !== moved && port !== undefined) {
|
|
97
|
+
return `Updated ${HTML_INDEX_PATH}: ${portMoveDetail(moved.from, port)}.`;
|
|
98
|
+
}
|
|
99
|
+
return served ? SNIPPET_SERVED : SNIPPET_ALREADY;
|
|
100
|
+
}
|
|
101
|
+
/** What to say for this kind of project before its snippet. */
|
|
102
|
+
function setupMessage(streamlit, django, otherwise) {
|
|
103
|
+
if (streamlit) {
|
|
104
|
+
return streamlitSetupMessage();
|
|
105
|
+
}
|
|
106
|
+
if (django) {
|
|
107
|
+
return djangoSetupMessage();
|
|
108
|
+
}
|
|
109
|
+
return otherwise;
|
|
110
|
+
}
|
|
79
111
|
function wireStaticPage(options, io, connect) {
|
|
80
112
|
writeTokenModule(io);
|
|
81
113
|
const html = io.readFile(HTML_INDEX_PATH) ?? '';
|
|
114
|
+
// Setup serves the page with Reticle's static server when the user names no url of their own.
|
|
115
|
+
// That server adds the snippet to the page it serves, so the file the user hands over is not
|
|
116
|
+
// edited (#1544). With `--url` or `--files-only`, nothing of ours serves it, so the file gets it.
|
|
117
|
+
const served = true === options.continuesToRuntime && undefined === options.url;
|
|
82
118
|
// A page that already connects still has its port compared: the snippet bakes the daemon URL, and
|
|
83
119
|
// a re-run with a new `--port` that answered "already" left the page dialling the old daemon.
|
|
84
|
-
const written = withStaticSnippet(html, connect);
|
|
120
|
+
const written = served ? null : withStaticSnippet(html, connect);
|
|
85
121
|
const moved = null === written ? retargetPort(html, options.port) : null;
|
|
86
122
|
const patched = written ?? moved?.code ?? null;
|
|
87
|
-
if (null !== patched)
|
|
123
|
+
if (null !== patched) {
|
|
88
124
|
io.writeFile(HTML_INDEX_PATH, patched);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: null !== moved && options.port !== undefined
|
|
92
|
-
? `Updated ${HTML_INDEX_PATH}: ${portMoveDetail(moved.from, options.port)}.`
|
|
93
|
-
: SNIPPET_ALREADY);
|
|
125
|
+
}
|
|
126
|
+
io.print(staticPageLine(written, moved, options.port, served && !hasStaticSnippet(html)));
|
|
94
127
|
const outcome = { ok: true, stack: Framework.HTML };
|
|
95
|
-
if (true !== options.deferOutcome)
|
|
128
|
+
if (true !== options.deferOutcome) {
|
|
96
129
|
io.host.reportOutcome(outcome);
|
|
130
|
+
}
|
|
97
131
|
return {
|
|
98
132
|
ok: true,
|
|
99
133
|
applied: null === patched ? 0 : 1,
|
|
@@ -118,8 +152,9 @@ export function initWithoutPackageJson(options, io) {
|
|
|
118
152
|
const answer = noPackageJsonAnswer(existsHere, io.rootFiles());
|
|
119
153
|
if (!streamlit && !django && answer.kind === NoPackageJsonCase.WRONG_DIRECTORY) {
|
|
120
154
|
io.print(namedDirectory(io) ?? answer.message);
|
|
121
|
-
if (!options.dryRun)
|
|
155
|
+
if (!options.dryRun) {
|
|
122
156
|
io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
157
|
+
}
|
|
123
158
|
return { ok: false, applied: 0, manual: 0 };
|
|
124
159
|
}
|
|
125
160
|
// Scope the project on disk before anything else, so the daemon can serve this page.
|
|
@@ -146,8 +181,9 @@ export function initWithoutPackageJson(options, io) {
|
|
|
146
181
|
io.writeFile(RETICLE_CONFIG_FILE, moved.code);
|
|
147
182
|
io.print(`Updated ${RETICLE_CONFIG_FILE}: ${portMoveDetail(moved.from, options.port)}.`);
|
|
148
183
|
}
|
|
149
|
-
if (io.exists(RETICLE_CONFIG_FILE))
|
|
184
|
+
if (io.exists(RETICLE_CONFIG_FILE)) {
|
|
150
185
|
return;
|
|
186
|
+
}
|
|
151
187
|
io.writeFile(RETICLE_CONFIG_FILE, reticleConfigContent(Framework.HTML, options.port, nonJsProjectId, io.host.installSource()));
|
|
152
188
|
rememberProjectOnDisk(io, nonJsProjectId, io.cwd(), Date.now());
|
|
153
189
|
io.print(`Wrote ${RETICLE_CONFIG_FILE} (project "${nonJsProjectId}").`);
|
|
@@ -170,7 +206,7 @@ export function initWithoutPackageJson(options, io) {
|
|
|
170
206
|
// a path problem and goes looking for a directory that cannot exist — reported from a
|
|
171
207
|
// Streamlit app, where the search continued into hunting for a browser bundle to inject by
|
|
172
208
|
// hand before the real answer surfaced.
|
|
173
|
-
streamlit
|
|
209
|
+
setupMessage(streamlit, django, answer.message));
|
|
174
210
|
writeConfig();
|
|
175
211
|
// The message says "add the snippet below". Print the snippet, or the message is the same broken
|
|
176
212
|
// promise in the other direction.
|
|
@@ -182,7 +218,8 @@ export function initWithoutPackageJson(options, io) {
|
|
|
182
218
|
}
|
|
183
219
|
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
184
220
|
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
185
|
-
if (!options.dryRun)
|
|
221
|
+
if (!options.dryRun) {
|
|
186
222
|
io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
223
|
+
}
|
|
187
224
|
return { ok: false, applied: 0, manual: 0 };
|
|
188
225
|
}
|
package/dist/patch/angular.js
CHANGED
|
@@ -143,6 +143,10 @@ export default {
|
|
|
143
143
|
'${ANGULAR_TOKEN_PATH}': {
|
|
144
144
|
target: 'http://127.0.0.1:9',
|
|
145
145
|
bypass(req, res) {
|
|
146
|
+
// webpack-dev-server calls this for every request. Answer only this path.
|
|
147
|
+
if ((req.url ?? '').split('?')[0] !== '${ANGULAR_TOKEN_PATH}') {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
146
150
|
const site = req.headers['sec-fetch-site'];
|
|
147
151
|
const allowed =
|
|
148
152
|
LOOPBACK.has(req.socket.remoteAddress ?? '') && (site === undefined || site === 'same-origin');
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A count, not a parser. A `{` or `}` inside a string literal would fool it. That only ever
|
|
5
5
|
* shortens or lengthens the window we search, so the worst case is a MANUAL bail or a missed
|
|
6
|
-
* merge — never a corrupted file written as APPLY.
|
|
6
|
+
* merge — never a corrupted file written as APPLY. A caller that must not be fooled runs the count
|
|
7
|
+
* on `maskStringsAndComments(source)`, which keeps every index where it was.
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* The text from just inside an opening brace to its match.
|
|
@@ -22,3 +23,16 @@ export declare function matchingBraceEnd(source: string, at: number): number | u
|
|
|
22
23
|
* `undefined` when the parens never balance.
|
|
23
24
|
*/
|
|
24
25
|
export declare function matchingParenEnd(source: string, openAt: number): number | undefined;
|
|
26
|
+
/** How many `{` are open at `index`, counted from the start of `source`. */
|
|
27
|
+
export declare function braceDepth(source: string, index: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* The source with every string literal, regex literal and comment blanked to spaces, newlines kept,
|
|
30
|
+
* so each index still points at the same place in the real source. Null when the scan does not end
|
|
31
|
+
* cleanly: a string, comment or regex that never closes, or braces that do not balance.
|
|
32
|
+
*
|
|
33
|
+
* The counts below are fooled by a `{` inside a string, and a key search by a `plugins: [` in a
|
|
34
|
+
* comment. A patcher that runs them on this masked copy is not, and edits the real source at the
|
|
35
|
+
* same indices. The Vite patcher used to add a second `plugins` key whose list Vite never reads
|
|
36
|
+
* (#2134). A source this scan cannot read is a manual step for the caller, never a guess.
|
|
37
|
+
*/
|
|
38
|
+
export declare function maskStringsAndComments(source: string): string | null;
|
package/dist/patch/brace-scan.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A count, not a parser. A `{` or `}` inside a string literal would fool it. That only ever
|
|
5
5
|
* shortens or lengthens the window we search, so the worst case is a MANUAL bail or a missed
|
|
6
|
-
* merge — never a corrupted file written as APPLY.
|
|
6
|
+
* merge — never a corrupted file written as APPLY. A caller that must not be fooled runs the count
|
|
7
|
+
* on `maskStringsAndComments(source)`, which keeps every index where it was.
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* The text from just inside an opening brace to its match.
|
|
@@ -31,16 +32,141 @@ export function matchingParenEnd(source, openAt) {
|
|
|
31
32
|
return undefined;
|
|
32
33
|
return matchingCloser(source, openAt + 1, '(', ')');
|
|
33
34
|
}
|
|
35
|
+
/** What `c` does to the open count: +1 for the opener, -1 for the closer, 0 for anything else. */
|
|
36
|
+
function depthStep(c, open, close) {
|
|
37
|
+
if (c === open) {
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
return c === close ? -1 : 0;
|
|
41
|
+
}
|
|
34
42
|
function matchingCloser(source, innerStart, open, close) {
|
|
35
43
|
let depth = 1;
|
|
36
44
|
for (let i = innerStart; i < source.length; i++) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
depth -= 1;
|
|
41
|
-
if (0 === depth)
|
|
42
|
-
return i;
|
|
45
|
+
depth += depthStep(source[i], open, close);
|
|
46
|
+
if (0 === depth) {
|
|
47
|
+
return i;
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
return undefined;
|
|
46
51
|
}
|
|
52
|
+
/** How many `{` are open at `index`, counted from the start of `source`. */
|
|
53
|
+
export function braceDepth(source, index) {
|
|
54
|
+
let depth = 0;
|
|
55
|
+
for (let i = 0; i < index; i += 1) {
|
|
56
|
+
depth += depthStep(source[i], '{', '}');
|
|
57
|
+
}
|
|
58
|
+
return depth;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The source with every string literal, regex literal and comment blanked to spaces, newlines kept,
|
|
62
|
+
* so each index still points at the same place in the real source. Null when the scan does not end
|
|
63
|
+
* cleanly: a string, comment or regex that never closes, or braces that do not balance.
|
|
64
|
+
*
|
|
65
|
+
* The counts below are fooled by a `{` inside a string, and a key search by a `plugins: [` in a
|
|
66
|
+
* comment. A patcher that runs them on this masked copy is not, and edits the real source at the
|
|
67
|
+
* same indices. The Vite patcher used to add a second `plugins` key whose list Vite never reads
|
|
68
|
+
* (#2134). A source this scan cannot read is a manual step for the caller, never a guess.
|
|
69
|
+
*/
|
|
70
|
+
export function maskStringsAndComments(source) {
|
|
71
|
+
const out = source.split('');
|
|
72
|
+
const blank = (from, to) => {
|
|
73
|
+
for (let k = from; k < to && k < out.length; k += 1) {
|
|
74
|
+
if ('\n' !== out[k]) {
|
|
75
|
+
out[k] = ' ';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/** The last character before `at` that is not whitespace, in the masked output so far. */
|
|
80
|
+
const previousToken = (at) => {
|
|
81
|
+
for (let k = at - 1; k >= 0; k -= 1) {
|
|
82
|
+
const c = out[k] ?? '';
|
|
83
|
+
if (!/\s/.test(c)) {
|
|
84
|
+
return c;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return '';
|
|
88
|
+
};
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < source.length) {
|
|
91
|
+
const ch = source[i] ?? '';
|
|
92
|
+
const next = source[i + 1];
|
|
93
|
+
let stop;
|
|
94
|
+
if ('/' === ch && '/' === next) {
|
|
95
|
+
const end = source.indexOf('\n', i);
|
|
96
|
+
stop = -1 === end ? source.length : end;
|
|
97
|
+
}
|
|
98
|
+
else if ('/' === ch && '*' === next) {
|
|
99
|
+
const end = source.indexOf('*/', i + 2);
|
|
100
|
+
stop = -1 === end ? null : end + 2;
|
|
101
|
+
}
|
|
102
|
+
else if ("'" === ch || '"' === ch || '`' === ch) {
|
|
103
|
+
stop = closingQuote(source, i, ch);
|
|
104
|
+
}
|
|
105
|
+
else if ('/' === ch && REGEX_MAY_FOLLOW.includes(previousToken(i))) {
|
|
106
|
+
stop = closingRegex(source, i);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
i += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (null === stop) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
blank(i, stop);
|
|
116
|
+
i = stop;
|
|
117
|
+
}
|
|
118
|
+
const masked = out.join('');
|
|
119
|
+
return 0 === braceDepth(masked, masked.length) ? masked : null;
|
|
120
|
+
}
|
|
121
|
+
/** Where a `/` starts a regex literal rather than a division: after one of these, or at the start. */
|
|
122
|
+
const REGEX_MAY_FOLLOW = ['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';'];
|
|
123
|
+
/** The index just past the quote that closes the string opened at `at`, or null. */
|
|
124
|
+
function closingQuote(source, at, quote) {
|
|
125
|
+
let j = at + 1;
|
|
126
|
+
while (j < source.length) {
|
|
127
|
+
const c = source[j];
|
|
128
|
+
if ('\\' === c) {
|
|
129
|
+
j += 2;
|
|
130
|
+
}
|
|
131
|
+
else if (quote === c) {
|
|
132
|
+
return j + 1;
|
|
133
|
+
}
|
|
134
|
+
else if ('\n' === c && '`' !== quote) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
j += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/** The index just past the flags of the regex literal opened at `at`, or null. */
|
|
144
|
+
function closingRegex(source, at) {
|
|
145
|
+
let j = at + 1;
|
|
146
|
+
let inClass = false;
|
|
147
|
+
while (j < source.length) {
|
|
148
|
+
const c = source[j];
|
|
149
|
+
if ('\\' === c) {
|
|
150
|
+
j += 2;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if ('\n' === c) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if ('[' === c) {
|
|
157
|
+
inClass = true;
|
|
158
|
+
}
|
|
159
|
+
else if (']' === c) {
|
|
160
|
+
inClass = false;
|
|
161
|
+
}
|
|
162
|
+
else if ('/' === c && !inClass) {
|
|
163
|
+
j += 1;
|
|
164
|
+
while (j < source.length && /[a-z]/i.test(source[j] ?? '')) {
|
|
165
|
+
j += 1;
|
|
166
|
+
}
|
|
167
|
+
return j;
|
|
168
|
+
}
|
|
169
|
+
j += 1;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
@@ -14,10 +14,21 @@ export declare const VitePatchKind: {
|
|
|
14
14
|
};
|
|
15
15
|
export type VitePatchKind = PatchKind;
|
|
16
16
|
type VitePatch = SourcePatch;
|
|
17
|
+
/**
|
|
18
|
+
* The package a new config imports `defineConfig` from.
|
|
19
|
+
*
|
|
20
|
+
* A Vite+ app imports it from `vite-plus`. Under pnpm it may not declare `vite` at all, so an
|
|
21
|
+
* import from `vite` would not resolve, and Vite+'s own lint rule refuses it.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DefineConfigSource: {
|
|
24
|
+
readonly VITE: "vite";
|
|
25
|
+
readonly VITE_PLUS: "vite-plus";
|
|
26
|
+
};
|
|
27
|
+
export type DefineConfigSource = (typeof DefineConfigSource)[keyof typeof DefineConfigSource];
|
|
17
28
|
/**
|
|
18
29
|
* A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
|
|
19
30
|
* rather than patched from an empty one, because it is a file the person will open and read.
|
|
20
31
|
*/
|
|
21
|
-
export declare function newViteConfig(port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): string;
|
|
32
|
+
export declare function newViteConfig(port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean, defineConfigFrom?: DefineConfigSource): string;
|
|
22
33
|
export declare function patchViteConfig(source: string, port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): VitePatch;
|
|
23
34
|
export {};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* bails to a `manual` result so we never half-edit a build config (a broken config is worse than a
|
|
5
5
|
* documented manual step).
|
|
6
6
|
*/
|
|
7
|
+
import { braceDepth, maskStringsAndComments, matchingBraceEnd } from './brace-scan.js';
|
|
7
8
|
import { PatchKind } from './patch-kind.js';
|
|
8
9
|
export const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
9
10
|
const RETICLE_MARKER = '@reticlehq/vite-plugin';
|
|
@@ -42,14 +43,23 @@ function reticlePluginCall(opts) {
|
|
|
42
43
|
];
|
|
43
44
|
return 0 === options.length ? 'reticle()' : `reticle({ ${options.join(', ')} })`;
|
|
44
45
|
}
|
|
45
|
-
/**
|
|
46
|
-
const
|
|
46
|
+
/** A `plugins:` key, up to the start of its value. */
|
|
47
|
+
const PLUGINS_KEY_COLON = /\bplugins\s*:\s*/g;
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* The start of a plugin list we can extend, read at the start of the key's value: a plain array
|
|
50
|
+
* literal, or the same array returned by Vite+'s `lazyPlugins(() => [...])`.
|
|
51
|
+
*
|
|
52
|
+
* `vp migrate` and `vp create` wrap the app's plugins in `lazyPlugins`, so Vite+ can read the lint
|
|
53
|
+
* and format blocks without loading every plugin. The list inside is still the app's plugin list,
|
|
54
|
+
* and Vite loads it for `vp dev`. Without this form, init left a migrated Laravel app as a manual
|
|
55
|
+
* step (#2134).
|
|
56
|
+
*/
|
|
57
|
+
const PLUGIN_LIST_START = /^(?:lazyPlugins\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*)?\[/;
|
|
58
|
+
/**
|
|
59
|
+
* The shorthand `{ plugins }` key. When it holds, the key is there but not as an array literal, so
|
|
60
|
+
* adding our own key would make a second one. The later key wins, and one of the two lists is lost.
|
|
51
61
|
*/
|
|
52
|
-
const
|
|
62
|
+
const PLUGINS_SHORTHAND = /\bplugins\s*[,}]/;
|
|
53
63
|
/** Matches an ES import statement (used to place our import after the last one). */
|
|
54
64
|
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
55
65
|
/**
|
|
@@ -62,9 +72,15 @@ const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
|
62
72
|
* bails — the rule stays "only edit a shape we can see whole".
|
|
63
73
|
*/
|
|
64
74
|
const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
|
|
75
|
+
/** Where the default export starts, for a config that is not an object literal. */
|
|
76
|
+
const EXPORT_DEFAULT = /\bexport\s+default\b/;
|
|
65
77
|
/** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
|
|
66
78
|
export const VitePatchKind = PatchKind;
|
|
79
|
+
const UNREADABLE_REASON = 'init could not read this config safely (a string, comment or regex that it could not follow), ' +
|
|
80
|
+
'so it did not change it. Add `reticle()` to the `plugins` list of the exported config.';
|
|
67
81
|
const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
|
|
82
|
+
const AMBIGUOUS_PLUGINS_REASON = 'the config function builds more than one `plugins` list at the same level, so init did not ' +
|
|
83
|
+
'pick one. Add `reticle()` to the `plugins` list of the config that the function returns.';
|
|
68
84
|
const PLUGINS_NOT_ARRAY_REASON = 'the `plugins` key is not an array literal, so init did not change it. Add `reticle()` to the ' +
|
|
69
85
|
'list that the key builds. Do not add a second `plugins` key: the later key replaces the first.';
|
|
70
86
|
function insertImport(source) {
|
|
@@ -83,15 +99,144 @@ function insertImport(source) {
|
|
|
83
99
|
* what a formatter rewrites, turning a one-line install into a diff against the user's own style. A
|
|
84
100
|
* single-line array needs the space, or the result reads `[reticle(),react()]`.
|
|
85
101
|
*/
|
|
86
|
-
function insertPlugin(source, call) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
function insertPlugin(source, call, at) {
|
|
103
|
+
const next = source[at] ?? '';
|
|
104
|
+
const separator = '' === next || /\s/.test(next) ? '' : ' ';
|
|
105
|
+
return `${source.slice(0, at)}${call},${separator}${source.slice(at)}`;
|
|
106
|
+
}
|
|
107
|
+
/** No single `plugins` key can be told to be the config's own: two lists compete for it. */
|
|
108
|
+
const AMBIGUOUS = 'ambiguous';
|
|
109
|
+
/**
|
|
110
|
+
* Where the value of the config's OWN `plugins` key starts, undefined when the config has none, or
|
|
111
|
+
* AMBIGUOUS when two lists compete and nothing says which one Vite reads.
|
|
112
|
+
*
|
|
113
|
+
* Other blocks can hold a `plugins` key too: Vite+'s `lint.plugins` names oxlint plugins, and
|
|
114
|
+
* `worker.plugins` and `build.rollupOptions.plugins` are other bundlers' lists. The first match in
|
|
115
|
+
* the file used to win. On a `vp create` React app that is `lint.plugins`, so init wrote
|
|
116
|
+
* `reticle()` into the lint settings, reported the step done, and the app never connected (#2134).
|
|
117
|
+
*
|
|
118
|
+
* So when the exported object is a literal we can see, only a key at that literal's own level, inside
|
|
119
|
+
* it, is its key. A config written as a function is read by `functionConfigPluginsAt`.
|
|
120
|
+
*/
|
|
121
|
+
function vitePluginsValueAt(source) {
|
|
122
|
+
const literal = exportedLiteralStart(source);
|
|
123
|
+
if (literal !== undefined) {
|
|
124
|
+
// The literal is in view, so only its own keys count. A key outside it is another object's: a
|
|
125
|
+
// hoisted `const lint = { plugins: [...] }` that the config names by shorthand.
|
|
126
|
+
return ownPluginsKeyAt(source, literal);
|
|
127
|
+
}
|
|
128
|
+
return functionConfigPluginsAt(source, exportedRegion(source));
|
|
129
|
+
}
|
|
130
|
+
/** Where the value of the `plugins` key of the object literal whose inner start is `literal` begins. */
|
|
131
|
+
function ownPluginsKeyAt(source, literal) {
|
|
132
|
+
const end = matchingBraceEnd(source, literal) ?? source.length;
|
|
133
|
+
const depth = braceDepth(source, literal);
|
|
134
|
+
for (const match of source.matchAll(PLUGINS_KEY_COLON)) {
|
|
135
|
+
if (match.index > literal && match.index < end && depth === braceDepth(source, match.index)) {
|
|
136
|
+
return match.index + match[0].length;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
/** An object literal a config function gives back: `return {` or an arrow's `=> ({`. */
|
|
142
|
+
const RETURNED_OBJECT = /\breturn\s*\(?\s*\{|=>\s*\(\s*\{/g;
|
|
143
|
+
/**
|
|
144
|
+
* The `plugins` value of a config built by a function (`defineConfig(({ mode }) => { ... })`).
|
|
145
|
+
*
|
|
146
|
+
* The config is the object the function returns. A body can build other objects with a `plugins`
|
|
147
|
+
* key first (`const x = { plugins: [legacy()] }`), at the same depth as the returned one, and the
|
|
148
|
+
* key that came first used to win. So the returned object literals are read first: one with its own
|
|
149
|
+
* `plugins` key is the config's list, and two (a config per mode) are AMBIGUOUS. A returned object
|
|
150
|
+
* inside another one is that object's value, such as an inline plugin's hook, and does not count.
|
|
151
|
+
*
|
|
152
|
+
* A function that returns a name (`return config`) or a call has no literal to read, and a returned
|
|
153
|
+
* literal may take its list from a spread (`...shared`). Then the key with the fewest open braces in
|
|
154
|
+
* the exported expression wins, and a tie is AMBIGUOUS. A
|
|
155
|
+
* const declared above the export (`const lint = { plugins: [...] }`) is never in that region.
|
|
156
|
+
*/
|
|
157
|
+
function functionConfigPluginsAt(source, region) {
|
|
158
|
+
const returned = [];
|
|
159
|
+
for (const match of source.matchAll(RETURNED_OBJECT)) {
|
|
160
|
+
const start = match.index + match[0].length;
|
|
161
|
+
if (match.index < region.from || match.index >= region.to) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (returned.some((outer) => start > outer.start && start <= outer.end)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
returned.push({ start, end: matchingBraceEnd(source, start) ?? source.length });
|
|
168
|
+
}
|
|
169
|
+
const keys = returned
|
|
170
|
+
.map((object) => ownPluginsKeyAt(source, object.start))
|
|
171
|
+
.filter((at) => at !== undefined);
|
|
172
|
+
if (1 < keys.length) {
|
|
173
|
+
return AMBIGUOUS;
|
|
174
|
+
}
|
|
175
|
+
if (1 === keys.length) {
|
|
176
|
+
return keys[0];
|
|
177
|
+
}
|
|
178
|
+
// A returned literal with no `plugins` key of its own is the whole config, and it has no list:
|
|
179
|
+
// a `plugins` key elsewhere in the body is another object's. Only a spread (`...shared`) can
|
|
180
|
+
// bring one in, and then the body's own keys are all there is to go on.
|
|
181
|
+
if (0 < returned.length &&
|
|
182
|
+
!returned.some((object) => source.slice(object.start, object.end).includes('...'))) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
let best;
|
|
186
|
+
for (const match of source.matchAll(PLUGINS_KEY_COLON)) {
|
|
187
|
+
if (match.index < region.from || match.index >= region.to) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const depth = braceDepth(source, match.index);
|
|
191
|
+
if (best === undefined || depth < best.depth) {
|
|
192
|
+
best = { depth, valueAt: match.index + match[0].length, tied: false };
|
|
193
|
+
}
|
|
194
|
+
else if (depth === best.depth) {
|
|
195
|
+
best.tied = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (best !== undefined && best.tied) {
|
|
199
|
+
return AMBIGUOUS;
|
|
200
|
+
}
|
|
201
|
+
return best?.valueAt;
|
|
202
|
+
}
|
|
203
|
+
/** The name a default export refers to: `export default config` or `export default defineConfig(config)`. */
|
|
204
|
+
const EXPORTED_NAME = /\bexport\s+default\s+(?:defineConfig\s*\(\s*)?([A-Za-z_$][\w$]*)\s*(?:[;)]|$)/m;
|
|
205
|
+
/** Where `const|let|var <name> =` declares the name the default export refers to, if it does. */
|
|
206
|
+
function exportedDeclaration(source) {
|
|
207
|
+
const name = EXPORTED_NAME.exec(source)?.[1];
|
|
208
|
+
if (name === undefined) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
return new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]*)?=\\s*(?:defineConfig\\s*\\(\\s*)?`).exec(source);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The index just past the `{` of the exported config object literal: written in the export itself,
|
|
216
|
+
* or in the declaration of the name it exports (`const config = defineConfig({...})`).
|
|
217
|
+
*/
|
|
218
|
+
function exportedLiteralStart(source) {
|
|
219
|
+
const inline = CONFIG_OBJECT.exec(source);
|
|
220
|
+
if (null !== inline) {
|
|
221
|
+
return inline.index + inline[0].length;
|
|
222
|
+
}
|
|
223
|
+
const declared = exportedDeclaration(source);
|
|
224
|
+
if (null !== declared && '{' === source[declared.index + declared[0].length]) {
|
|
225
|
+
return declared.index + declared[0].length + 1;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The part of the file that holds the exported config: from the declaration of the name it exports
|
|
231
|
+
* up to the export, or from the export to the end of the file.
|
|
232
|
+
*/
|
|
233
|
+
function exportedRegion(source) {
|
|
234
|
+
const exported = EXPORT_DEFAULT.exec(source)?.index;
|
|
235
|
+
const declared = exportedDeclaration(source)?.index;
|
|
236
|
+
if (declared !== undefined && exported !== undefined && declared < exported) {
|
|
237
|
+
return { from: declared, to: exported };
|
|
238
|
+
}
|
|
239
|
+
return { from: exported ?? 0, to: source.length };
|
|
95
240
|
}
|
|
96
241
|
/**
|
|
97
242
|
* Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
|
|
@@ -106,23 +251,46 @@ function insertPluginsKey(source, call) {
|
|
|
106
251
|
return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
|
|
107
252
|
});
|
|
108
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* The package a new config imports `defineConfig` from.
|
|
256
|
+
*
|
|
257
|
+
* A Vite+ app imports it from `vite-plus`. Under pnpm it may not declare `vite` at all, so an
|
|
258
|
+
* import from `vite` would not resolve, and Vite+'s own lint rule refuses it.
|
|
259
|
+
*/
|
|
260
|
+
export const DefineConfigSource = {
|
|
261
|
+
VITE: 'vite',
|
|
262
|
+
VITE_PLUS: 'vite-plus',
|
|
263
|
+
};
|
|
109
264
|
/**
|
|
110
265
|
* A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
|
|
111
266
|
* rather than patched from an empty one, because it is a file the person will open and read.
|
|
112
267
|
*/
|
|
113
|
-
export function newViteConfig(port, captureBodies = false, inject = true, sourceMapping = true) {
|
|
268
|
+
export function newViteConfig(port, captureBodies = false, inject = true, sourceMapping = true, defineConfigFrom = DefineConfigSource.VITE) {
|
|
114
269
|
const call = reticlePluginCall({ port, captureBodies, inject, sourceMapping });
|
|
115
|
-
return `import { defineConfig } from '
|
|
270
|
+
return `import { defineConfig } from '${defineConfigFrom}';\n${VITE_IMPORT}\n\nexport default defineConfig({\n plugins: [${call}],\n});\n`;
|
|
116
271
|
}
|
|
117
272
|
export function patchViteConfig(source, port, captureBodies = false, inject = true, sourceMapping = true) {
|
|
118
273
|
if (source.includes(RETICLE_MARKER)) {
|
|
119
274
|
return { kind: VitePatchKind.ALREADY };
|
|
120
275
|
}
|
|
121
276
|
const call = reticlePluginCall({ port, captureBodies, inject, sourceMapping });
|
|
122
|
-
|
|
123
|
-
|
|
277
|
+
const masked = maskStringsAndComments(source);
|
|
278
|
+
if (null === masked) {
|
|
279
|
+
return { kind: VitePatchKind.MANUAL, reason: UNREADABLE_REASON };
|
|
280
|
+
}
|
|
281
|
+
const valueAt = vitePluginsValueAt(masked);
|
|
282
|
+
if (AMBIGUOUS === valueAt) {
|
|
283
|
+
return { kind: VitePatchKind.MANUAL, reason: AMBIGUOUS_PLUGINS_REASON };
|
|
284
|
+
}
|
|
285
|
+
if (valueAt !== undefined) {
|
|
286
|
+
const list = PLUGIN_LIST_START.exec(source.slice(valueAt));
|
|
287
|
+
if (null === list) {
|
|
288
|
+
return { kind: VitePatchKind.MANUAL, reason: PLUGINS_NOT_ARRAY_REASON };
|
|
289
|
+
}
|
|
290
|
+
const code = insertPlugin(source, call, valueAt + list[0].length);
|
|
291
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(code) };
|
|
124
292
|
}
|
|
125
|
-
if (
|
|
293
|
+
if (PLUGINS_SHORTHAND.test(masked)) {
|
|
126
294
|
return { kind: VitePatchKind.MANUAL, reason: PLUGINS_NOT_ARRAY_REASON };
|
|
127
295
|
}
|
|
128
296
|
if (CONFIG_OBJECT.test(source)) {
|
|
@@ -62,3 +62,5 @@ export declare const STEP_TITLES: readonly StepTitle[];
|
|
|
62
62
|
export declare const CONNECT_STEP_TITLES: ReadonlySet<StepTitle>;
|
|
63
63
|
/** True when this step is what makes the app dial the daemon. */
|
|
64
64
|
export declare function isConnectStep(title: string): boolean;
|
|
65
|
+
/** True when this step patches the Vite config. */
|
|
66
|
+
export declare function isViteConfigStep(title: string): boolean;
|
|
@@ -115,3 +115,12 @@ export const CONNECT_STEP_TITLES = new Set([
|
|
|
115
115
|
export function isConnectStep(title) {
|
|
116
116
|
return CONNECT_STEP_TITLES.has(title);
|
|
117
117
|
}
|
|
118
|
+
/** The steps that patch the Vite config the dev server loads. */
|
|
119
|
+
const VITE_CONFIG_STEP_TITLES = new Set([
|
|
120
|
+
StepTitle.VITE_PLUGIN,
|
|
121
|
+
StepTitle.ELECTRON_VITE_PLUGIN,
|
|
122
|
+
]);
|
|
123
|
+
/** True when this step patches the Vite config. */
|
|
124
|
+
export function isViteConfigStep(title) {
|
|
125
|
+
return VITE_CONFIG_STEP_TITLES.has(title);
|
|
126
|
+
}
|
package/dist/plan/plan-vite.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Start, the Electron renderers) composes these steps, while the rest of `plan-framework.ts` is one
|
|
8
8
|
* builder per framework that has its own files to write.
|
|
9
9
|
*/
|
|
10
|
-
import { newViteConfig, patchViteConfig, VitePatchKind, VITE_IMPORT } from '../patch/vite-config.js';
|
|
10
|
+
import { DefineConfigSource, newViteConfig, patchViteConfig, VitePatchKind, VITE_IMPORT, } from '../patch/vite-config.js';
|
|
11
11
|
import { viteManual, viteDevModuleFile, VITE_DEV_MODULE_PATH } from '../patch/snippets.js';
|
|
12
12
|
import { hasOptOut, OPT_OUT_MARKER } from '../detect/declared/init-opt-out.js';
|
|
13
13
|
import { Framework } from '../detect/detect.js';
|
|
@@ -228,7 +228,9 @@ function viteConfigSteps(input, detail, inject = true) {
|
|
|
228
228
|
detail,
|
|
229
229
|
write: {
|
|
230
230
|
path,
|
|
231
|
-
content: newViteConfig(port, true === input.captureBodies, inject, stampSource
|
|
231
|
+
content: newViteConfig(port, true === input.captureBodies, inject, stampSource, undefined === input.detection.dependencies?.[DefineConfigSource.VITE_PLUS]
|
|
232
|
+
? DefineConfigSource.VITE
|
|
233
|
+
: DefineConfigSource.VITE_PLUS),
|
|
232
234
|
expect: [VITE_IMPORT, 'reticle('],
|
|
233
235
|
},
|
|
234
236
|
dependsOnInstall: true,
|
|
@@ -24,4 +24,4 @@ export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
|
|
|
24
24
|
* are told apart.
|
|
25
25
|
*/
|
|
26
26
|
export declare const SLASH_COMMAND_SIGNATURE = "description: Verify this app in the browser with Reticle";
|
|
27
|
-
export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Step 0 \u2014 is anything connected?\n\nCall `reticle_session { action: \"list\" }` first.\n\n**A session is listed?** Go to \"Pick ONE flow\".\n\n**No session?** Setup is not finished, and stopping to ask the user to run something is not an\nanswer. Every step below is yours to do without them. Work these in order, and say which one you are\non, one line each:\n\n0. **Is Reticle even wired into this project?** `cat .reticle.json`. If it is not there, this\n project was never set up. Tell the user in one line that `init` will change package.json,\n the lockfile and the build config, then run `npx @reticlehq/server@latest init`, fix every\n `\u26A0` and `\u2139` it prints, and carry on. Do not ask which framework or port \u2014 both come out\n of the repo you are sitting in.\n\n1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to\n step 2 \u2014 the cause is then almost always that the SDK never loaded in the page, not a missing\n server, so do not tell the user to start one they are already running. If nothing is listening:\n\n**A dev server already running when `reticle init` ran does not have Reticle in its bundle.** It read the build config at boot; `init` edited it afterwards. It serves the old bundle and no session appears. In order:\n\n1. **A dev server was already running?** Restart it, then hard-reload the tab. \"Something is listening\" does not mean the right bundle is served.\n2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. `reticle_session { action: \"list\" }` gives you this project's own dev command in `next_action`; use that, never compose one. Started after `init`, it needs no restart.\n\nStopping to ask is how a verification turn ends with nothing verified.\n\nFour guards, none optional:\n\n1. **Never run two at once.** One dev server on the app's port. Restarting a stale one means stopping it first, not starting a second alongside it.\n2. **Never guess the command.** It comes from `package.json` scripts. No recognisable dev script means say so and stop, not invent one.\n3. **Never kill anything you did not start**, and never a daemon or a port holder. The one exception is the restart above, and say in one line that you did it.\n4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.\n\n2. **Is a page open on it?** Do not ask the user to open a browser \u2014 open it:\n\n ```bash\n npx @reticlehq/server open <the url the dev server is serving>\n ```\n\n That reuses an already-connected tab or opens a new one, and waits for the page to register. It is\n also the answer on a machine with no browser of its own (CI, a container, an SSH session): the CLI\n is the one escape hatch that exists on every tool surface, because it is not a tool.\n\nThen, once something is serving the app and a page is on it:\n\n3. **Is the SDK actually in the app?** Read the app's entry file (`src/main.tsx`, `app/layout.tsx`,\n `src/app.vue`, whatever this project uses). You are looking for an `import` from\n `@reticlehq/react` or `@reticlehq/browser` and a `connect()` call, or the Reticle plugin in\n `vite.config.*`, or `withReticle` in `next.config.*`. **If it is not there, that is the bug.**\n Wire it in \u2014 see the framework snippets at https://docs.reticle.sh/instrumentation \u2014 then restart\n the dev server and re-check.\n4. **Is the connect guarded on `hostname === 'localhost'`?** If so, remove that guard. It is false on\n any non-localhost dev host, and `window` does not exist during SSR, so the guard silently prevents\n the connect and logs nothing. Use the framework's dev flag instead (`import.meta.env.DEV`,\n `process.env.NODE_ENV !== 'production'`, `import.meta.dev`).\n5. **Is the dev server serving that entry?** Look in `reticle_observe { action: \"console\" }`, or in the page you opened in\n step 2, for a line starting `[Reticle]`. That line, or its absence, tells you which side is at\n fault. Reticle itself never starts a dev server \u2014 starting it is your job, under the guards above.\n6. **Do both sides agree on the port?** Run `npx @reticlehq/server doctor` and compare the daemon\n port against the port the page dials.\n\nIf you are still stuck after all of those, file it: `reticle_session { action: \"feedback\" }` with kind `gap`, saying what you\nchecked and what you saw. Then tell the user exactly which step is blocked. Do not report the install\nas finished.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_look { action: \"page\" }` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n2. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n3. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n4. Finish with `reticle_observe { action: \"console\" }` and `reticle_observe { action: \"network\" }` to catch errors the DOM does not show.\n\n**Only `reticle_act_and_wait`, `reticle_assert`, `reticle_act { steps }` with an `expect`, and `reticle_verify` produce a verdict.** A run that ends on a\nsnapshot has proved nothing, however many tools it used.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n\n## If the user handed you a license key\n\nAppend `RETICLE_LICENSE_KEY=<key>` to the project's `.env` (create it if absent), make sure `.env`\nis in `.gitignore`, and confirm
|
|
27
|
+
export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Step 0 \u2014 is anything connected?\n\nCall `reticle_session { action: \"list\" }` first.\n\n**A session is listed?** Go to \"Pick ONE flow\".\n\n**No session?** Setup is not finished, and stopping to ask the user to run something is not an\nanswer. Every step below is yours to do without them. Work these in order, and say which one you are\non, one line each:\n\n0. **Is Reticle even wired into this project?** `cat .reticle.json`. If it is not there, this\n project was never set up. Tell the user in one line that `init` will change package.json,\n the lockfile and the build config, then run `npx @reticlehq/server@latest init`, fix every\n `\u26A0` and `\u2139` it prints, and carry on. Do not ask which framework or port \u2014 both come out\n of the repo you are sitting in.\n\n1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to\n step 2 \u2014 the cause is then almost always that the SDK never loaded in the page, not a missing\n server, so do not tell the user to start one they are already running. If nothing is listening:\n\n**A dev server already running when `reticle init` ran does not have Reticle in its bundle.** It read the build config at boot; `init` edited it afterwards. It serves the old bundle and no session appears. In order:\n\n1. **A dev server was already running?** Restart it, then hard-reload the tab. \"Something is listening\" does not mean the right bundle is served.\n2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. `reticle_session { action: \"list\" }` gives you this project's own dev command in `next_action`; use that, never compose one. Started after `init`, it needs no restart.\n\nStopping to ask is how a verification turn ends with nothing verified.\n\nFour guards, none optional:\n\n1. **Never run two at once.** One dev server on the app's port. Restarting a stale one means stopping it first, not starting a second alongside it.\n2. **Never guess the command.** It comes from `package.json` scripts. No recognisable dev script means say so and stop, not invent one.\n3. **Never kill anything you did not start**, and never a daemon or a port holder. The one exception is the restart above, and say in one line that you did it.\n4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.\n\n2. **Is a page open on it?** Do not ask the user to open a browser \u2014 open it:\n\n ```bash\n npx @reticlehq/server open <the url the dev server is serving>\n ```\n\n That reuses an already-connected tab or opens a new one, and waits for the page to register. It is\n also the answer on a machine with no browser of its own (CI, a container, an SSH session): the CLI\n is the one escape hatch that exists on every tool surface, because it is not a tool.\n\nThen, once something is serving the app and a page is on it:\n\n3. **Is the SDK actually in the app?** Read the app's entry file (`src/main.tsx`, `app/layout.tsx`,\n `src/app.vue`, whatever this project uses). You are looking for an `import` from\n `@reticlehq/react` or `@reticlehq/browser` and a `connect()` call, or the Reticle plugin in\n `vite.config.*`, or `withReticle` in `next.config.*`. **If it is not there, that is the bug.**\n Wire it in \u2014 see the framework snippets at https://docs.reticle.sh/instrumentation \u2014 then restart\n the dev server and re-check.\n4. **Is the connect guarded on `hostname === 'localhost'`?** If so, remove that guard. It is false on\n any non-localhost dev host, and `window` does not exist during SSR, so the guard silently prevents\n the connect and logs nothing. Use the framework's dev flag instead (`import.meta.env.DEV`,\n `process.env.NODE_ENV !== 'production'`, `import.meta.dev`).\n5. **Is the dev server serving that entry?** Look in `reticle_observe { action: \"console\" }`, or in the page you opened in\n step 2, for a line starting `[Reticle]`. That line, or its absence, tells you which side is at\n fault. Reticle itself never starts a dev server \u2014 starting it is your job, under the guards above.\n6. **Do both sides agree on the port?** Run `npx @reticlehq/server doctor` and compare the daemon\n port against the port the page dials.\n\nIf you are still stuck after all of those, file it: `reticle_session { action: \"feedback\" }` with kind `gap`, saying what you\nchecked and what you saw. Then tell the user exactly which step is blocked. Do not report the install\nas finished.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_look { action: \"page\" }` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n2. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n3. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n4. Finish with `reticle_observe { action: \"console\" }` and `reticle_observe { action: \"network\" }` to catch errors the DOM does not show.\n\n**Only `reticle_act_and_wait`, `reticle_assert`, `reticle_act { steps }` with an `expect`, and `reticle_verify` produce a verdict.** A run that ends on a\nsnapshot has proved nothing, however many tools it used.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n\n## If the user handed you a license key\n\nAppend `RETICLE_LICENSE_KEY=<key>` to the project's `.env` (create it if absent), make sure `.env`\nis in `.gitignore`, and confirm that `npx @reticlehq/server status --json` shows\n`license.status` `active`. The CLI folds a project-local `.env` in on every invocation, so\nnothing else changes. Never echo the key back, and never put it in a commit, a comment, or a feedback\nreport.\n\n## Two things afterwards\n\n- **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with\n `reticle_session { action: \"feedback\" }` now, before your context is gone. That report is the only signal that decides\n what gets fixed. Bugs in the app under test are Reticle working; those go in your answer to the user.\n- **If the verification actually succeeded**, add one line for the human: Reticle is open source at\n https://github.com/reticlehq/reticle, and a star helps other people find it. One line, once. Skip it\n entirely if the run did not produce a verdict.\n";
|
|
@@ -118,9 +118,10 @@ Do not weaken or skip an assertion to make the run pass — that is a finding, n
|
|
|
118
118
|
## If the user handed you a license key
|
|
119
119
|
|
|
120
120
|
Append \`RETICLE_LICENSE_KEY=<key>\` to the project's \`.env\` (create it if absent), make sure \`.env\`
|
|
121
|
-
is in \`.gitignore\`, and confirm
|
|
122
|
-
\`.env\` in on every invocation, so
|
|
123
|
-
a commit, a comment, or a feedback
|
|
121
|
+
is in \`.gitignore\`, and confirm that \`npx @reticlehq/server status --json\` shows
|
|
122
|
+
\`license.status\` \`active\`. The CLI folds a project-local \`.env\` in on every invocation, so
|
|
123
|
+
nothing else changes. Never echo the key back, and never put it in a commit, a comment, or a feedback
|
|
124
|
+
report.
|
|
124
125
|
|
|
125
126
|
## Two things afterwards
|
|
126
127
|
|
package/dist/run-types.d.ts
CHANGED
|
@@ -124,6 +124,11 @@ export interface InitContext {
|
|
|
124
124
|
readonly devCommand?: string | undefined;
|
|
125
125
|
/** Set when init redirected into a workspace app, naming the one it chose. */
|
|
126
126
|
readonly redirectedTo?: string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* The Vite config that init could not patch and left as a manual step. The dev server loads it,
|
|
129
|
+
* so a later dev-server stop names this step as the likely cause (#1263).
|
|
130
|
+
*/
|
|
131
|
+
readonly pendingViteConfig?: string | undefined;
|
|
127
132
|
}
|
|
128
133
|
export interface InitResult {
|
|
129
134
|
ok: boolean;
|
package/dist/run.js
CHANGED
|
@@ -15,7 +15,7 @@ import { wasMcpRegistered } from './register/mcp-registered.js';
|
|
|
15
15
|
import { PACKAGE_JSON } from './detect/workspace-apps.js';
|
|
16
16
|
import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
|
|
17
17
|
import { appsAt, enclosingWorkspaceRoot } from './detect/workspace-apps.js';
|
|
18
|
-
import { isConnectStep } from './plan/connect-steps.js';
|
|
18
|
+
import { isConnectStep, isViteConfigStep } from './plan/connect-steps.js';
|
|
19
19
|
import { formatGeneratedSource } from './patch/format-generated.js';
|
|
20
20
|
import { DEPS_TARGET, RETICLE_CONFIG_FILE, MCP_TARGET, buildPlan, StepStatus, } from './plan/plan.js';
|
|
21
21
|
import { InitFailure } from './diagnose/init-failure.js';
|
|
@@ -424,12 +424,16 @@ function runInitSteps(options, io) {
|
|
|
424
424
|
// The step's REAL final status, not the absence of a failure — see mcp-registered.
|
|
425
425
|
mcpRegistered: wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped)),
|
|
426
426
|
};
|
|
427
|
+
// A Vite config left for the reader. The dev server loads it, so setup names it on a stop (#1263).
|
|
428
|
+
const pendingViteConfig = plan.steps.find((s) => isViteConfigStep(s.title) &&
|
|
429
|
+
(StepStatus.MANUAL === s.status || failed.has(s.target) || skipped.has(s.target)))?.target;
|
|
427
430
|
const context = {
|
|
428
431
|
appDir: options.cwd,
|
|
429
432
|
framework: plan.framework,
|
|
430
433
|
packageManager: planInput.detection.packageManager,
|
|
431
434
|
...(undefined === devCommand ? {} : { devCommand }),
|
|
432
435
|
...(true === options.redirected ? { redirectedTo: options.cwd } : {}),
|
|
436
|
+
...(undefined === pendingViteConfig ? {} : { pendingViteConfig }),
|
|
433
437
|
};
|
|
434
438
|
/*
|
|
435
439
|
* The two INSTALL steps `init` is the only thing that can answer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/init",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.1",
|
|
4
4
|
"description": "Reticle project scaffolder — detects the framework, writes the config, patches the build and registers the MCP server. The build-time half of `reticle init`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "3.
|
|
16
|
+
"@reticlehq/core": "3.10.1"
|
|
17
17
|
},
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"author": "Reticle contributors",
|