@kubohiroya/turbowarp-structured-data 0.6.0 → 0.6.2

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.
@@ -0,0 +1,9 @@
1
+ import type { FixedInputEditorOptions } from '@kubohiroya/turbowarp-extension-data-editor';
2
+ export declare const fixedInputEditorDefinitions: readonly FixedInputEditorOptions[];
3
+ interface RuntimeLifetime {
4
+ on?: (event: string, listener: () => void) => void;
5
+ off?: (event: string, listener: () => void) => void;
6
+ }
7
+ /** Register metadata only; the GUI startup capability owns editor activation. */
8
+ export declare function registerFixedInputEditors(scratch: unknown, definitions: readonly FixedInputEditorOptions[], runtimeOverride?: RuntimeLifetime): () => void;
9
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { SchemaBundle } from '@kubohiroya/turbowarp-extension-data-editor/schema';
2
+ /** The on-disk owner Schema is canonical; this is its single-field projection. */
3
+ export declare const fixedInputSchemaBundle: SchemaBundle;
@@ -0,0 +1,8 @@
1
+ import type { SchemaBundle, SchemaDocument, SourceBinding, SourceEditorOptions } from '@kubohiroya/turbowarp-extension-data-editor/schema';
2
+ export declare function canonicalBundle(ownerId: string, contractId: string, document: SchemaDocument, ownerSource: {
3
+ path: string;
4
+ symbol: string;
5
+ }): SchemaBundle;
6
+ export declare function sourceBinding(argument: string, bundle: SchemaBundle, read: (source: string) => unknown, check?: (source: string) => unknown, format?: 'json' | 'yaml', uiSchema?: SourceBinding['variants'][number]['uiSchema']): SourceBinding;
7
+ /** The optional capability keeps legacy hosts and the startup flag safely inactive. */
8
+ export declare function registerSourceEditors(scratch: unknown, definitions: readonly SourceEditorOptions[]): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-structured-data",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Named structured data with JSON/YAML codecs and deterministic path operations for TurboWarp.",
5
5
  "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
6
6
  "license": "MPL-2.0",
@@ -27,11 +27,13 @@
27
27
  "dist/",
28
28
  "docs/",
29
29
  "src/",
30
+ "schemas/",
30
31
  "templates/",
31
32
  "README.md",
32
33
  "README.ja.md",
33
34
  "THIRD_PARTY_NOTICES.md",
34
- "LICENSE"
35
+ "LICENSE",
36
+ "lib-editor/"
35
37
  ],
