miaoda-game-devkit 0.8.0 → 0.8.1

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.
@@ -7,8 +7,8 @@ import {
7
7
  existsSync,
8
8
  lstatSync,
9
9
  mkdirSync,
10
- readFileSync,
11
10
  readdirSync,
11
+ readFileSync,
12
12
  renameSync,
13
13
  rmSync,
14
14
  statSync,
@@ -29,8 +29,7 @@ const SOURCE_ROOT = 'src/game-mechanics';
29
29
  const STATE_FILENAME = 'mechanics-source.json';
30
30
  const WORKSPACE_PATTERN = 'src/game-mechanics/*';
31
31
  const STATE_SCHEMA_VERSION = 2;
32
- const DEFAULT_SOURCE_INDEX_URL =
33
- 'https://resource-static.bj.bcebos.com/miaoda-game/stable.json';
32
+ const DEFAULT_SOURCE_INDEX_URL = 'https://resource-static.bj.bcebos.com/miaoda-game/stable.json';
34
33
 
35
34
  function fail(api, field, expected, repair) {
36
35
  throw new Error(`miaoda mechanics source ${api}: ${field} must ${expected}. ${repair}`);
@@ -41,9 +40,8 @@ function readJson(path, label = path) {
41
40
  try {
42
41
  value = JSON.parse(readFileSync(path, 'utf8'));
43
42
  } catch (error) {
44
- throw new Error(
45
- `miaoda mechanics source: cannot read ${label} as JSON: ${error instanceof Error ? error.message : String(error)}`,
46
- );
43
+ const detail = error instanceof Error ? error.message : String(error);
44
+ throw new Error(`miaoda mechanics source: cannot read ${label} as JSON: ${detail}`);
47
45
  }
48
46
  return value;
49
47
  }
@@ -53,15 +51,25 @@ function writeJson(path, value) {
53
51
  }
54
52
 
55
53
  function sourcePath(value) {
56
- if (typeof value !== 'string' || !value.startsWith('./dist/')) return value;
57
- if (value.endsWith('.d.ts')) return `./src/${value.slice('./dist/'.length, -'.d.ts'.length)}.ts`;
58
- if (value.endsWith('.js')) return `./src/${value.slice('./dist/'.length, -'.js'.length)}.ts`;
54
+ if (typeof value !== 'string' || !value.startsWith('./dist/')) {
55
+ return value;
56
+ }
57
+ if (value.endsWith('.d.ts')) {
58
+ return `./src/${value.slice('./dist/'.length, -'.d.ts'.length)}.ts`;
59
+ }
60
+ if (value.endsWith('.js')) {
61
+ return `./src/${value.slice('./dist/'.length, -'.js'.length)}.ts`;
62
+ }
59
63
  return value.replace('./dist/', './src/');
60
64
  }
61
65
 
62
66
  function rewriteExportPaths(value) {
63
- if (typeof value === 'string') return sourcePath(value);
64
- if (Array.isArray(value)) return value.map(rewriteExportPaths);
67
+ if (typeof value === 'string') {
68
+ return sourcePath(value);
69
+ }
70
+ if (Array.isArray(value)) {
71
+ return value.map(rewriteExportPaths);
72
+ }
65
73
  if (value && typeof value === 'object') {
66
74
  return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, rewriteExportPaths(child)]));
67
75
  }
@@ -69,9 +77,15 @@ function rewriteExportPaths(value) {
69
77
  }
70
78
 
71
79
  function stringValues(value) {
72
- if (typeof value === 'string') return [value];
73
- if (Array.isArray(value)) return value.flatMap(stringValues);
74
- if (value && typeof value === 'object') return Object.values(value).flatMap(stringValues);
80
+ if (typeof value === 'string') {
81
+ return [value];
82
+ }
83
+ if (Array.isArray(value)) {
84
+ return value.flatMap(stringValues);
85
+ }
86
+ if (value && typeof value === 'object') {
87
+ return Object.values(value).flatMap(stringValues);
88
+ }
75
89
  return [];
76
90
  }
77
91
 
