@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/bin/cli.js
CHANGED
|
@@ -3738,15 +3738,26 @@ async function scaffold(opts) {
|
|
|
3738
3738
|
throw new Error(`Target directory not empty: ${opts.targetDir}`);
|
|
3739
3739
|
}
|
|
3740
3740
|
fs2.mkdirSync(opts.targetDir, { recursive: true });
|
|
3741
|
+
const type = opts.type ?? "ui";
|
|
3741
3742
|
const vars = buildVars(opts);
|
|
3742
3743
|
await copyTree(src, opts.targetDir, vars);
|
|
3743
|
-
|
|
3744
|
+
applyType(opts.targetDir, type, opts.preset ?? "full", opts.templatesRoot, vars);
|
|
3744
3745
|
writeConfig(opts.targetDir, {
|
|
3745
3746
|
publisher: opts.publisher,
|
|
3746
3747
|
commandPrefix: vars.commandPrefix,
|
|
3747
|
-
|
|
3748
|
+
type,
|
|
3749
|
+
...type === "ui" ? { ui: opts.ui } : {}
|
|
3748
3750
|
});
|
|
3749
3751
|
}
|
|
3752
|
+
function applyType(targetDir, type, preset, templatesRoot, vars) {
|
|
3753
|
+
if (type === "ui") {
|
|
3754
|
+
applyPreset(targetDir, preset);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
stripWebview(targetDir);
|
|
3758
|
+
if (type === "language")
|
|
3759
|
+
applyLanguage(targetDir, templatesRoot, vars);
|
|
3760
|
+
}
|
|
3750
3761
|
function applyPreset(targetDir, preset) {
|
|
3751
3762
|
if (preset === "full")
|
|
3752
3763
|
return;
|
|
@@ -3769,15 +3780,104 @@ function applyPreset(targetDir, preset) {
|
|
|
3769
3780
|
`);
|
|
3770
3781
|
}
|
|
3771
3782
|
}
|
|
3783
|
+
function stripWebview(targetDir) {
|
|
3784
|
+
const removals = [
|
|
3785
|
+
"src/panels",
|
|
3786
|
+
"src/webview",
|
|
3787
|
+
"src/commands/hello.ts",
|
|
3788
|
+
"vite.config.ts"
|
|
3789
|
+
];
|
|
3790
|
+
for (const rel of removals) {
|
|
3791
|
+
const abs = path2.join(targetDir, rel);
|
|
3792
|
+
if (fs2.existsSync(abs))
|
|
3793
|
+
fs2.rmSync(abs, { recursive: true, force: true });
|
|
3794
|
+
}
|
|
3795
|
+
const apiPath = path2.join(targetDir, "src", "shared", "api.ts");
|
|
3796
|
+
if (fs2.existsSync(apiPath)) {
|
|
3797
|
+
fs2.writeFileSync(apiPath, `// RPC contracts go here (add a panel with \`vsceasy panel add\`).
|
|
3798
|
+
`);
|
|
3799
|
+
}
|
|
3800
|
+
trimReactFromPackageJson(targetDir);
|
|
3801
|
+
}
|
|
3802
|
+
function trimReactFromPackageJson(targetDir) {
|
|
3803
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
3804
|
+
if (!fs2.existsSync(pkgPath))
|
|
3805
|
+
return;
|
|
3806
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
3807
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
3808
|
+
if (!pkg[section])
|
|
3809
|
+
continue;
|
|
3810
|
+
for (const dep of Object.keys(pkg[section])) {
|
|
3811
|
+
if (REACT_DEPS.has(dep))
|
|
3812
|
+
delete pkg[section][dep];
|
|
3813
|
+
}
|
|
3814
|
+
if (Object.keys(pkg[section]).length === 0)
|
|
3815
|
+
delete pkg[section];
|
|
3816
|
+
}
|
|
3817
|
+
if (pkg.scripts) {
|
|
3818
|
+
for (const key of Object.keys(pkg.scripts)) {
|
|
3819
|
+
if (UI_SCRIPT_KEYS.has(key))
|
|
3820
|
+
delete pkg.scripts[key];
|
|
3821
|
+
}
|
|
3822
|
+
if (pkg.scripts.dev) {
|
|
3823
|
+
pkg.scripts.dev = "bun run gen:scan && bun run dev:ext";
|
|
3824
|
+
}
|
|
3825
|
+
for (const [k, v] of Object.entries(pkg.scripts)) {
|
|
3826
|
+
pkg.scripts[k] = v.replace(/\s*&&\s*bun run build:ui/g, "");
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
3830
|
+
`);
|
|
3831
|
+
}
|
|
3832
|
+
function applyLanguage(targetDir, templatesRoot, vars) {
|
|
3833
|
+
const assetsDir = path2.join(templatesRoot, "_assets", "language");
|
|
3834
|
+
if (!fs2.existsSync(assetsDir)) {
|
|
3835
|
+
throw new Error(`Language assets not found: ${assetsDir}`);
|
|
3836
|
+
}
|
|
3837
|
+
copyAssetsTree(assetsDir, targetDir, vars);
|
|
3838
|
+
const langReadme = path2.join(targetDir, "README.language.md");
|
|
3839
|
+
if (fs2.existsSync(langReadme)) {
|
|
3840
|
+
fs2.renameSync(langReadme, path2.join(targetDir, "README.md"));
|
|
3841
|
+
}
|
|
3842
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
3843
|
+
if (fs2.existsSync(pkgPath)) {
|
|
3844
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
3845
|
+
pkg.activationEvents = [`onLanguage:${vars.langId}`];
|
|
3846
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
3847
|
+
`);
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
function copyAssetsTree(srcDir, destDir, vars) {
|
|
3851
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
3852
|
+
if (SKIP_NAMES.has(entry.name))
|
|
3853
|
+
continue;
|
|
3854
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
3855
|
+
const destName = substitute(entry.name, vars);
|
|
3856
|
+
const destPath = path2.join(destDir, destName);
|
|
3857
|
+
if (entry.isDirectory()) {
|
|
3858
|
+
fs2.mkdirSync(destPath, { recursive: true });
|
|
3859
|
+
copyAssetsTree(srcPath, destPath, vars);
|
|
3860
|
+
} else if (entry.isFile()) {
|
|
3861
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
3862
|
+
fs2.writeFileSync(destPath, substitute(fs2.readFileSync(srcPath, "utf8"), vars));
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3772
3866
|
function buildVars(opts) {
|
|
3773
3867
|
const simpleName = opts.name.replace(/^@[^/]+\//, "");
|
|
3774
3868
|
const commandPrefix = simpleName.replace(/[^a-zA-Z0-9]+/g, "");
|
|
3869
|
+
const langId = simpleName.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
3870
|
+
const scopeName = `source.${langId}`;
|
|
3871
|
+
const langExt = langId.slice(0, 4).toUpperCase();
|
|
3775
3872
|
return {
|
|
3776
3873
|
name: opts.name,
|
|
3777
3874
|
displayName: opts.displayName,
|
|
3778
3875
|
description: opts.description,
|
|
3779
3876
|
publisher: opts.publisher,
|
|
3780
|
-
commandPrefix
|
|
3877
|
+
commandPrefix,
|
|
3878
|
+
langId,
|
|
3879
|
+
scopeName,
|
|
3880
|
+
langExt
|
|
3781
3881
|
};
|
|
3782
3882
|
}
|
|
3783
3883
|
async function copyTree(srcDir, destDir, vars) {
|
|
@@ -3803,7 +3903,7 @@ async function copyTree(srcDir, destDir, vars) {
|
|
|
3803
3903
|
function substitute(input, vars) {
|
|
3804
3904
|
return input.replace(/\{\{(\w+)\}\}/g, (_m, key) => vars[key] ?? `{{${key}}}`);
|
|
3805
3905
|
}
|
|
3806
|
-
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES;
|
|
3906
|
+
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES, REACT_DEPS, UI_SCRIPT_KEYS;
|
|
3807
3907
|
var init_scaffold = __esm(() => {
|
|
3808
3908
|
init_config();
|
|
3809
3909
|
fs2 = __toESM(require("fs"));
|
|
@@ -3823,12 +3923,326 @@ var init_scaffold = __esm(() => {
|
|
|
3823
3923
|
".yaml"
|
|
3824
3924
|
]);
|
|
3825
3925
|
SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
|
|
3926
|
+
REACT_DEPS = new Set([
|
|
3927
|
+
"react",
|
|
3928
|
+
"react-dom",
|
|
3929
|
+
"@types/react",
|
|
3930
|
+
"@types/react-dom",
|
|
3931
|
+
"@vitejs/plugin-react",
|
|
3932
|
+
"vite"
|
|
3933
|
+
]);
|
|
3934
|
+
UI_SCRIPT_KEYS = new Set(["dev:ui", "build:ui"]);
|
|
3826
3935
|
});
|
|
3827
3936
|
|
|
3828
3937
|
// src/lib/templatesData.ts
|
|
3829
|
-
var TEMPLATES_VERSION = "0.1.
|
|
3938
|
+
var TEMPLATES_VERSION = "0.1.10", TEMPLATE_FILES;
|
|
3830
3939
|
var init_templatesData = __esm(() => {
|
|
3831
3940
|
TEMPLATE_FILES = {
|
|
3941
|
+
"_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",
|
|
3942
|
+
"_assets/language/contributes.extra.json": `{
|
|
3943
|
+
"languages": [
|
|
3944
|
+
{
|
|
3945
|
+
"id": "{{langId}}",
|
|
3946
|
+
"aliases": ["{{displayName}}", "{{langId}}"],
|
|
3947
|
+
"extensions": [".{{langId}}"],
|
|
3948
|
+
"configuration": "./language-configuration.json"
|
|
3949
|
+
}
|
|
3950
|
+
],
|
|
3951
|
+
"grammars": [
|
|
3952
|
+
{
|
|
3953
|
+
"language": "{{langId}}",
|
|
3954
|
+
"scopeName": "{{scopeName}}",
|
|
3955
|
+
"path": "./syntaxes/{{langId}}.tmLanguage.json"
|
|
3956
|
+
}
|
|
3957
|
+
],
|
|
3958
|
+
"snippets": [
|
|
3959
|
+
{
|
|
3960
|
+
"language": "{{langId}}",
|
|
3961
|
+
"path": "./snippets/{{langId}}.json"
|
|
3962
|
+
}
|
|
3963
|
+
],
|
|
3964
|
+
"iconThemes": [
|
|
3965
|
+
{
|
|
3966
|
+
"id": "{{langId}}-icons",
|
|
3967
|
+
"label": "{{displayName}} Icons",
|
|
3968
|
+
"path": "./fileicons/{{langId}}-icon-theme.json"
|
|
3969
|
+
}
|
|
3970
|
+
],
|
|
3971
|
+
"configuration": {
|
|
3972
|
+
"title": "{{displayName}}",
|
|
3973
|
+
"properties": {
|
|
3974
|
+
"{{commandPrefix}}.colorize": {
|
|
3975
|
+
"type": "boolean",
|
|
3976
|
+
"default": true,
|
|
3977
|
+
"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."
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
`,
|
|
3983
|
+
"_assets/language/fileicons/{{langId}}-icon-theme.json": `{
|
|
3984
|
+
"iconDefinitions": {
|
|
3985
|
+
"{{langId}}_file": {
|
|
3986
|
+
"iconPath": "../icons/{{langId}}.svg"
|
|
3987
|
+
}
|
|
3988
|
+
},
|
|
3989
|
+
"languageIds": {
|
|
3990
|
+
"{{langId}}": "{{langId}}_file"
|
|
3991
|
+
},
|
|
3992
|
+
"fileExtensions": {
|
|
3993
|
+
"{{langId}}": "{{langId}}_file"
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
`,
|
|
3997
|
+
"_assets/language/icons/{{langId}}.svg": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
3998
|
+
<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"/>
|
|
3999
|
+
<path fill="#5a5a5a" d="M20 3l6 6h-6z"/>
|
|
4000
|
+
<text x="16" y="22" font-family="monospace" font-size="9" font-weight="bold" fill="#fff" text-anchor="middle">{{langExt}}</text>
|
|
4001
|
+
</svg>
|
|
4002
|
+
`,
|
|
4003
|
+
"_assets/language/language-configuration.json": `{
|
|
4004
|
+
"comments": {
|
|
4005
|
+
"lineComment": "#"
|
|
4006
|
+
},
|
|
4007
|
+
"brackets": [
|
|
4008
|
+
["{", "}"],
|
|
4009
|
+
["[", "]"],
|
|
4010
|
+
["(", ")"]
|
|
4011
|
+
],
|
|
4012
|
+
"autoClosingPairs": [
|
|
4013
|
+
{ "open": "{", "close": "}" },
|
|
4014
|
+
{ "open": "[", "close": "]" },
|
|
4015
|
+
{ "open": "(", "close": ")" },
|
|
4016
|
+
{ "open": "\\"", "close": "\\"", "notIn": ["string"] },
|
|
4017
|
+
{ "open": "'", "close": "'", "notIn": ["string"] }
|
|
4018
|
+
],
|
|
4019
|
+
"surroundingPairs": [
|
|
4020
|
+
["{", "}"],
|
|
4021
|
+
["[", "]"],
|
|
4022
|
+
["(", ")"],
|
|
4023
|
+
["\\"", "\\""],
|
|
4024
|
+
["'", "'"]
|
|
4025
|
+
],
|
|
4026
|
+
"folding": {
|
|
4027
|
+
"markers": {
|
|
4028
|
+
"start": "^\\\\s*#\\\\s*region\\\\b",
|
|
4029
|
+
"end": "^\\\\s*#\\\\s*endregion\\\\b"
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
`,
|
|
4034
|
+
"_assets/language/snippets/{{langId}}.json": `{
|
|
4035
|
+
"Section": {
|
|
4036
|
+
"prefix": "section",
|
|
4037
|
+
"body": ["[\${1:name}]", "$0"],
|
|
4038
|
+
"description": "A {{displayName}} section"
|
|
4039
|
+
},
|
|
4040
|
+
"Key/Value": {
|
|
4041
|
+
"prefix": "kv",
|
|
4042
|
+
"body": ["\${1:key} = \${2:value}"],
|
|
4043
|
+
"description": "A key/value pair"
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
`,
|
|
4047
|
+
"_assets/language/src/colorize.ts": `import type * as vscode from 'vscode';
|
|
4048
|
+
import { applyTokenColors, removeTokenColors, type TokenColorRule } from './helpers/colorize';
|
|
4049
|
+
|
|
4050
|
+
/** Root TextMate scope of this language — rules are applied only to these files. */
|
|
4051
|
+
export const SCOPE = '{{scopeName}}';
|
|
4052
|
+
|
|
4053
|
+
/**
|
|
4054
|
+
* Default token colors for {{displayName}}, emphasizing each construct. Edit
|
|
4055
|
+
* freely — they are applied to \`[{{scopeName}}]\` only, so other languages keep
|
|
4056
|
+
* the user's theme. Scope names must match your grammar
|
|
4057
|
+
* (syntaxes/{{langId}}.tmLanguage.json).
|
|
4058
|
+
*/
|
|
4059
|
+
export const RULES: TokenColorRule[] = [
|
|
4060
|
+
{ scope: 'comment.line.number-sign.{{langId}}', settings: { foreground: '#6b7a6e', fontStyle: 'italic' } },
|
|
4061
|
+
{ scope: 'string.quoted.double.basic.{{langId}}', settings: { foreground: '#98c379' } },
|
|
4062
|
+
{ scope: 'string.quoted.single.literal.{{langId}}', settings: { foreground: '#98c379' } },
|
|
4063
|
+
{ scope: 'constant.numeric.{{langId}}', settings: { foreground: '#d19a66' } },
|
|
4064
|
+
];
|
|
4065
|
+
|
|
4066
|
+
export async function applyColors(vscodeNs: typeof vscode): Promise<void> {
|
|
4067
|
+
await applyTokenColors(SCOPE, RULES);
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
export async function removeColors(vscodeNs: typeof vscode): Promise<void> {
|
|
4071
|
+
await removeTokenColors(SCOPE);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
/** True when the user has opted in (default) to automatic coloring. */
|
|
4075
|
+
export function colorizeEnabled(vscodeNs: typeof vscode): boolean {
|
|
4076
|
+
return vscodeNs.workspace.getConfiguration('{{commandPrefix}}').get<boolean>('colorize', true);
|
|
4077
|
+
}
|
|
4078
|
+
`,
|
|
4079
|
+
"_assets/language/src/commands/applyColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
4080
|
+
import { applyColors } from '../colorize';
|
|
4081
|
+
|
|
4082
|
+
export default defineCommand({
|
|
4083
|
+
id: 'applyColors',
|
|
4084
|
+
title: '{{displayName}}: Apply Colors',
|
|
4085
|
+
run: async (vscode) => {
|
|
4086
|
+
await applyColors(vscode);
|
|
4087
|
+
vscode.window.showInformationMessage('{{displayName}} colors applied.');
|
|
4088
|
+
},
|
|
4089
|
+
});
|
|
4090
|
+
`,
|
|
4091
|
+
"_assets/language/src/commands/removeColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
4092
|
+
import { removeColors } from '../colorize';
|
|
4093
|
+
|
|
4094
|
+
export default defineCommand({
|
|
4095
|
+
id: 'removeColors',
|
|
4096
|
+
title: '{{displayName}}: Remove Colors',
|
|
4097
|
+
run: async (vscode) => {
|
|
4098
|
+
await removeColors(vscode);
|
|
4099
|
+
vscode.window.showInformationMessage('{{displayName}} colors removed.');
|
|
4100
|
+
},
|
|
4101
|
+
});
|
|
4102
|
+
`,
|
|
4103
|
+
"_assets/language/src/extension/extension.ts": `import { bootstrap } from '../shared/vsceasy';
|
|
4104
|
+
import { registry } from './_registry';
|
|
4105
|
+
import { applyColors, removeColors, colorizeEnabled } from '../colorize';
|
|
4106
|
+
|
|
4107
|
+
export const activate = bootstrap(registry, {
|
|
4108
|
+
onActivate: [
|
|
4109
|
+
async (context, vscode) => {
|
|
4110
|
+
// Auto-apply scoped token colors on activate when opted in (default).
|
|
4111
|
+
// Scoped to {{scopeName}} only — other languages are untouched.
|
|
4112
|
+
if (colorizeEnabled(vscode)) {
|
|
4113
|
+
await applyColors(vscode);
|
|
4114
|
+
}
|
|
4115
|
+
// React to the user toggling \`{{commandPrefix}}.colorize\` at runtime.
|
|
4116
|
+
context.subscriptions.push(
|
|
4117
|
+
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
4118
|
+
if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
4119
|
+
if (colorizeEnabled(vscode)) await applyColors(vscode);
|
|
4120
|
+
else await removeColors(vscode);
|
|
4121
|
+
}),
|
|
4122
|
+
);
|
|
4123
|
+
},
|
|
4124
|
+
],
|
|
4125
|
+
});
|
|
4126
|
+
|
|
4127
|
+
export function deactivate() {}
|
|
4128
|
+
`,
|
|
4129
|
+
"_assets/language/src/helpers/colorize.ts": `import * as vscode from 'vscode';
|
|
4130
|
+
|
|
4131
|
+
/**
|
|
4132
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
4133
|
+
* language's root scope like \`{{scopeName}}\`), written to the user's
|
|
4134
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
4135
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
4136
|
+
* keeps the active theme's colors.
|
|
4137
|
+
*
|
|
4138
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
4139
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
4140
|
+
*/
|
|
4141
|
+
|
|
4142
|
+
export interface TokenColorRule {
|
|
4143
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
4144
|
+
scope: string;
|
|
4145
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
4149
|
+
|
|
4150
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
4151
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
4152
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
4153
|
+
|
|
4154
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
4155
|
+
|
|
4156
|
+
/**
|
|
4157
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
4158
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
4159
|
+
* replaces only previously-applied rules from this extension.
|
|
4160
|
+
*/
|
|
4161
|
+
export async function applyTokenColors(
|
|
4162
|
+
scope: string,
|
|
4163
|
+
rules: TokenColorRule[],
|
|
4164
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
4165
|
+
): Promise<void> {
|
|
4166
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
4167
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
4168
|
+
const key = blockKey(scope);
|
|
4169
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
4170
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
4171
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
4172
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
4173
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
4174
|
+
await cfg.update(SECTION, next, target);
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
4178
|
+
export async function removeTokenColors(
|
|
4179
|
+
scope: string,
|
|
4180
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
4181
|
+
): Promise<void> {
|
|
4182
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
4183
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
4184
|
+
const key = blockKey(scope);
|
|
4185
|
+
if (!current || !current[key]) return;
|
|
4186
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
4187
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
4188
|
+
|
|
4189
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
4190
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
4191
|
+
else delete nextBlock.textMateRules;
|
|
4192
|
+
|
|
4193
|
+
const next = { ...current };
|
|
4194
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
4195
|
+
else delete next[key];
|
|
4196
|
+
|
|
4197
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
4198
|
+
}
|
|
4199
|
+
`,
|
|
4200
|
+
"_assets/language/syntaxes/{{langId}}.tmLanguage.json": `{
|
|
4201
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
4202
|
+
"name": "{{displayName}}",
|
|
4203
|
+
"scopeName": "{{scopeName}}",
|
|
4204
|
+
"patterns": [
|
|
4205
|
+
{ "include": "#comments" },
|
|
4206
|
+
{ "include": "#strings" },
|
|
4207
|
+
{ "include": "#numbers" }
|
|
4208
|
+
],
|
|
4209
|
+
"repository": {
|
|
4210
|
+
"comments": {
|
|
4211
|
+
"patterns": [
|
|
4212
|
+
{
|
|
4213
|
+
"name": "comment.line.number-sign.{{langId}}",
|
|
4214
|
+
"match": "#.*$"
|
|
4215
|
+
}
|
|
4216
|
+
]
|
|
4217
|
+
},
|
|
4218
|
+
"strings": {
|
|
4219
|
+
"patterns": [
|
|
4220
|
+
{
|
|
4221
|
+
"name": "string.quoted.double.{{langId}}",
|
|
4222
|
+
"begin": "\\"",
|
|
4223
|
+
"end": "\\"",
|
|
4224
|
+
"patterns": [
|
|
4225
|
+
{ "name": "constant.character.escape.{{langId}}", "match": "\\\\\\\\." }
|
|
4226
|
+
]
|
|
4227
|
+
},
|
|
4228
|
+
{
|
|
4229
|
+
"name": "string.quoted.single.{{langId}}",
|
|
4230
|
+
"begin": "'",
|
|
4231
|
+
"end": "'"
|
|
4232
|
+
}
|
|
4233
|
+
]
|
|
4234
|
+
},
|
|
4235
|
+
"numbers": {
|
|
4236
|
+
"patterns": [
|
|
4237
|
+
{
|
|
4238
|
+
"name": "constant.numeric.{{langId}}",
|
|
4239
|
+
"match": "\\\\b[0-9]+(\\\\.[0-9]+)?\\\\b"
|
|
4240
|
+
}
|
|
4241
|
+
]
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
`,
|
|
3832
4246
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
3833
4247
|
|
|
3834
4248
|
export default defineCommand({
|
|
@@ -4038,6 +4452,7 @@ export function App() {
|
|
|
4038
4452
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
4039
4453
|
const [error, setError] = useState<string | null>(null);
|
|
4040
4454
|
const [saving, setSaving] = useState(false);
|
|
4455
|
+
{{relationOptionsState}}
|
|
4041
4456
|
|
|
4042
4457
|
const load = useCallback(async (initial: boolean) => {
|
|
4043
4458
|
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
@@ -4075,6 +4490,7 @@ export function App() {
|
|
|
4075
4490
|
document.removeEventListener('visibilitychange', onVisible);
|
|
4076
4491
|
};
|
|
4077
4492
|
}, [load]);
|
|
4493
|
+
{{relationOptionsLoad}}
|
|
4078
4494
|
|
|
4079
4495
|
const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
|
|
4080
4496
|
setForm((f) => ({ ...f, [k]: v }));
|
|
@@ -4138,7 +4554,7 @@ import { {{Name}}Service } from '../services/{{Name}}Service';
|
|
|
4138
4554
|
import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
|
|
4139
4555
|
import type { {{Name}}FormApi } from '../shared/api';
|
|
4140
4556
|
import type { {{Name}} } from '../models/{{Name}}';
|
|
4141
|
-
|
|
4557
|
+
{{relationImports}}
|
|
4142
4558
|
export default definePanel<{{Name}}FormApi>({
|
|
4143
4559
|
title: '{{title}}',
|
|
4144
4560
|
column: 'beside',
|
|
@@ -4160,7 +4576,7 @@ export default definePanel<{{Name}}FormApi>({
|
|
|
4160
4576
|
void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
|
|
4161
4577
|
return saved;
|
|
4162
4578
|
},
|
|
4163
|
-
async cancel() {
|
|
4579
|
+
{{relationOptionsHandler}} async cancel() {
|
|
4164
4580
|
// No-op — webview closes itself.
|
|
4165
4581
|
},
|
|
4166
4582
|
}),
|
|
@@ -4434,6 +4850,92 @@ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
|
|
|
4434
4850
|
|
|
4435
4851
|
return cache;
|
|
4436
4852
|
}
|
|
4853
|
+
`,
|
|
4854
|
+
"_generators/helper/colorize.ts.tpl": `import * as vscode from 'vscode';
|
|
4855
|
+
|
|
4856
|
+
/**
|
|
4857
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
4858
|
+
* language's root scope like \`source.toml\`), written to the user's
|
|
4859
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
4860
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
4861
|
+
* keeps the active theme's colors.
|
|
4862
|
+
*
|
|
4863
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
4864
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
4865
|
+
*
|
|
4866
|
+
* Typical use — auto-apply on activate behind an opt-out setting:
|
|
4867
|
+
*
|
|
4868
|
+
* // extension.ts (onActivate hook)
|
|
4869
|
+
* if (config.get<boolean>('colorize', true)) {
|
|
4870
|
+
* await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
4871
|
+
* }
|
|
4872
|
+
* vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
4873
|
+
* if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
4874
|
+
* if (config.get<boolean>('colorize', true)) await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
4875
|
+
* else await removeTokenColors('source.{{commandPrefix}}');
|
|
4876
|
+
* });
|
|
4877
|
+
*
|
|
4878
|
+
* Declare the opt-out in package.json#contributes.configuration:
|
|
4879
|
+
* "{{commandPrefix}}.colorize": { "type": "boolean", "default": true }
|
|
4880
|
+
*/
|
|
4881
|
+
|
|
4882
|
+
export interface TokenColorRule {
|
|
4883
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
4884
|
+
scope: string;
|
|
4885
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
4889
|
+
|
|
4890
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
4891
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
4892
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
4893
|
+
|
|
4894
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
4895
|
+
|
|
4896
|
+
/**
|
|
4897
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
4898
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
4899
|
+
* replaces only previously-applied rules from this extension.
|
|
4900
|
+
*/
|
|
4901
|
+
export async function applyTokenColors(
|
|
4902
|
+
scope: string,
|
|
4903
|
+
rules: TokenColorRule[],
|
|
4904
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
4905
|
+
): Promise<void> {
|
|
4906
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
4907
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
4908
|
+
const key = blockKey(scope);
|
|
4909
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
4910
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
4911
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
4912
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
4913
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
4914
|
+
await cfg.update(SECTION, next, target);
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
4918
|
+
export async function removeTokenColors(
|
|
4919
|
+
scope: string,
|
|
4920
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
4921
|
+
): Promise<void> {
|
|
4922
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
4923
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
4924
|
+
const key = blockKey(scope);
|
|
4925
|
+
if (!current || !current[key]) return;
|
|
4926
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
4927
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
4928
|
+
|
|
4929
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
4930
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
4931
|
+
else delete nextBlock.textMateRules;
|
|
4932
|
+
|
|
4933
|
+
const next = { ...current };
|
|
4934
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
4935
|
+
else delete next[key];
|
|
4936
|
+
|
|
4937
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
4938
|
+
}
|
|
4437
4939
|
`,
|
|
4438
4940
|
"_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
|
|
4439
4941
|
|
|
@@ -5006,7 +5508,7 @@ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
|
|
|
5006
5508
|
* import { {{Plural}}Repo } from '../models/{{Name}}';
|
|
5007
5509
|
* await {{Plural}}Repo().insert({ ... });
|
|
5008
5510
|
*/
|
|
5009
|
-
export const {{Plural}}Repo = () => db()({{Plural}});
|
|
5511
|
+
export const {{Plural}}Repo = () => db()({{Plural}});{{relationsBlock}}
|
|
5010
5512
|
`,
|
|
5011
5513
|
"_generators/panel/App.tsx.tpl": `import React from 'react';
|
|
5012
5514
|
{{apiBlock}}
|
|
@@ -5752,6 +6254,12 @@ bun run package # → {{name}}-0.0.1.vsix
|
|
|
5752
6254
|
"react/scripts/gen.ts": `#!/usr/bin/env bun
|
|
5753
6255
|
// Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
|
|
5754
6256
|
// and syncs package.json#contributes (commands, viewsContainers, views).
|
|
6257
|
+
//
|
|
6258
|
+
// Non-generated contributes (e.g. languages, grammars, snippets, themes,
|
|
6259
|
+
// iconThemes, walkthroughs) go in an optional \`contributes.extra.json\` at the
|
|
6260
|
+
// project root. It is deep-merged into package.json#contributes on every run —
|
|
6261
|
+
// the keys gen.ts owns (commands, keybindings, menus.commandPalette,
|
|
6262
|
+
// viewsContainers, views) always win, everything else from extra is preserved.
|
|
5755
6263
|
|
|
5756
6264
|
import * as fs from 'fs';
|
|
5757
6265
|
import * as path from 'path';
|
|
@@ -5767,6 +6275,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
|
|
|
5767
6275
|
const JOBS_DIR = path.join(SRC, 'jobs');
|
|
5768
6276
|
const OUT = path.join(SRC, 'extension', '_registry.ts');
|
|
5769
6277
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
6278
|
+
const EXTRA_PATH = path.join(ROOT, 'contributes.extra.json');
|
|
6279
|
+
|
|
6280
|
+
/** Keys gen.ts owns — never overridden by contributes.extra.json. */
|
|
6281
|
+
const GEN_OWNED_KEYS = new Set(['commands', 'keybindings', 'viewsContainers', 'views']);
|
|
5770
6282
|
|
|
5771
6283
|
interface Discovered {
|
|
5772
6284
|
id: string;
|
|
@@ -5955,9 +6467,54 @@ function syncPackageJson(
|
|
|
5955
6467
|
delete contributes.views;
|
|
5956
6468
|
}
|
|
5957
6469
|
|
|
6470
|
+
mergeExtraContributes(contributes);
|
|
6471
|
+
|
|
5958
6472
|
fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
|
|
5959
6473
|
}
|
|
5960
6474
|
|
|
6475
|
+
/**
|
|
6476
|
+
* Deep-merge the optional \`contributes.extra.json\` (project root) into the
|
|
6477
|
+
* package's \`contributes\`. Use for any contribution point gen.ts doesn't
|
|
6478
|
+
* generate — languages, grammars, snippets, themes, iconThemes, walkthroughs…
|
|
6479
|
+
*
|
|
6480
|
+
* Rules:
|
|
6481
|
+
* - gen-owned keys (commands, keybindings, viewsContainers, views) are ignored
|
|
6482
|
+
* if present in extra — gen.ts stays authoritative for those.
|
|
6483
|
+
* - plain objects merge recursively; arrays and primitives from extra replace.
|
|
6484
|
+
*
|
|
6485
|
+
* NOTE: this is an inline copy of src/lib/contributesMerge.ts in the vsceasy
|
|
6486
|
+
* source (the script must run standalone). Keep the two in sync.
|
|
6487
|
+
*/
|
|
6488
|
+
function mergeExtraContributes(contributes: Record<string, any>) {
|
|
6489
|
+
if (!fs.existsSync(EXTRA_PATH)) return;
|
|
6490
|
+
let extra: Record<string, any>;
|
|
6491
|
+
try {
|
|
6492
|
+
extra = JSON.parse(fs.readFileSync(EXTRA_PATH, 'utf8'));
|
|
6493
|
+
} catch (err) {
|
|
6494
|
+
console.warn(\`! Skipping contributes.extra.json — invalid JSON: \${(err as Error).message}\`);
|
|
6495
|
+
return;
|
|
6496
|
+
}
|
|
6497
|
+
if (!extra || typeof extra !== 'object') return;
|
|
6498
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
6499
|
+
if (GEN_OWNED_KEYS.has(key)) continue;
|
|
6500
|
+
contributes[key] = deepMerge(contributes[key], value);
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
|
|
6504
|
+
function isPlainObject(v: unknown): v is Record<string, any> {
|
|
6505
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
function deepMerge(base: any, override: any): any {
|
|
6509
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
6510
|
+
const out: Record<string, any> = { ...base };
|
|
6511
|
+
for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
|
|
6512
|
+
return out;
|
|
6513
|
+
}
|
|
6514
|
+
// arrays and primitives: override wins
|
|
6515
|
+
return override;
|
|
6516
|
+
}
|
|
6517
|
+
|
|
5961
6518
|
function loadDef(file: string): {
|
|
5962
6519
|
id?: string;
|
|
5963
6520
|
title?: string;
|
|
@@ -8114,6 +8671,12 @@ var init_interactive = __esm(() => {
|
|
|
8114
8671
|
function toTitle(s) {
|
|
8115
8672
|
return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
8116
8673
|
}
|
|
8674
|
+
function normalizeType(v) {
|
|
8675
|
+
if (v === undefined || v === null || v === "")
|
|
8676
|
+
return;
|
|
8677
|
+
const s = String(v).trim().toLowerCase();
|
|
8678
|
+
return EXTENSION_TYPES.includes(s) ? s : undefined;
|
|
8679
|
+
}
|
|
8117
8680
|
function toBool(v) {
|
|
8118
8681
|
if (v === undefined || v === null || v === "")
|
|
8119
8682
|
return;
|
|
@@ -8153,7 +8716,7 @@ Installing dependencies with ${pm}...
|
|
|
8153
8716
|
! ${pm} install failed — run it manually`);
|
|
8154
8717
|
return false;
|
|
8155
8718
|
}
|
|
8156
|
-
var import_cli_maker, path4, import_child_process, createCommand, create_default;
|
|
8719
|
+
var import_cli_maker, path4, import_child_process, EXTENSION_TYPES, createCommand, create_default;
|
|
8157
8720
|
var init_create = __esm(() => {
|
|
8158
8721
|
init_scaffold();
|
|
8159
8722
|
init_findProject();
|
|
@@ -8161,6 +8724,7 @@ var init_create = __esm(() => {
|
|
|
8161
8724
|
import_cli_maker = __toESM(require_dist(), 1);
|
|
8162
8725
|
path4 = __toESM(require("path"));
|
|
8163
8726
|
import_child_process = require("child_process");
|
|
8727
|
+
EXTENSION_TYPES = ["ui", "language", "empty"];
|
|
8164
8728
|
createCommand = {
|
|
8165
8729
|
name: "create",
|
|
8166
8730
|
description: "Scaffold a new VS Code extension project",
|
|
@@ -8169,8 +8733,9 @@ var init_create = __esm(() => {
|
|
|
8169
8733
|
{ name: "displayName", description: "Human-readable extension name", required: false, type: import_cli_maker.ParamType.Text },
|
|
8170
8734
|
{ name: "description", description: "Short description", required: false, type: import_cli_maker.ParamType.Text },
|
|
8171
8735
|
{ name: "publisher", description: "VS Code publisher id", required: false, type: import_cli_maker.ParamType.Text },
|
|
8172
|
-
{ name: "
|
|
8173
|
-
{ name: "
|
|
8736
|
+
{ name: "type", description: "Extension type: ui (React webview + RPC), language (syntax/snippets/icon), empty (bare)", required: false, type: import_cli_maker.ParamType.List, options: ["ui", "language", "empty"] },
|
|
8737
|
+
{ name: "ui", description: "UI framework (only for --type ui)", required: false, type: import_cli_maker.ParamType.List, options: ["react"] },
|
|
8738
|
+
{ name: "preset", description: "UI preset (only for --type ui): minimal = empty extension, full = panel + RPC sample", required: false, type: import_cli_maker.ParamType.List, options: ["minimal", "full"] },
|
|
8174
8739
|
{ name: "dir", description: "Target directory (defaults to ./<name>)", required: false, type: import_cli_maker.ParamType.Text },
|
|
8175
8740
|
{ name: "git", description: "Initialize a git repository (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean },
|
|
8176
8741
|
{ name: "install", description: "Install dependencies after scaffolding (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean }
|
|
@@ -8178,6 +8743,15 @@ var init_create = __esm(() => {
|
|
|
8178
8743
|
action: async (args) => {
|
|
8179
8744
|
const name = args.name;
|
|
8180
8745
|
const simpleName = name.replace(/^@[^/]+\//, "");
|
|
8746
|
+
const interactiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8747
|
+
let type = normalizeType(args.type);
|
|
8748
|
+
if (!type) {
|
|
8749
|
+
type = interactiveTty ? await select("What kind of extension?", [
|
|
8750
|
+
{ label: "UI / webview", value: "ui", hint: "React panel + typed RPC bridge" },
|
|
8751
|
+
{ label: "Language support", value: "language", hint: "syntax highlighting, config, snippets, icon" },
|
|
8752
|
+
{ label: "Empty", value: "empty", hint: "bare activate/deactivate, no UI" }
|
|
8753
|
+
]) : "ui";
|
|
8754
|
+
}
|
|
8181
8755
|
const ui = args.ui ?? "react";
|
|
8182
8756
|
const preset = args.preset ?? "full";
|
|
8183
8757
|
const targetDir = path4.resolve(process.cwd(), args.dir ?? simpleName);
|
|
@@ -8188,18 +8762,18 @@ var init_create = __esm(() => {
|
|
|
8188
8762
|
description: args.description ?? `${simpleName} VS Code extension`,
|
|
8189
8763
|
publisher: args.publisher ?? "your-publisher",
|
|
8190
8764
|
ui,
|
|
8765
|
+
type,
|
|
8191
8766
|
preset,
|
|
8192
8767
|
targetDir,
|
|
8193
8768
|
templatesRoot: findTemplatesRoot()
|
|
8194
8769
|
});
|
|
8195
8770
|
const rel = path4.relative(process.cwd(), targetDir) || ".";
|
|
8196
8771
|
console.log(`
|
|
8197
|
-
✓ Created ${name} at ${rel}
|
|
8772
|
+
✓ Created ${name} (${type}) at ${rel}
|
|
8198
8773
|
`);
|
|
8199
|
-
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8200
8774
|
const gitFlag = toBool(args.git);
|
|
8201
8775
|
const installFlag = toBool(args.install);
|
|
8202
|
-
const wantGit = gitFlag ?? (
|
|
8776
|
+
const wantGit = gitFlag ?? (interactiveTty ? await confirm("Initialize a git repository?", true) : false);
|
|
8203
8777
|
if (wantGit) {
|
|
8204
8778
|
if (which("git"))
|
|
8205
8779
|
initGit(targetDir);
|
|
@@ -8207,7 +8781,7 @@ var init_create = __esm(() => {
|
|
|
8207
8781
|
console.warn("! git not found — skipping repository init");
|
|
8208
8782
|
}
|
|
8209
8783
|
let pm = null;
|
|
8210
|
-
const wantInstall = installFlag ?? (
|
|
8784
|
+
const wantInstall = installFlag ?? (interactiveTty ? await confirm("Install dependencies?", true) : false);
|
|
8211
8785
|
let installed = false;
|
|
8212
8786
|
if (wantInstall) {
|
|
8213
8787
|
pm = which("bun") ? "bun" : which("npm") ? "npm" : null;
|
|
@@ -8222,9 +8796,16 @@ Next steps:`);
|
|
|
8222
8796
|
console.log(` cd ${rel}`);
|
|
8223
8797
|
if (!installed)
|
|
8224
8798
|
console.log(` ${run} install`);
|
|
8225
|
-
|
|
8226
|
-
|
|
8799
|
+
if (type === "language") {
|
|
8800
|
+
console.log(` ${run} run gen # sync package.json#contributes from contributes.extra.json`);
|
|
8801
|
+
console.log(` ${run} run launch # opens Extension Development Host — open a matching file to see highlighting`);
|
|
8802
|
+
console.log(` # edit syntaxes/, snippets/, language-configuration.json to refine the language
|
|
8803
|
+
`);
|
|
8804
|
+
} else {
|
|
8805
|
+
console.log(` ${run} run launch # builds + opens Extension Development Host`);
|
|
8806
|
+
console.log(` # or \`${run} run dev\` + F5 inside VS Code for watch mode
|
|
8227
8807
|
`);
|
|
8808
|
+
}
|
|
8228
8809
|
} catch (err) {
|
|
8229
8810
|
console.error(`
|
|
8230
8811
|
✗ Failed to scaffold: ${err.message}
|
|
@@ -8811,7 +9392,7 @@ var init_add4 = __esm(() => {
|
|
|
8811
9392
|
init_config();
|
|
8812
9393
|
fs9 = __toESM(require("fs"));
|
|
8813
9394
|
path10 = __toESM(require("path"));
|
|
8814
|
-
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
|
|
9395
|
+
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache", "colorize"];
|
|
8815
9396
|
});
|
|
8816
9397
|
|
|
8817
9398
|
// src/lib/db/init.ts
|
|
@@ -8863,11 +9444,37 @@ function addModel(opts) {
|
|
|
8863
9444
|
const pkField = explicitPk[0] ?? opts.fields.find((f) => f.name === "id") ?? opts.fields[0];
|
|
8864
9445
|
const primaryKey = pkField.name;
|
|
8865
9446
|
const indexes = opts.fields.filter((f) => f.indexed && f.name !== primaryKey).map((f) => f.name);
|
|
8866
|
-
const
|
|
9447
|
+
const modelsDir = path12.join(opts.projectRoot, "src", "models");
|
|
9448
|
+
for (const f of opts.fields) {
|
|
9449
|
+
if (!f.relation)
|
|
9450
|
+
continue;
|
|
9451
|
+
if (f.relation.model === Name) {} else if (!fs11.existsSync(path12.join(modelsDir, `${f.relation.model}.ts`))) {
|
|
9452
|
+
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.`);
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
const target = path12.join(modelsDir, `${Name}.ts`);
|
|
8867
9456
|
assertNoOverwrite(opts.projectRoot, target, "Model");
|
|
8868
9457
|
const tpl = path12.join(opts.templatesRoot, "_generators", "model", "model.ts.tpl");
|
|
8869
|
-
const
|
|
8870
|
-
|
|
9458
|
+
const fkName = (f) => f.relation ? `${f.name}Id` : f.name;
|
|
9459
|
+
const fieldLines = opts.fields.map((f) => {
|
|
9460
|
+
const ts = f.relation ? "string" : f.type;
|
|
9461
|
+
const note = f.relation ? ` // → ${f.relation.model}` : "";
|
|
9462
|
+
return ` ${fkName(f)}${f.optional ? "?" : ""}: ${ts};${note}`;
|
|
9463
|
+
}).join(`
|
|
9464
|
+
`);
|
|
9465
|
+
const relFields = opts.fields.filter((f) => f.relation);
|
|
9466
|
+
const relationsBlock = relFields.length ? `
|
|
9467
|
+
|
|
9468
|
+
/** Relation metadata — used by \`vsceasy crud add\` to populate pickers. */
|
|
9469
|
+
` + `export const ${Name}Relations = {
|
|
9470
|
+
` + relFields.map((f) => {
|
|
9471
|
+
const r = f.relation;
|
|
9472
|
+
const lbl = r.label ? `, label: '${r.label}'` : "";
|
|
9473
|
+
return ` ${fkName(f)}: { model: '${r.model}'${lbl} },`;
|
|
9474
|
+
}).join(`
|
|
9475
|
+
`) + `
|
|
9476
|
+
} as const;
|
|
9477
|
+
` : "";
|
|
8871
9478
|
const vars = {
|
|
8872
9479
|
name,
|
|
8873
9480
|
Name,
|
|
@@ -8876,11 +9483,12 @@ function addModel(opts) {
|
|
|
8876
9483
|
primaryKey,
|
|
8877
9484
|
fieldLines,
|
|
8878
9485
|
indexesLine: indexes.length ? `
|
|
8879
|
-
indexes: [${indexes.map((i) => `'${i}'`).join(", ")}],` : ""
|
|
9486
|
+
indexes: [${indexes.map((i) => `'${i}'`).join(", ")}],` : "",
|
|
9487
|
+
relationsBlock
|
|
8880
9488
|
};
|
|
8881
9489
|
fs11.mkdirSync(path12.dirname(target), { recursive: true });
|
|
8882
9490
|
fs11.writeFileSync(target, substitute(fs11.readFileSync(tpl, "utf8"), vars));
|
|
8883
|
-
return { created: [target], primaryKey, indexes };
|
|
9491
|
+
return { created: [target], primaryKey, indexes, relations: relFields.map((f) => ({ field: fkName(f), ...f.relation })) };
|
|
8884
9492
|
}
|
|
8885
9493
|
function normalizeCamel3(s) {
|
|
8886
9494
|
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
@@ -8899,7 +9507,27 @@ var init_add5 = __esm(() => {
|
|
|
8899
9507
|
|
|
8900
9508
|
// src/lib/model/parseFields.ts
|
|
8901
9509
|
function parseFieldsSpec(spec) {
|
|
8902
|
-
return spec
|
|
9510
|
+
return splitTopLevel(spec).map((s) => s.trim()).filter(Boolean).map(parseFieldLine);
|
|
9511
|
+
}
|
|
9512
|
+
function splitTopLevel(spec) {
|
|
9513
|
+
const out = [];
|
|
9514
|
+
let depth = 0;
|
|
9515
|
+
let cur = "";
|
|
9516
|
+
for (const ch of spec) {
|
|
9517
|
+
if (ch === "(")
|
|
9518
|
+
depth++;
|
|
9519
|
+
else if (ch === ")")
|
|
9520
|
+
depth = Math.max(0, depth - 1);
|
|
9521
|
+
if (ch === "," && depth === 0) {
|
|
9522
|
+
out.push(cur);
|
|
9523
|
+
cur = "";
|
|
9524
|
+
continue;
|
|
9525
|
+
}
|
|
9526
|
+
cur += ch;
|
|
9527
|
+
}
|
|
9528
|
+
if (cur.trim())
|
|
9529
|
+
out.push(cur);
|
|
9530
|
+
return out;
|
|
8903
9531
|
}
|
|
8904
9532
|
function parseFieldLine(raw) {
|
|
8905
9533
|
const line = raw.trim();
|
|
@@ -8917,6 +9545,10 @@ function parseFieldLine(raw) {
|
|
|
8917
9545
|
optional = true;
|
|
8918
9546
|
name = name.slice(0, -1);
|
|
8919
9547
|
}
|
|
9548
|
+
const relation = parseRef(type);
|
|
9549
|
+
if (relation) {
|
|
9550
|
+
return { name, type: "ref", optional, relation };
|
|
9551
|
+
}
|
|
8920
9552
|
let primaryKey = false;
|
|
8921
9553
|
let indexed = false;
|
|
8922
9554
|
while (type.endsWith("!") || type.endsWith("@")) {
|
|
@@ -8934,6 +9566,14 @@ function parseFieldLine(raw) {
|
|
|
8934
9566
|
throw new Error(`Field "${raw}" has no type after flags.`);
|
|
8935
9567
|
return { name, type, optional, primaryKey, indexed };
|
|
8936
9568
|
}
|
|
9569
|
+
function parseRef(type) {
|
|
9570
|
+
const m = /^ref\s*\(\s*([A-Za-z][A-Za-z0-9_]*)\s*(?:,\s*label\s*=\s*([A-Za-z][A-Za-z0-9_]*)\s*)?\)$/.exec(type.trim());
|
|
9571
|
+
if (!m)
|
|
9572
|
+
return null;
|
|
9573
|
+
const model = m[1];
|
|
9574
|
+
const label = m[2];
|
|
9575
|
+
return label ? { model, label } : { model };
|
|
9576
|
+
}
|
|
8937
9577
|
|
|
8938
9578
|
// src/lib/wizard/run.ts
|
|
8939
9579
|
async function runWizard(opts = {}) {
|
|
@@ -9589,6 +10229,9 @@ function runDoctor(opts) {
|
|
|
9589
10229
|
results.push(...checkStatusBars(root));
|
|
9590
10230
|
results.push(...checkSubpanels(root));
|
|
9591
10231
|
results.push(checkContributesSync(root, pkg));
|
|
10232
|
+
const langCheck = checkLanguageAssets(root, pkg);
|
|
10233
|
+
if (langCheck)
|
|
10234
|
+
results.push(langCheck);
|
|
9592
10235
|
results.push(checkActivationEvents(pkg));
|
|
9593
10236
|
results.push(checkMarketplaceIcon(root, pkg));
|
|
9594
10237
|
results.push(checkGenScript(root));
|
|
@@ -9851,6 +10494,50 @@ function checkContributesSync(root, pkg) {
|
|
|
9851
10494
|
details: stale
|
|
9852
10495
|
};
|
|
9853
10496
|
}
|
|
10497
|
+
function checkLanguageAssets(root, pkg) {
|
|
10498
|
+
const sources = [pkg?.contributes];
|
|
10499
|
+
const extraPath = path14.join(root, "contributes.extra.json");
|
|
10500
|
+
if (fs12.existsSync(extraPath)) {
|
|
10501
|
+
try {
|
|
10502
|
+
sources.push(JSON.parse(fs12.readFileSync(extraPath, "utf8")));
|
|
10503
|
+
} catch {
|
|
10504
|
+
return {
|
|
10505
|
+
id: "language",
|
|
10506
|
+
level: "error",
|
|
10507
|
+
message: "contributes.extra.json is not valid JSON"
|
|
10508
|
+
};
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
const refs = new Set;
|
|
10512
|
+
for (const c of sources) {
|
|
10513
|
+
if (!c)
|
|
10514
|
+
continue;
|
|
10515
|
+
for (const l of c.languages ?? [])
|
|
10516
|
+
if (l.configuration)
|
|
10517
|
+
refs.add(l.configuration);
|
|
10518
|
+
for (const g of c.grammars ?? [])
|
|
10519
|
+
if (g.path)
|
|
10520
|
+
refs.add(g.path);
|
|
10521
|
+
for (const s of c.snippets ?? [])
|
|
10522
|
+
if (s.path)
|
|
10523
|
+
refs.add(s.path);
|
|
10524
|
+
for (const t of c.iconThemes ?? [])
|
|
10525
|
+
if (t.path)
|
|
10526
|
+
refs.add(t.path);
|
|
10527
|
+
}
|
|
10528
|
+
if (refs.size === 0)
|
|
10529
|
+
return null;
|
|
10530
|
+
const missing = [...refs].filter((rel) => !fs12.existsSync(path14.join(root, rel)));
|
|
10531
|
+
if (missing.length === 0) {
|
|
10532
|
+
return { id: "language", level: "ok", message: `language ${refs.size} asset(s) present` };
|
|
10533
|
+
}
|
|
10534
|
+
return {
|
|
10535
|
+
id: "language",
|
|
10536
|
+
level: "error",
|
|
10537
|
+
message: `language: ${missing.length} referenced asset(s) missing`,
|
|
10538
|
+
details: missing
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
9854
10541
|
function checkGitignore(root) {
|
|
9855
10542
|
const file = path14.join(root, ".gitignore");
|
|
9856
10543
|
const required = ["dist", "node_modules"];
|
|
@@ -12036,7 +12723,7 @@ var init_add18 = __esm(() => {
|
|
|
12036
12723
|
path33 = __toESM(require("path"));
|
|
12037
12724
|
addHelperCommand = {
|
|
12038
12725
|
name: "add",
|
|
12039
|
-
description: "Generate a typed helper (secrets, config, state, notifications) into src/helpers/",
|
|
12726
|
+
description: "Generate a typed helper (secrets, config, state, notifications, cache, colorize) into src/helpers/",
|
|
12040
12727
|
params: [
|
|
12041
12728
|
{
|
|
12042
12729
|
name: "kind",
|
|
@@ -12082,6 +12769,15 @@ var init_add18 = __esm(() => {
|
|
|
12082
12769
|
import { createCache } from '../helpers/cache';
|
|
12083
12770
|
const cache = createCache<User>({ ttlMs: 60_000, max: 200 });
|
|
12084
12771
|
const u = await cache.wrap('user:' + id, () => orm(User).findById(id));
|
|
12772
|
+
`);
|
|
12773
|
+
} else if (args.kind === "colorize") {
|
|
12774
|
+
console.log(`
|
|
12775
|
+
Usage (auto-apply scoped token colors on activate):
|
|
12776
|
+
import { applyTokenColors } from '../helpers/colorize';
|
|
12777
|
+
await applyTokenColors('source.mylang', [
|
|
12778
|
+
{ scope: 'entity.name.section.mylang', settings: { foreground: '#e6c07b', fontStyle: 'bold' } },
|
|
12779
|
+
]);
|
|
12780
|
+
// add a "<prefix>.colorize" boolean to contributes.configuration to opt out
|
|
12085
12781
|
`);
|
|
12086
12782
|
} else {
|
|
12087
12783
|
console.log("");
|
|
@@ -12389,9 +13085,20 @@ var init_init4 = __esm(() => {
|
|
|
12389
13085
|
});
|
|
12390
13086
|
|
|
12391
13087
|
// src/commands/model/add.ts
|
|
12392
|
-
|
|
13088
|
+
function existingModels(projectRoot) {
|
|
13089
|
+
const dir = path38.join(projectRoot, "src", "models");
|
|
13090
|
+
if (!fs25.existsSync(dir))
|
|
13091
|
+
return [];
|
|
13092
|
+
return fs25.readdirSync(dir).filter((f) => /\.ts$/.test(f) && !f.endsWith(".crud.ts")).map((f) => f.replace(/\.ts$/, "")).sort();
|
|
13093
|
+
}
|
|
13094
|
+
async function promptFieldsLoop(projectRoot) {
|
|
12393
13095
|
console.log("\n Field syntax: `name:type` — flags: `!` (primary) `@` (indexed) `?` after name (optional)");
|
|
12394
13096
|
console.log(" Examples: `id:string!`, `email?:string@`, `score:number`");
|
|
13097
|
+
console.log(" Relation: `category:ref(Category)` — FK + dropdown of the related model.");
|
|
13098
|
+
const models = existingModels(projectRoot);
|
|
13099
|
+
if (models.length) {
|
|
13100
|
+
console.log(` Models you can relate to: ${models.join(", ")}`);
|
|
13101
|
+
}
|
|
12395
13102
|
console.log(` Empty line finishes.
|
|
12396
13103
|
`);
|
|
12397
13104
|
const fields = [];
|
|
@@ -12414,24 +13121,26 @@ function pascal4(s) {
|
|
|
12414
13121
|
function plural(s) {
|
|
12415
13122
|
return `${pascal4(s)}s`;
|
|
12416
13123
|
}
|
|
12417
|
-
var import_cli_maker17, path38, FIELD_HELP, addModelCommand, add_default10;
|
|
13124
|
+
var import_cli_maker17, fs25, path38, FIELD_HELP, addModelCommand, add_default10;
|
|
12418
13125
|
var init_add21 = __esm(() => {
|
|
12419
13126
|
init_add5();
|
|
12420
13127
|
init_init();
|
|
12421
13128
|
init_findProject();
|
|
12422
13129
|
import_cli_maker17 = __toESM(require_dist(), 1);
|
|
13130
|
+
fs25 = __toESM(require("fs"));
|
|
12423
13131
|
path38 = __toESM(require("path"));
|
|
12424
13132
|
FIELD_HELP = [
|
|
12425
13133
|
"",
|
|
12426
13134
|
"Interactive field loop: enter `name:type` per line, empty line to finish.",
|
|
12427
13135
|
" Examples:",
|
|
12428
|
-
" id:string!
|
|
12429
|
-
" name:string
|
|
12430
|
-
" email?:string
|
|
12431
|
-
" createdAt:number
|
|
12432
|
-
' role:"a"|"b"
|
|
12433
|
-
" tag:string@
|
|
12434
|
-
" score:number!@
|
|
13136
|
+
" id:string! — `!` after type = primary key",
|
|
13137
|
+
" name:string — required field",
|
|
13138
|
+
" email?:string — `?` after name = optional",
|
|
13139
|
+
" createdAt:number — number type",
|
|
13140
|
+
' role:"a"|"b" — literal union',
|
|
13141
|
+
" tag:string@ — `@` after type = indexed",
|
|
13142
|
+
" score:number!@ — primary key + indexed",
|
|
13143
|
+
" category:ref(Category) — relation → FK categoryId + dropdown of Category",
|
|
12435
13144
|
"",
|
|
12436
13145
|
"If no `!` is set, `id` (or first field) becomes the primary key."
|
|
12437
13146
|
].join(`
|
|
@@ -12471,7 +13180,7 @@ var init_add21 = __esm(() => {
|
|
|
12471
13180
|
if (args.fields) {
|
|
12472
13181
|
fields = parseFieldsSpec(String(args.fields));
|
|
12473
13182
|
} else {
|
|
12474
|
-
fields = await promptFieldsLoop();
|
|
13183
|
+
fields = await promptFieldsLoop(projectRoot);
|
|
12475
13184
|
}
|
|
12476
13185
|
if (fields.length === 0) {
|
|
12477
13186
|
throw new Error("At least one field is required.");
|
|
@@ -12490,6 +13199,12 @@ var init_add21 = __esm(() => {
|
|
|
12490
13199
|
`);
|
|
12491
13200
|
for (const f of result.created)
|
|
12492
13201
|
console.log(` + ${rel(f)}`);
|
|
13202
|
+
if (result.relations.length) {
|
|
13203
|
+
console.log("");
|
|
13204
|
+
for (const r of result.relations) {
|
|
13205
|
+
console.log(` ↪ ${r.field} → ${r.model}${r.label ? ` (label: ${r.label})` : ""} — crud will render a dropdown`);
|
|
13206
|
+
}
|
|
13207
|
+
}
|
|
12493
13208
|
console.log(`
|
|
12494
13209
|
Usage:
|
|
12495
13210
|
import { ${plural(args.name)}Repo } from '../models/${pascal4(args.name)}';
|
|
@@ -12508,9 +13223,9 @@ var init_add21 = __esm(() => {
|
|
|
12508
13223
|
|
|
12509
13224
|
// src/lib/crud/parseModel.ts
|
|
12510
13225
|
function parseModelFile(file) {
|
|
12511
|
-
if (!
|
|
13226
|
+
if (!fs26.existsSync(file))
|
|
12512
13227
|
throw new Error(`Model file not found: ${file}`);
|
|
12513
|
-
const src =
|
|
13228
|
+
const src = fs26.readFileSync(file, "utf8");
|
|
12514
13229
|
const ifaceMatch = /export\s+interface\s+([A-Z][A-Za-z0-9_]*)\s*\{([\s\S]*?)\n\}/.exec(src);
|
|
12515
13230
|
if (!ifaceMatch) {
|
|
12516
13231
|
throw new Error(`Model "${path39.basename(file)}" does not declare \`export interface\`.`);
|
|
@@ -12541,7 +13256,26 @@ function parseModelFile(file) {
|
|
|
12541
13256
|
indexes.push(m[1]);
|
|
12542
13257
|
}
|
|
12543
13258
|
const id = path39.basename(file).replace(/\.(ts|tsx)$/, "");
|
|
12544
|
-
|
|
13259
|
+
const relations = parseRelations(src, name);
|
|
13260
|
+
for (const f of fields) {
|
|
13261
|
+
const r = relations[f.name];
|
|
13262
|
+
if (r)
|
|
13263
|
+
f.relation = r;
|
|
13264
|
+
}
|
|
13265
|
+
return { name, id, plural: plural2, collection, primaryKey: pk, indexes, fields, relations, path: file };
|
|
13266
|
+
}
|
|
13267
|
+
function parseRelations(src, name) {
|
|
13268
|
+
const out = {};
|
|
13269
|
+
const block = new RegExp(`export\\s+const\\s+${name}Relations\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*as\\s+const`, "m").exec(src);
|
|
13270
|
+
if (!block)
|
|
13271
|
+
return out;
|
|
13272
|
+
const body = block[1];
|
|
13273
|
+
const re = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*\{\s*model\s*:\s*['"`]([^'"`]+)['"`]\s*(?:,\s*label\s*:\s*['"`]([^'"`]+)['"`]\s*)?\}/g;
|
|
13274
|
+
let m;
|
|
13275
|
+
while (m = re.exec(body)) {
|
|
13276
|
+
out[m[1]] = { field: m[1], model: m[2], label: m[3] };
|
|
13277
|
+
}
|
|
13278
|
+
return out;
|
|
12545
13279
|
}
|
|
12546
13280
|
function parseInterfaceBody(body) {
|
|
12547
13281
|
const fields = [];
|
|
@@ -12585,19 +13319,19 @@ function inferInputSpec(type) {
|
|
|
12585
13319
|
return { kind: "text" };
|
|
12586
13320
|
return { kind: "text" };
|
|
12587
13321
|
}
|
|
12588
|
-
var
|
|
13322
|
+
var fs26, path39;
|
|
12589
13323
|
var init_parseModel = __esm(() => {
|
|
12590
|
-
|
|
13324
|
+
fs26 = __toESM(require("fs"));
|
|
12591
13325
|
path39 = __toESM(require("path"));
|
|
12592
13326
|
});
|
|
12593
13327
|
|
|
12594
13328
|
// src/lib/crud/crudConfig.ts
|
|
12595
13329
|
function readCrudConfig(projectRoot, modelName) {
|
|
12596
13330
|
const file = path40.join(projectRoot, "src", "models", `${modelName}.crud.ts`);
|
|
12597
|
-
if (!
|
|
13331
|
+
if (!fs27.existsSync(file))
|
|
12598
13332
|
return {};
|
|
12599
13333
|
try {
|
|
12600
|
-
const src =
|
|
13334
|
+
const src = fs27.readFileSync(file, "utf8");
|
|
12601
13335
|
return parseConfigSource(src);
|
|
12602
13336
|
} catch {
|
|
12603
13337
|
return {};
|
|
@@ -12632,9 +13366,9 @@ function parseConfigSource(src) {
|
|
|
12632
13366
|
return {};
|
|
12633
13367
|
}
|
|
12634
13368
|
}
|
|
12635
|
-
var
|
|
13369
|
+
var fs27, path40;
|
|
12636
13370
|
var init_crudConfig = __esm(() => {
|
|
12637
|
-
|
|
13371
|
+
fs27 = __toESM(require("fs"));
|
|
12638
13372
|
path40 = __toESM(require("path"));
|
|
12639
13373
|
});
|
|
12640
13374
|
|
|
@@ -12665,7 +13399,7 @@ function addCrud(opts) {
|
|
|
12665
13399
|
assertNoOverwrite(opts.projectRoot, formPanelPath, "Form panel");
|
|
12666
13400
|
assertNoOverwrite(opts.projectRoot, listWebDir, "List webview dir");
|
|
12667
13401
|
assertNoOverwrite(opts.projectRoot, formWebDir, "Form webview dir");
|
|
12668
|
-
const pkg = JSON.parse(
|
|
13402
|
+
const pkg = JSON.parse(fs28.readFileSync(path41.join(opts.projectRoot, "package.json"), "utf8"));
|
|
12669
13403
|
const prefix = pkg.vsceasy?.commandPrefix ?? pkg.name?.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9]+/g, "") ?? "ext";
|
|
12670
13404
|
const baseVars = {
|
|
12671
13405
|
Name: model.name,
|
|
@@ -12678,13 +13412,27 @@ function addCrud(opts) {
|
|
|
12678
13412
|
formId,
|
|
12679
13413
|
prefix
|
|
12680
13414
|
};
|
|
13415
|
+
const modelsDir = path41.dirname(modelFile);
|
|
13416
|
+
const relations = visible.filter((f) => f.relation).map((f) => {
|
|
13417
|
+
const r = f.relation;
|
|
13418
|
+
const related = parseModelFile(path41.join(modelsDir, `${r.model}.ts`));
|
|
13419
|
+
const labelField = r.label ?? firstStringField(related) ?? related.primaryKey;
|
|
13420
|
+
return {
|
|
13421
|
+
field: f.name,
|
|
13422
|
+
model: r.model,
|
|
13423
|
+
plural: related.plural,
|
|
13424
|
+
pk: related.primaryKey,
|
|
13425
|
+
labelField
|
|
13426
|
+
};
|
|
13427
|
+
});
|
|
13428
|
+
const relVars = buildRelationVars(relations);
|
|
12681
13429
|
const created = [];
|
|
12682
13430
|
const modified = [];
|
|
12683
13431
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "service.ts.tpl"), servicePath, baseVars, created);
|
|
12684
13432
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "formNav.ts.tpl"), path41.join(opts.projectRoot, "src", "services", `${camelLower(model.name)}FormNav.ts`), baseVars, created);
|
|
12685
13433
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "listPanel.ts.tpl"), listPanelPath, baseVars, created);
|
|
12686
|
-
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "formPanel.ts.tpl"), formPanelPath, baseVars, created);
|
|
12687
|
-
|
|
13434
|
+
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "formPanel.ts.tpl"), formPanelPath, { ...baseVars, ...relVars }, created);
|
|
13435
|
+
fs28.mkdirSync(listWebDir, { recursive: true });
|
|
12688
13436
|
const listVars = {
|
|
12689
13437
|
...baseVars,
|
|
12690
13438
|
listHeaderCells: visible.map((f) => ` <th style={{ padding: '6px 8px' }}>${escapeJsx(label(f, cfg))}</th>`).join(`
|
|
@@ -12695,14 +13443,14 @@ function addCrud(opts) {
|
|
|
12695
13443
|
};
|
|
12696
13444
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "listApp.tsx.tpl"), path41.join(listWebDir, "App.tsx"), listVars, created);
|
|
12697
13445
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "main.tsx.tpl"), path41.join(listWebDir, "main.tsx"), baseVars, created);
|
|
12698
|
-
|
|
13446
|
+
fs28.mkdirSync(formWebDir, { recursive: true });
|
|
12699
13447
|
const formInputs = visible.filter((f) => !cfg.fields?.[f.name]?.hideInForm).map((f) => renderInput(f, cfg.fields?.[f.name])).join(`
|
|
12700
13448
|
`);
|
|
12701
13449
|
const emptyLit = buildEmptyFormLiteral(visible, cfg);
|
|
12702
|
-
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "formApp.tsx.tpl"), path41.join(formWebDir, "App.tsx"), { ...baseVars, formFieldInputs: formInputs, emptyFormLiteral: emptyLit }, created);
|
|
13450
|
+
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "formApp.tsx.tpl"), path41.join(formWebDir, "App.tsx"), { ...baseVars, ...relVars, formFieldInputs: formInputs, emptyFormLiteral: emptyLit }, created);
|
|
12703
13451
|
writeFromTpl(path41.join(opts.templatesRoot, "_generators", "crud", "main.tsx.tpl"), path41.join(formWebDir, "main.tsx"), baseVars, created);
|
|
12704
13452
|
appendApi3(apiPath, listApiName, model, created, modified);
|
|
12705
|
-
appendApiForm(apiPath, formApiName, model, created, modified);
|
|
13453
|
+
appendApiForm(apiPath, formApiName, model, created, modified, relations.length > 0);
|
|
12706
13454
|
let menuInfo;
|
|
12707
13455
|
if (opts.menu && opts.menu !== "none") {
|
|
12708
13456
|
menuInfo = wireMenu(opts, model, cfg, listId, formId);
|
|
@@ -12713,10 +13461,10 @@ function addCrud(opts) {
|
|
|
12713
13461
|
return { created, modified, menu: menuInfo, genRan };
|
|
12714
13462
|
}
|
|
12715
13463
|
function writeFromTpl(tpl, target, vars, created) {
|
|
12716
|
-
if (!
|
|
13464
|
+
if (!fs28.existsSync(tpl))
|
|
12717
13465
|
throw new Error(`CRUD template missing: ${tpl}`);
|
|
12718
|
-
|
|
12719
|
-
|
|
13466
|
+
fs28.mkdirSync(path41.dirname(target), { recursive: true });
|
|
13467
|
+
fs28.writeFileSync(target, substitute(fs28.readFileSync(tpl, "utf8"), vars));
|
|
12720
13468
|
created.push(target);
|
|
12721
13469
|
}
|
|
12722
13470
|
function orderFields(fields, order) {
|
|
@@ -12756,6 +13504,14 @@ function renderInput(field, override) {
|
|
|
12756
13504
|
` + ` <span style={{ opacity: 0.8 }}>${labelText}</span>
|
|
12757
13505
|
` + `${input}
|
|
12758
13506
|
` + ` </label>`;
|
|
13507
|
+
if (field.relation) {
|
|
13508
|
+
return wrap(` <select${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)}>
|
|
13509
|
+
` + ` <option value=""></option>
|
|
13510
|
+
` + ` {(relOptions['${name}'] ?? []).map((o) => (
|
|
13511
|
+
` + ` <option key={o.value} value={o.value}>{o.label}</option>
|
|
13512
|
+
` + ` ))}
|
|
13513
|
+
` + ` </select>`);
|
|
13514
|
+
}
|
|
12759
13515
|
switch (spec.kind) {
|
|
12760
13516
|
case "number":
|
|
12761
13517
|
return wrap(` <input type="number"${placeholderAttr}${required} value={form.${name} as any ?? ''} onChange={(e) => onChange('${name}', e.target.value === '' ? undefined : Number(e.target.value))} />`);
|
|
@@ -12806,45 +13562,47 @@ export interface ${apiName} {
|
|
|
12806
13562
|
ensureImport(apiPath, model.name);
|
|
12807
13563
|
appendIfMissing(apiPath, apiName, sig, created, modified);
|
|
12808
13564
|
}
|
|
12809
|
-
function appendApiForm(apiPath, apiName, model, created, modified) {
|
|
13565
|
+
function appendApiForm(apiPath, apiName, model, created, modified, hasRelations) {
|
|
13566
|
+
const optionsLine = hasRelations ? ` options(): Promise<Record<string, { value: string; label: string }[]>>;
|
|
13567
|
+
` : "";
|
|
12810
13568
|
const sig = `
|
|
12811
13569
|
export interface ${apiName} {
|
|
12812
13570
|
` + ` pendingId(): Promise<${model.name}['${model.primaryKey}'] | null>;
|
|
12813
13571
|
` + ` get(id: ${model.name}['${model.primaryKey}'] | null): Promise<${model.name} | null>;
|
|
12814
13572
|
` + ` save(row: ${model.name}): Promise<${model.name}>;
|
|
12815
|
-
` + ` cancel(): Promise<void>;
|
|
13573
|
+
` + optionsLine + ` cancel(): Promise<void>;
|
|
12816
13574
|
` + `}
|
|
12817
13575
|
`;
|
|
12818
13576
|
ensureImport(apiPath, model.name);
|
|
12819
13577
|
appendIfMissing(apiPath, apiName, sig, created, modified);
|
|
12820
13578
|
}
|
|
12821
13579
|
function ensureImport(apiPath, modelName) {
|
|
12822
|
-
if (!
|
|
12823
|
-
|
|
12824
|
-
|
|
13580
|
+
if (!fs28.existsSync(apiPath)) {
|
|
13581
|
+
fs28.mkdirSync(path41.dirname(apiPath), { recursive: true });
|
|
13582
|
+
fs28.writeFileSync(apiPath, `import type { ${modelName} } from '../models/${modelName}';
|
|
12825
13583
|
`);
|
|
12826
13584
|
return;
|
|
12827
13585
|
}
|
|
12828
|
-
let src =
|
|
13586
|
+
let src = fs28.readFileSync(apiPath, "utf8");
|
|
12829
13587
|
if (new RegExp(`from\\s+['"]\\.\\./models/${modelName}['"]`).test(src))
|
|
12830
13588
|
return;
|
|
12831
13589
|
src = `import type { ${modelName} } from '../models/${modelName}';
|
|
12832
13590
|
` + src;
|
|
12833
|
-
|
|
13591
|
+
fs28.writeFileSync(apiPath, src);
|
|
12834
13592
|
}
|
|
12835
13593
|
function appendIfMissing(apiPath, apiName, block, created, modified) {
|
|
12836
|
-
if (!
|
|
12837
|
-
|
|
13594
|
+
if (!fs28.existsSync(apiPath)) {
|
|
13595
|
+
fs28.writeFileSync(apiPath, block.trimStart());
|
|
12838
13596
|
created.push(apiPath);
|
|
12839
13597
|
return;
|
|
12840
13598
|
}
|
|
12841
|
-
const src =
|
|
13599
|
+
const src = fs28.readFileSync(apiPath, "utf8");
|
|
12842
13600
|
if (new RegExp(`\\bexport\\s+interface\\s+${apiName}\\b`).test(src))
|
|
12843
13601
|
return;
|
|
12844
13602
|
const sep = src.endsWith(`
|
|
12845
13603
|
`) ? "" : `
|
|
12846
13604
|
`;
|
|
12847
|
-
|
|
13605
|
+
fs28.writeFileSync(apiPath, src + sep + block);
|
|
12848
13606
|
if (!modified.includes(apiPath))
|
|
12849
13607
|
modified.push(apiPath);
|
|
12850
13608
|
}
|
|
@@ -12900,7 +13658,31 @@ function runGen11(cwd) {
|
|
|
12900
13658
|
function which12(cmd) {
|
|
12901
13659
|
return import_child_process13.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
|
|
12902
13660
|
}
|
|
12903
|
-
|
|
13661
|
+
function firstStringField(model) {
|
|
13662
|
+
return model.fields.find((f) => f.type.trim() === "string" && f.name !== model.primaryKey)?.name;
|
|
13663
|
+
}
|
|
13664
|
+
function buildRelationVars(relations) {
|
|
13665
|
+
if (relations.length === 0) {
|
|
13666
|
+
return { relationImports: "", relationOptionsHandler: "", relationOptionsState: "", relationOptionsLoad: "" };
|
|
13667
|
+
}
|
|
13668
|
+
const uniqueRepos = [...new Map(relations.map((r) => [r.plural, r])).values()];
|
|
13669
|
+
const relationImports = uniqueRepos.map((r) => `import { ${r.plural}Repo } from '../models/${r.model}';`).join(`
|
|
13670
|
+
`) + `
|
|
13671
|
+
`;
|
|
13672
|
+
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(`
|
|
13673
|
+
`);
|
|
13674
|
+
const relationOptionsHandler = ` async options() {
|
|
13675
|
+
` + ` return {
|
|
13676
|
+
` + `${handlerLines}
|
|
13677
|
+
` + ` };
|
|
13678
|
+
` + ` },
|
|
13679
|
+
`;
|
|
13680
|
+
const relationOptionsState = ` const [relOptions, setRelOptions] = useState<Record<string, { value: string; label: string }[]>>({});`;
|
|
13681
|
+
const relationOptionsLoad = `
|
|
13682
|
+
useEffect(() => { void api.options().then(setRelOptions); }, []);`;
|
|
13683
|
+
return { relationImports, relationOptionsHandler, relationOptionsState, relationOptionsLoad };
|
|
13684
|
+
}
|
|
13685
|
+
var fs28, path41, import_child_process13;
|
|
12904
13686
|
var init_add22 = __esm(() => {
|
|
12905
13687
|
init_scaffold();
|
|
12906
13688
|
init_validate();
|
|
@@ -12908,20 +13690,20 @@ var init_add22 = __esm(() => {
|
|
|
12908
13690
|
init_crudConfig();
|
|
12909
13691
|
init_add7();
|
|
12910
13692
|
init_edit();
|
|
12911
|
-
|
|
13693
|
+
fs28 = __toESM(require("fs"));
|
|
12912
13694
|
path41 = __toESM(require("path"));
|
|
12913
13695
|
import_child_process13 = require("child_process");
|
|
12914
13696
|
});
|
|
12915
13697
|
|
|
12916
13698
|
// src/commands/crud/add.ts
|
|
12917
|
-
var import_cli_maker18, path42,
|
|
13699
|
+
var import_cli_maker18, path42, fs29, NONE_SENTINEL2 = "(no menu)", NEW_SENTINEL = "(create new menu)", addCrudCommand, add_default11;
|
|
12918
13700
|
var init_add23 = __esm(() => {
|
|
12919
13701
|
init_add22();
|
|
12920
13702
|
init_edit();
|
|
12921
13703
|
init_findProject();
|
|
12922
13704
|
import_cli_maker18 = __toESM(require_dist(), 1);
|
|
12923
13705
|
path42 = __toESM(require("path"));
|
|
12924
|
-
|
|
13706
|
+
fs29 = __toESM(require("fs"));
|
|
12925
13707
|
addCrudCommand = {
|
|
12926
13708
|
name: "add",
|
|
12927
13709
|
description: "Generate full CRUD (service + list panel + form panel + RPC + optional menu) for an existing model. Rails-style scaffolding.",
|
|
@@ -12933,10 +13715,10 @@ var init_add23 = __esm(() => {
|
|
|
12933
13715
|
type: import_cli_maker18.ParamType.List,
|
|
12934
13716
|
optionsLoader: () => {
|
|
12935
13717
|
const dir = path42.join(findProjectRoot(), "src", "models");
|
|
12936
|
-
if (!
|
|
13718
|
+
if (!fs29.existsSync(dir)) {
|
|
12937
13719
|
throw new Error("No models found. Run `vsceasy db init && vsceasy model add --name User` first.");
|
|
12938
13720
|
}
|
|
12939
|
-
const names =
|
|
13721
|
+
const names = fs29.readdirSync(dir).filter((f) => f.endsWith(".ts") && !f.endsWith(".crud.ts")).map((f) => f.replace(/\.ts$/, "")).sort();
|
|
12940
13722
|
if (names.length === 0) {
|
|
12941
13723
|
throw new Error("No models found in src/models/. Run `vsceasy model add --name User` first.");
|
|
12942
13724
|
}
|
|
@@ -13066,14 +13848,14 @@ function addStore(opts) {
|
|
|
13066
13848
|
const storeTs = path44.join(opts.projectRoot, "src", "stores", `${name}.ts`);
|
|
13067
13849
|
assertNoOverwrite(opts.projectRoot, storeTs, "Store");
|
|
13068
13850
|
const tplPath = path44.join(opts.templatesRoot, "_generators", "store", "store.ts.tpl");
|
|
13069
|
-
const body = substitute(
|
|
13851
|
+
const body = substitute(fs30.readFileSync(tplPath, "utf8"), {
|
|
13070
13852
|
name,
|
|
13071
13853
|
type: TS_TYPE[type],
|
|
13072
13854
|
initial,
|
|
13073
13855
|
example: EXAMPLE[type]
|
|
13074
13856
|
});
|
|
13075
|
-
|
|
13076
|
-
|
|
13857
|
+
fs30.mkdirSync(path44.dirname(storeTs), { recursive: true });
|
|
13858
|
+
fs30.writeFileSync(storeTs, body);
|
|
13077
13859
|
return { created: [storeTs] };
|
|
13078
13860
|
}
|
|
13079
13861
|
function normalizeCamel9(s) {
|
|
@@ -13082,11 +13864,11 @@ function normalizeCamel9(s) {
|
|
|
13082
13864
|
return "";
|
|
13083
13865
|
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
13084
13866
|
}
|
|
13085
|
-
var
|
|
13867
|
+
var fs30, path44, DEFAULT_INITIAL, TS_TYPE, EXAMPLE;
|
|
13086
13868
|
var init_add25 = __esm(() => {
|
|
13087
13869
|
init_scaffold();
|
|
13088
13870
|
init_validate();
|
|
13089
|
-
|
|
13871
|
+
fs30 = __toESM(require("fs"));
|
|
13090
13872
|
path44 = __toESM(require("path"));
|
|
13091
13873
|
DEFAULT_INITIAL = {
|
|
13092
13874
|
number: "0",
|
|
@@ -13242,7 +14024,7 @@ var init_cli = __esm(() => {
|
|
|
13242
14024
|
import_cli_maker21 = __toESM(require_dist(), 1);
|
|
13243
14025
|
cli = new import_cli_maker21.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
|
|
13244
14026
|
interactive: true,
|
|
13245
|
-
version: "0.1.
|
|
14027
|
+
version: "0.1.10",
|
|
13246
14028
|
introAnimation: {
|
|
13247
14029
|
enabled: true,
|
|
13248
14030
|
preset: "retro-space",
|