@vsceasy/cli 0.1.9 → 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/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
- applyPreset(opts.targetDir, opts.preset ?? "full");
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
- ui: opts.ui
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.9", TEMPLATE_FILES;
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({
@@ -4436,6 +4850,92 @@ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
4436
4850
 
4437
4851
  return cache;
4438
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
+ }
4439
4939
  `,
4440
4940
  "_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
4441
4941
 
@@ -5754,6 +6254,12 @@ bun run package # → {{name}}-0.0.1.vsix
5754
6254
  "react/scripts/gen.ts": `#!/usr/bin/env bun
5755
6255
  // Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
5756
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.
5757
6263
 
5758
6264
  import * as fs from 'fs';
5759
6265
  import * as path from 'path';
@@ -5769,6 +6275,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
5769
6275
  const JOBS_DIR = path.join(SRC, 'jobs');
5770
6276
  const OUT = path.join(SRC, 'extension', '_registry.ts');
5771
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']);
5772
6282
 
5773
6283
  interface Discovered {
5774
6284
  id: string;
@@ -5957,9 +6467,54 @@ function syncPackageJson(
5957
6467
  delete contributes.views;
5958
6468
  }
5959
6469
 
6470
+ mergeExtraContributes(contributes);
6471
+
5960
6472
  fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
5961
6473
  }
5962
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
+
5963
6518
  function loadDef(file: string): {
5964
6519
  id?: string;
5965
6520
  title?: string;
@@ -8116,6 +8671,12 @@ var init_interactive = __esm(() => {
8116
8671
  function toTitle(s) {
8117
8672
  return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
8118
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
+ }
8119
8680
  function toBool(v) {
8120
8681
  if (v === undefined || v === null || v === "")
8121
8682
  return;
@@ -8155,7 +8716,7 @@ Installing dependencies with ${pm}...
8155
8716
  ! ${pm} install failed — run it manually`);
8156
8717
  return false;
8157
8718
  }
8158
- 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;
8159
8720
  var init_create = __esm(() => {
8160
8721
  init_scaffold();
8161
8722
  init_findProject();
@@ -8163,6 +8724,7 @@ var init_create = __esm(() => {
8163
8724
  import_cli_maker = __toESM(require_dist(), 1);
8164
8725
  path4 = __toESM(require("path"));
8165
8726
  import_child_process = require("child_process");
8727
+ EXTENSION_TYPES = ["ui", "language", "empty"];
8166
8728
  createCommand = {
8167
8729
  name: "create",
8168
8730
  description: "Scaffold a new VS Code extension project",
@@ -8171,8 +8733,9 @@ var init_create = __esm(() => {
8171
8733
  { name: "displayName", description: "Human-readable extension name", required: false, type: import_cli_maker.ParamType.Text },
8172
8734
  { name: "description", description: "Short description", required: false, type: import_cli_maker.ParamType.Text },
8173
8735
  { name: "publisher", description: "VS Code publisher id", required: false, type: import_cli_maker.ParamType.Text },
8174
- { name: "ui", description: "UI framework", required: false, type: import_cli_maker.ParamType.List, options: ["react"] },
8175
- { name: "preset", description: "Project preset (minimal = empty extension, full = panel + RPC sample)", required: false, type: import_cli_maker.ParamType.List, options: ["minimal", "full"] },
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"] },
8176
8739
  { name: "dir", description: "Target directory (defaults to ./<name>)", required: false, type: import_cli_maker.ParamType.Text },
8177
8740
  { name: "git", description: "Initialize a git repository (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean },
8178
8741
  { name: "install", description: "Install dependencies after scaffolding (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean }
@@ -8180,6 +8743,15 @@ var init_create = __esm(() => {
8180
8743
  action: async (args) => {
8181
8744
  const name = args.name;
8182
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
+ }
8183
8755
  const ui = args.ui ?? "react";
8184
8756
  const preset = args.preset ?? "full";
8185
8757
  const targetDir = path4.resolve(process.cwd(), args.dir ?? simpleName);
@@ -8190,18 +8762,18 @@ var init_create = __esm(() => {
8190
8762
  description: args.description ?? `${simpleName} VS Code extension`,
8191
8763
  publisher: args.publisher ?? "your-publisher",
8192
8764
  ui,
8765
+ type,
8193
8766
  preset,
8194
8767
  targetDir,
8195
8768
  templatesRoot: findTemplatesRoot()
8196
8769
  });
8197
8770
  const rel = path4.relative(process.cwd(), targetDir) || ".";
8198
8771
  console.log(`
8199
- ✓ Created ${name} at ${rel}
8772
+ ✓ Created ${name} (${type}) at ${rel}
8200
8773
  `);
8201
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
8202
8774
  const gitFlag = toBool(args.git);
8203
8775
  const installFlag = toBool(args.install);
8204
- const wantGit = gitFlag ?? (interactive ? await confirm("Initialize a git repository?", true) : false);
8776
+ const wantGit = gitFlag ?? (interactiveTty ? await confirm("Initialize a git repository?", true) : false);
8205
8777
  if (wantGit) {
8206
8778
  if (which("git"))
8207
8779
  initGit(targetDir);
@@ -8209,7 +8781,7 @@ var init_create = __esm(() => {
8209
8781
  console.warn("! git not found — skipping repository init");
8210
8782
  }
8211
8783
  let pm = null;
8212
- const wantInstall = installFlag ?? (interactive ? await confirm("Install dependencies?", true) : false);
8784
+ const wantInstall = installFlag ?? (interactiveTty ? await confirm("Install dependencies?", true) : false);
8213
8785
  let installed = false;
8214
8786
  if (wantInstall) {
8215
8787
  pm = which("bun") ? "bun" : which("npm") ? "npm" : null;
@@ -8224,9 +8796,16 @@ Next steps:`);
8224
8796
  console.log(` cd ${rel}`);
8225
8797
  if (!installed)
8226
8798
  console.log(` ${run} install`);
8227
- console.log(` ${run} run launch # builds + opens Extension Development Host`);
8228
- console.log(` # or \`${run} run dev\` + F5 inside VS Code for watch mode
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
8229
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
8807
+ `);
8808
+ }
8230
8809
  } catch (err) {
8231
8810
  console.error(`
8232
8811
  ✗ Failed to scaffold: ${err.message}
@@ -8813,7 +9392,7 @@ var init_add4 = __esm(() => {
8813
9392
  init_config();
8814
9393
  fs9 = __toESM(require("fs"));
8815
9394
  path10 = __toESM(require("path"));
8816
- HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
9395
+ HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache", "colorize"];
8817
9396
  });
8818
9397
 
8819
9398
  // src/lib/db/init.ts
@@ -9650,6 +10229,9 @@ function runDoctor(opts) {
9650
10229
  results.push(...checkStatusBars(root));
9651
10230
  results.push(...checkSubpanels(root));
9652
10231
  results.push(checkContributesSync(root, pkg));
10232
+ const langCheck = checkLanguageAssets(root, pkg);
10233
+ if (langCheck)
10234
+ results.push(langCheck);
9653
10235
  results.push(checkActivationEvents(pkg));
9654
10236
  results.push(checkMarketplaceIcon(root, pkg));
9655
10237
  results.push(checkGenScript(root));
@@ -9912,6 +10494,50 @@ function checkContributesSync(root, pkg) {
9912
10494
  details: stale
9913
10495
  };
9914
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
+ }
9915
10541
  function checkGitignore(root) {
9916
10542
  const file = path14.join(root, ".gitignore");
9917
10543
  const required = ["dist", "node_modules"];
@@ -12097,7 +12723,7 @@ var init_add18 = __esm(() => {
12097
12723
  path33 = __toESM(require("path"));
12098
12724
  addHelperCommand = {
12099
12725
  name: "add",
12100
- 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/",
12101
12727
  params: [
12102
12728
  {
12103
12729
  name: "kind",
@@ -12143,6 +12769,15 @@ var init_add18 = __esm(() => {
12143
12769
  import { createCache } from '../helpers/cache';
12144
12770
  const cache = createCache<User>({ ttlMs: 60_000, max: 200 });
12145
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
12146
12781
  `);
12147
12782
  } else {
12148
12783
  console.log("");
@@ -13389,7 +14024,7 @@ var init_cli = __esm(() => {
13389
14024
  import_cli_maker21 = __toESM(require_dist(), 1);
13390
14025
  cli = new import_cli_maker21.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
13391
14026
  interactive: true,
13392
- version: "0.1.9",
14027
+ version: "0.1.10",
13393
14028
  introAnimation: {
13394
14029
  enabled: true,
13395
14030
  preset: "retro-space",