@dolphy-app/create-extension 0.2.0 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +10 -8
  2. package/dist/cli/main.js +258 -162
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,23 +1,25 @@
1
1
  # @dolphy-app/create-extension
2
2
 
3
- Генератор проекта расширения: create-dolphy-extension <каталог>
3
+ Extension project generator: create-dolphy-extension <directory>
4
4
 
5
- Версия пакетов равна версии приложения Dolphy, из релиза которого они опубликованы (0.2.0).
5
+ The package version equals the version of the Dolphy app release it was published from (0.3.0).
6
6
 
7
- Генератор проекта расширения.
7
+ Generator of an extension project: `src/index.ts` with the host and a
8
+ view, a manifest, tests, and typed ids (`.dolphy/ids.d.ts`, generated from
9
+ `extension.json`).
8
10
 
9
11
  ```sh
10
- npx @dolphy-app/create-extension <каталог>
12
+ npx @dolphy-app/create-extension <directory>
11
13
  ```
12
14
 
13
- Проект получает тесты, сборку и README.
15
+ The project gets tests, a build and a README.
14
16
 
15
- ## Установка
17
+ ## Installation
16
18
 
17
19
  ```sh
18
20
  npm install @dolphy-app/create-extension
19
21
  ```
20
22
 
21
- ## Документация
23
+ ## Documentation
22
24
 
