@vsceasy/cli 0.1.8 → 0.1.10
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/CHANGELOG.md +4 -0
- package/README.md +40 -0
- package/dist/bin/cli.js +858 -76
- package/dist/index.js +717 -19
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/contributesMerge.d.ts +18 -0
- package/dist/lib/crud/parseModel.d.ts +12 -0
- package/dist/lib/helper/add.d.ts +1 -1
- package/dist/lib/model/add.d.ts +18 -0
- package/dist/lib/model/parseFields.d.ts +9 -3
- package/dist/lib/scaffold.d.ts +4 -0
- package/dist/lib/templatesData.d.ts +1 -1
- package/package.json +1 -1
- package/templates/_assets/language/README.language.md +39 -0
- package/templates/_assets/language/contributes.extra.json +40 -0
- package/templates/_assets/language/fileicons/{{langId}}-icon-theme.json +13 -0
- package/templates/_assets/language/icons/{{langId}}.svg +5 -0
- package/templates/_assets/language/language-configuration.json +30 -0
- package/templates/_assets/language/snippets/{{langId}}.json +12 -0
- package/templates/_assets/language/src/colorize.ts +31 -0
- package/templates/_assets/language/src/commands/applyColors.ts +11 -0
- package/templates/_assets/language/src/commands/removeColors.ts +11 -0
- package/templates/_assets/language/src/extension/extension.ts +25 -0
- package/templates/_assets/language/src/helpers/colorize.ts +70 -0
- package/templates/_assets/language/syntaxes/{{langId}}.tmLanguage.json +45 -0
- package/templates/_generators/crud/formApp.tsx.tpl +2 -0
- package/templates/_generators/crud/formPanel.ts.tpl +2 -2
- package/templates/_generators/helper/colorize.ts.tpl +85 -0
- package/templates/_generators/model/model.ts.tpl +1 -1
- package/templates/react/scripts/gen.ts +55 -0
package/dist/index.js
CHANGED
|
@@ -134,15 +134,26 @@ async function scaffold(opts) {
|
|
|
134
134
|
throw new Error(`Target directory not empty: ${opts.targetDir}`);
|
|
135
135
|
}
|
|
136
136
|
fs2.mkdirSync(opts.targetDir, { recursive: true });
|
|
137
|
+
const type = opts.type ?? "ui";
|
|
137
138
|
const vars = buildVars(opts);
|
|
138
139
|
await copyTree(src, opts.targetDir, vars);
|
|
139
|
-
|
|
140
|
+
applyType(opts.targetDir, type, opts.preset ?? "full", opts.templatesRoot, vars);
|
|
140
141
|
writeConfig(opts.targetDir, {
|
|
141
142
|
publisher: opts.publisher,
|
|
142
143
|
commandPrefix: vars.commandPrefix,
|
|
143
|
-
|
|
144
|
+
type,
|
|
145
|
+
...type === "ui" ? { ui: opts.ui } : {}
|
|
144
146
|
});
|
|
145
147
|
}
|
|
148
|
+
function applyType(targetDir, type, preset, templatesRoot, vars) {
|
|
149
|
+
if (type === "ui") {
|
|
150
|
+
applyPreset(targetDir, preset);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
stripWebview(targetDir);
|
|
154
|
+
if (type === "language")
|
|
155
|
+
applyLanguage(targetDir, templatesRoot, vars);
|
|
156
|
+
}
|
|
146
157
|
function applyPreset(targetDir, preset) {
|
|
147
158
|
if (preset === "full")
|
|
148
159
|
return;
|
|
@@ -165,15 +176,104 @@ function applyPreset(targetDir, preset) {
|
|
|
165
176
|
`);
|
|
166
177
|
}
|
|
167
178
|
}
|
|
179
|
+
function stripWebview(targetDir) {
|
|
180
|
+
const removals = [
|
|
181
|
+
"src/panels",
|
|
182
|
+
"src/webview",
|
|
183
|
+
"src/commands/hello.ts",
|
|
184
|
+
"vite.config.ts"
|
|
185
|
+
];
|
|
186
|
+
for (const rel of removals) {
|
|
187
|
+
const abs = path2.join(targetDir, rel);
|
|
188
|
+
if (fs2.existsSync(abs))
|
|
189
|
+
fs2.rmSync(abs, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
const apiPath = path2.join(targetDir, "src", "shared", "api.ts");
|
|
192
|
+
if (fs2.existsSync(apiPath)) {
|
|
193
|
+
fs2.writeFileSync(apiPath, `// RPC contracts go here (add a panel with \`vsceasy panel add\`).
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
trimReactFromPackageJson(targetDir);
|
|
197
|
+
}
|
|
198
|
+
function trimReactFromPackageJson(targetDir) {
|
|
199
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
200
|
+
if (!fs2.existsSync(pkgPath))
|
|
201
|
+
return;
|
|
202
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
203
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
204
|
+
if (!pkg[section])
|
|
205
|
+
continue;
|
|
206
|
+
for (const dep of Object.keys(pkg[section])) {
|
|
207
|
+
if (REACT_DEPS.has(dep))
|
|
208
|
+
delete pkg[section][dep];
|
|
209
|
+
}
|
|
210
|
+
if (Object.keys(pkg[section]).length === 0)
|
|
211
|
+
delete pkg[section];
|
|
212
|
+
}
|
|
213
|
+
if (pkg.scripts) {
|
|
214
|
+
for (const key of Object.keys(pkg.scripts)) {
|
|
215
|
+
if (UI_SCRIPT_KEYS.has(key))
|
|
216
|
+
delete pkg.scripts[key];
|
|
217
|
+
}
|
|
218
|
+
if (pkg.scripts.dev) {
|
|
219
|
+
pkg.scripts.dev = "bun run gen:scan && bun run dev:ext";
|
|
220
|
+
}
|
|
221
|
+
for (const [k, v] of Object.entries(pkg.scripts)) {
|
|
222
|
+
pkg.scripts[k] = v.replace(/\s*&&\s*bun run build:ui/g, "");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
function applyLanguage(targetDir, templatesRoot, vars) {
|
|
229
|
+
const assetsDir = path2.join(templatesRoot, "_assets", "language");
|
|
230
|
+
if (!fs2.existsSync(assetsDir)) {
|
|
231
|
+
throw new Error(`Language assets not found: ${assetsDir}`);
|
|
232
|
+
}
|
|
233
|
+
copyAssetsTree(assetsDir, targetDir, vars);
|
|
234
|
+
const langReadme = path2.join(targetDir, "README.language.md");
|
|
235
|
+
if (fs2.existsSync(langReadme)) {
|
|
236
|
+
fs2.renameSync(langReadme, path2.join(targetDir, "README.md"));
|
|
237
|
+
}
|
|
238
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
239
|
+
if (fs2.existsSync(pkgPath)) {
|
|
240
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
241
|
+
pkg.activationEvents = [`onLanguage:${vars.langId}`];
|
|
242
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function copyAssetsTree(srcDir, destDir, vars) {
|
|
247
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
248
|
+
if (SKIP_NAMES.has(entry.name))
|
|
249
|
+
continue;
|
|
250
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
251
|
+
const destName = substitute(entry.name, vars);
|
|
252
|
+
const destPath = path2.join(destDir, destName);
|
|
253
|
+
if (entry.isDirectory()) {
|
|
254
|
+
fs2.mkdirSync(destPath, { recursive: true });
|
|
255
|
+
copyAssetsTree(srcPath, destPath, vars);
|
|
256
|
+
} else if (entry.isFile()) {
|
|
257
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
258
|
+
fs2.writeFileSync(destPath, substitute(fs2.readFileSync(srcPath, "utf8"), vars));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
168
262
|
function buildVars(opts) {
|
|
169
263
|
const simpleName = opts.name.replace(/^@[^/]+\//, "");
|
|
170
264
|
const commandPrefix = simpleName.replace(/[^a-zA-Z0-9]+/g, "");
|
|
265
|
+
const langId = simpleName.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
266
|
+
const scopeName = `source.${langId}`;
|
|
267
|
+
const langExt = langId.slice(0, 4).toUpperCase();
|
|
171
268
|
return {
|
|
172
269
|
name: opts.name,
|
|
173
270
|
displayName: opts.displayName,
|
|
174
271
|
description: opts.description,
|
|
175
272
|
publisher: opts.publisher,
|
|
176
|
-
commandPrefix
|
|
273
|
+
commandPrefix,
|
|
274
|
+
langId,
|
|
275
|
+
scopeName,
|
|
276
|
+
langExt
|
|
177
277
|
};
|
|
178
278
|
}
|
|
179
279
|
async function copyTree(srcDir, destDir, vars) {
|
|
@@ -199,7 +299,7 @@ async function copyTree(srcDir, destDir, vars) {
|
|
|
199
299
|
function substitute(input, vars) {
|
|
200
300
|
return input.replace(/\{\{(\w+)\}\}/g, (_m, key) => vars[key] ?? `{{${key}}}`);
|
|
201
301
|
}
|
|
202
|
-
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES;
|
|
302
|
+
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES, REACT_DEPS, UI_SCRIPT_KEYS;
|
|
203
303
|
var init_scaffold = __esm(() => {
|
|
204
304
|
init_config();
|
|
205
305
|
fs2 = __toESM(require("fs"));
|
|
@@ -219,6 +319,15 @@ var init_scaffold = __esm(() => {
|
|
|
219
319
|
".yaml"
|
|
220
320
|
]);
|
|
221
321
|
SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
|
|
322
|
+
REACT_DEPS = new Set([
|
|
323
|
+
"react",
|
|
324
|
+
"react-dom",
|
|
325
|
+
"@types/react",
|
|
326
|
+
"@types/react-dom",
|
|
327
|
+
"@vitejs/plugin-react",
|
|
328
|
+
"vite"
|
|
329
|
+
]);
|
|
330
|
+
UI_SCRIPT_KEYS = new Set(["dev:ui", "build:ui"]);
|
|
222
331
|
});
|
|
223
332
|
|
|
224
333
|
// src/lib/validate.ts
|
|
@@ -1580,6 +1689,9 @@ function runDoctor(opts) {
|
|
|
1580
1689
|
results.push(...checkStatusBars(root));
|
|
1581
1690
|
results.push(...checkSubpanels(root));
|
|
1582
1691
|
results.push(checkContributesSync(root, pkg));
|
|
1692
|
+
const langCheck = checkLanguageAssets(root, pkg);
|
|
1693
|
+
if (langCheck)
|
|
1694
|
+
results.push(langCheck);
|
|
1583
1695
|
results.push(checkActivationEvents(pkg));
|
|
1584
1696
|
results.push(checkMarketplaceIcon(root, pkg));
|
|
1585
1697
|
results.push(checkGenScript(root));
|
|
@@ -1842,6 +1954,50 @@ function checkContributesSync(root, pkg) {
|
|
|
1842
1954
|
details: stale
|
|
1843
1955
|
};
|
|
1844
1956
|
}
|
|
1957
|
+
function checkLanguageAssets(root, pkg) {
|
|
1958
|
+
const sources = [pkg?.contributes];
|
|
1959
|
+
const extraPath = path12.join(root, "contributes.extra.json");
|
|
1960
|
+
if (fs12.existsSync(extraPath)) {
|
|
1961
|
+
try {
|
|
1962
|
+
sources.push(JSON.parse(fs12.readFileSync(extraPath, "utf8")));
|
|
1963
|
+
} catch {
|
|
1964
|
+
return {
|
|
1965
|
+
id: "language",
|
|
1966
|
+
level: "error",
|
|
1967
|
+
message: "contributes.extra.json is not valid JSON"
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
const refs = new Set;
|
|
1972
|
+
for (const c of sources) {
|
|
1973
|
+
if (!c)
|
|
1974
|
+
continue;
|
|
1975
|
+
for (const l of c.languages ?? [])
|
|
1976
|
+
if (l.configuration)
|
|
1977
|
+
refs.add(l.configuration);
|
|
1978
|
+
for (const g of c.grammars ?? [])
|
|
1979
|
+
if (g.path)
|
|
1980
|
+
refs.add(g.path);
|
|
1981
|
+
for (const s of c.snippets ?? [])
|
|
1982
|
+
if (s.path)
|
|
1983
|
+
refs.add(s.path);
|
|
1984
|
+
for (const t of c.iconThemes ?? [])
|
|
1985
|
+
if (t.path)
|
|
1986
|
+
refs.add(t.path);
|
|
1987
|
+
}
|
|
1988
|
+
if (refs.size === 0)
|
|
1989
|
+
return null;
|
|
1990
|
+
const missing = [...refs].filter((rel) => !fs12.existsSync(path12.join(root, rel)));
|
|
1991
|
+
if (missing.length === 0) {
|
|
1992
|
+
return { id: "language", level: "ok", message: `language ${refs.size} asset(s) present` };
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
id: "language",
|
|
1996
|
+
level: "error",
|
|
1997
|
+
message: `language: ${missing.length} referenced asset(s) missing`,
|
|
1998
|
+
details: missing
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
1845
2001
|
function checkGitignore(root) {
|
|
1846
2002
|
const file = path12.join(root, ".gitignore");
|
|
1847
2003
|
const required = ["dist", "node_modules"];
|
|
@@ -2238,9 +2394,314 @@ var init_upgrade = __esm(() => {
|
|
|
2238
2394
|
});
|
|
2239
2395
|
|
|
2240
2396
|
// src/lib/templatesData.ts
|
|
2241
|
-
var TEMPLATES_VERSION = "0.1.
|
|
2397
|
+
var TEMPLATES_VERSION = "0.1.10", TEMPLATE_FILES;
|
|
2242
2398
|
var init_templatesData = __esm(() => {
|
|
2243
2399
|
TEMPLATE_FILES = {
|
|
2400
|
+
"_assets/language/README.language.md": "## {{displayName}} language support\n\nThis extension was scaffolded with `vsceasy create --type language`. It provides\neditor support for `.{{langId}}` files:\n\n- **Syntax highlighting** — TextMate grammar in `syntaxes/{{langId}}.tmLanguage.json`\n- **Language configuration** — comments, brackets, auto-closing pairs, folding in\n `language-configuration.json`\n- **Snippets** — `snippets/{{langId}}.json`\n- **File icon** (opt-in) — `fileicons/{{langId}}-icon-theme.json`\n\n### How contributions are wired\n\n`vsceasy`'s `scripts/gen.ts` regenerates the generated parts of\n`package.json#contributes` (commands, views…) on every build. Language\ncontributions are **not** generated — they live in **`contributes.extra.json`**\nat the project root and are deep-merged into `package.json` by `gen.ts`. Edit\n`contributes.extra.json` to change languages / grammars / snippets / iconThemes,\nthen run `bun run gen`.\n\n### File icon is opt-in\n\nVS Code file icons are provided by an **icon theme**, which is global: activating\nit replaces *all* file icons in the workbench, not just `.{{langId}}`. This\nextension ships a `{{displayName}} Icons` theme but does **not** force it. To use\nit: `Preferences: File Icon Theme` → pick `{{displayName}} Icons`. If you don't\nwant to override every icon, leave it unselected — highlighting, config and\nsnippets work regardless.\n\n### Develop\n\n```sh\nbun install\nbun run gen # sync package.json#contributes\nbun run package # build a .vsix\n```\n\nPress `F5` (or run `bun run launch`) to open an Extension Development Host and\nopen a `.{{langId}}` file to see highlighting.\n",
|
|
2401
|
+
"_assets/language/contributes.extra.json": `{
|
|
2402
|
+
"languages": [
|
|
2403
|
+
{
|
|
2404
|
+
"id": "{{langId}}",
|
|
2405
|
+
"aliases": ["{{displayName}}", "{{langId}}"],
|
|
2406
|
+
"extensions": [".{{langId}}"],
|
|
2407
|
+
"configuration": "./language-configuration.json"
|
|
2408
|
+
}
|
|
2409
|
+
],
|
|
2410
|
+
"grammars": [
|
|
2411
|
+
{
|
|
2412
|
+
"language": "{{langId}}",
|
|
2413
|
+
"scopeName": "{{scopeName}}",
|
|
2414
|
+
"path": "./syntaxes/{{langId}}.tmLanguage.json"
|
|
2415
|
+
}
|
|
2416
|
+
],
|
|
2417
|
+
"snippets": [
|
|
2418
|
+
{
|
|
2419
|
+
"language": "{{langId}}",
|
|
2420
|
+
"path": "./snippets/{{langId}}.json"
|
|
2421
|
+
}
|
|
2422
|
+
],
|
|
2423
|
+
"iconThemes": [
|
|
2424
|
+
{
|
|
2425
|
+
"id": "{{langId}}-icons",
|
|
2426
|
+
"label": "{{displayName}} Icons",
|
|
2427
|
+
"path": "./fileicons/{{langId}}-icon-theme.json"
|
|
2428
|
+
}
|
|
2429
|
+
],
|
|
2430
|
+
"configuration": {
|
|
2431
|
+
"title": "{{displayName}}",
|
|
2432
|
+
"properties": {
|
|
2433
|
+
"{{commandPrefix}}.colorize": {
|
|
2434
|
+
"type": "boolean",
|
|
2435
|
+
"default": true,
|
|
2436
|
+
"markdownDescription": "Automatically apply distinct section colors to \`.{{langId}}\` files (scoped to \`{{scopeName}}\`, so other languages are unaffected). Turn off to keep your theme's default colors."
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
`,
|
|
2442
|
+
"_assets/language/fileicons/{{langId}}-icon-theme.json": `{
|
|
2443
|
+
"iconDefinitions": {
|
|
2444
|
+
"{{langId}}_file": {
|
|
2445
|
+
"iconPath": "../icons/{{langId}}.svg"
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
"languageIds": {
|
|
2449
|
+
"{{langId}}": "{{langId}}_file"
|
|
2450
|
+
},
|
|
2451
|
+
"fileExtensions": {
|
|
2452
|
+
"{{langId}}": "{{langId}}_file"
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
`,
|
|
2456
|
+
"_assets/language/icons/{{langId}}.svg": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2457
|
+
<path fill="#8a8a8a" d="M6 3h14l6 6v20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
|
|
2458
|
+
<path fill="#5a5a5a" d="M20 3l6 6h-6z"/>
|
|
2459
|
+
<text x="16" y="22" font-family="monospace" font-size="9" font-weight="bold" fill="#fff" text-anchor="middle">{{langExt}}</text>
|
|
2460
|
+
</svg>
|
|
2461
|
+
`,
|
|
2462
|
+
"_assets/language/language-configuration.json": `{
|
|
2463
|
+
"comments": {
|
|
2464
|
+
"lineComment": "#"
|
|
2465
|
+
},
|
|
2466
|
+
"brackets": [
|
|
2467
|
+
["{", "}"],
|
|
2468
|
+
["[", "]"],
|
|
2469
|
+
["(", ")"]
|
|
2470
|
+
],
|
|
2471
|
+
"autoClosingPairs": [
|
|
2472
|
+
{ "open": "{", "close": "}" },
|
|
2473
|
+
{ "open": "[", "close": "]" },
|
|
2474
|
+
{ "open": "(", "close": ")" },
|
|
2475
|
+
{ "open": "\\"", "close": "\\"", "notIn": ["string"] },
|
|
2476
|
+
{ "open": "'", "close": "'", "notIn": ["string"] }
|
|
2477
|
+
],
|
|
2478
|
+
"surroundingPairs": [
|
|
2479
|
+
["{", "}"],
|
|
2480
|
+
["[", "]"],
|
|
2481
|
+
["(", ")"],
|
|
2482
|
+
["\\"", "\\""],
|
|
2483
|
+
["'", "'"]
|
|
2484
|
+
],
|
|
2485
|
+
"folding": {
|
|
2486
|
+
"markers": {
|
|
2487
|
+
"start": "^\\\\s*#\\\\s*region\\\\b",
|
|
2488
|
+
"end": "^\\\\s*#\\\\s*endregion\\\\b"
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
`,
|
|
2493
|
+
"_assets/language/snippets/{{langId}}.json": `{
|
|
2494
|
+
"Section": {
|
|
2495
|
+
"prefix": "section",
|
|
2496
|
+
"body": ["[\${1:name}]", "$0"],
|
|
2497
|
+
"description": "A {{displayName}} section"
|
|
2498
|
+
},
|
|
2499
|
+
"Key/Value": {
|
|
2500
|
+
"prefix": "kv",
|
|
2501
|
+
"body": ["\${1:key} = \${2:value}"],
|
|
2502
|
+
"description": "A key/value pair"
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
`,
|
|
2506
|
+
"_assets/language/src/colorize.ts": `import type * as vscode from 'vscode';
|
|
2507
|
+
import { applyTokenColors, removeTokenColors, type TokenColorRule } from './helpers/colorize';
|
|
2508
|
+
|
|
2509
|
+
/** Root TextMate scope of this language — rules are applied only to these files. */
|
|
2510
|
+
export const SCOPE = '{{scopeName}}';
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* Default token colors for {{displayName}}, emphasizing each construct. Edit
|
|
2514
|
+
* freely — they are applied to \`[{{scopeName}}]\` only, so other languages keep
|
|
2515
|
+
* the user's theme. Scope names must match your grammar
|
|
2516
|
+
* (syntaxes/{{langId}}.tmLanguage.json).
|
|
2517
|
+
*/
|
|
2518
|
+
export const RULES: TokenColorRule[] = [
|
|
2519
|
+
{ scope: 'comment.line.number-sign.{{langId}}', settings: { foreground: '#6b7a6e', fontStyle: 'italic' } },
|
|
2520
|
+
{ scope: 'string.quoted.double.basic.{{langId}}', settings: { foreground: '#98c379' } },
|
|
2521
|
+
{ scope: 'string.quoted.single.literal.{{langId}}', settings: { foreground: '#98c379' } },
|
|
2522
|
+
{ scope: 'constant.numeric.{{langId}}', settings: { foreground: '#d19a66' } },
|
|
2523
|
+
];
|
|
2524
|
+
|
|
2525
|
+
export async function applyColors(vscodeNs: typeof vscode): Promise<void> {
|
|
2526
|
+
await applyTokenColors(SCOPE, RULES);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
export async function removeColors(vscodeNs: typeof vscode): Promise<void> {
|
|
2530
|
+
await removeTokenColors(SCOPE);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** True when the user has opted in (default) to automatic coloring. */
|
|
2534
|
+
export function colorizeEnabled(vscodeNs: typeof vscode): boolean {
|
|
2535
|
+
return vscodeNs.workspace.getConfiguration('{{commandPrefix}}').get<boolean>('colorize', true);
|
|
2536
|
+
}
|
|
2537
|
+
`,
|
|
2538
|
+
"_assets/language/src/commands/applyColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
2539
|
+
import { applyColors } from '../colorize';
|
|
2540
|
+
|
|
2541
|
+
export default defineCommand({
|
|
2542
|
+
id: 'applyColors',
|
|
2543
|
+
title: '{{displayName}}: Apply Colors',
|
|
2544
|
+
run: async (vscode) => {
|
|
2545
|
+
await applyColors(vscode);
|
|
2546
|
+
vscode.window.showInformationMessage('{{displayName}} colors applied.');
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
`,
|
|
2550
|
+
"_assets/language/src/commands/removeColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
2551
|
+
import { removeColors } from '../colorize';
|
|
2552
|
+
|
|
2553
|
+
export default defineCommand({
|
|
2554
|
+
id: 'removeColors',
|
|
2555
|
+
title: '{{displayName}}: Remove Colors',
|
|
2556
|
+
run: async (vscode) => {
|
|
2557
|
+
await removeColors(vscode);
|
|
2558
|
+
vscode.window.showInformationMessage('{{displayName}} colors removed.');
|
|
2559
|
+
},
|
|
2560
|
+
});
|
|
2561
|
+
`,
|
|
2562
|
+
"_assets/language/src/extension/extension.ts": `import { bootstrap } from '../shared/vsceasy';
|
|
2563
|
+
import { registry } from './_registry';
|
|
2564
|
+
import { applyColors, removeColors, colorizeEnabled } from '../colorize';
|
|
2565
|
+
|
|
2566
|
+
export const activate = bootstrap(registry, {
|
|
2567
|
+
onActivate: [
|
|
2568
|
+
async (context, vscode) => {
|
|
2569
|
+
// Auto-apply scoped token colors on activate when opted in (default).
|
|
2570
|
+
// Scoped to {{scopeName}} only — other languages are untouched.
|
|
2571
|
+
if (colorizeEnabled(vscode)) {
|
|
2572
|
+
await applyColors(vscode);
|
|
2573
|
+
}
|
|
2574
|
+
// React to the user toggling \`{{commandPrefix}}.colorize\` at runtime.
|
|
2575
|
+
context.subscriptions.push(
|
|
2576
|
+
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
2577
|
+
if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
2578
|
+
if (colorizeEnabled(vscode)) await applyColors(vscode);
|
|
2579
|
+
else await removeColors(vscode);
|
|
2580
|
+
}),
|
|
2581
|
+
);
|
|
2582
|
+
},
|
|
2583
|
+
],
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
export function deactivate() {}
|
|
2587
|
+
`,
|
|
2588
|
+
"_assets/language/src/helpers/colorize.ts": `import * as vscode from 'vscode';
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
2592
|
+
* language's root scope like \`{{scopeName}}\`), written to the user's
|
|
2593
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
2594
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
2595
|
+
* keeps the active theme's colors.
|
|
2596
|
+
*
|
|
2597
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
2598
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
2599
|
+
*/
|
|
2600
|
+
|
|
2601
|
+
export interface TokenColorRule {
|
|
2602
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
2603
|
+
scope: string;
|
|
2604
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
2608
|
+
|
|
2609
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
2610
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
2611
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
2612
|
+
|
|
2613
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
2617
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
2618
|
+
* replaces only previously-applied rules from this extension.
|
|
2619
|
+
*/
|
|
2620
|
+
export async function applyTokenColors(
|
|
2621
|
+
scope: string,
|
|
2622
|
+
rules: TokenColorRule[],
|
|
2623
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
2624
|
+
): Promise<void> {
|
|
2625
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
2626
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
2627
|
+
const key = blockKey(scope);
|
|
2628
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
2629
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
2630
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
2631
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
2632
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
2633
|
+
await cfg.update(SECTION, next, target);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
2637
|
+
export async function removeTokenColors(
|
|
2638
|
+
scope: string,
|
|
2639
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
2640
|
+
): Promise<void> {
|
|
2641
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
2642
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
2643
|
+
const key = blockKey(scope);
|
|
2644
|
+
if (!current || !current[key]) return;
|
|
2645
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
2646
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
2647
|
+
|
|
2648
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
2649
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
2650
|
+
else delete nextBlock.textMateRules;
|
|
2651
|
+
|
|
2652
|
+
const next = { ...current };
|
|
2653
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
2654
|
+
else delete next[key];
|
|
2655
|
+
|
|
2656
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
2657
|
+
}
|
|
2658
|
+
`,
|
|
2659
|
+
"_assets/language/syntaxes/{{langId}}.tmLanguage.json": `{
|
|
2660
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
2661
|
+
"name": "{{displayName}}",
|
|
2662
|
+
"scopeName": "{{scopeName}}",
|
|
2663
|
+
"patterns": [
|
|
2664
|
+
{ "include": "#comments" },
|
|
2665
|
+
{ "include": "#strings" },
|
|
2666
|
+
{ "include": "#numbers" }
|
|
2667
|
+
],
|
|
2668
|
+
"repository": {
|
|
2669
|
+
"comments": {
|
|
2670
|
+
"patterns": [
|
|
2671
|
+
{
|
|
2672
|
+
"name": "comment.line.number-sign.{{langId}}",
|
|
2673
|
+
"match": "#.*$"
|
|
2674
|
+
}
|
|
2675
|
+
]
|
|
2676
|
+
},
|
|
2677
|
+
"strings": {
|
|
2678
|
+
"patterns": [
|
|
2679
|
+
{
|
|
2680
|
+
"name": "string.quoted.double.{{langId}}",
|
|
2681
|
+
"begin": "\\"",
|
|
2682
|
+
"end": "\\"",
|
|
2683
|
+
"patterns": [
|
|
2684
|
+
{ "name": "constant.character.escape.{{langId}}", "match": "\\\\\\\\." }
|
|
2685
|
+
]
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
"name": "string.quoted.single.{{langId}}",
|
|
2689
|
+
"begin": "'",
|
|
2690
|
+
"end": "'"
|
|
2691
|
+
}
|
|
2692
|
+
]
|
|
2693
|
+
},
|
|
2694
|
+
"numbers": {
|
|
2695
|
+
"patterns": [
|
|
2696
|
+
{
|
|
2697
|
+
"name": "constant.numeric.{{langId}}",
|
|
2698
|
+
"match": "\\\\b[0-9]+(\\\\.[0-9]+)?\\\\b"
|
|
2699
|
+
}
|
|
2700
|
+
]
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
`,
|
|
2244
2705
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
2245
2706
|
|
|
2246
2707
|
export default defineCommand({
|
|
@@ -2450,6 +2911,7 @@ export function App() {
|
|
|
2450
2911
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
2451
2912
|
const [error, setError] = useState<string | null>(null);
|
|
2452
2913
|
const [saving, setSaving] = useState(false);
|
|
2914
|
+
{{relationOptionsState}}
|
|
2453
2915
|
|
|
2454
2916
|
const load = useCallback(async (initial: boolean) => {
|
|
2455
2917
|
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
@@ -2487,6 +2949,7 @@ export function App() {
|
|
|
2487
2949
|
document.removeEventListener('visibilitychange', onVisible);
|
|
2488
2950
|
};
|
|
2489
2951
|
}, [load]);
|
|
2952
|
+
{{relationOptionsLoad}}
|
|
2490
2953
|
|
|
2491
2954
|
const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
|
|
2492
2955
|
setForm((f) => ({ ...f, [k]: v }));
|
|
@@ -2550,7 +3013,7 @@ import { {{Name}}Service } from '../services/{{Name}}Service';
|
|
|
2550
3013
|
import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
|
|
2551
3014
|
import type { {{Name}}FormApi } from '../shared/api';
|
|
2552
3015
|
import type { {{Name}} } from '../models/{{Name}}';
|
|
2553
|
-
|
|
3016
|
+
{{relationImports}}
|
|
2554
3017
|
export default definePanel<{{Name}}FormApi>({
|
|
2555
3018
|
title: '{{title}}',
|
|
2556
3019
|
column: 'beside',
|
|
@@ -2572,7 +3035,7 @@ export default definePanel<{{Name}}FormApi>({
|
|
|
2572
3035
|
void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
|
|
2573
3036
|
return saved;
|
|
2574
3037
|
},
|
|
2575
|
-
async cancel() {
|
|
3038
|
+
{{relationOptionsHandler}} async cancel() {
|
|
2576
3039
|
// No-op — webview closes itself.
|
|
2577
3040
|
},
|
|
2578
3041
|
}),
|
|
@@ -2846,6 +3309,92 @@ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
|
|
|
2846
3309
|
|
|
2847
3310
|
return cache;
|
|
2848
3311
|
}
|
|
3312
|
+
`,
|
|
3313
|
+
"_generators/helper/colorize.ts.tpl": `import * as vscode from 'vscode';
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
3317
|
+
* language's root scope like \`source.toml\`), written to the user's
|
|
3318
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
3319
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
3320
|
+
* keeps the active theme's colors.
|
|
3321
|
+
*
|
|
3322
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
3323
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
3324
|
+
*
|
|
3325
|
+
* Typical use — auto-apply on activate behind an opt-out setting:
|
|
3326
|
+
*
|
|
3327
|
+
* // extension.ts (onActivate hook)
|
|
3328
|
+
* if (config.get<boolean>('colorize', true)) {
|
|
3329
|
+
* await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
3330
|
+
* }
|
|
3331
|
+
* vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
3332
|
+
* if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
3333
|
+
* if (config.get<boolean>('colorize', true)) await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
3334
|
+
* else await removeTokenColors('source.{{commandPrefix}}');
|
|
3335
|
+
* });
|
|
3336
|
+
*
|
|
3337
|
+
* Declare the opt-out in package.json#contributes.configuration:
|
|
3338
|
+
* "{{commandPrefix}}.colorize": { "type": "boolean", "default": true }
|
|
3339
|
+
*/
|
|
3340
|
+
|
|
3341
|
+
export interface TokenColorRule {
|
|
3342
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
3343
|
+
scope: string;
|
|
3344
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
3348
|
+
|
|
3349
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
3350
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
3351
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
3352
|
+
|
|
3353
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
3354
|
+
|
|
3355
|
+
/**
|
|
3356
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
3357
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
3358
|
+
* replaces only previously-applied rules from this extension.
|
|
3359
|
+
*/
|
|
3360
|
+
export async function applyTokenColors(
|
|
3361
|
+
scope: string,
|
|
3362
|
+
rules: TokenColorRule[],
|
|
3363
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
3364
|
+
): Promise<void> {
|
|
3365
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
3366
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
3367
|
+
const key = blockKey(scope);
|
|
3368
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
3369
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
3370
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
3371
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
3372
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
3373
|
+
await cfg.update(SECTION, next, target);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
3377
|
+
export async function removeTokenColors(
|
|
3378
|
+
scope: string,
|
|
3379
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
3380
|
+
): Promise<void> {
|
|
3381
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
3382
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
3383
|
+
const key = blockKey(scope);
|
|
3384
|
+
if (!current || !current[key]) return;
|
|
3385
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
3386
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
3387
|
+
|
|
3388
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
3389
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
3390
|
+
else delete nextBlock.textMateRules;
|
|
3391
|
+
|
|
3392
|
+
const next = { ...current };
|
|
3393
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
3394
|
+
else delete next[key];
|
|
3395
|
+
|
|
3396
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
3397
|
+
}
|
|
2849
3398
|
`,
|
|
2850
3399
|
"_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
|
|
2851
3400
|
|
|
@@ -3418,7 +3967,7 @@ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
|
|
|
3418
3967
|
* import { {{Plural}}Repo } from '../models/{{Name}}';
|
|
3419
3968
|
* await {{Plural}}Repo().insert({ ... });
|
|
3420
3969
|
*/
|
|
3421
|
-
export const {{Plural}}Repo = () => db()({{Plural}});
|
|
3970
|
+
export const {{Plural}}Repo = () => db()({{Plural}});{{relationsBlock}}
|
|
3422
3971
|
`,
|
|
3423
3972
|
"_generators/panel/App.tsx.tpl": `import React from 'react';
|
|
3424
3973
|
{{apiBlock}}
|
|
@@ -4164,6 +4713,12 @@ bun run package # → {{name}}-0.0.1.vsix
|
|
|
4164
4713
|
"react/scripts/gen.ts": `#!/usr/bin/env bun
|
|
4165
4714
|
// Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
|
|
4166
4715
|
// and syncs package.json#contributes (commands, viewsContainers, views).
|
|
4716
|
+
//
|
|
4717
|
+
// Non-generated contributes (e.g. languages, grammars, snippets, themes,
|
|
4718
|
+
// iconThemes, walkthroughs) go in an optional \`contributes.extra.json\` at the
|
|
4719
|
+
// project root. It is deep-merged into package.json#contributes on every run —
|
|
4720
|
+
// the keys gen.ts owns (commands, keybindings, menus.commandPalette,
|
|
4721
|
+
// viewsContainers, views) always win, everything else from extra is preserved.
|
|
4167
4722
|
|
|
4168
4723
|
import * as fs from 'fs';
|
|
4169
4724
|
import * as path from 'path';
|
|
@@ -4179,6 +4734,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
|
|
|
4179
4734
|
const JOBS_DIR = path.join(SRC, 'jobs');
|
|
4180
4735
|
const OUT = path.join(SRC, 'extension', '_registry.ts');
|
|
4181
4736
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
4737
|
+
const EXTRA_PATH = path.join(ROOT, 'contributes.extra.json');
|
|
4738
|
+
|
|
4739
|
+
/** Keys gen.ts owns — never overridden by contributes.extra.json. */
|
|
4740
|
+
const GEN_OWNED_KEYS = new Set(['commands', 'keybindings', 'viewsContainers', 'views']);
|
|
4182
4741
|
|
|
4183
4742
|
interface Discovered {
|
|
4184
4743
|
id: string;
|
|
@@ -4367,9 +4926,54 @@ function syncPackageJson(
|
|
|
4367
4926
|
delete contributes.views;
|
|
4368
4927
|
}
|
|
4369
4928
|
|
|
4929
|
+
mergeExtraContributes(contributes);
|
|
4930
|
+
|
|
4370
4931
|
fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
|
|
4371
4932
|
}
|
|
4372
4933
|
|
|
4934
|
+
/**
|
|
4935
|
+
* Deep-merge the optional \`contributes.extra.json\` (project root) into the
|
|
4936
|
+
* package's \`contributes\`. Use for any contribution point gen.ts doesn't
|
|
4937
|
+
* generate — languages, grammars, snippets, themes, iconThemes, walkthroughs…
|
|
4938
|
+
*
|
|
4939
|
+
* Rules:
|
|
4940
|
+
* - gen-owned keys (commands, keybindings, viewsContainers, views) are ignored
|
|
4941
|
+
* if present in extra — gen.ts stays authoritative for those.
|
|
4942
|
+
* - plain objects merge recursively; arrays and primitives from extra replace.
|
|
4943
|
+
*
|
|
4944
|
+
* NOTE: this is an inline copy of src/lib/contributesMerge.ts in the vsceasy
|
|
4945
|
+
* source (the script must run standalone). Keep the two in sync.
|
|
4946
|
+
*/
|
|
4947
|
+
function mergeExtraContributes(contributes: Record<string, any>) {
|
|
4948
|
+
if (!fs.existsSync(EXTRA_PATH)) return;
|
|
4949
|
+
let extra: Record<string, any>;
|
|
4950
|
+
try {
|
|
4951
|
+
extra = JSON.parse(fs.readFileSync(EXTRA_PATH, 'utf8'));
|
|
4952
|
+
} catch (err) {
|
|
4953
|
+
console.warn(\`! Skipping contributes.extra.json — invalid JSON: \${(err as Error).message}\`);
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
if (!extra || typeof extra !== 'object') return;
|
|
4957
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
4958
|
+
if (GEN_OWNED_KEYS.has(key)) continue;
|
|
4959
|
+
contributes[key] = deepMerge(contributes[key], value);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
function isPlainObject(v: unknown): v is Record<string, any> {
|
|
4964
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
function deepMerge(base: any, override: any): any {
|
|
4968
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
4969
|
+
const out: Record<string, any> = { ...base };
|
|
4970
|
+
for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
|
|
4971
|
+
return out;
|
|
4972
|
+
}
|
|
4973
|
+
// arrays and primitives: override wins
|
|
4974
|
+
return override;
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4373
4977
|
function loadDef(file: string): {
|
|
4374
4978
|
id?: string;
|
|
4375
4979
|
title?: string;
|
|
@@ -6535,7 +7139,7 @@ var init_add9 = __esm(() => {
|
|
|
6535
7139
|
init_config();
|
|
6536
7140
|
fs18 = __toESM(require("fs"));
|
|
6537
7141
|
path18 = __toESM(require("path"));
|
|
6538
|
-
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
|
|
7142
|
+
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache", "colorize"];
|
|
6539
7143
|
});
|
|
6540
7144
|
|
|
6541
7145
|
// src/lib/job/add.ts
|
|
@@ -6722,7 +7326,26 @@ function parseModelFile(file) {
|
|
|
6722
7326
|
indexes.push(m[1]);
|
|
6723
7327
|
}
|
|
6724
7328
|
const id = path22.basename(file).replace(/\.(ts|tsx)$/, "");
|
|
6725
|
-
|
|
7329
|
+
const relations = parseRelations(src, name);
|
|
7330
|
+
for (const f of fields) {
|
|
7331
|
+
const r = relations[f.name];
|
|
7332
|
+
if (r)
|
|
7333
|
+
f.relation = r;
|
|
7334
|
+
}
|
|
7335
|
+
return { name, id, plural, collection, primaryKey: pk, indexes, fields, relations, path: file };
|
|
7336
|
+
}
|
|
7337
|
+
function parseRelations(src, name) {
|
|
7338
|
+
const out = {};
|
|
7339
|
+
const block = new RegExp(`export\\s+const\\s+${name}Relations\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*as\\s+const`, "m").exec(src);
|
|
7340
|
+
if (!block)
|
|
7341
|
+
return out;
|
|
7342
|
+
const body = block[1];
|
|
7343
|
+
const re = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*\{\s*model\s*:\s*['"`]([^'"`]+)['"`]\s*(?:,\s*label\s*:\s*['"`]([^'"`]+)['"`]\s*)?\}/g;
|
|
7344
|
+
let m;
|
|
7345
|
+
while (m = re.exec(body)) {
|
|
7346
|
+
out[m[1]] = { field: m[1], model: m[2], label: m[3] };
|
|
7347
|
+
}
|
|
7348
|
+
return out;
|
|
6726
7349
|
}
|
|
6727
7350
|
function parseInterfaceBody(body) {
|
|
6728
7351
|
const fields = [];
|
|
@@ -6859,12 +7482,26 @@ function addCrud(opts) {
|
|
|
6859
7482
|
formId,
|
|
6860
7483
|
prefix
|
|
6861
7484
|
};
|
|
7485
|
+
const modelsDir = path24.dirname(modelFile);
|
|
7486
|
+
const relations = visible.filter((f) => f.relation).map((f) => {
|
|
7487
|
+
const r = f.relation;
|
|
7488
|
+
const related = parseModelFile(path24.join(modelsDir, `${r.model}.ts`));
|
|
7489
|
+
const labelField = r.label ?? firstStringField(related) ?? related.primaryKey;
|
|
7490
|
+
return {
|
|
7491
|
+
field: f.name,
|
|
7492
|
+
model: r.model,
|
|
7493
|
+
plural: related.plural,
|
|
7494
|
+
pk: related.primaryKey,
|
|
7495
|
+
labelField
|
|
7496
|
+
};
|
|
7497
|
+
});
|
|
7498
|
+
const relVars = buildRelationVars(relations);
|
|
6862
7499
|
const created = [];
|
|
6863
7500
|
const modified = [];
|
|
6864
7501
|
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "service.ts.tpl"), servicePath, baseVars, created);
|
|
6865
7502
|
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formNav.ts.tpl"), path24.join(opts.projectRoot, "src", "services", `${camelLower(model.name)}FormNav.ts`), baseVars, created);
|
|
6866
7503
|
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "listPanel.ts.tpl"), listPanelPath, baseVars, created);
|
|
6867
|
-
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formPanel.ts.tpl"), formPanelPath, baseVars, created);
|
|
7504
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formPanel.ts.tpl"), formPanelPath, { ...baseVars, ...relVars }, created);
|
|
6868
7505
|
fs24.mkdirSync(listWebDir, { recursive: true });
|
|
6869
7506
|
const listVars = {
|
|
6870
7507
|
...baseVars,
|
|
@@ -6880,10 +7517,10 @@ function addCrud(opts) {
|
|
|
6880
7517
|
const formInputs = visible.filter((f) => !cfg.fields?.[f.name]?.hideInForm).map((f) => renderInput(f, cfg.fields?.[f.name])).join(`
|
|
6881
7518
|
`);
|
|
6882
7519
|
const emptyLit = buildEmptyFormLiteral(visible, cfg);
|
|
6883
|
-
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formApp.tsx.tpl"), path24.join(formWebDir, "App.tsx"), { ...baseVars, formFieldInputs: formInputs, emptyFormLiteral: emptyLit }, created);
|
|
7520
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formApp.tsx.tpl"), path24.join(formWebDir, "App.tsx"), { ...baseVars, ...relVars, formFieldInputs: formInputs, emptyFormLiteral: emptyLit }, created);
|
|
6884
7521
|
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "main.tsx.tpl"), path24.join(formWebDir, "main.tsx"), baseVars, created);
|
|
6885
7522
|
appendApi3(apiPath, listApiName, model, created, modified);
|
|
6886
|
-
appendApiForm(apiPath, formApiName, model, created, modified);
|
|
7523
|
+
appendApiForm(apiPath, formApiName, model, created, modified, relations.length > 0);
|
|
6887
7524
|
let menuInfo;
|
|
6888
7525
|
if (opts.menu && opts.menu !== "none") {
|
|
6889
7526
|
menuInfo = wireMenu(opts, model, cfg, listId, formId);
|
|
@@ -6937,6 +7574,14 @@ function renderInput(field, override) {
|
|
|
6937
7574
|
` + ` <span style={{ opacity: 0.8 }}>${labelText}</span>
|
|
6938
7575
|
` + `${input}
|
|
6939
7576
|
` + ` </label>`;
|
|
7577
|
+
if (field.relation) {
|
|
7578
|
+
return wrap(` <select${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)}>
|
|
7579
|
+
` + ` <option value=""></option>
|
|
7580
|
+
` + ` {(relOptions['${name}'] ?? []).map((o) => (
|
|
7581
|
+
` + ` <option key={o.value} value={o.value}>{o.label}</option>
|
|
7582
|
+
` + ` ))}
|
|
7583
|
+
` + ` </select>`);
|
|
7584
|
+
}
|
|
6940
7585
|
switch (spec.kind) {
|
|
6941
7586
|
case "number":
|
|
6942
7587
|
return wrap(` <input type="number"${placeholderAttr}${required} value={form.${name} as any ?? ''} onChange={(e) => onChange('${name}', e.target.value === '' ? undefined : Number(e.target.value))} />`);
|
|
@@ -6987,13 +7632,15 @@ export interface ${apiName} {
|
|
|
6987
7632
|
ensureImport(apiPath, model.name);
|
|
6988
7633
|
appendIfMissing(apiPath, apiName, sig, created, modified);
|
|
6989
7634
|
}
|
|
6990
|
-
function appendApiForm(apiPath, apiName, model, created, modified) {
|
|
7635
|
+
function appendApiForm(apiPath, apiName, model, created, modified, hasRelations) {
|
|
7636
|
+
const optionsLine = hasRelations ? ` options(): Promise<Record<string, { value: string; label: string }[]>>;
|
|
7637
|
+
` : "";
|
|
6991
7638
|
const sig = `
|
|
6992
7639
|
export interface ${apiName} {
|
|
6993
7640
|
` + ` pendingId(): Promise<${model.name}['${model.primaryKey}'] | null>;
|
|
6994
7641
|
` + ` get(id: ${model.name}['${model.primaryKey}'] | null): Promise<${model.name} | null>;
|
|
6995
7642
|
` + ` save(row: ${model.name}): Promise<${model.name}>;
|
|
6996
|
-
` + ` cancel(): Promise<void>;
|
|
7643
|
+
` + optionsLine + ` cancel(): Promise<void>;
|
|
6997
7644
|
` + `}
|
|
6998
7645
|
`;
|
|
6999
7646
|
ensureImport(apiPath, model.name);
|
|
@@ -7081,6 +7728,30 @@ function runGen11(cwd) {
|
|
|
7081
7728
|
function which11(cmd) {
|
|
7082
7729
|
return import_child_process12.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
|
|
7083
7730
|
}
|
|
7731
|
+
function firstStringField(model) {
|
|
7732
|
+
return model.fields.find((f) => f.type.trim() === "string" && f.name !== model.primaryKey)?.name;
|
|
7733
|
+
}
|
|
7734
|
+
function buildRelationVars(relations) {
|
|
7735
|
+
if (relations.length === 0) {
|
|
7736
|
+
return { relationImports: "", relationOptionsHandler: "", relationOptionsState: "", relationOptionsLoad: "" };
|
|
7737
|
+
}
|
|
7738
|
+
const uniqueRepos = [...new Map(relations.map((r) => [r.plural, r])).values()];
|
|
7739
|
+
const relationImports = uniqueRepos.map((r) => `import { ${r.plural}Repo } from '../models/${r.model}';`).join(`
|
|
7740
|
+
`) + `
|
|
7741
|
+
`;
|
|
7742
|
+
const handlerLines = relations.map((r) => ` ${r.field}: (await ${r.plural}Repo().findMany()).map((x) => ({ value: String(x.${r.pk}), label: String(x.${r.labelField}) })),`).join(`
|
|
7743
|
+
`);
|
|
7744
|
+
const relationOptionsHandler = ` async options() {
|
|
7745
|
+
` + ` return {
|
|
7746
|
+
` + `${handlerLines}
|
|
7747
|
+
` + ` };
|
|
7748
|
+
` + ` },
|
|
7749
|
+
`;
|
|
7750
|
+
const relationOptionsState = ` const [relOptions, setRelOptions] = useState<Record<string, { value: string; label: string }[]>>({});`;
|
|
7751
|
+
const relationOptionsLoad = `
|
|
7752
|
+
useEffect(() => { void api.options().then(setRelOptions); }, []);`;
|
|
7753
|
+
return { relationImports, relationOptionsHandler, relationOptionsState, relationOptionsLoad };
|
|
7754
|
+
}
|
|
7084
7755
|
var fs24, path24, import_child_process12;
|
|
7085
7756
|
var init_add11 = __esm(() => {
|
|
7086
7757
|
init_scaffold();
|
|
@@ -7113,11 +7784,37 @@ function addModel(opts) {
|
|
|
7113
7784
|
const pkField = explicitPk[0] ?? opts.fields.find((f) => f.name === "id") ?? opts.fields[0];
|
|
7114
7785
|
const primaryKey = pkField.name;
|
|
7115
7786
|
const indexes = opts.fields.filter((f) => f.indexed && f.name !== primaryKey).map((f) => f.name);
|
|
7116
|
-
const
|
|
7787
|
+
const modelsDir = path25.join(opts.projectRoot, "src", "models");
|
|
7788
|
+
for (const f of opts.fields) {
|
|
7789
|
+
if (!f.relation)
|
|
7790
|
+
continue;
|
|
7791
|
+
if (f.relation.model === Name) {} else if (!fs25.existsSync(path25.join(modelsDir, `${f.relation.model}.ts`))) {
|
|
7792
|
+
throw new Error(`Field "${f.name}" references model "${f.relation.model}", but src/models/${f.relation.model}.ts does not exist. ` + `Run \`vsceasy model add --name ${f.relation.model}\` first.`);
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
const target = path25.join(modelsDir, `${Name}.ts`);
|
|
7117
7796
|
assertNoOverwrite(opts.projectRoot, target, "Model");
|
|
7118
7797
|
const tpl = path25.join(opts.templatesRoot, "_generators", "model", "model.ts.tpl");
|
|
7119
|
-
const
|
|
7798
|
+
const fkName = (f) => f.relation ? `${f.name}Id` : f.name;
|
|
7799
|
+
const fieldLines = opts.fields.map((f) => {
|
|
7800
|
+
const ts = f.relation ? "string" : f.type;
|
|
7801
|
+
const note = f.relation ? ` // → ${f.relation.model}` : "";
|
|
7802
|
+
return ` ${fkName(f)}${f.optional ? "?" : ""}: ${ts};${note}`;
|
|
7803
|
+
}).join(`
|
|
7120
7804
|
`);
|
|
7805
|
+
const relFields = opts.fields.filter((f) => f.relation);
|
|
7806
|
+
const relationsBlock = relFields.length ? `
|
|
7807
|
+
|
|
7808
|
+
/** Relation metadata — used by \`vsceasy crud add\` to populate pickers. */
|
|
7809
|
+
` + `export const ${Name}Relations = {
|
|
7810
|
+
` + relFields.map((f) => {
|
|
7811
|
+
const r = f.relation;
|
|
7812
|
+
const lbl = r.label ? `, label: '${r.label}'` : "";
|
|
7813
|
+
return ` ${fkName(f)}: { model: '${r.model}'${lbl} },`;
|
|
7814
|
+
}).join(`
|
|
7815
|
+
`) + `
|
|
7816
|
+
} as const;
|
|
7817
|
+
` : "";
|
|
7121
7818
|
const vars = {
|
|
7122
7819
|
name,
|
|
7123
7820
|
Name,
|
|
@@ -7126,11 +7823,12 @@ function addModel(opts) {
|
|
|
7126
7823
|
primaryKey,
|
|
7127
7824
|
fieldLines,
|
|
7128
7825
|
indexesLine: indexes.length ? `
|
|
7129
|
-
indexes: [${indexes.map((i) => `'${i}'`).join(", ")}],` : ""
|
|
7826
|
+
indexes: [${indexes.map((i) => `'${i}'`).join(", ")}],` : "",
|
|
7827
|
+
relationsBlock
|
|
7130
7828
|
};
|
|
7131
7829
|
fs25.mkdirSync(path25.dirname(target), { recursive: true });
|
|
7132
7830
|
fs25.writeFileSync(target, substitute(fs25.readFileSync(tpl, "utf8"), vars));
|
|
7133
|
-
return { created: [target], primaryKey, indexes };
|
|
7831
|
+
return { created: [target], primaryKey, indexes, relations: relFields.map((f) => ({ field: fkName(f), ...f.relation })) };
|
|
7134
7832
|
}
|
|
7135
7833
|
function normalizeCamel8(s) {
|
|
7136
7834
|
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|