@@ -83,15 +97,23 @@ export function createSourceManifest(manifest, resolvedPackages) {
83
97
  delete generated.files;
84
98
  delete generated.scripts;
85
99
  for (const field of ['main', 'module', 'types']) {
86
- if (field in generated) generated[field] = sourcePath(generated[field]);
100
+ if (field in generated) {
101
+ generated[field] = sourcePath(generated[field]);
102
+ }
103
+ }
104
+ if (generated.exports) {
105
+ generated.exports = rewriteExportPaths(generated.exports);
87
106
  }
88
- if (generated.exports) generated.exports = rewriteExportPaths(generated.exports);
89
107
 
90
108
  for (const field of ['dependencies', 'optionalDependencies']) {
91
- if (!generated[field]) continue;
109
+ if (!generated[field]) {
110
+ continue;
111
+ }
92
112
  for (const dependencyName of Object.keys(generated[field])) {
93
113
  const dependency = resolvedPackages.get(dependencyName);
94
- if (dependency) generated[field][dependencyName] = `workspace:${dependency.version}`;
114
+ if (dependency) {
115
+ generated[field][dependencyName] = `workspace:${dependency.version}`;
116
+ }
95
117
  }
96
118
  }
97
119
  return generated;
@@ -99,25 +121,52 @@ export function createSourceManifest(manifest, resolvedPackages) {
99
121
 
100
122
  export function validateMechanicSelection(projectManifest, resolvedPackages, capabilityDocument) {
101
123
  const config = projectManifest.miaodaGame;
102
- if (config === undefined) return;
124
+ if (config === undefined) {
125
+ return;
126
+ }
103
127
  if (!config || typeof config !== 'object' || Array.isArray(config)) {
104
- fail('add', 'package.json miaodaGame', 'be an object when defined', 'Remove it or declare schemaVersion, engine, uses, and blockedPackages.');
128
+ fail(
129
+ 'add',
130
+ 'package.json miaodaGame',
131
+ 'be an object when defined',
132
+ 'Remove it or declare schemaVersion, engine, uses, and blockedPackages.',
133
+ );
105
134
  }
106
135
  const engines = new Set(['neutral', 'react', 'phaser', 'cocos']);
107
136
  if (config.schemaVersion !== 1 || !engines.has(config.engine)) {
108
- fail('add', 'package.json miaodaGame', 'use schemaVersion 1 and a neutral, react, phaser, or cocos engine', 'Correct the project mechanic selection metadata.');
137
+ fail(
138
+ 'add',
139
+ 'package.json miaodaGame',
140
+ 'use schemaVersion 1 and a neutral, react, phaser, or cocos engine',
141
+ 'Correct the project mechanic selection metadata.',
142
+ );
109
143
  }
110
144
  if (!config.uses || typeof config.uses !== 'object' || Array.isArray(config.uses)) {
111
- fail('add', 'package.json miaodaGame.uses', 'be an object', 'Map selected package names to their intended owned capabilities.');
145
+ fail(
146
+ 'add',
147
+ 'package.json miaodaGame.uses',
148
+ 'be an object',
149
+ 'Map selected package names to their intended owned capabilities.',
150
+ );
112
151
  }
113
152
  if (!Array.isArray(config.blockedPackages)) {
114
- fail('add', 'package.json miaodaGame.blockedPackages', 'be an array', 'Use an empty array when no package is blocked.');
153
+ fail(
154
+ 'add',
155
+ 'package.json miaodaGame.blockedPackages',
156
+ 'be an array',
157
+ 'Use an empty array when no package is blocked.',
158
+ );
115
159
  }
116
160
  const capabilities = capabilityDocument?.packages ?? {};
117
161
  const owners = new Map();
118
162
  for (const [name, resolvedPackage] of resolvedPackages) {
119
163
  if (config.blockedPackages.includes(name)) {
120
- fail('add', name, 'not be listed in miaodaGame.blockedPackages', 'Remove the package request or unblock it explicitly.');
164
+ fail(
165
+ 'add',
166
+ name,
167
+ 'not be listed in miaodaGame.blockedPackages',
168
+ 'Remove the package request or unblock it explicitly.',
169
+ );
121
170
  }
122
171
  const capability = capabilities[name];
123
172
  if (
@@ -126,27 +175,56 @@ export function validateMechanicSelection(projectManifest, resolvedPackages, cap
126
175
  config.engine !== 'neutral' &&
127
176
  capability.engine !== config.engine
128
177
  ) {
129
- fail('add', `${name} engine`, `match project engine ${config.engine}`, `Choose a ${config.engine} adapter or an engine-neutral core package.`);
178
+ fail(
179
+ 'add',
180
+ `${name} engine`,
181
+ `match project engine ${config.engine}`,
182
+ `Choose a ${config.engine} adapter or an engine-neutral core package.`,
183
+ );
130
184
  }
131
185
  const uses = config.uses[name];
132
- if (uses === undefined) continue;
186
+ if (uses === undefined) {
187
+ continue;
188
+ }
133
189
  if (!capability) {
134
- fail('add', `${name} capability metadata`, 'exist before declaring miaodaGame.uses', 'Update the Devkit capability snapshot or remove the declaration.');
190
+ fail(
191
+ 'add',
192
+ `${name} capability metadata`,
193
+ 'exist before declaring miaodaGame.uses',
194
+ 'Update the Devkit capability snapshot or remove the declaration.',
195
+ );
135
196
  }
136
197
  if (!Array.isArray(uses) || uses.length === 0 || new Set(uses).size !== uses.length) {
137
- fail('add', `miaodaGame.uses.${name}`, 'be a non-empty array without duplicates', 'Declare each intended owned capability once.');
198
+ fail(
199
+ 'add',
200
+ `miaodaGame.uses.${name}`,
201
+ 'be a non-empty array without duplicates',
202
+ 'Declare each intended owned capability once.',
203
+ );
138
204
  }
139
205
  for (const use of uses) {
140
206
  if (!capability.owns.includes(use)) {
141
- fail('add', `${name} use ${use}`, `be one of its owned capabilities: ${capability.owns.join(', ')}`, 'Select the correct package or capability name.');
207
+ fail(
208
+ 'add',
209
+ `${name} use ${use}`,
210
+ `be one of its owned capabilities: ${capability.owns.join(', ')}`,
211
+ 'Select the correct package or capability name.',
212
+ );
142
213
  }
143
214
  const existing = owners.get(use);
144
215
  if (existing && existing !== name) {
145
- fail('add', `capability ${use}`, `have one owner, but both ${existing} and ${name} are selected`, 'Remove the duplicate ownership assignment.');
216
+ fail(
217
+ 'add',
218
+ `capability ${use}`,
219
+ `have one owner, but both ${existing} and ${name} are selected`,
220
+ 'Remove the duplicate ownership assignment.',
221
+ );
146
222
  }
147
223
  owners.set(use, name);
148
224
  }
149
- if (resolvedPackage.version.length === 0) fail('add', `${name} version`, 'be non-empty', 'Repack the package.');
225
+ if (resolvedPackage.version.length === 0) {
226
+ fail('add', `${name} version`, 'be non-empty', 'Repack the package.');
227
+ }
150
228
  }
151
229
  }
152
230
 
@@ -165,14 +243,20 @@ function validateSourceEntrypoints(packageDirectory, manifest) {
165
243
  }
166
244
 
167
245
  function listFiles(root, current = root) {
168
- const entries = readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
246
+ const entries = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
247
+ left.name.localeCompare(right.name),
248
+ );
169
249
  const files = [];
170
250
  for (const entry of entries) {
171
- if (entry.name === 'node_modules') continue;
251
+ if (entry.name === 'node_modules') {
252
+ continue;
253
+ }
172
254
  const path = join(current, entry.name);
173
- if (entry.isDirectory()) files.push(...listFiles(root, path));
174
- else if (entry.isFile()) files.push(path);
175
- else if (entry.isSymbolicLink()) {
255
+ if (entry.isDirectory()) {
256
+ files.push(...listFiles(root, path));
257
+ } else if (entry.isFile()) {
258
+ files.push(path);
259
+ } else if (entry.isSymbolicLink()) {
176
260
  fail(
177
261
  'add',
178
262
  `${relative(root, path)} in ${root}`,
@@ -197,10 +281,17 @@ function hashTree(root) {
197
281
 
198
282
  function readState(metadataRoot) {
199
283
  const statePath = join(metadataRoot, STATE_FILENAME);
200
- if (!existsSync(statePath)) return { schemaVersion: STATE_SCHEMA_VERSION, roots: {}, packages: {} };
284
+ if (!existsSync(statePath)) {
285
+ return { schemaVersion: STATE_SCHEMA_VERSION, roots: {}, packages: {} };
286
+ }
201
287
  const state = readJson(statePath, STATE_FILENAME);
202
288
  if (state.schemaVersion !== STATE_SCHEMA_VERSION || !state.roots || !state.packages) {
203
- fail('add', STATE_FILENAME, `use schemaVersion ${STATE_SCHEMA_VERSION}`, `Preserve src/game-mechanics, then remove ${STATE_FILENAME} to adopt it again.`);
289
+ fail(
290
+ 'add',
291
+ STATE_FILENAME,
292
+ `use schemaVersion ${STATE_SCHEMA_VERSION}`,
293
+ `Preserve src/game-mechanics, then remove ${STATE_FILENAME} to adopt it again.`,
294
+ );
204
295
  }
205
296
  return state;
206
297
  }
@@ -209,12 +300,22 @@ function inspectExistingPackages(sourceRoot, state) {
209
300
  const statuses = {};
210
301
  if (!existsSync(sourceRoot)) {
211
302
  if (Object.keys(state.packages).length > 0) {
212
- fail('add', sourceRoot, 'exist for the recorded state', `Restore it or preserve intentional changes and remove ${STATE_FILENAME} to reinstall.`);
303
+ fail(
304
+ 'add',
305
+ sourceRoot,
306
+ 'exist for the recorded state',
307
+ `Restore it or preserve intentional changes and remove ${STATE_FILENAME} to reinstall.`,
308
+ );
213
309
  }
214
310
  return statuses;
215
311
  }
216
312
  if (lstatSync(sourceRoot).isSymbolicLink()) {
217
- fail('add', sourceRoot, 'be a real project directory, not a symbolic link', 'Replace the link after preserving its contents.');
313
+ fail(
314
+ 'add',
315
+ sourceRoot,
316
+ 'be a real project directory, not a symbolic link',
317
+ 'Replace the link after preserving its contents.',
318
+ );
218
319
  }
219
320
  for (const [name, record] of Object.entries(state.packages)) {
220
321
  const directory = join(sourceRoot, name);
@@ -223,7 +324,12 @@ function inspectExistingPackages(sourceRoot, state) {
223
324
  continue;
224
325
  }
225
326
  if (lstatSync(directory).isSymbolicLink()) {
226
- fail('add', `${name} source`, 'be a real directory, not a symbolic link', 'Replace the link with the editable source directory.');
327
+ fail(
328
+ 'add',
329
+ `${name} source`,
330
+ 'be a real directory, not a symbolic link',
331
+ 'Replace the link with the editable source directory.',
332
+ );
227
333
  }
228
334
  statuses[name] = hashTree(directory) === record.sha256 ? 'clean' : 'modified';
229
335
  }
@@ -257,15 +363,20 @@ function ensureWorkspacePattern(projectRoot) {
257
363
  return { path: workspacePath, previous };
258
364
  }
259
365
  let insertAt = packagesIndex + 1;
260
- while (insertAt < lines.length && (lines[insertAt].trim() === '' || /^\s+/.test(lines[insertAt]))) insertAt += 1;
366
+ while (insertAt < lines.length && (lines[insertAt].trim() === '' || /^\s+/.test(lines[insertAt]))) {
367
+ insertAt += 1;
368
+ }
261
369
  lines.splice(insertAt, 0, ` - ${quotedPattern}`);
262
370
  writeFileSync(workspacePath, lines.join('\n'));
263
371
  return { path: workspacePath, previous };
264
372
  }
265
373
 
266
374
  function restoreFile(path, previous) {
267
- if (previous === undefined) rmSync(path, { force: true });
268
- else writeFileSync(path, previous);
375
+ if (previous === undefined) {
376
+ rmSync(path, { force: true });
377
+ } else {
378
+ writeFileSync(path, previous);
379
+ }
269
380
  }
270
381
 
271
382
  function updateProjectManifest(projectRoot, roots, resolvedPackages) {
@@ -276,7 +387,12 @@ function updateProjectManifest(projectRoot, roots, resolvedPackages) {
276
387
  for (const name of Object.keys(roots).sort()) {
277
388
  const resolvedPackage = resolvedPackages.get(name);
278
389
  if (!resolvedPackage) {
279
- fail('add', `direct package ${name}`, 'be present in the resolved Miaoda graph', 'Check the source package name and registry metadata.');
390
+ fail(
391
+ 'add',
392
+ `direct package ${name}`,
393
+ 'be present in the resolved Miaoda graph',
394
+ 'Check the source package name and registry metadata.',
395
+ );
280
396
  }
281
397
  manifest.dependencies[name] = `workspace:${resolvedPackage.version}`;
282
398
  }
@@ -288,7 +404,12 @@ function pnpmVersion(pnpmCommand, cwd) {
288
404
  const result = spawnSync(pnpmCommand, ['--version'], { cwd, encoding: 'utf8' });
289
405
  if (result.status !== 0) {
290
406
  const detail = result.error?.message || result.stderr?.trim() || result.stdout?.trim();
291
- fail('add', 'pnpm command', 'run successfully', detail || `Install pnpm 10 or 11 and ensure ${pnpmCommand} is on PATH.`);
407
+ fail(
408
+ 'add',
409
+ 'pnpm command',
410
+ 'run successfully',
411
+ detail || `Install pnpm 10 or 11 and ensure ${pnpmCommand} is on PATH.`,
412
+ );
292
413
  }
293
414
  const version = result.stdout.trim();
294
415
  const major = Number.parseInt(version.split('.')[0], 10);
@@ -323,7 +444,9 @@ function copyResolvedPackage(target, resolvedPackage, resolvedPackages) {
323
444
  recursive: true,
324
445
  filter: (source) => {
325
446
  const sourceName = basename(source);
326
- if (['node_modules', 'dist', 'coverage', '.nx', '__tests__'].includes(sourceName)) return false;
447
+ if (['node_modules', 'dist', 'coverage', '.nx', '__tests__'].includes(sourceName)) {
448
+ return false;
449
+ }
327
450
  if (/\.(?:spec|test)\.[cm]?[jt]sx?$/.test(sourceName) || /^vitest\.config\.[cm]?[jt]s$/.test(sourceName)) {
328
451
  return false;
329
452
  }
@@ -337,19 +460,56 @@ function copyResolvedPackage(target, resolvedPackage, resolvedPackages) {
337
460
  return hashTree(target);
338
461
  }
339
462
 
340
- const legacyAgentsSource = '# Editable game mechanics\n\n- This directory contains editable TypeScript source, not generated build output.\n- Preserve package boundaries and import packages by their `miaoda-game-*` names.\n- Modify each package under `<package>/src/`; do not edit `node_modules`.\n- Run `pnpm exec miaoda mechanics status` before updating a locally modified package.\n';
463
+ const legacyAgentsSource = [
464
+ '# Editable game mechanics',
465
+ '',
466
+ '- This directory contains editable TypeScript source, not generated build output.',
467
+ '- Preserve package boundaries and import packages by their `miaoda-game-*` names.',
468
+ '- Modify each package under `<package>/src/`; do not edit `node_modules`.',
469
+ '- Run `pnpm exec miaoda mechanics status` before updating a locally modified package.',
470
+ '',
471
+ ].join('\n');
341
472
  const generatedReadmeMarker = '<!-- Generated by miaoda mechanics. -->';
342
473
 
343
- function sourceReadme(resolvedPackages, capabilityDocument) {
474
+ function readmeDetail(label, value, separator = ', ') {
475
+ if (Array.isArray(value)) {
476
+ return value.length > 0 ? [`- ${label}: ${value.join(separator)}`] : [];
477
+ }
478
+ return typeof value === 'string' && value.length > 0 ? [`- ${label}: ${value}`] : [];
479
+ }
480
+
481
+ function capabilityReadme(capability) {
482
+ if (!capability) {
483
+ return ['- Capability metadata: unavailable; read the package README and source before assigning ownership.'];
484
+ }
485
+ return [
486
+ ...readmeDetail('Engine', capability.engine),
487
+ ...readmeDetail('Domains', capability.domains),
488
+ ...readmeDetail('Owns', capability.owns),
489
+ ...readmeDetail('Keep outside', capability.doesNotOwn),
490
+ ...readmeDetail('Compatible with', capability.compatibleWith),
491
+ ...readmeDetail('Use for', capability.guidance?.useFor, ' '),
492
+ ...readmeDetail('Boundary guidance', capability.guidance?.keepOutside, ' '),
493
+ ...readmeDetail('Rules', capability.guidance?.rules, ' '),
494
+ ...(capability.useInsteadWhen ?? []).map(
495
+ ({ package: packageName, condition }) => `- Use ${packageName} instead when: ${condition}`,
496
+ ),
497
+ ...readmeDetail('Persistence', capability.persistence),
498
+ ...readmeDetail('Observe', capability.testability?.methods?.observe),
499
+ ...readmeDetail('Advance', capability.testability?.methods?.advance),
500
+ ];
501
+ }
502
+
503
+ export function createSourceReadme(resolvedPackages, capabilityDocument) {
344
504
  const packages = [...resolvedPackages]
345
505
  .sort(([left], [right]) => left.localeCompare(right))
346
506
  .map(([name, resolvedPackage]) => {
347
507
  const capability = capabilityDocument?.packages?.[name];
348
508
  const description = resolvedPackage.manifest.description || 'Miaoda game mechanic package.';
349
- const owns = capability?.owns?.length ? `\n - Provides: ${capability.owns.join(', ')}` : '';
350
- return `- \`${name}@${resolvedPackage.version}\` — ${description}${owns}`;
509
+ const details = [`- Source: \`./${name}/\``, ...capabilityReadme(capability)].join('\n');
510
+ return `### \`${name}@${resolvedPackage.version}\`\n\n${description}\n\n${details}`;
351
511
  })
352
- .join('\n');
512
+ .join('\n\n');
353
513
  return `${generatedReadmeMarker}
354
514
  # Game mechanics
355
515
 
@@ -377,7 +537,7 @@ function ensureSourceGuidance(sourceRoot, resolvedPackages, capabilityDocument)
377
537
  existingReadme.startsWith(generatedReadmeMarker) ||
378
538
  existingReadme.startsWith('# Game mechanics source\n')
379
539
  ) {
380
- writeFileSync(readmePath, sourceReadme(resolvedPackages, capabilityDocument));
540
+ writeFileSync(readmePath, createSourceReadme(resolvedPackages, capabilityDocument));
381
541
  }
382
542
  const agentsPath = join(sourceRoot, 'AGENTS.md');
383
543
  if (existsSync(agentsPath) && readFileSync(agentsPath, 'utf8') === legacyAgentsSource) {
@@ -407,7 +567,12 @@ function materializePackages(
407
567
  const previous = previousState.packages[name];
408
568
  if (previous && previous.version === resolvedPackage.version) {
409
569
  if (statuses[name] === 'missing') {
410
- fail('add', `${name} source`, 'still exist', `Restore ${join(sourceRoot, name)} or remove its state entry before reinstalling.`);
570
+ fail(
571
+ 'add',
572
+ `${name} source`,
573
+ 'still exist',
574
+ `Restore ${join(sourceRoot, name)} or remove its state entry before reinstalling.`,
575
+ );
411
576
  }
412
577
  records[name] = previous;
413
578
  continue;
@@ -417,16 +582,25 @@ function materializePackages(
417
582
  'add',
418
583
  `${name} ${previous.version} source`,
419
584
  `be clean before updating to ${resolvedPackage.version}`,
420
- `Preserve or commit ${join(sourceRoot, name)}, then explicitly restore it before retrying the update. Nothing was overwritten.`,
585
+ `Preserve or commit ${join(sourceRoot, name)}, then explicitly restore it before retrying ` +
586
+ 'the update. Nothing was overwritten.',
421
587
  );
422
588
  }
423
589
  if (existsSync(target)) {
424
590
  if (!previous) {
425
- fail('add', `${name} destination`, 'not contain an unmanaged package', `Move ${join(sourceRoot, name)} elsewhere or adopt it explicitly.`);
591
+ fail(
592
+ 'add',
593
+ `${name} destination`,
594
+ 'not contain an unmanaged package',
595
+ `Move ${join(sourceRoot, name)} elsewhere or adopt it explicitly.`,
596
+ );
426
597
  }
427
598
  rmSync(target, { recursive: true, force: true });
428
599
  }
429
- records[name] = { version: resolvedPackage.version, sha256: copyResolvedPackage(target, resolvedPackage, resolvedPackages) };
600
+ records[name] = {
601
+ version: resolvedPackage.version,
602
+ sha256: copyResolvedPackage(target, resolvedPackage, resolvedPackages),
603
+ };
430
604
  }
431
605
  return records;
432
606
  }
@@ -434,11 +608,15 @@ function materializePackages(
434
608
  function switchManagedDirectory(sourceRoot, stagedSourceRoot, metadataRoot) {
435
609
  const backupRoot = join(metadataRoot, `.game-mechanics-backup-${randomUUID()}`);
436
610
  mkdirSync(dirname(sourceRoot), { recursive: true });
437
- if (existsSync(sourceRoot)) renameSync(sourceRoot, backupRoot);
611
+ if (existsSync(sourceRoot)) {
612
+ renameSync(sourceRoot, backupRoot);
613
+ }
438
614
  try {
439
615
  renameSync(stagedSourceRoot, sourceRoot);
440
616
  } catch (error) {
441
- if (existsSync(backupRoot)) renameSync(backupRoot, sourceRoot);
617
+ if (existsSync(backupRoot)) {
618
+ renameSync(backupRoot, sourceRoot);
619
+ }
442
620
  throw error;
443
621
  }
444
622
  return { packagesRoot: sourceRoot, backupRoot: existsSync(backupRoot) ? backupRoot : undefined };
@@ -446,7 +624,9 @@ function switchManagedDirectory(sourceRoot, stagedSourceRoot, metadataRoot) {
446
624
 
447
625
  function restoreManagedDirectory({ packagesRoot, backupRoot }) {
448
626
  rmSync(packagesRoot, { recursive: true, force: true });
449
- if (backupRoot) renameSync(backupRoot, packagesRoot);
627
+ if (backupRoot) {
628
+ renameSync(backupRoot, packagesRoot);
629
+ }
450
630
  }
451
631
 
452
632
  function parseArguments(argv) {
@@ -454,7 +634,9 @@ function parseArguments(argv) {
454
634
  const projectArgument = argv.find((argument) => argument.startsWith('--project='));
455
635
  const pnpmArgument = argv.find((argument) => argument.startsWith('--pnpm='));
456
636
  const sourceIndexArgument = argv.find((argument) => argument.startsWith('--source-index='));
457
- const options = new Set(argv.filter((argument) => argument.startsWith('--')).map((argument) => argument.split('=')[0]));
637
+ const options = new Set(
638
+ argv.filter((argument) => argument.startsWith('--')).map((argument) => argument.split('=')[0]),
639
+ );
458
640
  const specs = argv.slice(1).filter((argument) => !argument.startsWith('--'));
459
641
  return {
460
642
  command,
@@ -514,7 +696,12 @@ function validateProject(projectRoot) {
514
696
 
515
697
  function validateMetadataRoot(metadataRoot) {
516
698
  if (existsSync(metadataRoot) && lstatSync(metadataRoot).isSymbolicLink()) {
517
- fail('add', metadataRoot, 'be a real directory, not a symbolic link', `Replace ${METADATA_ROOT} with a project-local directory.`);
699
+ fail(
700
+ 'add',
701
+ metadataRoot,
702
+ 'be a real directory, not a symbolic link',
703
+ `Replace ${METADATA_ROOT} with a project-local directory.`,
704
+ );
518
705
  }
519
706
  }
520
707
 
@@ -526,12 +713,14 @@ export function mechanicSourceStatus({ projectRoot }) {
526
713
  const state = readState(metadataRoot);
527
714
  const sourceRoot = join(projectRoot, SOURCE_ROOT);
528
715
  const statuses = inspectExistingPackages(sourceRoot, state);
529
- return Object.keys(state.packages).sort().map((name) => ({
530
- name,
531
- version: state.packages[name].version,
532
- status: statuses[name],
533
- path: join(sourceRoot, name),
534
- }));
716
+ return Object.keys(state.packages)
717
+ .sort()
718
+ .map((name) => ({
719
+ name,
720
+ version: state.packages[name].version,
721
+ status: statuses[name],
722
+ path: join(sourceRoot, name),
723
+ }));
535
724
  }
536
725
 
537
726
  export async function addMechanicSources({
@@ -544,7 +733,12 @@ export async function addMechanicSources({
544
733
  projectRoot = resolve(projectRoot);
545
734
  validateProject(projectRoot);
546
735
  if (!Array.isArray(specs) || specs.length === 0) {
547
- fail('add', 'package specs', 'contain at least one miaoda-game-* package', 'Pass a package name with an optional @version.');
736
+ fail(
737
+ 'add',
738
+ 'package specs',
739
+ 'contain at least one miaoda-game-* package',
740
+ 'Pass a package name with an optional @version.',
741
+ );
548
742
  }
549
743
  if (!sourceIndexUrl) {
550
744
  fail(
@@ -628,8 +822,12 @@ export async function addMechanicSources({
628
822
  sourceIndexUrl: indexedResolution.sourceIndexUrl,
629
823
  });
630
824
 
631
- if (!skipInstall) runPnpm(pnpmCommand, ['install', '--ignore-scripts', '--no-frozen-lockfile'], projectRoot);
632
- if (switched.backupRoot) rmSync(switched.backupRoot, { recursive: true, force: true });
825
+ if (!skipInstall) {
826
+ runPnpm(pnpmCommand, ['install', '--ignore-scripts', '--no-frozen-lockfile'], projectRoot);
827
+ }
828
+ if (switched.backupRoot) {
829
+ rmSync(switched.backupRoot, { recursive: true, force: true });
830
+ }
633
831
  return {
634
832
  directPackages: directNames,
635
833
  packageNames: [...resolvedPackages.keys()].sort(),
@@ -637,12 +835,20 @@ export async function addMechanicSources({
637
835
  pnpmVersion: version,
638
836
  };
639
837
  } catch (error) {
640
- if (projectManifestChange) restoreFile(projectManifestChange.path, projectManifestChange.previous);
641
- if (workspaceChange) restoreFile(workspaceChange.path, workspaceChange.previous);
642
- if (switched) restoreManagedDirectory(switched);
838
+ if (projectManifestChange) {
839
+ restoreFile(projectManifestChange.path, projectManifestChange.previous);
840
+ }
841
+ if (workspaceChange) {
842
+ restoreFile(workspaceChange.path, workspaceChange.previous);
843
+ }
844
+ if (switched) {
845
+ restoreManagedDirectory(switched);
846
+ }
643
847
  restoreFile(statePath, previousStateText);
644
848
  restoreFile(lockfilePath, previousLockfileText);
645
- if (!nodeModulesExisted) rmSync(nodeModulesPath, { recursive: true, force: true });
849
+ if (!nodeModulesExisted) {
850
+ rmSync(nodeModulesPath, { recursive: true, force: true });
851
+ }
646
852
  throw error;
647
853
  } finally {
648
854
  cleanupIndexedMechanics(indexedResolution);
@@ -654,7 +860,7 @@ export async function main(argv = process.argv.slice(2)) {
654
860
  const mechanicsArguments = argv[0] === 'mechanics' ? argv.slice(1) : argv;
655
861
  const command = mechanicsArguments[0];
656
862
  const wantsHelp = mechanicsArguments.includes('--help') || mechanicsArguments.includes('-h');
657
- if (!command || command === 'help' || wantsHelp && !['add', 'status'].includes(command)) {
863
+ if (!command || command === 'help' || (wantsHelp && !['add', 'status'].includes(command))) {
658
864
  console.log(ROOT_HELP);
659
865
  return;
660
866
  }
@@ -675,17 +881,20 @@ export async function main(argv = process.argv.slice(2)) {
675
881
  }
676
882
  const options = parseArguments(mechanicsArguments);
677
883
  const statuses = mechanicSourceStatus(options);
678
- if (statuses.length === 0) console.log('No Miaoda game-mechanic source packages are installed.');
679
- else for (const entry of statuses) console.log(`${entry.name}@${entry.version} ${entry.status} ${entry.path}`);
884
+ if (statuses.length === 0) {
885
+ console.log('No Miaoda game-mechanic source packages are installed.');
886
+ } else {
887
+ for (const entry of statuses) {
888
+ console.log(`${entry.name}@${entry.version} ${entry.status} ${entry.path}`);
889
+ }
890
+ }
680
891
  return statuses;
681
892
  }
682
893
  throw new Error(`Unknown miaoda mechanics command: ${command}\n\n${ROOT_HELP}`);
683
894
  }
684
895
 
685
896
  export function formatAddResult(result) {
686
- const packagePaths = result.packageNames
687
- .map((name) => ` - src/game-mechanics/${name}/`)
688
- .join('\n');
897
+ const packagePaths = result.packageNames.map((name) => ` - src/game-mechanics/${name}/`).join('\n');
689
898
  return `✓ Added ${result.packageNames.length} editable game-mechanic source packages
690
899
 
691
900
  Added source:
@@ -1,10 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { x as extractTarball } from 'tar';
6
6
 
7
- const MECHANIC_PREFIX = 'miaoda-game-';
8
7
  const INDEX_SCHEMA_VERSION = 1;
9
8
  const MAX_INDEX_BYTES = 5 * 1024 * 1024;
10
9
  const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
@@ -18,9 +17,13 @@ function isMechanicName(value) {
18
17
  }
19
18
 
20
19
  function parsePackageSpec(spec) {
21
- if (typeof spec !== 'string') return undefined;
20
+ if (typeof spec !== 'string') {
21
+ return undefined;
22
+ }
22
23
  const match = /^(miaoda-game-[a-z0-9][a-z0-9._-]*?)(?:@([^/]+))?$/.exec(spec);
23
- if (!match) return undefined;
24
+ if (!match) {
25
+ return undefined;
26
+ }
24
27
  return { name: match[1], version: match[2] };
25
28
  }
26
29
 
@@ -30,13 +33,19 @@ export function areIndexedPackageSpecs(specs) {
30
33
 
31
34
  async function readResponse(response, limit, label) {
32
35
  if (!response.ok) {
33
- fail(label, `download successfully, but received HTTP ${response.status}`, 'Check the public source URL and object permissions.');
36
+ fail(
37
+ label,
38
+ `download successfully, but received HTTP ${response.status}`,
39
+ 'Check the public source URL and object permissions.',
40
+ );
34
41
  }
35
42
  const declaredLength = Number(response.headers.get('content-length'));
36
43
  if (Number.isFinite(declaredLength) && declaredLength > limit) {
37
44
  fail(label, `be at most ${limit} bytes`, 'Publish a smaller source artifact.');
38
45
  }
39
- if (!response.body) fail(label, 'contain a response body', 'Upload the source object again.');
46
+ if (!response.body) {
47
+ fail(label, 'contain a response body', 'Upload the source object again.');
48
+ }
40
49
  const chunks = [];
41
50
  let total = 0;
42
51
  for await (const chunk of response.body) {
@@ -76,7 +85,11 @@ function validateVersionRecord(name, version, record, indexUrl) {
76
85
  fail(`${name}@${version} url`, 'use HTTP or HTTPS', 'Upload the source archive to the configured public storage.');
77
86
  }
78
87
  if (typeof record.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(record.sha256)) {
79
- fail(`${name}@${version} sha256`, 'contain 64 hexadecimal characters', 'Regenerate the source index after packing.');
88
+ fail(
89
+ `${name}@${version} sha256`,
90
+ 'contain 64 hexadecimal characters',
91
+ 'Regenerate the source index after packing.',
92
+ );
80
93
  }
81
94
  const dependencies = record.dependencies ?? {};
82
95
  if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
@@ -113,11 +126,19 @@ function selectVersion(index, name, requestedVersion, indexUrl) {
113
126
  function resolveGraph(index, indexUrl, previousRoots, specs) {
114
127
  const roots = new Map();
115
128
  for (const [name, version] of Object.entries(previousRoots ?? {})) {
116
- if (isMechanicName(name) && typeof version === 'string' && version.length > 0) roots.set(name, version);
129
+ if (isMechanicName(name) && typeof version === 'string' && version.length > 0) {
130
+ roots.set(name, version);
131
+ }
117
132
  }
118
133
  for (const spec of specs) {
119
134
  const parsed = parsePackageSpec(spec);
120
- if (!parsed) fail(`package spec ${spec}`, 'be a miaoda-game-* name with an optional @version', 'Pass TGZ URLs without --source-index.');
135
+ if (!parsed) {
136
+ fail(
137
+ `package spec ${spec}`,
138
+ 'be a miaoda-game-* name with an optional @version',
139
+ 'Pass TGZ URLs without --source-index.',
140
+ );
141
+ }
121
142
  const selected = selectVersion(index, parsed.name, parsed.version, indexUrl);
122
143
  roots.set(parsed.name, selected.version);
123
144
  }
@@ -140,7 +161,11 @@ function resolveGraph(index, indexUrl, previousRoots, specs) {
140
161
  const selected = selectVersion(index, request.name, request.version, indexUrl);
141
162
  selectedPackages.set(request.name, selected);
142
163
  for (const [dependencyName, dependencyVersion] of Object.entries(selected.dependencies).sort()) {
143
- queue.push({ name: dependencyName, version: dependencyVersion, requestedBy: `${selected.name}@${selected.version}` });
164
+ queue.push({
165
+ name: dependencyName,
166
+ version: dependencyVersion,
167
+ requestedBy: `${selected.name}@${selected.version}`,
168
+ });
144
169
  }
145
170
  }
146
171
  return { roots: Object.fromEntries([...roots].sort()), selectedPackages };
@@ -164,7 +189,11 @@ async function readIndex(indexUrl) {
164
189
  fail('stable.json', 'contain valid JSON', error instanceof Error ? error.message : String(error));
165
190
  }
166
191
  if (index?.schemaVersion !== INDEX_SCHEMA_VERSION || !index.packages || typeof index.packages !== 'object') {
167
- fail('stable.json', `use schemaVersion ${INDEX_SCHEMA_VERSION} and a packages object`, 'Regenerate the source index.');
192
+ fail(
193
+ 'stable.json',
194
+ `use schemaVersion ${INDEX_SCHEMA_VERSION} and a packages object`,
195
+ 'Regenerate the source index.',
196
+ );
168
197
  }
169
198
  return { index, indexUrl: normalizedUrl };
170
199
  }
@@ -175,9 +204,15 @@ function assertExtractedTree(root, name) {
175
204
  const current = stack.pop();
176
205
  for (const entry of readdirSync(current, { withFileTypes: true })) {
177
206
  if (entry.isSymbolicLink()) {
178
- fail(`${name} ${relative(root, join(current, entry.name))}`, 'not be a symbolic link', 'Repack the source archive without links.');
207
+ fail(
208
+ `${name} ${relative(root, join(current, entry.name))}`,
209
+ 'not be a symbolic link',
210
+ 'Repack the source archive without links.',
211
+ );
212
+ }
213
+ if (entry.isDirectory()) {
214
+ stack.push(join(current, entry.name));
179
215
  }
180
- if (entry.isDirectory()) stack.push(join(current, entry.name));
181
216
  }
182
217
  }
183
218
  }
@@ -202,16 +237,24 @@ async function extractPackage(targetRoot, selected, bytes) {
202
237
  strict: true,
203
238
  preservePaths: false,
204
239
  filter(path, entry) {
205
- if (!path.startsWith('package/')) return false;
240
+ if (!path.startsWith('package/')) {
241
+ return false;
242
+ }
206
243
  if (entry.type === 'SymbolicLink' || entry.type === 'Link') {
207
- fail(`${selected.name}@${selected.version} archive`, 'not contain links', 'Repack the source archive with regular files only.');
244
+ fail(
245
+ `${selected.name}@${selected.version} archive`,
246
+ 'not contain links',
247
+ 'Repack the source archive with regular files only.',
248
+ );
208
249
  }
209
250
  return true;
210
251
  },
211
252
  });
212
253
  assertExtractedTree(packageDirectory, selected.name);
213
254
  const manifestPath = join(packageDirectory, 'package.json');
214
- if (!existsSync(manifestPath)) fail(`${selected.name} package.json`, 'exist in the TGZ', 'Repack the source package.');
255
+ if (!existsSync(manifestPath)) {
256
+ fail(`${selected.name} package.json`, 'exist in the TGZ', 'Repack the source package.');
257
+ }
215
258
  let manifest;
216
259
  try {
217
260
  manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
@@ -230,9 +273,19 @@ async function extractPackage(targetRoot, selected, bytes) {
230
273
  );
231
274
  }
232
275
  if (!existsSync(join(packageDirectory, 'src'))) {
233
- fail(`${selected.name}@${selected.version} src`, 'exist in the TGZ', 'Pack production TypeScript source before uploading.');
276
+ fail(
277
+ `${selected.name}@${selected.version} src`,
278
+ 'exist in the TGZ',
279
+ 'Pack production TypeScript source before uploading.',
280
+ );
234
281
  }
235
- return { directory: packageDirectory, manifest, version: selected.version, sourceUrl: selected.url, artifactSha256: actualHash };
282
+ return {
283
+ directory: packageDirectory,
284
+ manifest,
285
+ version: selected.version,
286
+ sourceUrl: selected.url,
287
+ artifactSha256: actualHash,
288
+ };
236
289
  }
237
290
 
238
291
  export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, specs }) {
@@ -241,7 +294,9 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
241
294
  const temporaryRoot = mkdtempSync(join(tmpdir(), 'miaoda-mechanics-source-index-'));
242
295
  const resolvedPackages = new Map();
243
296
  try {
244
- for (const selected of [...graph.selectedPackages.values()].sort((left, right) => left.name.localeCompare(right.name))) {
297
+ for (const selected of [...graph.selectedPackages.values()].sort((left, right) =>
298
+ left.name.localeCompare(right.name),
299
+ )) {
245
300
  const bytes = await download(selected.url, MAX_TARBALL_BYTES, `${selected.name}@${selected.version}`);
246
301
  resolvedPackages.set(selected.name, await extractPackage(temporaryRoot, selected, bytes));
247
302
  }
@@ -258,5 +313,7 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
258
313
  }
259
314
 
260
315
  export function cleanupIndexedMechanics(resolution) {
261
- if (resolution?.temporaryRoot) rmSync(resolution.temporaryRoot, { recursive: true, force: true });
316
+ if (resolution?.temporaryRoot) {
317
+ rmSync(resolution.temporaryRoot, { recursive: true, force: true });
318
+ }
262
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Shared React and Phaser game lint plus deterministic testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",