23
- [Расширения Dolphy](https://github.com/dolphy-app/dolphy/blob/main/docs/design/extensions.md).
25
+ [Dolphy extensions](https://github.com/dolphy-app/dolphy/blob/main/docs/design/extensions.md).
package/dist/cli/main.js CHANGED
@@ -4,12 +4,39 @@ import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
4
4
 
5
5
  //#region packages/extension-api/src/index.ts
6
6
  const EXTENSION_ID_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/;
7
- /** Тег элемента по умолчанию: `dolphy.sql` → `dolphy-sql-answer`. */
8
- const defaultElementName = (id) => `${id.replaceAll(".", "-")}-answer`;
7
+ /** Limits on commands and panels (R1, R3); they match those checked by the manifest, host, and engine. */
8
+ const EXTENSION_COMMAND_LIMITS = Object.freeze({
9
+ /** Commands per extension. */
10
+ commands: 64,
11
+ /** Panels per extension. */
12
+ panels: 8,
13
+ titleLength: 60,
14
+ categoryLength: 40,
15
+ descriptionLength: 200,
16
+ /** `JSON.stringify(args).length` at the engine boundary. */
17
+ argsChars: 2e5,
18
+ /** JSON text of the result in UTF-8 bytes. */
19
+ resultBytes: 65536,
20
+ /** Length of `notify` in UTF-16 code units. */
21
+ notifyChars: 500,
22
+ /** Handler budget, ms. */
23
+ handlerMs: 1e4
24
+ });
25
+ /** Extension storage limits (R2); they match the engine's limits, which enforces them. */
26
+ const EXTENSION_STORAGE_LIMITS = Object.freeze({
27
+ /** Key length in UTF-16 code units. */
28
+ keyLength: 128,
29
+ /** JSON text of a single value in UTF-8 bytes. */
30
+ valueBytes: 65536,
31
+ /** Number of keys. */
32
+ keys: 256,
33
+ /** Sum of JSON texts of all values in UTF-8 bytes. */
34
+ totalBytes: 1048576
35
+ });
9
36
 
10
37
  //#endregion
11
38
  //#region packages/create-extension/src/template.ts
12
- /** Версия расширения в шаблоне и его умолчание для `apiVersion`. */
39
+ /** Extension version in the template and its default for `apiVersion`. */
13
40
  const INITIAL_VERSION = "0.1.0";
14
41
  const lines = (parts) => `${parts.join("\n")}\n`;
15
42
  const packageJson = ({ id, dependencies }) => `${JSON.stringify({
@@ -20,6 +47,8 @@ const packageJson = ({ id, dependencies }) => `${JSON.stringify({
20
47
  scripts: {
21
48
  build: "dolphy-ext build",
22
49
  dev: "dolphy-ext build --watch",
50
+ types: "dolphy-ext types",
51
+ typecheck: "dolphy-ext types && tsc",
23
52
  validate: `dolphy-ext validate dist-ext/${id}`,
24
53
  test: "vitest run"
25
54
  },
@@ -48,13 +77,14 @@ const tsconfigJson = () => lines([
48
77
  " \"skipLibCheck\": true,",
49
78
  " \"noEmit\": true",
50
79
  " },",
51
- " \"include\": [\"src\", \"test\"]",
80
+ " \"include\": [\"src\", \"test\", \".dolphy/ids.d.ts\"]",
52
81
  "}"
53
82
  ]);
54
83
  const manifestJson = (id) => `{
55
84
  "id": "${id}",
56
85
  "version": "${INITIAL_VERSION}",
57
86
  "apiVersion": 1,
87
+ "tags": ["learning"],
58
88
  "contributes": {
59
89
  "exerciseTypes": [
60
90
  {
@@ -70,26 +100,53 @@ const manifestJson = (id) => `{
70
100
  },
71
101
  "answerSchema": { "type": "string" }
72
102
  }
103
+ ],
104
+ "settings": [
105
+ {
106
+ "id": "${id}.trim",
107
+ "type": "boolean",
108
+ "label": "Ignore spaces around the answer",
109
+ "default": true
110
+ }
111
+ ],
112
+ "commands": [
113
+ { "id": "${id}.status", "title": "Show how answers are compared" }
73
114
  ]
74
115
  }
75
116
  }
76
117
  `;
77
- const mainTs = (id) => `import { defineExerciseType, defineExtension } from '@dolphy-app/extension-sdk';
118
+ const indexTs = (id) => `import {
119
+ defineAnswerView,
120
+ defineExerciseType,
121
+ defineExtension,
122
+ inActivate,
123
+ notify,
124
+ } from '@dolphy-app/extension-sdk';
125
+ import type { ExtensionViews } from '@dolphy-app/extension-sdk';
78
126
 
79
127
  interface Spec {
80
128
  expected: string;
81
129
  ignoreCase?: boolean;
82
130
  }
83
131
 
132
+ // filled from the setting in \`activate\`, read by the handlers below
133
+ const options = { trim: true };
134
+
84
135
  const matches = (answer: string, spec: Spec): boolean => {
136
+ const given = options.trim ? answer.trim() : answer;
85
137
  if (spec.ignoreCase === true) {
86
- return answer.toLowerCase() === spec.expected.toLowerCase();
138
+ return given.toLowerCase() === spec.expected.toLowerCase();
87
139
  }
88
- return answer === spec.expected;
140
+ return given === spec.expected;
89
141
  };
90
142
 
91
- // схемы из extension.json уже проверили spec и ответ до вызова обработчиков
92
- export default defineExtension({
143
+ // extension code: runs in the extension process of the app
144
+ // the schemas from extension.json have already checked \`spec\` and the answer
145
+ // before the handlers run
146
+ // the ids come from extension.json: \`dolphy-ext types\` (and every build)
147
+ // writes them to .dolphy/ids.d.ts, so a misspelt id, a declared id without a
148
+ // handler or an undeclared setting fails \`pnpm typecheck\`
149
+ export const host = defineExtension({
93
150
  exerciseTypes: {
94
151
  '${id}': defineExerciseType<Spec, string, Record<string, never>>({
95
152
  project: () => ({}),
@@ -100,50 +157,72 @@ export default defineExtension({
100
157
  referenceAnswer: ({ spec }) => spec.expected,
101
158
  }),
102
159
  },
160
+ // this command is registered in \`activate\`: the marker names the id there
161
+ commands: { '${id}.status': inActivate },
162
+ activate(ctx) {
163
+ options.trim = ctx.settings.get('${id}.trim');
164
+ ctx.settings.onDidChange((change) => {
165
+ if (change.id === '${id}.trim') options.trim = change.value;
166
+ });
167
+ ctx.commands.register('${id}.status', () =>
168
+ notify(
169
+ options.trim
170
+ ? 'Answers are compared without the spaces around them.'
171
+ : 'Answers are compared exactly as typed.',
172
+ ),
173
+ );
174
+ },
103
175
  });
176
+
177
+ // the answer input: runs in the app window; the build defines the custom
178
+ // element with the tag from extension.json
179
+ export const views = {
180
+ '${id}': defineAnswerView((api, initial) => {
181
+ const input = document.createElement('input');
182
+ input.type = 'text';
183
+ input.spellcheck = false;
184
+ if (api.label !== null) input.setAttribute('aria-label', api.label);
185
+
186
+ const applyValue = (value: unknown) => {
187
+ input.value = typeof value === 'string' ? value : '';
188
+ };
189
+ let appliedValue = initial.value;
190
+ applyValue(appliedValue);
191
+ input.disabled = initial.disabled;
192
+
193
+ input.addEventListener('input', () => {
194
+ api.setAnswer(input.value, input.value.trim().length > 0);
195
+ });
196
+ input.addEventListener('keydown', (event) => {
197
+ if (event.key === 'Enter') api.submit();
198
+ });
199
+ api.root.append(input);
200
+
201
+ return {
202
+ update: (props) => {
203
+ input.disabled = props.disabled;
204
+ // apply the value only when the app really changed it
205
+ if (props.value !== appliedValue) {
206
+ appliedValue = props.value;
207
+ applyValue(appliedValue);
208
+ }
209
+ },
210
+ };
211
+ }),
212
+ } satisfies ExtensionViews;
104
213
  `;
105
- const viewTs = (id) => `import { defineAnswerElement } from '@dolphy-app/extension-sdk';
106
-
107
- defineAnswerElement('${defaultElementName(id)}', (api, initial) => {
108
- const input = document.createElement('input');
109
- input.type = 'text';
110
- input.spellcheck = false;
111
- if (api.label !== null) input.setAttribute('aria-label', api.label);
112
-
113
- const applyValue = (value: unknown) => {
114
- input.value = typeof value === 'string' ? value : '';
115
- };
116
- let appliedValue = initial.value;
117
- applyValue(appliedValue);
118
- input.disabled = initial.disabled;
119
-
120
- input.addEventListener('input', () => {
121
- api.setAnswer(input.value, input.value.trim().length > 0);
122
- });
123
- input.addEventListener('keydown', (event) => {
124
- if (event.key === 'Enter') api.submit();
125
- });
126
- api.root.append(input);
127
-
128
- return {
129
- update: (props) => {
130
- input.disabled = props.disabled;
131
- // value применяется, только когда приложение его действительно сменило
132
- if (props.value !== appliedValue) {
133
- appliedValue = props.value;
134
- applyValue(appliedValue);
135
- }
136
- },
137
- };
138
- });
139
- `;
140
- const mainTestTs = (id) => `import {
214
+ const indexTestTs = (id) => `// @vitest-environment happy-dom
215
+ import type { SettingContribution } from '@dolphy-app/extension-sdk';
216
+ import {
217
+ createMemorySettings,
141
218
  createSchemaValidator,
219
+ loadCommands,
142
220
  loadExerciseType,
221
+ loadView,
143
222
  } from '@dolphy-app/extension-sdk/testing';
144
223
  import { afterEach, describe, expect, it } from 'vitest';
145
224
  import manifest from '../extension.json';
146
- import module from '../src/main.ts';
225
+ import { host, views } from '../src/index.ts';
147
226
 
148
227
  const [contribution] = manifest.contributes.exerciseTypes;
149
228
  const validateSpec = createSchemaValidator(contribution.specSchema);
@@ -151,24 +230,35 @@ const validateAnswer = createSchemaValidator(contribution.answerSchema);
151
230
 
152
231
  const spec = { expected: 'Hello' };
153
232
 
154
- const disposables: { dispose(): Promise<void> }[] = [];
233
+ const disposables: { dispose(): unknown }[] = [];
155
234
  afterEach(async () => {
156
235
  await Promise.all(disposables.splice(0).map((item) => item.dispose()));
157
236
  });
158
237
 
159
- const load = async () => {
160
- const type = await loadExerciseType(module, '${id}');
238
+ const newSettings = () =>
239
+ createMemorySettings(manifest.contributes.settings as SettingContribution[]);
240
+
241
+ const load = async (settings = newSettings()) => {
242
+ const type = await loadExerciseType(host, '${id}', { settings });
161
243
  disposables.push(type);
162
244
  return type;
163
245
  };
164
246
 
165
- describe('${id}: обработчик', () => {
166
- it('project не раскрывает эталон', async () => {
247
+ const mount = async (label?: string) => {
248
+ const view = await loadView(views, '${id}', label === undefined ? {} : { label });
249
+ disposables.push(view);
250
+ const input = view.query<HTMLInputElement>('input');
251
+ if (input === null) throw new Error('no input');
252
+ return { view, input };
253
+ };
254
+
255
+ describe('${id}: handler', () => {
256
+ it('project does not reveal the reference', async () => {
167
257
  const type = await load();
168
258
  expect(await type.project(spec)).toEqual({});
169
259
  });
170
260
 
171
- it('grade: совпадение засчитывается, расхождение нет', async () => {
261
+ it('grade: a match passes, a mismatch does not', async () => {
172
262
  const type = await load();
173
263
  expect(await type.grade({ spec, answer: 'Hello' })).toEqual({
174
264
  outcome: 'passed',
@@ -179,7 +269,7 @@ describe('${id}: обработчик', () => {
179
269
  });
180
270
  });
181
271
 
182
- it('grade: ignoreCase отключает различие регистров', async () => {
272
+ it('grade: ignoreCase turns case sensitivity off', async () => {
183
273
  const type = await load();
184
274
  const relaxed = { ...spec, ignoreCase: true };
185
275
  expect(await type.grade({ spec: relaxed, answer: 'hELLO' })).toEqual({
@@ -187,7 +277,7 @@ describe('${id}: обработчик', () => {
187
277
  });
188
278
  });
189
279
 
190
- it('referenceAnswer сам проходит проверку', async () => {
280
+ it('referenceAnswer passes the check itself', async () => {
191
281
  const type = await load();
192
282
  const reference = await type.referenceAnswer(spec);
193
283
  expect(reference).toEqual({ found: true, answer: 'Hello' });
@@ -198,158 +288,166 @@ describe('${id}: обработчик', () => {
198
288
  });
199
289
  });
200
290
 
201
- describe('${id}: схемы', () => {
291
+ describe('${id}: settings and commands', () => {
292
+ it('the trim setting decides whether the spaces around an answer count', async () => {
293
+ const settings = newSettings();
294
+ const type = await load(settings);
295
+ expect(await type.grade({ spec, answer: ' Hello ' })).toEqual({
296
+ outcome: 'passed',
297
+ });
298
+ await settings.set('${id}.trim', false);
299
+ expect(await type.grade({ spec, answer: ' Hello ' })).toEqual({
300
+ outcome: 'failed',
301
+ reason: 'mismatch',
302
+ });
303
+ });
304
+
305
+ it('the status command reports the current mode', async () => {
306
+ const settings = newSettings();
307
+ const commands = await loadCommands(host, {
308
+ declaredCommands: ['${id}.status'],
309
+ settings,
310
+ });
311
+ disposables.push(commands);
312
+ expect(await commands.run('${id}.status')).toEqual({
313
+ kind: 'notify',
314
+ text: 'Answers are compared without the spaces around them.',
315
+ });
316
+ await settings.set('${id}.trim', false);
317
+ expect(await commands.run('${id}.status')).toEqual({
318
+ kind: 'notify',
319
+ text: 'Answers are compared exactly as typed.',
320
+ });
321
+ });
322
+ });
323
+
324
+ describe('${id}: schemas', () => {
202
325
  it.each([[{ expected: 'a' }], [{ expected: 'a', ignoreCase: true }]])(
203
- 'spec %j допустим',
326
+ 'spec %j is valid',
204
327
  (value) => {
205
328
  expect(validateSpec(value)).toEqual([]);
206
329
  },
207
330
  );
208
331
 
209
332
  it.each([
210
- ['нет expected', {}],
211
- ['пустой expected', { expected: '' }],
212
- ['ignoreCase не boolean', { expected: 'a', ignoreCase: 'yes' }],
213
- ['лишнее поле', { expected: 'a', extra: 1 }],
214
- ])('spec: %s отклоняется', (_name, value) => {
333
+ ['no expected', {}],
334
+ ['empty expected', { expected: '' }],
335
+ ['ignoreCase is not a boolean', { expected: 'a', ignoreCase: 'yes' }],
336
+ ['an extra field', { expected: 'a', extra: 1 }],
337
+ ])('spec: %s is rejected', (_name, value) => {
215
338
  expect(validateSpec(value)).not.toEqual([]);
216
339
  });
217
340
 
218
- it('answer: строка допустима, число нет', () => {
341
+ it('answer: a string is valid, a number is not', () => {
219
342
  expect(validateAnswer('text')).toEqual([]);
220
343
  expect(validateAnswer(42)).not.toEqual([]);
221
344
  });
222
345
  });
223
- `;
224
- const viewTestTs = (id) => {
225
- const tag = defaultElementName(id);
226
- return `// @vitest-environment happy-dom
227
- import { ANSWER_EVENT } from '@dolphy-app/extension-sdk';
228
- import type { AnswerChangeDetail } from '@dolphy-app/extension-sdk';
229
- import { afterEach, describe, expect, it } from 'vitest';
230
- import '../src/view.ts';
231
-
232
- interface AnswerElement extends HTMLElement {
233
- view: unknown;
234
- value: unknown;
235
- disabled: boolean;
236
- }
237
-
238
- const flush = () => Promise.resolve();
239
-
240
- const mountElement = async (label: string | null = null) => {
241
- const element = document.createElement('${tag}') as AnswerElement;
242
- if (label !== null) element.setAttribute('aria-label', label);
243
- document.body.append(element);
244
- await flush();
245
- const changes: AnswerChangeDetail[] = [];
246
- element.addEventListener(ANSWER_EVENT.change, (event) => {
247
- changes.push((event as CustomEvent<AnswerChangeDetail>).detail);
248
- });
249
- const input = element.shadowRoot?.querySelector('input');
250
- if (input === null || input === undefined) throw new Error('no input');
251
- return { element, input, changes };
252
- };
253
-
254
- afterEach(() => {
255
- document.body.replaceChildren();
256
- });
257
346
 
258
- describe('${tag}', () => {
259
- it('ввод текста сообщает ответ; пустой ввод неполный', async () => {
260
- const { input, changes } = await mountElement();
347
+ describe('${id}: view', () => {
348
+ it('typing reports the answer; an empty input is incomplete', async () => {
349
+ const { view, input } = await mount();
261
350
  input.value = 'Hello';
262
351
  input.dispatchEvent(new Event('input'));
263
352
  input.value = ' ';
264
353
  input.dispatchEvent(new Event('input'));
265
- expect(changes).toEqual([
354
+ expect(view.changes).toEqual([
266
355
  { value: 'Hello', complete: true },
267
356
  { value: ' ', complete: false },
268
357
  ]);
269
358
  });
270
359
 
271
- it('Enter отправляет ответ', async () => {
272
- const { element, input } = await mountElement();
273
- let submits = 0;
274
- element.addEventListener(ANSWER_EVENT.submit, () => void (submits += 1));
360
+ it('Enter submits the answer', async () => {
361
+ const { view, input } = await mount();
275
362
  input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
276
- expect(submits).toBe(1);
363
+ expect(view.submissions).toBe(1);
277
364
  });
278
365
 
279
- it('disabled блокирует поле', async () => {
280
- const { element, input } = await mountElement();
281
- element.disabled = true;
282
- await flush();
366
+ it('disabled blocks the input', async () => {
367
+ const { view, input } = await mount();
368
+ await view.update({ disabled: true });
283
369
  expect(input.disabled).toBe(true);
284
370
  });
285
371
 
286
- it('value восстанавливает ответ без событий', async () => {
287
- const { element, input, changes } = await mountElement();
288
- element.value = 'Hello';
289
- await flush();
372
+ it('value restores the answer without events', async () => {
373
+ const { view, input } = await mount();
374
+ await view.update({ value: 'Hello' });
290
375
  expect(input.value).toBe('Hello');
291
- expect(changes).toEqual([]);
376
+ expect(view.changes).toEqual([]);
292
377
  });
293
378
 
294
- it('aria-label хоста попадает на поле', async () => {
295
- const { input } = await mountElement('Ваш ответ');
296
- expect(input.getAttribute('aria-label')).toBe('Ваш ответ');
379
+ it('the aria-label of the host goes to the input', async () => {
380
+ const { input } = await mount('Your answer');
381
+ expect(input.getAttribute('aria-label')).toBe('Your answer');
297
382
  });
298
383
  });
299
384
  `;
300
- };
301
385
  const readme = (id) => lines([
302
386
  `# ${id}`,
303
387
  "",
304
- "Расширение Dolphy: вид задания «text match» (ученик вводит строку, она",
305
- "сравнивается с `spec.expected`). Сгенерировано `create-dolphy-extension`.",
388
+ "A Dolphy extension: the \"text match\" exercise type (the learner types a",
389
+ "string, it is compared with `spec.expected`), a setting and a command.",
390
+ "Generated by `create-dolphy-extension`.",
306
391
  "",
307
- "## Раскладка",
392
+ "## Layout",
308
393
  "",
309
- "- `extension.json` — манифест (схемы `spec` и ответа записаны прямо в нём);",
310
- "- `src/main.ts` — код расширения: `defineExtension` + `defineExerciseType`;",
311
- "- `src/view.ts` — элемент ввода ответа: `defineAnswerElement`;",
312
- "- `test/` — тесты обработчика и элемента (`vitest`, `happy-dom`).",
394
+ "- `extension.json` — the manifest (the `spec` and answer schemas are written",
395
+ " in it);",
396
+ "- `src/index.ts` — all the extension code: `host` (`defineExtension` +",
397
+ " `defineExerciseType`) and `views` (`defineAnswerView`); the build splits",
398
+ " it into `main.mjs` and `view.mjs`;",
399
+ "- `.dolphy/ids.d.ts` — generated from `extension.json` by",
400
+ " `dolphy-ext types` (and by every build): the ids the manifest declares,",
401
+ " as TypeScript types. Not committed. A misspelt id, a declared id without",
402
+ " a handler or a view, or `ctx.settings.get` of an undeclared setting fails",
403
+ " `pnpm typecheck`;",
404
+ "- `test/index.test.ts` — tests (`vitest`, `happy-dom`).",
313
405
  "",
314
- "## Цикл разработки",
406
+ "## Development loop",
315
407
  "",
316
408
  "```sh",
317
409
  "pnpm install",
318
- `pnpm dev # dolphy-ext build --watch: пересборка в dist-ext/${id}`,
410
+ `pnpm dev # dolphy-ext build --watch: rebuilds into dist-ext/${id}`,
319
411
  "```",
320
412
  "",
321
- "Запустите приложение с корнем режима разработчика — каталогом `dist-ext`",
322
- "этого проекта (абсолютный путь):",
413
+ "Start the app with the developer root pointing at the `dist-ext` directory",
414
+ "of this project (an absolute path):",
323
415
  "",
324
416
  "```sh",
325
- "DOLPHY_DEV_EXTENSIONS=<путь к проекту>/dist-ext pnpm dev # из репозитория Dolphy",
417
+ "DOLPHY_DEV_EXTENSIONS=<path to the project>/dist-ext pnpm dev # from the Dolphy repository",
326
418
  "```",
327
419
  "",
328
- "Правка файла в `dist-ext` перезапускает хосты и перезагружает окно.",
329
- "Ошибки загрузки видны в «Настройки → Расширения».",
420
+ "A change to a file in `dist-ext` is applied live: the window does not",
421
+ "reload, mounted answer inputs are recreated. Load errors are shown in",
422
+ "\"Settings → Extensions\".",
330
423
  "",
331
- "## Сборка, проверка, тесты",
424
+ "## Build, check, test",
332
425
  "",
333
426
  "```sh",
334
- `pnpm build # dist-ext/${id}`,
335
- "pnpm validate # тот же разбор манифеста, что делает приложение",
427
+ `pnpm build # dist-ext/${id}`,
428
+ "pnpm validate # the same manifest parsing the app does",
429
+ "pnpm typecheck # writes .dolphy/ids.d.ts, then tsc",
336
430
  "pnpm test",
337
431
  "```",
338
432
  "",
339
- "## Установка вручную",
433
+ "## Manual installation",
340
434
  "",
341
- `Скопируйте каталог \`dist-ext/${id}\` в \`<userData>/extensions/\``,
342
- "и перезапустите приложение. Установки из приложения пока нет."
435
+ `Copy the \`dist-ext/${id}\` directory to \`<userData>/extensions/\``,
436
+ "and restart the app. There is no installation from the app yet."
437
+ ]);
438
+ const gitignore = () => lines([
439
+ "node_modules",
440
+ "dist-ext",
441
+ ".dolphy"
343
442
  ]);
344
- const gitignore = () => lines(["node_modules", "dist-ext"]);
345
443
 
346
444
  //#endregion
347
445
  //#region packages/create-extension/src/generate.ts
348
- /** Совпадает с ограничением манифеста (`parseManifest`). */
446
+ /** Matches the manifest limit (`parseManifest`). */
349
447
  const MAX_ID_CHARS = 64;
350
- /** Без сборки (исходники, `--local`) версия пакетов условная. */
448
+ /** Without a build (sources, `--local`) the package version is a placeholder. */
351
449
  const UNPUBLISHED_VERSION = "^0.0.0";
352
- const builtPackageVersion = () => "0.2.0";
450
+ const builtPackageVersion = () => "0.3.0";
353
451
  var GenerateError = class extends Error {
354
452
  code;
355
453
  constructor(code, message) {
@@ -395,17 +493,15 @@ const assertEmpty = async (dir) => {
395
493
  if (info === null) return;
396
494
  if ((info.isDirectory() ? await readdir(dir) : ["(file)"]).length > 0) throw new GenerateError("target-not-empty", `${dir} is not empty`);
397
495
  };
398
- /** Файлы проекта: относительный путь → содержимое. */
496
+ /** Project files: relative path → content. */
399
497
  const renderProject = (input) => {
400
498
  const { id } = input;
401
499
  return /* @__PURE__ */ new Map([
402
500
  ["package.json", packageJson(input)],
403
501
  ["tsconfig.json", tsconfigJson()],
404
502
  ["extension.json", manifestJson(id)],
405
- ["src/main.ts", mainTs(id)],
406
- ["src/view.ts", viewTs(id)],
407
- ["test/main.test.ts", mainTestTs(id)],
408
- ["test/view.test.ts", viewTestTs(id)],
503
+ ["src/index.ts", indexTs(id)],
504
+ ["test/index.test.ts", indexTestTs(id)],
409
505
  ["README.md", readme(id)],
410
506
  [".gitignore", gitignore()]
411
507
  ]);
@@ -441,11 +537,11 @@ const EXIT_PROBLEMS = 1;
441
537
  const EXIT_USAGE = 2;
442
538
  const USAGE = `usage: create-dolphy-extension <dir> [--id <id>] [--local <repoRoot>]
443
539
 
444
- <dir> каталог нового проекта (должен быть пуст или отсутствовать)
445
- --id <id> id расширения (по умолчанию — kebab-case имени каталога)
446
- --local <repoRoot> корень репозитория Dolphy: @dolphy-app/extension-sdk и
447
- @dolphy-app/extension-tools подключаются как link:<repoRoot>/packages/...
448
- --help эта справка
540
+ <dir> new project directory (must be empty or not exist)
541
+ --id <id> extension id (default: kebab-case of the directory name)
542
+ --local <repoRoot> Dolphy repository root: @dolphy-app/extension-sdk and
543
+ @dolphy-app/extension-tools are linked as link:<repoRoot>/packages/...
544
+ --help show this help
449
545
  `;
450
546
  const parseArgs = (argv) => {
451
547
  if (argv.includes("--help") || argv.includes("-h")) return { help: true };
@@ -455,40 +551,40 @@ const parseArgs = (argv) => {
455
551
  const arg = argv[i];
456
552
  if (arg === "--id" || arg === "--local") {
457
553
  const value = argv[++i];
458
- if (value === void 0) return { usageError: `${arg} требует значение` };
554
+ if (value === void 0) return { usageError: `${arg} requires a value` };
459
555
  values.set(arg, value);
460
- } else if (arg.startsWith("-")) return { usageError: `неизвестный флаг: ${arg}` };
556
+ } else if (arg.startsWith("-")) return { usageError: `unknown flag: ${arg}` };
461
557
  else positional.push(arg);
462
558
  }
463
559
  const [dir, ...extra] = positional;
464
- if (dir === void 0) return { usageError: "не указан каталог" };
465
- if (extra.length > 0) return { usageError: `лишние аргументы: ${extra.join(" ")}` };
560
+ if (dir === void 0) return { usageError: "no directory given" };
561
+ if (extra.length > 0) return { usageError: `unexpected arguments: ${extra.join(" ")}` };
466
562
  return {
467
563
  dir,
468
564
  id: values.get("--id"),
469
565
  local: values.get("--local")
470
566
  };
471
567
  };
472
- const PLACEHOLDER_NOTE = "\nЗамечание: @dolphy-app/extension-sdk и @dolphy-app/extension-tools не опубликованы, версия ^0.0.0 не установится.\nУкажите пути к репозиторию Dolphy: create-dolphy-extension <dir> --local <repoRoot>.\n";
568
+ const PLACEHOLDER_NOTE = "\nNote: @dolphy-app/extension-sdk and @dolphy-app/extension-tools are not published, version ^0.0.0 cannot be installed.\nPoint to the Dolphy repository: create-dolphy-extension <dir> --local <repoRoot>.\n";
473
569
  const installNote = ({ isLocal, isPublished }) => {
474
570
  if (isLocal) return "";
475
571
  return isPublished ? "" : PLACEHOLDER_NOTE;
476
572
  };
477
573
  const nextSteps = (result) => {
478
574
  const { dir, id } = result;
479
- return `\nДальше:\n${[
575
+ return `\nNext steps:\n${[
480
576
  ` cd ${dir}`,
481
577
  " pnpm install",
482
578
  " pnpm test",
483
- ` pnpm dev # пересборка в dist-ext/${id}`,
579
+ ` pnpm dev # rebuilds into dist-ext/${id}`,
484
580
  "",
485
- "Запуск приложения с вашим расширением (из репозитория Dolphy):",
581
+ "Run the app with your extension (from the Dolphy repository):",
486
582
  ` DOLPHY_DEV_EXTENSIONS=${path.join(dir, "dist-ext")} pnpm dev`
487
583
  ].join("\n")}\n${installNote(result)}`;
488
584
  };
489
585
  /**
490
- * `create-dolphy-extension`; `argv` без `node` и имени скрипта, относительные пути
491
- * считаются от `cwd`.
586
+ * `create-dolphy-extension`; `argv` without `node` and the script name; relative paths
587
+ * are resolved against `cwd`.
492
588
  */
493
589
  const runCli = async (argv, io, cwd = process.cwd()) => {
494
590
  const parsed = parseArgs(argv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dolphy-app/create-extension",
3
- "version": "0.2.0",
4
- "description": "Генератор проекта расширения: create-dolphy-extension <каталог>",
3
+ "version": "0.3.0",
4
+ "description": "Extension project generator: create-dolphy-extension <directory>",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-dolphy-extension": "./dist/cli/main.js"