36
38
  "publishConfig": {
37
39
  "access": "public",
@@ -44,22 +46,23 @@
44
46
  "scripts": {
45
47
  "dev": "vite build --watch",
46
48
  "typecheck": "tsc --noEmit",
47
- "build": "vite build",
48
- "lint": "eslint src tests scripts vite.config.ts eslint.config.ts",
49
+ "build": "vite build && pnpm run build:editor",
50
+ "lint": "eslint src tests scripts vite.config.ts vite.editor.config.ts eslint.config.ts",
49
51
  "test": "vitest run",
50
- "docs": "node scripts/generate-readme.ts",
51
- "docs:check": "node scripts/generate-readme.ts --check",
52
+ "docs": "node scripts/generate-readme.ts && node scripts/generate-fixed-input-docs.mjs",
53
+ "docs:check": "node scripts/generate-readme.ts --check && node scripts/generate-fixed-input-docs.mjs --check",
52
54
  "template:readme": "node scripts/apply-readme-template.ts",
53
55
  "repo:check": "node scripts/check-repo.ts",
54
56
  "pack:check": "npm pack --dry-run --ignore-scripts",
55
57
  "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run docs:check && pnpm run check:dist && pnpm run repo:check && pnpm run pack:check",
56
- "check:dist": "pnpm run build && node scripts/check-dist.ts"
58
+ "check:dist": "pnpm run build && node scripts/check-dist.ts",
59
+ "build:editor": "tsc -p tsconfig.editor.json && vite build --config vite.editor.config.ts"
57
60
  },
58
61
  "devDependencies": {
59
62
  "@eslint/js": "^9.39.5",
60
- "@kubohiroya/turbowarp-extension-manifest": "0.3.0",
61
- "@kubohiroya/turbowarp-named-data": "0.1.0",
62
- "@kubohiroya/vite-plugin-turbowarp-extension": "0.3.0",
63
+ "@kubohiroya/turbowarp-extension-manifest": "0.3.2",
64
+ "@kubohiroya/turbowarp-named-data": "0.3.1",
65
+ "@kubohiroya/vite-plugin-turbowarp-extension": "0.6.1",
63
66
  "@types/node": "^24.13.3",
64
67
  "eslint": "^9.39.5",
65
68
  "jiti": "^2.7.0",
@@ -67,13 +70,26 @@
67
70
  "typescript-eslint": "^8.66.0",
68
71
  "vite": "^8.2.2",
69
72
  "vitest": "^5.0.0",
70
- "react-dom": "18.3.1"
73
+ "react-dom": "18.3.1",
74
+ "@kubohiroya/turbowarp-extension-data-editor": "0.2.1"
71
75
  },
72
76
  "dependencies": {
73
- "yaml": "^2.9.1",
74
- "@kubohiroya/turbowarp-extension-data-editor": "git+https://github.com/kubohiroya/turbowarp-extension-data-editor.git#76fa1d50a3c188082ceef723ea018a9d8769ea6e"
77
+ "yaml": "^2.9.1"
75
78
  },
76
79
  "peerDependencies": {
77
- "@kubohiroya/turbowarp-named-data": ">=0.1.0 <1"
80
+ "@kubohiroya/turbowarp-named-data": "0.3.1"
81
+ },
82
+ "gitHead": "385a31c2a7957b51003686fab0b2d76199db705a",
83
+ "x-turbowarp-source": {
84
+ "revision": "385a31c2a7957b51003686fab0b2d76199db705a",
85
+ "revisionRole": "base-commit",
86
+ "sourceState": "local-release-overlay"
87
+ },
88
+ "exports": {
89
+ "./*": "./*",
90
+ "./editor-metadata": {
91
+ "types": "./lib-editor/src/editor-metadata.d.ts",
92
+ "import": "./lib-editor/src/editor-metadata.js"
93
+ }
78
94
  }
79
95
  }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kubohiroya.github.io/turbowarp-structured-data/schemas/block-editor-fixed-inputs.schema.json",
