@vsceasy/cli 0.1.6 → 0.1.8
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 +11 -0
- package/README.md +8 -2
- package/dist/bin/cli.js +382 -32
- package/dist/commands/groups.d.ts +1 -0
- package/dist/commands/store/add.d.ts +3 -0
- package/dist/index.js +244 -24
- package/dist/lib/store/add.d.ts +14 -0
- package/dist/lib/templatesData.d.ts +1 -1
- package/package.json +1 -1
- package/templates/_generators/crud/formApp.tsx.tpl +25 -10
- package/templates/_generators/helper/db.ts.tpl +35 -1
- package/templates/_generators/store/store.ts.tpl +23 -0
- package/templates/react/src/shared/vsceasy/bootstrap.ts +19 -6
- package/templates/react/src/shared/vsceasy/client.ts +27 -0
- package/templates/react/src/shared/vsceasy/define.ts +34 -4
- package/templates/react/src/shared/vsceasy/index.ts +4 -1
- package/templates/react/src/shared/vsceasy/store.ts +73 -0
package/dist/bin/cli.js
CHANGED
|
@@ -3826,7 +3826,7 @@ var init_scaffold = __esm(() => {
|
|
|
3826
3826
|
});
|
|
3827
3827
|
|
|
3828
3828
|
// src/lib/templatesData.ts
|
|
3829
|
-
var TEMPLATES_VERSION = "0.1.
|
|
3829
|
+
var TEMPLATES_VERSION = "0.1.8", TEMPLATE_FILES;
|
|
3830
3830
|
var init_templatesData = __esm(() => {
|
|
3831
3831
|
TEMPLATE_FILES = {
|
|
3832
3832
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
@@ -4025,21 +4025,35 @@ type FormState = Partial<{{Name}}>;
|
|
|
4025
4025
|
|
|
4026
4026
|
const emptyForm: FormState = {{emptyFormLiteral}};
|
|
4027
4027
|
|
|
4028
|
+
// \`<input type="date">\` only accepts a \`yyyy-MM-dd\` value. Stored dates may be
|
|
4029
|
+
// ISO strings or Date objects, so normalize before binding to the input.
|
|
4030
|
+
function toDateInput(v: unknown): string {
|
|
4031
|
+
if (v == null || v === '') return '';
|
|
4032
|
+
const d = v instanceof Date ? v : new Date(v as string);
|
|
4033
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4028
4036
|
export function App() {
|
|
4029
4037
|
const [form, setForm] = useState<FormState>(emptyForm);
|
|
4030
4038
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
4031
4039
|
const [error, setError] = useState<string | null>(null);
|
|
4032
4040
|
const [saving, setSaving] = useState(false);
|
|
4033
4041
|
|
|
4034
|
-
const load = useCallback(async () => {
|
|
4035
|
-
// The list stashes
|
|
4036
|
-
// clears it after handing it over)
|
|
4042
|
+
const load = useCallback(async (initial: boolean) => {
|
|
4043
|
+
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
4044
|
+
// clears it after handing it over).
|
|
4037
4045
|
const id = await api.pendingId();
|
|
4038
4046
|
if (id == null || id === '') {
|
|
4039
|
-
|
|
4040
|
-
|
|
4047
|
+
// No row was requested. On the first mount, start with an empty "new" form.
|
|
4048
|
+
// On later reveals (focus/visibility), DON'T reset — that would wipe a form
|
|
4049
|
+
// the user is busy filling in. Just leave the current state as-is.
|
|
4050
|
+
if (initial) {
|
|
4051
|
+
setForm(emptyForm);
|
|
4052
|
+
setEditingId(null);
|
|
4053
|
+
}
|
|
4041
4054
|
return;
|
|
4042
4055
|
}
|
|
4056
|
+
// The list asked to edit a specific row — load it, replacing the current form.
|
|
4043
4057
|
const row = await api.get(id);
|
|
4044
4058
|
if (row) {
|
|
4045
4059
|
setForm(row);
|
|
@@ -4048,11 +4062,12 @@ export function App() {
|
|
|
4048
4062
|
}, []);
|
|
4049
4063
|
|
|
4050
4064
|
useEffect(() => {
|
|
4051
|
-
void load();
|
|
4052
|
-
// Webviews retain state when hidden, so re-
|
|
4053
|
-
//
|
|
4054
|
-
|
|
4055
|
-
const
|
|
4065
|
+
void load(true);
|
|
4066
|
+
// Webviews retain state when hidden, so re-check on reveal: the list may have
|
|
4067
|
+
// asked to edit a different row. When nothing is pending, \`load\` leaves the
|
|
4068
|
+
// in-progress form untouched (see above).
|
|
4069
|
+
const onFocus = () => { void load(false); };
|
|
4070
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(false); };
|
|
4056
4071
|
window.addEventListener('focus', onFocus);
|
|
4057
4072
|
document.addEventListener('visibilitychange', onVisible);
|
|
4058
4073
|
return () => {
|
|
@@ -4523,6 +4538,35 @@ export interface Repository<T> {
|
|
|
4523
4538
|
clear(): Promise<void>;
|
|
4524
4539
|
}
|
|
4525
4540
|
|
|
4541
|
+
// ── Change notifications (reactivity) ────────────────────────────────────────
|
|
4542
|
+
|
|
4543
|
+
/**
|
|
4544
|
+
* Per-entity change listeners. Any mutation (insert/upsert/update/delete/...) on
|
|
4545
|
+
* an entity fires its listeners with the entity name. This is the host-side hook
|
|
4546
|
+
* the \`watch()\` helper builds on to push updates to webviews.
|
|
4547
|
+
*/
|
|
4548
|
+
const entityListeners = new Map<string, Set<(entity: string) => void>>();
|
|
4549
|
+
|
|
4550
|
+
function emitChange(entity: string): void {
|
|
4551
|
+
entityListeners.get(entity)?.forEach((cb) => {
|
|
4552
|
+
try { cb(entity); } catch { /* a bad listener must not break a write */ }
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
/**
|
|
4557
|
+
* Subscribe to changes on an entity. Returns an unsubscribe function.
|
|
4558
|
+
*
|
|
4559
|
+
* watchEntity(Todos, () => server.emit('todos:changed'));
|
|
4560
|
+
*
|
|
4561
|
+
* Prefer the higher-level \`watch()\` helper, which wires this to an RPC emit.
|
|
4562
|
+
*/
|
|
4563
|
+
export function watchEntity<T>(entity: Entity<T>, cb: (entity: string) => void): () => void {
|
|
4564
|
+
let set = entityListeners.get(entity.name);
|
|
4565
|
+
if (!set) entityListeners.set(entity.name, (set = new Set()));
|
|
4566
|
+
set.add(cb);
|
|
4567
|
+
return () => { set!.delete(cb); };
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4526
4570
|
// ── Provider interface (future-proof for sqlite/etc.) ────────────────────────
|
|
4527
4571
|
|
|
4528
4572
|
export interface Provider {
|
|
@@ -4693,6 +4737,7 @@ function makeDb(provider: Provider): Db {
|
|
|
4693
4737
|
}
|
|
4694
4738
|
rows.push(row);
|
|
4695
4739
|
await save(rows);
|
|
4740
|
+
emitChange(entity.name);
|
|
4696
4741
|
return row;
|
|
4697
4742
|
},
|
|
4698
4743
|
async upsert(row) {
|
|
@@ -4700,6 +4745,7 @@ function makeDb(provider: Provider): Db {
|
|
|
4700
4745
|
const i = rows.findIndex((r) => r[pk] === row[pk]);
|
|
4701
4746
|
if (i >= 0) rows[i] = row; else rows.push(row);
|
|
4702
4747
|
await save(rows);
|
|
4748
|
+
emitChange(entity.name);
|
|
4703
4749
|
return row;
|
|
4704
4750
|
},
|
|
4705
4751
|
async update(id, patch) {
|
|
@@ -4708,6 +4754,7 @@ function makeDb(provider: Provider): Db {
|
|
|
4708
4754
|
if (i < 0) return null;
|
|
4709
4755
|
rows[i] = { ...rows[i], ...patch };
|
|
4710
4756
|
await save(rows);
|
|
4757
|
+
emitChange(entity.name);
|
|
4711
4758
|
return rows[i];
|
|
4712
4759
|
},
|
|
4713
4760
|
async delete(id) {
|
|
@@ -4716,17 +4763,19 @@ function makeDb(provider: Provider): Db {
|
|
|
4716
4763
|
const next = rows.filter((r) => r[pk] !== id);
|
|
4717
4764
|
if (next.length === before) return false;
|
|
4718
4765
|
await save(next);
|
|
4766
|
+
emitChange(entity.name);
|
|
4719
4767
|
return true;
|
|
4720
4768
|
},
|
|
4721
4769
|
async deleteMany(where) {
|
|
4722
4770
|
const rows = await load();
|
|
4723
4771
|
const next = rows.filter((r) => !match(r, where));
|
|
4724
4772
|
const removed = rows.length - next.length;
|
|
4725
|
-
if (removed > 0) await save(next);
|
|
4773
|
+
if (removed > 0) { await save(next); emitChange(entity.name); }
|
|
4726
4774
|
return removed;
|
|
4727
4775
|
},
|
|
4728
4776
|
async clear() {
|
|
4729
4777
|
await save([]);
|
|
4778
|
+
emitChange(entity.name);
|
|
4730
4779
|
},
|
|
4731
4780
|
};
|
|
4732
4781
|
};
|
|
@@ -5151,6 +5200,30 @@ export default defineStatusBar({
|
|
|
5151
5200
|
alignment: '{{alignment}}',
|
|
5152
5201
|
priority: {{priority}},{{commandLine}}{{panelLine}}
|
|
5153
5202
|
});
|
|
5203
|
+
`,
|
|
5204
|
+
"_generators/store/store.ts.tpl": `import { defineStore } from '../shared/vsceasy';
|
|
5205
|
+
|
|
5206
|
+
/**
|
|
5207
|
+
* Reactive store: a single observable value. Mutate it with \`.set()\` / \`.update()\`
|
|
5208
|
+
* and anything subscribed reacts.
|
|
5209
|
+
*
|
|
5210
|
+
* import { {{name}} } from '../stores/{{name}}';
|
|
5211
|
+
* {{name}}.set({{example}});
|
|
5212
|
+
* {{name}}.update((v) => v);
|
|
5213
|
+
*
|
|
5214
|
+
* To push changes to a webview, watch it on the host and emit over RPC:
|
|
5215
|
+
*
|
|
5216
|
+
* // inside a panel/subpanel rpc():
|
|
5217
|
+
* import { watch } from '../shared/vsceasy';
|
|
5218
|
+
* import { {{name}} } from '../stores/{{name}}';
|
|
5219
|
+
* watch({{name}}, () => server.emit('{{name}}:changed', {{name}}.get()));
|
|
5220
|
+
*
|
|
5221
|
+
* Then listen in the webview:
|
|
5222
|
+
*
|
|
5223
|
+
* import { listen } from '../shared/vsceasy/client';
|
|
5224
|
+
* listen(api, '{{name}}:changed', (v) => render(v));
|
|
5225
|
+
*/
|
|
5226
|
+
export const {{name}} = defineStore<{{type}}>({{initial}});
|
|
5154
5227
|
`,
|
|
5155
5228
|
"_generators/subpanel/App.tsx.tpl": `import React from 'react';
|
|
5156
5229
|
{{apiBlock}}
|
|
@@ -6347,6 +6420,13 @@ function registerTreeView(
|
|
|
6347
6420
|
vscode.commands.registerCommand(refreshCmd, () => provider.refresh()),
|
|
6348
6421
|
);
|
|
6349
6422
|
|
|
6423
|
+
// Keep the tree live: let the def subscribe to a data source and re-run
|
|
6424
|
+
// getChildren on change. The unsubscribe is disposed on deactivate.
|
|
6425
|
+
if (def.watch) {
|
|
6426
|
+
const off = def.watch(() => provider.refresh(), vscode, context);
|
|
6427
|
+
if (off) context.subscriptions.push({ dispose: off });
|
|
6428
|
+
}
|
|
6429
|
+
|
|
6350
6430
|
const dispatchCmd = \`\${registry.prefix}._tree.\${def.id ?? id}.run\`;
|
|
6351
6431
|
context.subscriptions.push(
|
|
6352
6432
|
vscode.commands.registerCommand(dispatchCmd, async (node: TreeNode) => {
|
|
@@ -6423,9 +6503,11 @@ function registerSubpanel(
|
|
|
6423
6503
|
const ui = def.ui ?? \`subpanels/\${def.id ?? id}\`;
|
|
6424
6504
|
view.webview.html = renderHtml(view.webview, context, ui, def.title);
|
|
6425
6505
|
if (def.rpc) {
|
|
6426
|
-
|
|
6427
|
-
const
|
|
6428
|
-
|
|
6506
|
+
let server: ReturnType<typeof createRpcServer> | undefined;
|
|
6507
|
+
const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
|
|
6508
|
+
const handlers = def.rpc(vscode, context, emit);
|
|
6509
|
+
server = createRpcServer(webviewTransport(view.webview), handlers);
|
|
6510
|
+
view.onDidDispose(() => server!.dispose());
|
|
6429
6511
|
}
|
|
6430
6512
|
},
|
|
6431
6513
|
};
|
|
@@ -6661,9 +6743,13 @@ function openPanel(context: vscode.ExtensionContext, prefix: string, id: string,
|
|
|
6661
6743
|
panel.webview.html = renderHtml(panel.webview, context, ui, def.title);
|
|
6662
6744
|
|
|
6663
6745
|
if (def.rpc) {
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6746
|
+
// \`emit\` is wired lazily: the rpc factory may call watch()/watchEntity()
|
|
6747
|
+
// immediately, but the server (and thus the real emit) only exists after.
|
|
6748
|
+
let server: ReturnType<typeof createRpcServer> | undefined;
|
|
6749
|
+
const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
|
|
6750
|
+
const handlers = def.rpc(vscode, context, emit);
|
|
6751
|
+
server = createRpcServer(webviewTransport(panel.webview), handlers);
|
|
6752
|
+
panel.onDidDispose(() => server!.dispose());
|
|
6667
6753
|
}
|
|
6668
6754
|
|
|
6669
6755
|
openPanels.set(key, panel);
|
|
@@ -6781,6 +6867,33 @@ export {
|
|
|
6781
6867
|
webviewState,
|
|
6782
6868
|
} from './rpc';
|
|
6783
6869
|
export type { RpcClient, Handlers, Transport, RpcClientOptions, WebviewApi } from './rpc';
|
|
6870
|
+
|
|
6871
|
+
export { defineStore } from './store';
|
|
6872
|
+
export type { Store } from './store';
|
|
6873
|
+
|
|
6874
|
+
import type { RpcClient, Handlers } from './rpc';
|
|
6875
|
+
|
|
6876
|
+
/**
|
|
6877
|
+
* Listen for a change pushed from the host over the RPC event channel, and run a
|
|
6878
|
+
* callback when it arrives. This is the webview side of the reactivity model —
|
|
6879
|
+
* the host emits with \`server.emit(topic, …)\` (usually wired via \`watch()\` /
|
|
6880
|
+
* \`watchEntity()\`), and your visual element reacts here. Returns an unsubscribe
|
|
6881
|
+
* function; call it on unmount.
|
|
6882
|
+
*
|
|
6883
|
+
* const api = connectWebview<TodoStatsViewApi>();
|
|
6884
|
+
* // re-read + re-render whenever todos change on the host:
|
|
6885
|
+
* const off = listen(api, 'todos:changed', () => refresh());
|
|
6886
|
+
*
|
|
6887
|
+
* It's a thin, named wrapper over \`api.on(topic, handler)\` so the place you
|
|
6888
|
+
* listen reads clearly in the UI code. The payload (if any) is passed through.
|
|
6889
|
+
*/
|
|
6890
|
+
export function listen<H extends Handlers>(
|
|
6891
|
+
api: RpcClient<H>,
|
|
6892
|
+
topic: string,
|
|
6893
|
+
handler: (payload?: unknown) => void,
|
|
6894
|
+
): () => void {
|
|
6895
|
+
return api.on(topic, handler);
|
|
6896
|
+
}
|
|
6784
6897
|
`,
|
|
6785
6898
|
"react/src/shared/vsceasy/codiconNames.ts": `/**
|
|
6786
6899
|
* AUTO-GENERATED by scripts/genCodiconTypes.ts — do not edit.
|
|
@@ -6983,6 +7096,18 @@ export type CodiconName =
|
|
|
6983
7096
|
import type { Handlers } from './rpc';
|
|
6984
7097
|
import type { CodiconName } from './codiconNames';
|
|
6985
7098
|
|
|
7099
|
+
/**
|
|
7100
|
+
* Push a change to this webview over the RPC event channel. The webview reacts
|
|
7101
|
+
* with \`listen(api, topic, …)\`. Wire it to a data source with \`watch()\` /
|
|
7102
|
+
* \`watchEntity()\` inside \`rpc()\`:
|
|
7103
|
+
*
|
|
7104
|
+
* rpc: (vscode, ctx, emit) => {
|
|
7105
|
+
* watchEntity(Todos, () => emit('todos:changed'));
|
|
7106
|
+
* return { stats: () => … };
|
|
7107
|
+
* }
|
|
7108
|
+
*/
|
|
7109
|
+
export type RpcEmit = (topic: string, payload?: unknown) => void;
|
|
7110
|
+
|
|
6986
7111
|
export interface PanelDef<H extends Handlers = Handlers> {
|
|
6987
7112
|
/** Stable id. Default: file basename. Used as command suffix and webview key. */
|
|
6988
7113
|
id?: string;
|
|
@@ -6994,8 +7119,11 @@ export interface PanelDef<H extends Handlers = Handlers> {
|
|
|
6994
7119
|
column?: 'active' | 'beside' | 'one' | 'two' | 'three';
|
|
6995
7120
|
/** Keep DOM alive when hidden. Default: true. */
|
|
6996
7121
|
retainContext?: boolean;
|
|
6997
|
-
/**
|
|
6998
|
-
|
|
7122
|
+
/**
|
|
7123
|
+
* RPC handlers — receives the vscode namespace, the extension context, and
|
|
7124
|
+
* \`emit\` for pushing change events to this webview (see {@link RpcEmit}).
|
|
7125
|
+
*/
|
|
7126
|
+
rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
|
|
6999
7127
|
/** Optional command palette entry that opens this panel. Default: true. */
|
|
7000
7128
|
command?:
|
|
7001
7129
|
| boolean
|
|
@@ -7106,8 +7234,11 @@ export interface SubpanelDef<H extends Handlers = Handlers> {
|
|
|
7106
7234
|
ui?: string;
|
|
7107
7235
|
/** Keep DOM alive when hidden. Default: true. */
|
|
7108
7236
|
retainContext?: boolean;
|
|
7109
|
-
/**
|
|
7110
|
-
|
|
7237
|
+
/**
|
|
7238
|
+
* RPC handlers — receives the vscode namespace, the extension context, and
|
|
7239
|
+
* \`emit\` for pushing change events to this webview (see {@link RpcEmit}).
|
|
7240
|
+
*/
|
|
7241
|
+
rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
|
|
7111
7242
|
}
|
|
7112
7243
|
|
|
7113
7244
|
export function defineSubpanel<H extends Handlers = Handlers>(def: SubpanelDef<H>): SubpanelDef<H> {
|
|
@@ -7209,6 +7340,18 @@ export interface TreeViewDef {
|
|
|
7209
7340
|
vscode: typeof import('vscode'),
|
|
7210
7341
|
ctx: vscode.ExtensionContext,
|
|
7211
7342
|
) => TreeNode[] | Promise<TreeNode[]>;
|
|
7343
|
+
/**
|
|
7344
|
+
* Keep the tree live. Receives \`refresh\` — call it (directly or as a callback)
|
|
7345
|
+
* to re-run \`getChildren\`. Subscribe to a data source here so the tree updates
|
|
7346
|
+
* itself; return an unsubscribe to clean up.
|
|
7347
|
+
*
|
|
7348
|
+
* watch: (refresh) => watchEntity(Todos, refresh),
|
|
7349
|
+
*/
|
|
7350
|
+
watch?: (
|
|
7351
|
+
refresh: () => void,
|
|
7352
|
+
vscode: typeof import('vscode'),
|
|
7353
|
+
ctx: vscode.ExtensionContext,
|
|
7354
|
+
) => (() => void) | void;
|
|
7212
7355
|
}
|
|
7213
7356
|
|
|
7214
7357
|
export function defineTreeView(def: TreeViewDef): TreeViewDef {
|
|
@@ -7250,7 +7393,7 @@ export function defineJob(def: JobDef): JobDef {
|
|
|
7250
7393
|
}
|
|
7251
7394
|
`,
|
|
7252
7395
|
"react/src/shared/vsceasy/index.ts": `export { definePanel, defineCommand, defineMenu, defineStatusBar, defineSubpanel, defineTreeView, defineJob } from './define';
|
|
7253
|
-
export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName } from './define';
|
|
7396
|
+
export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName, RpcEmit } from './define';
|
|
7254
7397
|
export { bootstrap } from './bootstrap';
|
|
7255
7398
|
export type { Registry, BootstrapOptions, ActivateHook } from './bootstrap';
|
|
7256
7399
|
export {
|
|
@@ -7262,6 +7405,9 @@ export {
|
|
|
7262
7405
|
webviewState,
|
|
7263
7406
|
} from './rpc';
|
|
7264
7407
|
export type { Transport, RpcClient, Handlers, RpcClientOptions, WebviewApi } from './rpc';
|
|
7408
|
+
export { defineStore, watch } from './store';
|
|
7409
|
+
export type { Store, Watchable } from './store';
|
|
7410
|
+
export { listen } from './client';
|
|
7265
7411
|
`,
|
|
7266
7412
|
"react/src/shared/vsceasy/rpc.ts": `// Typed RPC bridge — webview <-> extension.
|
|
7267
7413
|
// Used by both sides. Transport-agnostic core + thin adapters.
|
|
@@ -7477,6 +7623,80 @@ export function webviewState<T>(defaults: T): {
|
|
|
7477
7623
|
},
|
|
7478
7624
|
};
|
|
7479
7625
|
}
|
|
7626
|
+
`,
|
|
7627
|
+
"react/src/shared/vsceasy/store.ts": `/**
|
|
7628
|
+
* Reactive stores — a tiny, framework-agnostic observable value.
|
|
7629
|
+
*
|
|
7630
|
+
* A store holds one value and notifies subscribers when it changes. It's the
|
|
7631
|
+
* non-ORM half of the reactivity model: use it for arbitrary state (a counter,
|
|
7632
|
+
* a flag, a selection) that a visual element should track.
|
|
7633
|
+
*
|
|
7634
|
+
* const counter = defineStore(0);
|
|
7635
|
+
* counter.subscribe((v) => console.log('now', v));
|
|
7636
|
+
* counter.set(1); // logs: now 1
|
|
7637
|
+
* counter.update((n) => n + 1);
|
|
7638
|
+
*
|
|
7639
|
+
* Pair it with \`watch()\` on the host to push changes to a webview, and
|
|
7640
|
+
* \`listen()\` on the webview to react — see those helpers below and in client.ts.
|
|
7641
|
+
*/
|
|
7642
|
+
export interface Store<T> {
|
|
7643
|
+
/** Read the current value. */
|
|
7644
|
+
get(): T;
|
|
7645
|
+
/** Replace the value and notify subscribers (no-op if \`Object.is\`-equal). */
|
|
7646
|
+
set(next: T): void;
|
|
7647
|
+
/** Derive the next value from the current one, then \`set\` it. */
|
|
7648
|
+
update(fn: (current: T) => T): void;
|
|
7649
|
+
/** Subscribe to changes. Returns an unsubscribe function. */
|
|
7650
|
+
subscribe(cb: (value: T) => void): () => void;
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7653
|
+
export function defineStore<T>(initial: T): Store<T> {
|
|
7654
|
+
let value = initial;
|
|
7655
|
+
const subs = new Set<(value: T) => void>();
|
|
7656
|
+
|
|
7657
|
+
return {
|
|
7658
|
+
get: () => value,
|
|
7659
|
+
set(next) {
|
|
7660
|
+
if (Object.is(value, next)) return;
|
|
7661
|
+
value = next;
|
|
7662
|
+
subs.forEach((cb) => {
|
|
7663
|
+
try { cb(value); } catch { /* a bad subscriber must not break a set */ }
|
|
7664
|
+
});
|
|
7665
|
+
},
|
|
7666
|
+
update(fn) {
|
|
7667
|
+
this.set(fn(value));
|
|
7668
|
+
},
|
|
7669
|
+
subscribe(cb) {
|
|
7670
|
+
subs.add(cb);
|
|
7671
|
+
return () => { subs.delete(cb); };
|
|
7672
|
+
},
|
|
7673
|
+
};
|
|
7674
|
+
}
|
|
7675
|
+
|
|
7676
|
+
/**
|
|
7677
|
+
* Anything that can be watched: it exposes a \`subscribe(cb)\` returning an
|
|
7678
|
+
* unsubscribe function. Stores satisfy this directly. (ORM entities are watched
|
|
7679
|
+
* with \`watchEntity\` from your generated \`db.ts\`, which has the same shape.)
|
|
7680
|
+
*/
|
|
7681
|
+
export interface Watchable {
|
|
7682
|
+
subscribe(cb: (...args: any[]) => void): () => void;
|
|
7683
|
+
}
|
|
7684
|
+
|
|
7685
|
+
/**
|
|
7686
|
+
* Bridge a watchable source to a side-effect — typically an RPC emit that pushes
|
|
7687
|
+
* the change to a subscribed webview. Runs \`effect\` on every change. Returns an
|
|
7688
|
+
* unsubscribe function; register it on the panel's \`ctx.subscriptions\` (wrapped
|
|
7689
|
+
* in \`{ dispose }\`) so it's cleaned up when the extension deactivates.
|
|
7690
|
+
*
|
|
7691
|
+
* // host side, in a panel's rpc():
|
|
7692
|
+
* watch(badgeCount, () => server.emit('badge:changed'));
|
|
7693
|
+
*
|
|
7694
|
+
* For ORM entities, use \`watchEntity(Todos, () => server.emit('todos:changed'))\`
|
|
7695
|
+
* from your generated db.ts — same idea, same return.
|
|
7696
|
+
*/
|
|
7697
|
+
export function watch(source: Watchable, effect: () => void): () => void {
|
|
7698
|
+
return source.subscribe(() => effect());
|
|
7699
|
+
}
|
|
7480
7700
|
`,
|
|
7481
7701
|
"react/src/webview/panels/dashboard/App.tsx": `import React, { useEffect, useState } from 'react';
|
|
7482
7702
|
import { connectWebview } from '../../../shared/vsceasy/client';
|
|
@@ -12542,7 +12762,7 @@ function renderInput(field, override) {
|
|
|
12542
12762
|
case "boolean":
|
|
12543
12763
|
return wrap(` <input type="checkbox" checked={!!form.${name}} onChange={(e) => onChange('${name}', e.target.checked as any)} />`);
|
|
12544
12764
|
case "date":
|
|
12545
|
-
return wrap(` <input type="date"${required} value={(form.${name}
|
|
12765
|
+
return wrap(` <input type="date"${required} value={toDateInput(form.${name})} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
12546
12766
|
case "select": {
|
|
12547
12767
|
const opts = (spec.options ?? []).map((o) => ` <option value=${JSON.stringify(o)}>${escapeJsx(o)}</option>`).join(`
|
|
12548
12768
|
`);
|
|
@@ -12747,14 +12967,16 @@ var init_add23 = __esm(() => {
|
|
|
12747
12967
|
const projectRoot = findProjectRoot();
|
|
12748
12968
|
const templatesRoot = findTemplatesRoot();
|
|
12749
12969
|
let menuSpec;
|
|
12750
|
-
const choice = String(args.menu ?? NONE_SENTINEL2);
|
|
12751
|
-
if (choice === NONE_SENTINEL2) {
|
|
12970
|
+
const choice = String(args.menu ?? NONE_SENTINEL2).trim();
|
|
12971
|
+
if (choice === NONE_SENTINEL2 || choice === "none") {
|
|
12752
12972
|
menuSpec = "none";
|
|
12753
12973
|
} else if (choice === NEW_SENTINEL) {
|
|
12754
12974
|
const id = args.newMenuId ? String(args.newMenuId).trim() : await import_cli_maker18.prompt(" new menu id: ");
|
|
12755
12975
|
if (!id)
|
|
12756
12976
|
throw new Error("New menu id required.");
|
|
12757
12977
|
menuSpec = `new:${id}`;
|
|
12978
|
+
} else if (choice.startsWith("new:") || choice.startsWith("existing:")) {
|
|
12979
|
+
menuSpec = choice;
|
|
12758
12980
|
} else {
|
|
12759
12981
|
menuSpec = `existing:${choice}`;
|
|
12760
12982
|
}
|
|
@@ -12836,6 +13058,122 @@ var init_add24 = __esm(() => {
|
|
|
12836
13058
|
add_default12 = addComponentsCommand;
|
|
12837
13059
|
});
|
|
12838
13060
|
|
|
13061
|
+
// src/lib/store/add.ts
|
|
13062
|
+
function addStore(opts) {
|
|
13063
|
+
const name = assertId("store name", normalizeCamel9(opts.name));
|
|
13064
|
+
const type = opts.type ?? "number";
|
|
13065
|
+
const initial = opts.initial?.trim() || DEFAULT_INITIAL[type];
|
|
13066
|
+
const storeTs = path44.join(opts.projectRoot, "src", "stores", `${name}.ts`);
|
|
13067
|
+
assertNoOverwrite(opts.projectRoot, storeTs, "Store");
|
|
13068
|
+
const tplPath = path44.join(opts.templatesRoot, "_generators", "store", "store.ts.tpl");
|
|
13069
|
+
const body = substitute(fs29.readFileSync(tplPath, "utf8"), {
|
|
13070
|
+
name,
|
|
13071
|
+
type: TS_TYPE[type],
|
|
13072
|
+
initial,
|
|
13073
|
+
example: EXAMPLE[type]
|
|
13074
|
+
});
|
|
13075
|
+
fs29.mkdirSync(path44.dirname(storeTs), { recursive: true });
|
|
13076
|
+
fs29.writeFileSync(storeTs, body);
|
|
13077
|
+
return { created: [storeTs] };
|
|
13078
|
+
}
|
|
13079
|
+
function normalizeCamel9(s) {
|
|
13080
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
13081
|
+
if (!cleaned)
|
|
13082
|
+
return "";
|
|
13083
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
13084
|
+
}
|
|
13085
|
+
var fs29, path44, DEFAULT_INITIAL, TS_TYPE, EXAMPLE;
|
|
13086
|
+
var init_add25 = __esm(() => {
|
|
13087
|
+
init_scaffold();
|
|
13088
|
+
init_validate();
|
|
13089
|
+
fs29 = __toESM(require("fs"));
|
|
13090
|
+
path44 = __toESM(require("path"));
|
|
13091
|
+
DEFAULT_INITIAL = {
|
|
13092
|
+
number: "0",
|
|
13093
|
+
string: "''",
|
|
13094
|
+
boolean: "false",
|
|
13095
|
+
json: "null"
|
|
13096
|
+
};
|
|
13097
|
+
TS_TYPE = {
|
|
13098
|
+
number: "number",
|
|
13099
|
+
string: "string",
|
|
13100
|
+
boolean: "boolean",
|
|
13101
|
+
json: "any"
|
|
13102
|
+
};
|
|
13103
|
+
EXAMPLE = {
|
|
13104
|
+
number: "1",
|
|
13105
|
+
string: "'hello'",
|
|
13106
|
+
boolean: "true",
|
|
13107
|
+
json: "{ open: true }"
|
|
13108
|
+
};
|
|
13109
|
+
});
|
|
13110
|
+
|
|
13111
|
+
// src/commands/store/add.ts
|
|
13112
|
+
var import_cli_maker20, path45, addStoreCommand, add_default13;
|
|
13113
|
+
var init_add26 = __esm(() => {
|
|
13114
|
+
init_add25();
|
|
13115
|
+
init_findProject();
|
|
13116
|
+
import_cli_maker20 = __toESM(require_dist(), 1);
|
|
13117
|
+
path45 = __toESM(require("path"));
|
|
13118
|
+
addStoreCommand = {
|
|
13119
|
+
name: "add",
|
|
13120
|
+
description: "Add a reactive store (an observable value) under src/stores/. Watch it on the host and listen in a webview to keep UI in sync.",
|
|
13121
|
+
params: [
|
|
13122
|
+
{ name: "name", description: "Store name (camelCase, e.g. badgeCount)", required: true, type: import_cli_maker20.ParamType.Text },
|
|
13123
|
+
{
|
|
13124
|
+
name: "type",
|
|
13125
|
+
description: "Value type",
|
|
13126
|
+
required: false,
|
|
13127
|
+
type: import_cli_maker20.ParamType.List,
|
|
13128
|
+
options: ["number", "string", "boolean", "json"]
|
|
13129
|
+
},
|
|
13130
|
+
{
|
|
13131
|
+
name: "initial",
|
|
13132
|
+
description: "Initial value expression (default per type: 0 / '' / false / null)",
|
|
13133
|
+
required: false,
|
|
13134
|
+
type: import_cli_maker20.ParamType.Text
|
|
13135
|
+
}
|
|
13136
|
+
],
|
|
13137
|
+
action: async (args) => {
|
|
13138
|
+
try {
|
|
13139
|
+
const projectRoot = findProjectRoot();
|
|
13140
|
+
const templatesRoot = findTemplatesRoot();
|
|
13141
|
+
const result = addStore({
|
|
13142
|
+
name: String(args.name).trim(),
|
|
13143
|
+
type: args.type ? String(args.type) : undefined,
|
|
13144
|
+
initial: args.initial ? String(args.initial) : undefined,
|
|
13145
|
+
projectRoot,
|
|
13146
|
+
templatesRoot
|
|
13147
|
+
});
|
|
13148
|
+
const rel = (p) => path45.relative(projectRoot, p);
|
|
13149
|
+
const name = String(args.name).trim();
|
|
13150
|
+
console.log(`
|
|
13151
|
+
✓ Store "${name}" created.
|
|
13152
|
+
`);
|
|
13153
|
+
for (const f of result.created)
|
|
13154
|
+
console.log(` + ${rel(f)}`);
|
|
13155
|
+
console.log(`
|
|
13156
|
+
Use it:
|
|
13157
|
+
import { ${name} } from '../stores/${name}';
|
|
13158
|
+
${name}.set(...); ${name}.update((v) => v);
|
|
13159
|
+
`);
|
|
13160
|
+
console.log(` Push changes to a webview — host side, in a panel rpc():
|
|
13161
|
+
watch(${name}, () => server.emit('${name}:changed', ${name}.get()));
|
|
13162
|
+
`);
|
|
13163
|
+
console.log(` React in the webview:
|
|
13164
|
+
listen(api, '${name}:changed', (v) => render(v));
|
|
13165
|
+
`);
|
|
13166
|
+
} catch (err) {
|
|
13167
|
+
console.error(`
|
|
13168
|
+
✗ ${err.message}
|
|
13169
|
+
`);
|
|
13170
|
+
process.exitCode = 1;
|
|
13171
|
+
}
|
|
13172
|
+
}
|
|
13173
|
+
};
|
|
13174
|
+
add_default13 = addStoreCommand;
|
|
13175
|
+
});
|
|
13176
|
+
|
|
12839
13177
|
// src/commands/groups.ts
|
|
12840
13178
|
function group(name, description, subcommands) {
|
|
12841
13179
|
return {
|
|
@@ -12852,7 +13190,7 @@ Use one of: ${subcommands.map((s) => s.name).join(", ")}
|
|
|
12852
13190
|
}
|
|
12853
13191
|
};
|
|
12854
13192
|
}
|
|
12855
|
-
var PanelGroup, MenuGroup, CommandGroup, RpcGroup, StatusBarGroup, SubpanelGroup, TreeViewGroup, TestGroup, PublishGroup, HelperGroup, JobGroup, DbGroup, ModelGroup, CrudGroup, ComponentsGroup;
|
|
13193
|
+
var PanelGroup, MenuGroup, CommandGroup, RpcGroup, StatusBarGroup, SubpanelGroup, TreeViewGroup, TestGroup, PublishGroup, HelperGroup, JobGroup, DbGroup, ModelGroup, CrudGroup, ComponentsGroup, StoreGroup;
|
|
12856
13194
|
var init_groups = __esm(() => {
|
|
12857
13195
|
init_add6();
|
|
12858
13196
|
init_add8();
|
|
@@ -12870,6 +13208,7 @@ var init_groups = __esm(() => {
|
|
|
12870
13208
|
init_add21();
|
|
12871
13209
|
init_add23();
|
|
12872
13210
|
init_add24();
|
|
13211
|
+
init_add26();
|
|
12873
13212
|
PanelGroup = group("panel", "Manage panels (webview + RPC bridge)", [add_default]);
|
|
12874
13213
|
MenuGroup = group("menu", "Manage sidebar menus (activity bar tree views)", [add_default2, edit_default]);
|
|
12875
13214
|
CommandGroup = group("command", "Manage palette commands", [add_default3]);
|
|
@@ -12885,6 +13224,7 @@ var init_groups = __esm(() => {
|
|
|
12885
13224
|
ModelGroup = group("model", "Manage typed models (entities + repos) under src/models/", [add_default10]);
|
|
12886
13225
|
CrudGroup = group("crud", "Scaffold full CRUD UI (service + list panel + form panel + RPC) for a model", [add_default11]);
|
|
12887
13226
|
ComponentsGroup = group("components", "Generate a theme-aware React component library for webviews", [add_default12]);
|
|
13227
|
+
StoreGroup = group("store", "Manage reactive stores (observable values) under src/stores/", [add_default13]);
|
|
12888
13228
|
});
|
|
12889
13229
|
|
|
12890
13230
|
// src/cli.ts
|
|
@@ -12892,22 +13232,31 @@ var exports_cli = {};
|
|
|
12892
13232
|
__export(exports_cli, {
|
|
12893
13233
|
cli: () => cli
|
|
12894
13234
|
});
|
|
12895
|
-
var
|
|
13235
|
+
var import_cli_maker21, cli;
|
|
12896
13236
|
var init_cli = __esm(() => {
|
|
12897
13237
|
init_create();
|
|
12898
13238
|
init_wizard();
|
|
12899
13239
|
init_doctor2();
|
|
12900
13240
|
init_upgrade2();
|
|
12901
13241
|
init_groups();
|
|
12902
|
-
|
|
12903
|
-
cli = new
|
|
13242
|
+
import_cli_maker21 = __toESM(require_dist(), 1);
|
|
13243
|
+
cli = new import_cli_maker21.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
|
|
12904
13244
|
interactive: true,
|
|
12905
|
-
version: "0.1.
|
|
13245
|
+
version: "0.1.8",
|
|
12906
13246
|
introAnimation: {
|
|
12907
13247
|
enabled: true,
|
|
12908
13248
|
preset: "retro-space",
|
|
12909
13249
|
title: "vsceasy",
|
|
12910
|
-
subtitle: "VS Code Extension Framework"
|
|
13250
|
+
subtitle: "VS Code Extension Framework",
|
|
13251
|
+
asciiArt: [
|
|
13252
|
+
' .-""""-.',
|
|
13253
|
+
" / o o \\",
|
|
13254
|
+
" | .. |",
|
|
13255
|
+
" \\ '--' /",
|
|
13256
|
+
" /`-.__.-`\\",
|
|
13257
|
+
" _/ /|/||\\|\\ \\_",
|
|
13258
|
+
" `--`-`-``-`-`--`"
|
|
13259
|
+
]
|
|
12911
13260
|
},
|
|
12912
13261
|
defaultCommands: {
|
|
12913
13262
|
rotatePassphrase: false,
|
|
@@ -12931,6 +13280,7 @@ var init_cli = __esm(() => {
|
|
|
12931
13280
|
cli.command(ModelGroup);
|
|
12932
13281
|
cli.command(CrudGroup);
|
|
12933
13282
|
cli.command(ComponentsGroup);
|
|
13283
|
+
cli.command(StoreGroup);
|
|
12934
13284
|
cli.command(doctor_default);
|
|
12935
13285
|
cli.command(upgrade_default);
|
|
12936
13286
|
cli.parse(process.argv);
|