vize 0.303.0 → 0.310.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1589 -3
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +12 -3
- package/src/init/args.ts +135 -0
- package/src/init/detect.ts +221 -0
- package/src/init/edit-config.ts +161 -0
- package/src/init/lint-target.ts +202 -0
- package/src/init/plan-bundler.ts +97 -0
- package/src/init/plan-editor.ts +98 -0
- package/src/init/plan-lint.ts +68 -0
- package/src/init/plan-project.ts +114 -0
- package/src/init/plan-types.ts +77 -0
- package/src/init/plan.ts +167 -0
- package/src/init/prompt.ts +161 -0
- package/src/init/report.ts +136 -0
- package/src/init/select.ts +153 -0
- package/src/init/templates.ts +175 -0
- package/src/init/top-level.ts +143 -0
- package/src/init.ts +189 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ProjectDetection } from "./detect.js";
|
|
2
|
+
import { unreadOxlintConfig } from "./lint-target.js";
|
|
3
|
+
import type { InitPlan } from "./plan.js";
|
|
4
|
+
import { EDITOR_INTEGRATIONS } from "./templates.js";
|
|
5
|
+
|
|
6
|
+
const PREFIX = "[vize init]";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detection summary, printed before any prompt.
|
|
10
|
+
*
|
|
11
|
+
* Users need to see what `init` concluded before they are asked to act on it;
|
|
12
|
+
* an unexpected line here is the cheapest place to catch a wrong root or a
|
|
13
|
+
* missing lockfile.
|
|
14
|
+
*/
|
|
15
|
+
export function renderDetection(detection: ProjectDetection): string {
|
|
16
|
+
const lines = [
|
|
17
|
+
`${PREFIX} detected in ${detection.root}:`,
|
|
18
|
+
` framework: ${describeFramework(detection)}`,
|
|
19
|
+
` package manager: ${detection.packageManager ?? "none detected (defaulting to npm)"}`,
|
|
20
|
+
` language: ${detection.typescript ? "TypeScript" : "JavaScript"}${
|
|
21
|
+
detection.tsconfig === null ? " (no tsconfig.json)" : " (tsconfig.json)"
|
|
22
|
+
}`,
|
|
23
|
+
` lint command: ${detection.usesVitePlus ? "vp lint" : "oxlint"}`,
|
|
24
|
+
` vize config: ${detection.vizeConfig ?? "none"}`,
|
|
25
|
+
` oxlint config: ${describeOxlintConfig(detection)}`,
|
|
26
|
+
];
|
|
27
|
+
return `${lines.join("\n")}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function describeFramework(detection: ProjectDetection): string {
|
|
31
|
+
if (detection.framework === "nuxt") {
|
|
32
|
+
return `Nuxt (${detection.nuxtConfig ?? "nuxt dependency, no nuxt.config"})`;
|
|
33
|
+
}
|
|
34
|
+
if (detection.framework === "vite") {
|
|
35
|
+
const configs = detection.viteConfigs.join(", ");
|
|
36
|
+
return detection.usesVitePlus ? `Vite+ (${configs})` : `Vite (${configs})`;
|
|
37
|
+
}
|
|
38
|
+
return "none (no vite.config or nuxt.config)";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function describeOxlintConfig(detection: ProjectDetection): string {
|
|
42
|
+
const unread = unreadOxlintConfig(detection);
|
|
43
|
+
if (unread !== null) {
|
|
44
|
+
return `${unread} — present but oxlint does not read this name (#3474)`;
|
|
45
|
+
}
|
|
46
|
+
return detection.oxlintConfig ?? "none";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The full plan.
|
|
51
|
+
*
|
|
52
|
+
* Printed before anything is written in both modes, so the wording is what the
|
|
53
|
+
* run is about to do, not what it has done. `--dry-run` differs only in stopping
|
|
54
|
+
* afterwards.
|
|
55
|
+
*/
|
|
56
|
+
export function renderPlan(plan: InitPlan, dryRun: boolean): string {
|
|
57
|
+
const verb = dryRun ? "would" : "will";
|
|
58
|
+
const lines: string[] = [`${PREFIX} plan:`];
|
|
59
|
+
for (const feature of plan.features) {
|
|
60
|
+
lines.push(` ${feature.id.padEnd(9)} ${feature.outcome.padEnd(10)} ${feature.detail}`);
|
|
61
|
+
}
|
|
62
|
+
for (const filename of plan.createdFiles) {
|
|
63
|
+
lines.push(`${PREFIX} ${verb} create ${filename}`);
|
|
64
|
+
}
|
|
65
|
+
for (const filename of plan.updatedFiles) {
|
|
66
|
+
lines.push(`${PREFIX} ${verb} update ${filename}`);
|
|
67
|
+
}
|
|
68
|
+
if (plan.addedScripts.length > 0) {
|
|
69
|
+
lines.push(`${PREFIX} ${verb} add scripts: ${plan.addedScripts.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
for (const command of plan.commands) {
|
|
72
|
+
lines.push(`${PREFIX} ${verb} run: ${command.command} ${command.args.join(" ")}`);
|
|
73
|
+
}
|
|
74
|
+
if (plan.createdFiles.length + plan.updatedFiles.length + plan.commands.length === 0) {
|
|
75
|
+
lines.push(`${PREFIX} nothing to do; the project is already configured`);
|
|
76
|
+
}
|
|
77
|
+
return `${lines.join("\n")}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Snippets for anything `init` refused to edit.
|
|
82
|
+
*
|
|
83
|
+
* A blocked feature is deliberately loud. The alternative for the lint feature
|
|
84
|
+
* would be writing an Oxlint config the project's lint command never reads,
|
|
85
|
+
* which reports zero Vize diagnostics and exits 0 (#3389).
|
|
86
|
+
*/
|
|
87
|
+
export function renderBlocked(plan: InitPlan): string {
|
|
88
|
+
const blocked = plan.features.filter((feature) => feature.outcome === "blocked");
|
|
89
|
+
if (blocked.length === 0) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
for (const feature of blocked) {
|
|
94
|
+
lines.push(`${PREFIX} ${feature.id}: NOT configured — ${feature.detail}`);
|
|
95
|
+
if (feature.snippet !== null) {
|
|
96
|
+
lines.push("", indent(feature.snippet), "");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join("\n")}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderEditors(): string {
|
|
103
|
+
const lines = [`${PREFIX} editor integrations shipped with Vize:`];
|
|
104
|
+
for (const integration of EDITOR_INTEGRATIONS) {
|
|
105
|
+
lines.push(` ${integration}`);
|
|
106
|
+
}
|
|
107
|
+
return `${lines.join("\n")}\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Printed when the prompt ends without a confirmation.
|
|
112
|
+
*
|
|
113
|
+
* Covers both a declined confirmation and an input stream that closed
|
|
114
|
+
* mid-prompt. Saying so is what keeps a closed stdin from looking like a
|
|
115
|
+
* successful run that happened to change nothing.
|
|
116
|
+
*/
|
|
117
|
+
export function renderCancelled(): string {
|
|
118
|
+
return `${PREFIX} cancelled; nothing was written.\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderNonInteractiveRefusal(): string {
|
|
122
|
+
return (
|
|
123
|
+
`${PREFIX} stdin is not a TTY, so init will not prompt.\n` +
|
|
124
|
+
`${PREFIX} pass --yes with the features you want, for example:\n` +
|
|
125
|
+
`${PREFIX} vize init --yes --lint --vite --fmt --typecheck --editor\n` +
|
|
126
|
+
`${PREFIX} or run with --dry-run to print the plan without writing.\n`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function indent(source: string): string {
|
|
131
|
+
return source
|
|
132
|
+
.split("\n")
|
|
133
|
+
.map((line) => (line === "" ? line : ` ${line}`))
|
|
134
|
+
.join("\n")
|
|
135
|
+
.trimEnd();
|
|
136
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ProjectDetection } from "./detect.js";
|
|
2
|
+
import { discoveredOxlintConfig, unreadOxlintConfig } from "./lint-target.js";
|
|
3
|
+
|
|
4
|
+
export const FEATURE_IDS = ["lint", "bundler", "fmt", "typecheck", "editor"] as const;
|
|
5
|
+
|
|
6
|
+
export type FeatureId = (typeof FEATURE_IDS)[number];
|
|
7
|
+
|
|
8
|
+
export type FeatureSelection = Readonly<Record<FeatureId, boolean>>;
|
|
9
|
+
|
|
10
|
+
export interface FeatureOffer {
|
|
11
|
+
readonly id: FeatureId;
|
|
12
|
+
/** Label shown in the prompt and in the plan. Reflects what detection found. */
|
|
13
|
+
readonly label: string;
|
|
14
|
+
/** False when the project cannot support the feature at all. */
|
|
15
|
+
readonly available: boolean;
|
|
16
|
+
/** True when the project already has this feature wired up. */
|
|
17
|
+
readonly configured: boolean;
|
|
18
|
+
/** Why the feature is unavailable or already configured. Empty when neither. */
|
|
19
|
+
readonly note: string;
|
|
20
|
+
readonly defaultSelected: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Turns detection into the five offers `init` presents.
|
|
25
|
+
*
|
|
26
|
+
* Already-configured features stay selected by default so a re-run is a no-op
|
|
27
|
+
* the user can confirm rather than a set of boxes they have to re-tick.
|
|
28
|
+
*/
|
|
29
|
+
export function offerFeatures(detection: ProjectDetection): readonly FeatureOffer[] {
|
|
30
|
+
return [
|
|
31
|
+
lintOffer(detection),
|
|
32
|
+
bundlerOffer(detection),
|
|
33
|
+
fmtOffer(detection),
|
|
34
|
+
typecheckOffer(detection),
|
|
35
|
+
editorOffer(detection),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Selection implied by detection alone, used by `--yes` and as the prompt default. */
|
|
40
|
+
export function defaultSelection(offers: readonly FeatureOffer[]): FeatureSelection {
|
|
41
|
+
const selection: Record<FeatureId, boolean> = {
|
|
42
|
+
lint: false,
|
|
43
|
+
bundler: false,
|
|
44
|
+
fmt: false,
|
|
45
|
+
typecheck: false,
|
|
46
|
+
editor: false,
|
|
47
|
+
};
|
|
48
|
+
for (const offer of offers) {
|
|
49
|
+
selection[offer.id] = offer.defaultSelected;
|
|
50
|
+
}
|
|
51
|
+
return selection;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function lintOffer(detection: ProjectDetection): FeatureOffer {
|
|
55
|
+
const configured = detection.usesVitePlus
|
|
56
|
+
? detection.hasVitePlusLintBlock
|
|
57
|
+
: discoveredOxlintConfig(detection) !== null;
|
|
58
|
+
const unread = unreadOxlintConfig(detection);
|
|
59
|
+
const label = detection.usesVitePlus
|
|
60
|
+
? "oxlint plugin (vp lint reads the `lint` block in the Vite config)"
|
|
61
|
+
: "oxlint plugin (the oxlint binary reads oxlint.config.ts)";
|
|
62
|
+
return {
|
|
63
|
+
id: "lint",
|
|
64
|
+
label,
|
|
65
|
+
available: true,
|
|
66
|
+
configured,
|
|
67
|
+
note: configured
|
|
68
|
+
? "already configured"
|
|
69
|
+
: unread === null
|
|
70
|
+
? ""
|
|
71
|
+
: `${unread} exists but oxlint never reads it (#3474)`,
|
|
72
|
+
defaultSelected: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bundlerOffer(detection: ProjectDetection): FeatureOffer {
|
|
77
|
+
if (detection.framework === "nuxt") {
|
|
78
|
+
return {
|
|
79
|
+
id: "bundler",
|
|
80
|
+
label: "nuxt module (@vizejs/nuxt)",
|
|
81
|
+
available: detection.nuxtConfig !== null,
|
|
82
|
+
configured: detection.hasVizeNuxtModule,
|
|
83
|
+
note: detection.hasVizeNuxtModule
|
|
84
|
+
? "already configured"
|
|
85
|
+
: detection.nuxtConfig === null
|
|
86
|
+
? "no nuxt.config file to add @vizejs/nuxt to"
|
|
87
|
+
: "",
|
|
88
|
+
defaultSelected: detection.nuxtConfig !== null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (detection.framework === "vite") {
|
|
92
|
+
const single = detection.viteConfigs.length === 1;
|
|
93
|
+
return {
|
|
94
|
+
id: "bundler",
|
|
95
|
+
label: "vite plugin (@vizejs/vite-plugin)",
|
|
96
|
+
available: single,
|
|
97
|
+
configured: detection.hasVizeVitePlugin,
|
|
98
|
+
note: detection.hasVizeVitePlugin
|
|
99
|
+
? "already configured"
|
|
100
|
+
: single
|
|
101
|
+
? ""
|
|
102
|
+
: `several Vite configs (${detection.viteConfigs.join(", ")})`,
|
|
103
|
+
defaultSelected: single,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: "bundler",
|
|
108
|
+
label: "vite plugin or nuxt module",
|
|
109
|
+
available: false,
|
|
110
|
+
configured: false,
|
|
111
|
+
note: "no vite.config or nuxt.config found; the other features work without one",
|
|
112
|
+
defaultSelected: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fmtOffer(detection: ProjectDetection): FeatureOffer {
|
|
117
|
+
const configured = detection.vizeConfig !== null && "vize:fmt" in detection.scripts;
|
|
118
|
+
return {
|
|
119
|
+
id: "fmt",
|
|
120
|
+
label: "fmt (vize fmt)",
|
|
121
|
+
available: true,
|
|
122
|
+
configured,
|
|
123
|
+
note: configured ? "already configured" : "",
|
|
124
|
+
defaultSelected: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function typecheckOffer(detection: ProjectDetection): FeatureOffer {
|
|
129
|
+
const configured = detection.vizeConfig !== null && "vize:check" in detection.scripts;
|
|
130
|
+
return {
|
|
131
|
+
id: "typecheck",
|
|
132
|
+
label: "typecheck (vize check)",
|
|
133
|
+
available: detection.tsconfig !== null,
|
|
134
|
+
configured,
|
|
135
|
+
note: configured
|
|
136
|
+
? "already configured"
|
|
137
|
+
: detection.tsconfig === null
|
|
138
|
+
? "needs a tsconfig.json"
|
|
139
|
+
: "",
|
|
140
|
+
defaultSelected: detection.tsconfig !== null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function editorOffer(detection: ProjectDetection): FeatureOffer {
|
|
145
|
+
return {
|
|
146
|
+
id: "editor",
|
|
147
|
+
label: "editor extension (.vscode/extensions.json recommendation)",
|
|
148
|
+
available: true,
|
|
149
|
+
configured: detection.vscodeRecommendsVize,
|
|
150
|
+
note: detection.vscodeRecommendsVize ? "already recommended" : "",
|
|
151
|
+
defaultSelected: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config sources `vize init` writes.
|
|
3
|
+
*
|
|
4
|
+
* Every Oxlint-facing template here is derived from one settings object so the
|
|
5
|
+
* `vp lint` block and the `oxlint` config can never describe different presets.
|
|
6
|
+
* See `lint-target.ts` for why writing the wrong one of the two is silent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Preset both Oxlint entry points run with. The bridge's own default. */
|
|
10
|
+
export const INIT_LINT_PRESET = "general-recommended";
|
|
11
|
+
|
|
12
|
+
/** `settings.vize.helpLevel` both Oxlint entry points run with. */
|
|
13
|
+
export const INIT_LINT_HELP_LEVEL = "short";
|
|
14
|
+
|
|
15
|
+
/** VS Code extension id published from `editors/vscode`. */
|
|
16
|
+
export const VSCODE_EXTENSION_ID = "ubugeeei.vize";
|
|
17
|
+
|
|
18
|
+
export interface VizeConfigFeatures {
|
|
19
|
+
readonly lint: boolean;
|
|
20
|
+
readonly fmt: boolean;
|
|
21
|
+
readonly typecheck: boolean;
|
|
22
|
+
readonly vite: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds `vize.config.ts` from the selected features.
|
|
27
|
+
*
|
|
28
|
+
* Only selected features contribute a block, so a project that asked for the
|
|
29
|
+
* formatter alone does not silently get a type checker it never opted into.
|
|
30
|
+
*/
|
|
31
|
+
export function renderVizeConfig(features: VizeConfigFeatures): string {
|
|
32
|
+
const blocks: string[] = [
|
|
33
|
+
` compiler: {
|
|
34
|
+
templateSyntax: "standard",
|
|
35
|
+
},`,
|
|
36
|
+
];
|
|
37
|
+
if (features.lint) {
|
|
38
|
+
blocks.push(` linter: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
preset: "${INIT_LINT_PRESET}",
|
|
41
|
+
},`);
|
|
42
|
+
}
|
|
43
|
+
if (features.fmt) {
|
|
44
|
+
blocks.push(` formatter: {
|
|
45
|
+
singleAttributePerLine: false,
|
|
46
|
+
sortBlocks: true,
|
|
47
|
+
},`);
|
|
48
|
+
}
|
|
49
|
+
if (features.typecheck) {
|
|
50
|
+
blocks.push(` typeChecker: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
strict: true,
|
|
53
|
+
},`);
|
|
54
|
+
}
|
|
55
|
+
if (features.vite) {
|
|
56
|
+
blocks.push(` vite: {
|
|
57
|
+
scanPatterns: ["src/**/*.vue"],
|
|
58
|
+
},`);
|
|
59
|
+
}
|
|
60
|
+
return `import { defineConfig } from "vize";
|
|
61
|
+
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
${blocks.join("\n")}
|
|
64
|
+
});
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Config for the `oxlint` binary.
|
|
70
|
+
*
|
|
71
|
+
* `.oxlintrc.json` cannot import `configs.recommended`, and the bridge only runs
|
|
72
|
+
* `vize/*` rules that appear in `rules`, so a JSON config would need every rule
|
|
73
|
+
* id inlined and would rot on the next rule addition. `oxlint.config.ts` is
|
|
74
|
+
* Oxlint's TypeScript config format and is auto-discovered (verified against
|
|
75
|
+
* oxlint 1.64; `oxlint.config.mjs`, `.js`, `.cjs`, `.mts` and `.cts` are not --
|
|
76
|
+
* see #3474), so it is the only form that stays correct over time.
|
|
77
|
+
*/
|
|
78
|
+
export const INIT_OXLINT_CONFIG = `import { defineConfig } from "oxlint";
|
|
79
|
+
import { configs } from "oxlint-plugin-vize";
|
|
80
|
+
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
plugins: ["vue"],
|
|
83
|
+
jsPlugins: ["oxlint-plugin-vize"],
|
|
84
|
+
settings: {
|
|
85
|
+
vize: {
|
|
86
|
+
preset: "${INIT_LINT_PRESET}",
|
|
87
|
+
helpLevel: "${INIT_LINT_HELP_LEVEL}",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
rules: configs.recommended,
|
|
91
|
+
});
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
/** Import line the Vite+ `lint` block needs. */
|
|
95
|
+
export const VITE_LINT_IMPORT =
|
|
96
|
+
'import { createVizeLintConfig } from "oxlint-plugin-vize";\n' as const;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The Vite+ `lint` block, the only Oxlint configuration `vp lint` and `vp check`
|
|
100
|
+
* read.
|
|
101
|
+
*
|
|
102
|
+
* `createVizeLintConfig()` returns the whole block rather than fragments, which
|
|
103
|
+
* is what makes the `jsPlugins` entry impossible to omit. Hand-assembling the
|
|
104
|
+
* block is how a config ends up looking wired while reporting nothing.
|
|
105
|
+
*/
|
|
106
|
+
export const VITE_LINT_BLOCK = ` lint: createVizeLintConfig({
|
|
107
|
+
preset: "${INIT_LINT_PRESET}",
|
|
108
|
+
settings: {
|
|
109
|
+
helpLevel: "${INIT_LINT_HELP_LEVEL}",
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
/** Snippet printed when a Vite config has no `lint` block and cannot be edited safely. */
|
|
115
|
+
export const VITE_LINT_SNIPPET = `import { createVizeLintConfig } from "oxlint-plugin-vize";
|
|
116
|
+
|
|
117
|
+
export default defineConfig({
|
|
118
|
+
${VITE_LINT_BLOCK}});
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Snippet printed when the Vite config already has a `lint` block.
|
|
123
|
+
*
|
|
124
|
+
* Spreading is the documented way to keep an existing block's other keys while
|
|
125
|
+
* still taking the whole Vize block, `jsPlugins` included.
|
|
126
|
+
*/
|
|
127
|
+
export const VITE_LINT_MERGE_SNIPPET = `import { createVizeLintConfig } from "oxlint-plugin-vize";
|
|
128
|
+
|
|
129
|
+
export default defineConfig({
|
|
130
|
+
lint: {
|
|
131
|
+
...createVizeLintConfig({
|
|
132
|
+
preset: "${INIT_LINT_PRESET}",
|
|
133
|
+
settings: {
|
|
134
|
+
helpLevel: "${INIT_LINT_HELP_LEVEL}",
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
// keep your existing lint keys here
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
export const VITE_PLUGIN_IMPORT = 'import vize from "@vizejs/vite-plugin";\n' as const;
|
|
143
|
+
|
|
144
|
+
export const VITE_PLUGIN_SNIPPET = `import vize from "@vizejs/vite-plugin";
|
|
145
|
+
|
|
146
|
+
export default defineConfig({
|
|
147
|
+
plugins: [vize()],
|
|
148
|
+
});
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
export const NUXT_MODULE_SNIPPET = `export default defineNuxtConfig({
|
|
152
|
+
modules: ["@vizejs/nuxt"],
|
|
153
|
+
});
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `.vscode/extensions.json` written when no file exists yet.
|
|
158
|
+
*
|
|
159
|
+
* Recommendations are chosen over `code --install-extension` because they are
|
|
160
|
+
* checked in, apply to the whole team, and change nothing on the machine that
|
|
161
|
+
* runs `init`.
|
|
162
|
+
*/
|
|
163
|
+
export function renderVscodeExtensions(indent: string | number): string {
|
|
164
|
+
return `${JSON.stringify({ recommendations: [VSCODE_EXTENSION_ID] }, null, indent)}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Editor integrations shipped from this repo, reported alongside the VS Code one. */
|
|
168
|
+
export const EDITOR_INTEGRATIONS = [
|
|
169
|
+
"VS Code: ubugeeei.vize (recommended in .vscode/extensions.json)",
|
|
170
|
+
"Zed: tools/zed-vize",
|
|
171
|
+
"Neovim: tools/nvim-vize",
|
|
172
|
+
"Vim: tools/vim-vize",
|
|
173
|
+
"Helix: tools/helix-vize",
|
|
174
|
+
"Emacs: tools/emacs-vize",
|
|
175
|
+
] as const;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth-aware lookup of a top-level key in a `defineConfig({ ... })` call.
|
|
3
|
+
*
|
|
4
|
+
* A plain regex cannot tell the config's own `plugins` key from the `plugins`
|
|
5
|
+
* key inside a `lint: { ... }` block, and picking the wrong one rewrites a part
|
|
6
|
+
* of the user's config they never asked to change. This scanner tracks bracket
|
|
7
|
+
* depth and skips strings, template literals and comments, so a key only matches
|
|
8
|
+
* at depth 0 of the config object.
|
|
9
|
+
*
|
|
10
|
+
* It is not a JavaScript parser and does not try to be: template-literal
|
|
11
|
+
* substitutions and regex literals are treated as ordinary text. Both make the
|
|
12
|
+
* scan give up or miss, which turns into a refusal to edit -- the safe direction.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const IDENTIFIER = /^[$A-Z_a-z][$\w]*/u;
|
|
16
|
+
const KEY_SEPARATOR = /^\s*:/u;
|
|
17
|
+
|
|
18
|
+
export interface TopLevelKey {
|
|
19
|
+
/** Index of the first character of the key. */
|
|
20
|
+
readonly keyStart: number;
|
|
21
|
+
/** Index of the first character after the `:`. */
|
|
22
|
+
readonly valueStart: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Finds `key` at the top level of `callee({ ... })`, or `null`. */
|
|
26
|
+
export function findTopLevelKey(source: string, callee: string, key: string): TopLevelKey | null {
|
|
27
|
+
const opening = new RegExp(`\\b${callee}\\s*\\(\\s*\\{`, "u").exec(source);
|
|
28
|
+
if (opening === null) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
let index = opening.index + opening[0].length;
|
|
32
|
+
let depth = 0;
|
|
33
|
+
while (index < source.length) {
|
|
34
|
+
const char = source[index]!;
|
|
35
|
+
const skipped = skipNonCode(source, index);
|
|
36
|
+
if (skipped !== index) {
|
|
37
|
+
index = skipped;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
41
|
+
depth += 1;
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
46
|
+
if (depth === 0) {
|
|
47
|
+
// Closing brace of the config object itself: the key is not here.
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
depth -= 1;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const identifier = IDENTIFIER.exec(source.slice(index));
|
|
55
|
+
if (identifier === null) {
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const separator = KEY_SEPARATOR.exec(source.slice(index + identifier[0].length));
|
|
60
|
+
if (depth === 0 && identifier[0] === key && separator !== null) {
|
|
61
|
+
return {
|
|
62
|
+
keyStart: index,
|
|
63
|
+
valueStart: index + identifier[0].length + separator[0].length,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
index += identifier[0].length;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Number of `callee({` openings in the source. */
|
|
72
|
+
export function countConfigCalls(source: string, callee: string): number {
|
|
73
|
+
return [...source.matchAll(new RegExp(`\\b${callee}\\s*\\(\\s*\\{`, "gu"))].length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ArrayValue {
|
|
77
|
+
/** Index just after the opening `[`. */
|
|
78
|
+
readonly contentStart: number;
|
|
79
|
+
/** True when the array holds nothing but whitespace. */
|
|
80
|
+
readonly empty: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads the array literal a top-level key is assigned to.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` when the value is not an array literal -- a spread from a
|
|
87
|
+
* variable, or a helper call -- because inserting into those would change what
|
|
88
|
+
* the config evaluates to.
|
|
89
|
+
*/
|
|
90
|
+
export function readTopLevelArray(source: string, callee: string, key: string): ArrayValue | null {
|
|
91
|
+
const found = findTopLevelKey(source, callee, key);
|
|
92
|
+
if (found === null) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const rest = source.slice(found.valueStart);
|
|
96
|
+
const leading = /^\s*/u.exec(rest)![0];
|
|
97
|
+
if (rest[leading.length] !== "[") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const contentStart = found.valueStart + leading.length + 1;
|
|
101
|
+
return { contentStart, empty: /^\s*\]/u.test(source.slice(contentStart)) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Advances past a string, template literal or comment starting at `index`.
|
|
106
|
+
*
|
|
107
|
+
* Returns `index` unchanged when nothing at that position needs skipping.
|
|
108
|
+
*/
|
|
109
|
+
function skipNonCode(source: string, index: number): number {
|
|
110
|
+
const char = source[index]!;
|
|
111
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
112
|
+
return skipQuoted(source, index, char);
|
|
113
|
+
}
|
|
114
|
+
if (char !== "/") {
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
const next = source[index + 1];
|
|
118
|
+
if (next === "/") {
|
|
119
|
+
const end = source.indexOf("\n", index);
|
|
120
|
+
return end === -1 ? source.length : end;
|
|
121
|
+
}
|
|
122
|
+
if (next === "*") {
|
|
123
|
+
const end = source.indexOf("*/", index + 2);
|
|
124
|
+
return end === -1 ? source.length : end + 2;
|
|
125
|
+
}
|
|
126
|
+
return index;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function skipQuoted(source: string, index: number, quote: string): number {
|
|
130
|
+
let cursor = index + 1;
|
|
131
|
+
while (cursor < source.length) {
|
|
132
|
+
const char = source[cursor]!;
|
|
133
|
+
if (char === "\\") {
|
|
134
|
+
cursor += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === quote) {
|
|
138
|
+
return cursor + 1;
|
|
139
|
+
}
|
|
140
|
+
cursor += 1;
|
|
141
|
+
}
|
|
142
|
+
return source.length;
|
|
143
|
+
}
|