4
+ "$defs": {
5
+ "forEachAtPath": {
6
+ "type": "object",
7
+ "properties": {
8
+ "MAX": {
9
+ "type": "integer",
10
+ "minimum": 1,
11
+ "maximum": 1000
12
+ }
13
+ },
14
+ "required": [
15
+ "MAX"
16
+ ],
17
+ "additionalProperties": true
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ /** Lightweight declarations and owner callbacks; the optional GUI owns Schema compilation. */
2
+ export {sourceEditorDefinitions} from './editor-schema.js';
3
+ export {canonicalBundle, registerSourceEditors, sourceBinding} from './source-editor.js';
4
+ export {fixedInputEditorDefinitions, registerFixedInputEditors} from './fixed-input-editors.js';
5
+ export {fixedInputSchemaBundle} from './fixed-input-schema.js';
package/src/extension.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import {fixedInputEditorDefinitions, registerFixedInputEditors} from './fixed-input-editors.js';
1
2
  import {sourceEditorDefinitions} from './editor-schema.js';
2
3
  import {registerSourceEditors} from './source-editor.js';
3
4
  import definitions from './block-definitions.json';
@@ -74,7 +75,10 @@ const blockDefinitions = definitions.blocks as readonly BlockDefinition[];
74
75
 
75
76
  export class StructuredDataExtension implements TurboWarpExtension {
76
77
  private readonly enabled: boolean;
78
+ private disposed = false;
79
+ private readonly releaseRuntimeListeners: () => void;
77
80
  private readonly unregisterSourceEditors: () => void;
81
+ private readonly unregisterFixedInputEditors: () => void;
78
82
  private registries = new WeakMap<object, Map<string, StructuredDataBinding>>();
79
83
  private iterationStacks = new WeakMap<object, ActiveIteration[]>();
80
84
  private nextRevision = 1;
@@ -89,6 +93,17 @@ export class StructuredDataExtension implements TurboWarpExtension {
89
93
  ) {
90
94
  this.enabled = enabled;
91
95
  this.unregisterSourceEditors = registerSourceEditors(Scratch, enabled ? sourceEditorDefinitions : []);
96
+ const runtime = Scratch.vm?.runtime;
97
+ const onDispose = () => {void this.dispose();};
98
+ const onStop = () => this.clearRuntimeState();
99
+ this.releaseRuntimeListeners = () => {
100
+ runtime?.off?.('RUNTIME_DISPOSED', onDispose);
101
+ runtime?.off?.('PROJECT_STOP_ALL', onStop);
102
+ };
103
+ let unregisterFixed = () => {};
104
+ try {
105
+ unregisterFixed = registerFixedInputEditors(Scratch, enabled ? fixedInputEditorDefinitions : []);
106
+ this.unregisterFixedInputEditors = unregisterFixed;
92
107
  if (namedDataEnabled) {
93
108
  this.namedDataProvider = new StructuredDataNamedDataProvider({
94
109
  get: (target, name) => this.registries.get(target)?.get(name)
@@ -104,12 +119,18 @@ export class StructuredDataExtension implements TurboWarpExtension {
104
119
  runtime,
105
120
  this.namedDataRegistry
106
121
  );
107
- runtime.on?.('RUNTIME_DISPOSED', () => {
108
- void this.dispose();
109
- });
122
+ runtime.on?.('RUNTIME_DISPOSED', onDispose);
110
123
  }
111
124
  }
112
- Scratch.vm?.runtime?.on?.('PROJECT_STOP_ALL', () => this.clearRuntimeState());
125
+ runtime?.on?.('PROJECT_STOP_ALL', onStop);
126
+ } catch (error) {
127
+ this.releaseRuntimeListeners();
128
+ unregisterFixed();
129
+ this.unregisterSourceEditors();
130
+ this.unbindNamedDataRegistryLifecycle?.();
131
+ void this.namedDataRegistration?.unregister().catch(() => {});
132
+ throw error;
133
+ }
113
134
  }
114
135
 
115
136
  public getNamedDataProvider(): StructuredDataNamedDataProvider | undefined {
@@ -121,6 +142,10 @@ export class StructuredDataExtension implements TurboWarpExtension {
121
142
  }
122
143
 
123
144
  public async dispose(): Promise<void> {
145
+ if (this.disposed) return;
146
+ this.disposed = true;
147
+ this.releaseRuntimeListeners();
148
+ this.unregisterFixedInputEditors();
124
149
  this.unregisterSourceEditors();
125
150
  await this.namedDataRegistration?.unregister();
126
151
  this.unbindNamedDataRegistryLifecycle?.();
@@ -0,0 +1,67 @@
1
+ import {parseFixedNumberInput} from '@kubohiroya/turbowarp-extension-data-editor';
2
+ import type {FixedInputEditorOptions} from '@kubohiroya/turbowarp-extension-data-editor';
3
+ import {extensionConfig} from './config.js';
4
+ import {fixedInputSchemaBundle} from './fixed-input-schema.js';
5
+
6
+
7
+ export const fixedInputEditorDefinitions: readonly FixedInputEditorOptions[] = [{
8
+ apiVersion: 1, extensionId: extensionConfig.id, editorId: 'forEachAtPath-fixed-inputs',
9
+ opcodes: [`${extensionConfig.id}_forEachAtPath`],
10
+ contractId: 'turbowarp-structured-data:forEachAtPath.MAX', contractRevision: 'fixed-inputs-v1',
11
+ bindings: [{argument: 'MAX', schemaBundle: fixedInputSchemaBundle,
12
+ control: {kind: 'number', label: 'MAX'},
13
+ read(value) {
14
+ if (typeof value !== 'string' && typeof value !== 'number') throw new TypeError('Expected a literal maximum.');
15
+ if (typeof value === 'string' && value.trim() === '') throw new TypeError('Blank maximum stays readonly.');
16
+ // Existing nonempty finite Number aliases may project; no-op saving retains the exact raw token.
17
+ const number = Number(value);
18
+ if (!Number.isFinite(number)) throw new TypeError('Maximum must be finite.');
19
+ return number;
20
+ },
21
+ encode(value) {
22
+ const parsed = parseFixedNumberInput(value);
23
+ if (parsed.kind !== 'valid') throw new TypeError('Expected a finite numeric token.');
24
+ return parsed.token;
25
+ },
26
+ validateCandidate(value) {
27
+ const parsed = parseFixedNumberInput(value);
28
+ return parsed.kind === 'valid' && Number.isInteger(parsed.value) && parsed.value >= 1 && parsed.value <= 1000 ? []
29
+ : [{path: '/inputs/MAX', message: 'Maximum must be an integer from 1 to 1000.'}];
30
+ }
31
+ }],
32
+ validationContext: () => ({consumerId: extensionConfig.id, nowMs: Date.now(), settings: null})
33
+ }];
34
+
35
+ interface RuntimeLifetime {
36
+ on?: (event: string, listener: () => void) => void;
37
+ off?: (event: string, listener: () => void) => void;
38
+ }
39
+
40
+ /** Register metadata only; the GUI startup capability owns editor activation. */
41
+ export function registerFixedInputEditors(
42
+ scratch: unknown, definitions: readonly FixedInputEditorOptions[], runtimeOverride?: RuntimeLifetime
43
+ ): () => void {
44
+ const host = scratch as {gui?: {blockEditors?: {apiVersion?: number;
45
+ registerFixedInputEditor?: (options: FixedInputEditorOptions) => () => void}};
46
+ vm?: {runtime?: RuntimeLifetime}};
47
+ const api = host.gui?.blockEditors;
48
+ if (!definitions.length || api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== 'function') return () => {};
49
+ const runtime = runtimeOverride ?? host.vm?.runtime;
50
+ const removers: (() => void)[] = [];
51
+ let active = true;
52
+ const dispose = () => {
53
+ if (!active) return;
54
+ active = false;
55
+ runtime?.off?.('RUNTIME_DISPOSED', dispose);
56
+ for (const remove of removers.splice(0).reverse()) remove();
57
+ };
58
+ try {
59
+ for (const definition of definitions) {
60
+ const remove = api.registerFixedInputEditor.call(api, definition);
61
+ if (typeof remove !== 'function') throw new TypeError('Invalid fixed input registration disposer.');
62
+ removers.push(remove);
63
+ }
64
+ runtime?.on?.('RUNTIME_DISPOSED', dispose);
65
+ } catch (error) {dispose(); throw error;}
66
+ return dispose;
67
+ }
@@ -0,0 +1,16 @@
1
+ import type {SchemaBundle} from '@kubohiroya/turbowarp-extension-data-editor/schema';
2
+ import document from '../schemas/block-editor-fixed-inputs.schema.json' with {type: 'json'};
3
+
4
+ const source = {repository: 'turbowarp-structured-data', path: 'schemas/block-editor-fixed-inputs.schema.json',
5
+ symbol: '#/$defs/forEachAtPath/properties/MAX'};
6
+
7
+ /** The on-disk owner Schema is canonical; this is its single-field projection. */
8
+ export const fixedInputSchemaBundle: SchemaBundle = {
9
+ identity: {ownerId: 'turbowarp-structured-data', contractId: 'forEachAtPath.MAX', revision: 'fixed-inputs-v1', variantId: 'default'},
10
+ dialect: '2020-12',
11
+ root: {retrievalUri: 'https://kubohiroya.github.io/turbowarp-structured-data/schemas/forEachAtPath-MAX.schema.json',
12
+ document: document.$defs.forEachAtPath.properties.MAX, artifact: {role: 'canonical-schema', source}},
13
+ resources: [],
14
+ provenance: {canonicalSources: [source, {repository: 'turbowarp-structured-data', path: 'src/core.ts', symbol: 'iterationEntries'}],
15
+ generatedFiles: [{repository: 'turbowarp-structured-data', path: 'docs/schemas/block-editor-fixed-inputs.schema.json'}], referenceSchemas: [], generationSources: [{repository: 'turbowarp-structured-data', path: 'scripts/generate-fixed-input-docs.mjs'}]}
16
+ };
@@ -1,4 +1,3 @@
1
- import {compileSchemaBundle, validateSchemaInput} from '@kubohiroya/turbowarp-extension-data-editor/schema';
2
1
  import type {SchemaBundle, SchemaDocument, SchemaJsonValue, SourceBinding, SourceEditorOptions} from '@kubohiroya/turbowarp-extension-data-editor/schema';
3
2
 
4
3
  export function canonicalBundle(ownerId: string, contractId: string, document: SchemaDocument, ownerSource: {path: string; symbol: string}): SchemaBundle {
@@ -16,21 +15,25 @@ export function canonicalBundle(ownerId: string, contractId: string, document: S
16
15
  export function sourceBinding(argument: string, bundle: SchemaBundle,
17
16
  read: (source: string) => unknown, check: (source: string) => unknown = read,
18
17
  format: 'json' | 'yaml' = 'json', uiSchema?: SourceBinding['variants'][number]['uiSchema']): SourceBinding {
19
- const compiled = compileSchemaBundle(bundle);
20
18
  return {
21
19
  argument,
22
20
  variants: [{variantId: 'default', format, schemaBundle: bundle, ...(uiSchema ? {uiSchema} : {})}],
23
21
  decode(source) {return {source, format, variantId: 'default', data: read(source) as SchemaJsonValue};},
24
22
  validateSource(source, context) {
25
- if (compiled.kind !== 'compiled') return {source, result: {kind: 'schema-error', diagnostics: [{code: 'source-schema-invalid'}]}};
23
+ try {read(source);}
24
+ catch {return {source, result: {kind: 'invalid', diagnostics: [{code: 'source-decode-invalid'}]}};}
26
25
  try {
27
- const data = read(source) as SchemaJsonValue;
28
- return {source, result: validateSchemaInput(compiled.validator, data, {context,
29
- ownerValidate: () => {
30
- try {check(source); return {kind: 'valid', issues: []};}
31
- catch {return {kind: 'invalid', issues: [{dataPath: '', ruleId: 'source-owner-invalid'}]};}
32
- }})};
33
- } catch {return {source, result: {kind: 'invalid', diagnostics: [{code: 'source-decode-invalid'}]}};}
26
+ const descriptors = Object.getOwnPropertyDescriptors(context);
27
+ if (typeof descriptors.consumerId?.value !== 'string' ||
28
+ typeof descriptors.nowMs?.value !== 'number' || !Number.isFinite(descriptors.nowMs.value) ||
29
+ !descriptors.settings || !('value' in descriptors.settings)) {
30
+ return {source, result: {kind: 'execution-error', diagnostics: [{code: 'owner-context-invalid'}]}};
31
+ }
32
+ } catch {return {source, result: {kind: 'execution-error', diagnostics: [{code: 'owner-context-invalid'}]}};}
33
+ // The GUI checks Schema constraints; the owner checks the exact source without compiling.
34
+ try {check(source);}
35
+ catch {return {source, result: {kind: 'invalid', diagnostics: [{code: 'source-owner-invalid'}]}};}
36
+ return {source, result: {kind: 'valid', diagnostics: []}};
34
37
  }
35
38
  };
36
39
  }
@@ -181,3 +181,16 @@ release consistency checkでは、exact-version README/CDN例に加え、`docs
181
181
 
182
182
  third-party componentを含む場合は
183
183
  [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)に記載します。
184
+
185
+ ## 固定引数の編集フォーム(S3-4)
186
+
187
+ 共通エディターの起動時フラグ `blockEditorFixedInputs` とDialogsがONのGUIでは、`forEachAtPath` の `MAX` だけを固定フォームとして登録します。旧GUI・フラグOFFでは追加登録を行いません。Schemaの正本は `schemas/block-editor-fixed-inputs.schema.json#/$defs/forEachAtPath/properties/MAX`、codecと登録は `src/fixed-input-editors.ts` です。既存raw値・未変更alias・未知enumは無操作保存で書き換えず、reporterを評価しません。明示変更だけを検査し、他の引数やブロック宣言、保存形式を変更しません。
188
+
189
+ MAXは整数1〜1000です。新しい入力には共通の有限数値token規則も適用し、途中入力を保存しません。MVP flagがOFFなら登録せず、NAME/PATH・staticLiteral由来の接続可否を変更しません。
190
+
191
+ 登録はoptional API(apiVersion:1)に限定し、RUNTIME_DISPOSED/明示disposeで解除します。起動フラグOFF+再読込で従来経路へ切り戻せます。依存を戻す場合はmanifest/lock/実解決を一組で復旧します。npm公開や後続段階の動的候補は含みません。
192
+
193
+ Schemaが記述する契約の根拠は実getInfoの宣言と既存の純粋なruntime変換です。SchemaBundleのretrievalUriはこのローカル文書の識別子であり、URL配信済み・HTTP取得によるref解決を意味しません。Schemaは同梱文書だけでcompileします。
194
+ 既存MAXの非空finiteなNumber alias(例`001`・`+1`・`0x10`)はNumberによる表示投影だけを行い、無変更rawを保存し直しません。空白・非finite・投影不能値はreadonlyとし、0/defaultを補いません。新しい入力にはこれらaliasを許可せずNP-finite-new-input-v1を適用します。
195
+
196
+ 登録は拡張インスタンスの構築時に存在する任意の editor API を使用します。後からの API 追加や差し替えには自動追従しません。起動設定を変更した場合は GUI を再読み込みし、拡張インスタンスを再構築してください。
@@ -196,3 +196,16 @@ site and `blockIconURI` remains a `data:image/svg+xml;base64,...` URI.
196
196
 
197
197
  Third-party components, when present, are listed in
198
198
  [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
199
+
200
+ ## 固定引数の編集フォーム(S3-4)
201
+
202
+ 共通エディターの起動時フラグ `blockEditorFixedInputs` とDialogsがONのGUIでは、`forEachAtPath` の `MAX` だけを固定フォームとして登録します。旧GUI・フラグOFFでは追加登録を行いません。Schemaの正本は `schemas/block-editor-fixed-inputs.schema.json#/$defs/forEachAtPath/properties/MAX`、codecと登録は `src/fixed-input-editors.ts` です。既存raw値・未変更alias・未知enumは無操作保存で書き換えず、reporterを評価しません。明示変更だけを検査し、他の引数やブロック宣言、保存形式を変更しません。
203
+
204
+ MAXは整数1〜1000です。新しい入力には共通の有限数値token規則も適用し、途中入力を保存しません。MVP flagがOFFなら登録せず、NAME/PATH・staticLiteral由来の接続可否を変更しません。
205
+
206
+ 登録はoptional API(apiVersion:1)に限定し、RUNTIME_DISPOSED/明示disposeで解除します。起動フラグOFF+再読込で従来経路へ切り戻せます。依存を戻す場合はmanifest/lock/実解決を一組で復旧します。npm公開や後続段階の動的候補は含みません。
207
+
208
+ Schemaが記述する契約の根拠は実getInfoの宣言と既存の純粋なruntime変換です。SchemaBundleのretrievalUriはこのローカル文書の識別子であり、URL配信済み・HTTP取得によるref解決を意味しません。Schemaは同梱文書だけでcompileします。
209
+ 既存MAXの非空finiteなNumber alias(例`001`・`+1`・`0x10`)はNumberによる表示投影だけを行い、無変更rawを保存し直しません。空白・非finite・投影不能値はreadonlyとし、0/defaultを補いません。新しい入力にはこれらaliasを許可せずNP-finite-new-input-v1を適用します。
210
+
211
+ Registration uses the optional editor API available when the extension instance is constructed. Adding or replacing that API later is not followed automatically; reload the GUI and reconstruct the extension instance after changing the startup configuration.