@silkweaver/build 1.0.0 → 1.2.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.
package/dist/build.d.ts CHANGED
@@ -15,6 +15,16 @@ import type { project_file as project_data } from '@silkweaver/project';
15
15
  export type { project_data };
16
16
  /** Reads and parses a project's project.json. */
17
17
  export declare function read_project(project_folder: string): Promise<project_data>;
18
+ /** Public: the engine version this toolchain ships — what "Update engine" would pin a project to. */
19
+ export declare function toolchain_engine_version(): string;
20
+ /**
21
+ * Vendors the toolchain's current engine into a project as one self-contained bundle
22
+ * (`.engine/engine.mjs`, matter-js inlined) and records its version (`.engine/version.json` +
23
+ * project.json `engineVersion`). Called at project creation; safe to re-run to upgrade the pin.
24
+ * @param project_folder - Absolute path to the project folder
25
+ * @returns The vendored engine version.
26
+ */
27
+ export declare function vendor_engine(project_folder: string): Promise<string>;
18
28
  /**
19
29
  * Builds the game to a single bundled game.js at out_path, for the in-IDE preview.
20
30
  * @param out_path - Where to write game.js (the caller decides — e.g. exports/game.js)
package/dist/build.js CHANGED
@@ -47,6 +47,8 @@ var __importStar = (this && this.__importStar) || (function () {
47
47
  })();
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.read_project = read_project;
50
+ exports.toolchain_engine_version = toolchain_engine_version;
51
+ exports.vendor_engine = vendor_engine;
50
52
  exports.build_preview = build_preview;
51
53
  exports.export_html5 = export_html5;
52
54
  exports.export_executable = export_executable;
@@ -54,6 +56,7 @@ const fs = __importStar(require("node:fs"));
54
56
  const path = __importStar(require("node:path"));
55
57
  const os = __importStar(require("node:os"));
56
58
  const node_url_1 = require("node:url");
59
+ const object_format_js_1 = require("./object_format.js");
57
60
  /** Reads and parses a project's project.json. */
58
61
  async function read_project(project_folder) {
59
62
  const proj_text = await fs.promises.readFile(path.join(project_folder, 'project.json'), 'utf8');
@@ -64,18 +67,73 @@ async function read_project(project_folder) {
64
67
  * entry can import the whole API (keeping it in sync with the IDE's autocomplete).
65
68
  * esbuild tree-shakes whatever the game doesn't actually use.
66
69
  */
67
- let _engine_names = null;
70
+ const _engine_names = new Map(); // engine path → its export names (per vendored engine)
68
71
  async function engine_export_names(engine_path) {
69
- if (_engine_names)
70
- return _engine_names;
72
+ const cached = _engine_names.get(engine_path);
73
+ if (cached)
74
+ return cached;
71
75
  const mod = await import((0, node_url_1.pathToFileURL)(engine_path).href);
72
- _engine_names = Object.keys(mod).filter(n => n !== 'default' && /^[A-Za-z_$][\w$]*$/.test(n));
73
- return _engine_names;
76
+ const names = Object.keys(mod).filter(n => n !== 'default' && /^[A-Za-z_$][\w$]*$/.test(n));
77
+ _engine_names.set(engine_path, names);
78
+ return names;
74
79
  }
75
- /** Absolute path to the engine package entry the esbuild alias target + export-name source. */
80
+ /** Absolute path to the toolchain's own engine entry (the vendoring source + pre-vendoring fallback). */
76
81
  function engine_entry() {
77
82
  return require.resolve('@silkweaver/engine');
78
83
  }
84
+ /** The version of the toolchain's own engine (what a freshly-vendored project pins). */
85
+ function engine_version() {
86
+ try {
87
+ return JSON.parse(fs.readFileSync(path.join(path.dirname(engine_entry()), '..', 'package.json'), 'utf8')).version;
88
+ }
89
+ catch {
90
+ return '0.0.0';
91
+ }
92
+ }
93
+ /** Public: the engine version this toolchain ships — what "Update engine" would pin a project to. */
94
+ function toolchain_engine_version() {
95
+ return engine_version();
96
+ }
97
+ /**
98
+ * Resolves the engine a project builds against: its vendored copy (`.engine/engine.mjs`) when
99
+ * present, else the toolchain's own engine (projects created before per-project vendoring). This is
100
+ * what lets a project keep building against the exact engine it was made with — IDE updates don't
101
+ * touch it.
102
+ */
103
+ function resolve_engine(project_folder) {
104
+ const vendored = path.join(project_folder, '.engine', 'engine.mjs');
105
+ return fs.existsSync(vendored) ? vendored : engine_entry();
106
+ }
107
+ /**
108
+ * Vendors the toolchain's current engine into a project as one self-contained bundle
109
+ * (`.engine/engine.mjs`, matter-js inlined) and records its version (`.engine/version.json` +
110
+ * project.json `engineVersion`). Called at project creation; safe to re-run to upgrade the pin.
111
+ * @param project_folder - Absolute path to the project folder
112
+ * @returns The vendored engine version.
113
+ */
114
+ async function vendor_engine(project_folder) {
115
+ const version = engine_version();
116
+ const dir = path.join(project_folder, '.engine');
117
+ await fs.promises.mkdir(dir, { recursive: true });
118
+ const esbuild_api = require('esbuild');
119
+ await esbuild_api.build({
120
+ entryPoints: [engine_entry()],
121
+ bundle: true,
122
+ format: 'esm',
123
+ outfile: path.join(dir, 'engine.mjs'),
124
+ keepNames: true, // the engine reads constructor.name at runtime
125
+ });
126
+ await fs.promises.writeFile(path.join(dir, 'version.json'), JSON.stringify({ version, vendoredAt: new Date().toISOString() }, null, 2) + '\n', 'utf8');
127
+ // Pin the version in project.json so the IDE can display it / gate features on it.
128
+ try {
129
+ const proj_path = path.join(project_folder, 'project.json');
130
+ const proj = JSON.parse(await fs.promises.readFile(proj_path, 'utf8'));
131
+ proj.engineVersion = version;
132
+ await fs.promises.writeFile(proj_path, JSON.stringify(proj, null, 2) + '\n', 'utf8');
133
+ }
134
+ catch { /* no project.json yet — caller sets engineVersion */ }
135
+ return version;
136
+ }
79
137
  /**
80
138
  * Strips leading `import …` lines from a code snippet so it can be inlined inside a
81
139
  * function body (timeline moments). Engine references auto-resolve via the inject shim,
@@ -100,23 +158,25 @@ function hex_to_bgr(hex) {
100
158
  * Generates the bootstrapper (_entry.ts) source for a project.
101
159
  * @param asset_mode - 'preview' (file:// into project) or 'export' (relative assets/)
102
160
  */
103
- async function generate_entry_code(project_folder, proj, asset_mode) {
104
- const object_names = Object.keys(proj.resources.objects ?? {});
161
+ async function generate_entry_code(project_folder, proj, asset_mode, engine_path) {
162
+ // Parent-before-child so generated imports/registrations init objects in dependency order.
163
+ const object_names = await _objects_parent_first(project_folder, Object.keys(proj.resources.objects ?? {}));
105
164
  const room_names = Object.keys(proj.resources.rooms ?? {});
106
165
  const sprite_names = Object.keys(proj.resources.sprites ?? {});
107
166
  const script_names = Object.keys(proj.resources.scripts ?? {});
108
167
  const font_names = Object.keys(proj.resources.fonts ?? {});
109
168
  const path_names = Object.keys(proj.resources.paths ?? {});
110
169
  const timeline_names = Object.keys(proj.resources.timelines ?? {});
111
- // The engine is resolved from the @silkweaver/engine package (no path coupling).
112
- const engine_path = engine_entry();
170
+ // engine_path is whatever the project resolves to its vendored copy, or the toolchain's.
113
171
  // Each object is a single class file: objects/<name>.ts (a gm_object subclass).
114
172
  // It is imported as-is; metadata/variables/events all live in the class.
115
173
  const object_imports = [];
174
+ const object_registrations = []; // object_register_name('x', x) → resolves object_get('x')
116
175
  for (const obj_name of object_names) {
117
176
  const class_file = path.join(project_folder, 'objects', `${obj_name}.ts`);
118
177
  if (await _path_exists(class_file)) {
119
178
  object_imports.push(`import { ${obj_name} } from '${class_file.replace(/\\/g, '/')}'`);
179
+ object_registrations.push(`object_register_name('${obj_name}', ${obj_name})`);
120
180
  }
121
181
  else {
122
182
  console.warn(`[build] object '${obj_name}' has no objects/${obj_name}.ts — skipped`);
@@ -323,6 +383,9 @@ ${engine_import}
323
383
 
324
384
  ${object_imports.join('\n')}
325
385
 
386
+ // Register objects by name so object_get('name') resolves them at runtime.
387
+ ${object_registrations.join('\n')}
388
+
326
389
  ${script_imports}
327
390
 
328
391
  // ── Sprite loader ───────────────────────────────────────────────────────────
@@ -485,7 +548,8 @@ ${room_physics[start_room] ? ` physics_world_create(${room_physics[start_room
485
548
  * ESM game.js at out_path via esbuild, then removes the temporary file.
486
549
  */
487
550
  async function bundle_game(project_folder, proj, asset_mode, out_path, minify) {
488
- const entry_code = await generate_entry_code(project_folder, proj, asset_mode);
551
+ const engine_path = resolve_engine(project_folder);
552
+ const entry_code = await generate_entry_code(project_folder, proj, asset_mode, engine_path);
489
553
  const entry_path = path.join(project_folder, '_entry.ts');
490
554
  const globals_path = path.join(project_folder, '_engine_globals.ts');
491
555
  await fs.promises.writeFile(entry_path, entry_code, 'utf8');
@@ -493,8 +557,24 @@ async function bundle_game(project_folder, proj, asset_mode, out_path, minify) {
493
557
  // `inject`, it makes any *bare* engine reference in an object/script file (e.g. `gm_object`,
494
558
  // `draw_sprite`) resolve to an auto-injected import — tree-shaken — so users never write or
495
559
  // manage `import … from '@silkweaver/engine'` themselves.
496
- const engine_names = await engine_export_names(engine_entry());
497
- await fs.promises.writeFile(globals_path, `export { ${engine_names.join(', ')} } from '@silkweaver/engine'\n`, 'utf8');
560
+ const engine_names = await engine_export_names(engine_path);
561
+ // Also re-export the project's OBJECT classes by name, so they can be referenced bare —
562
+ // GMS-style, e.g. `place_meeting(x, y, obj_wall)` or `static parent = par_solid` — with no
563
+ // import (matching what the IDE editor already declares). The defining file won't self-import;
564
+ // skip any object whose name would collide with an engine export.
565
+ // Parent-before-child order: the re-export order here becomes the module init order, so an
566
+ // object's `static parent = par` resolves to a defined class instead of undefined.
567
+ const object_order = await _objects_parent_first(project_folder, Object.keys(proj.resources.objects ?? {}));
568
+ const object_lines = [];
569
+ for (const obj_name of object_order) {
570
+ if (engine_names.includes(obj_name))
571
+ continue;
572
+ const class_file = path.join(project_folder, 'objects', `${obj_name}.ts`);
573
+ if (await _path_exists(class_file)) {
574
+ object_lines.push(`export { ${obj_name} } from '${class_file.replace(/\\/g, '/')}'`);
575
+ }
576
+ }
577
+ await fs.promises.writeFile(globals_path, `export { ${engine_names.join(', ')} } from '@silkweaver/engine'\n${object_lines.join('\n')}\n`, 'utf8');
498
578
  try {
499
579
  // Run esbuild via its Node API. Alias the package specifier to the resolved engine
500
580
  // entry so the generated entry AND any class-per-object files resolve to the same
@@ -506,7 +586,11 @@ async function bundle_game(project_folder, proj, asset_mode, out_path, minify) {
506
586
  outfile: out_path,
507
587
  format: 'esm',
508
588
  minify,
509
- alias: { '@silkweaver/engine': engine_entry() },
589
+ // Preserve class/function `.name` even when minifying — the engine reads
590
+ // `this.constructor.name` (resource.name, room type checks, object_get_name),
591
+ // so identifier mangling would otherwise break exported (minified) games.
592
+ keepNames: true,
593
+ alias: { '@silkweaver/engine': engine_path },
510
594
  inject: [globals_path],
511
595
  });
512
596
  }
@@ -686,6 +770,48 @@ async function _path_exists(p) {
686
770
  return false;
687
771
  }
688
772
  }
773
+ /**
774
+ * Orders object names so every object's `static parent` (when it's another project object)
775
+ * appears BEFORE it. esbuild lazy-initializes the object modules (the inject shim and each
776
+ * object form an import cycle), running them in declaration order inside one initializer; a
777
+ * child whose `static parent = par` is evaluated before `par`'s module would capture
778
+ * `undefined`. Initializing parents first makes the reference resolve to the real class.
779
+ * A parent cycle (user error) is broken gracefully so this never loops.
780
+ */
781
+ async function _objects_parent_first(project_folder, names) {
782
+ const present = new Set(names);
783
+ const parent_of = new Map();
784
+ for (const name of names) {
785
+ const file = path.join(project_folder, 'objects', `${name}.ts`);
786
+ let parent;
787
+ if (await _path_exists(file)) {
788
+ try {
789
+ const p = (0, object_format_js_1.parse_object)(await fs.promises.readFile(file, 'utf8')).parent;
790
+ if (p && p !== name && present.has(p))
791
+ parent = p;
792
+ }
793
+ catch { /* unparseable — treat as no parent */ }
794
+ }
795
+ parent_of.set(name, parent);
796
+ }
797
+ const out = [];
798
+ const done = new Set();
799
+ const onstack = new Set();
800
+ const visit = (n) => {
801
+ if (done.has(n) || onstack.has(n))
802
+ return; // onstack guard breaks any parent cycle
803
+ onstack.add(n);
804
+ const p = parent_of.get(n);
805
+ if (p)
806
+ visit(p);
807
+ onstack.delete(n);
808
+ done.add(n);
809
+ out.push(n);
810
+ };
811
+ for (const n of names)
812
+ visit(n);
813
+ return out;
814
+ }
689
815
  /** Recursively copies a directory tree from src to dst. */
690
816
  async function _copy_dir(src, dst) {
691
817
  await fs.promises.mkdir(dst, { recursive: true });
package/dist/index.d.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * - export_executable → a packaged desktop application
8
8
  *
9
9
  * Also re-exports the class-file object read/write layer (object_format) used by the
10
- * IDE's object editor. Pure Node drives both the IDE (over IPC) and the CLI.
10
+ * IDE's object editor, and the starter-template materializer (templates). Pure Node
11
+ * drives both the IDE (over IPC) and the CLI.
11
12
  */
12
13
  export * from './build.js';
13
14
  export * from './object_format.js';
15
+ export * from './templates.js';
package/dist/index.js CHANGED
@@ -8,7 +8,8 @@
8
8
  * - export_executable → a packaged desktop application
9
9
  *
10
10
  * Also re-exports the class-file object read/write layer (object_format) used by the
11
- * IDE's object editor. Pure Node drives both the IDE (over IPC) and the CLI.
11
+ * IDE's object editor, and the starter-template materializer (templates). Pure Node
12
+ * drives both the IDE (over IPC) and the CLI.
12
13
  */
13
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
15
  if (k2 === undefined) k2 = k;
@@ -27,3 +28,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
27
28
  Object.defineProperty(exports, "__esModule", { value: true });
28
29
  __exportStar(require("./build.js"), exports);
29
30
  __exportStar(require("./object_format.js"), exports);
31
+ __exportStar(require("./templates.js"), exports);
@@ -11,6 +11,8 @@
11
11
  */
12
12
  /** Known static-metadata field names an object class may declare. */
13
13
  export type meta_field = 'sprite' | 'solid' | 'visible' | 'persistent' | 'depth' | 'parent';
14
+ /** Canonical GMS-style order of `on_*` event methods (used to keep events ordered in code). */
15
+ export declare const EVENT_ORDER: string[];
14
16
  export interface object_model {
15
17
  class_name: string;
16
18
  sprite: string | null;
@@ -27,6 +29,14 @@ export interface object_model {
27
29
  }
28
30
  /** Extracts the object model from a class-file source. */
29
31
  export declare function parse_object(src: string): object_model;
32
+ /**
33
+ * Collects every instance member reachable through `this.` in a class: declared instance fields
34
+ * *plus* members created/assigned at runtime inside any event (e.g. `this.velocity = 0` in on_create,
35
+ * read in on_step). This drives `this.` autocomplete, so a variable made in one event surfaces in all
36
+ * the others — matching the usual split of constants on the object, runtime state in Create. Static
37
+ * fields and method/event names are excluded (engine members are merged in separately).
38
+ */
39
+ export declare function this_members(src: string): string[];
30
40
  /**
31
41
  * Sets (adds or updates) a static metadata field. `expr` is the raw initializer
32
42
  * text (e.g. `'spr_player'`, `true`, `-5`, `obj_base`).
@@ -38,9 +48,29 @@ export declare function remove_static(src: string, name: meta_field): string;
38
48
  export declare function set_field(src: string, name: string, expr: string): string;
39
49
  /** Removes an instance variable field. */
40
50
  export declare function remove_field(src: string, name: string): string;
41
- /** Adds an `on_*` event method stub if it is not already present. */
51
+ /**
52
+ * Adds an `on_*` event method stub if it is not already present, inserted in canonical event
53
+ * order (before the first existing event method that comes later in EVENT_ORDER).
54
+ */
42
55
  export declare function add_method(src: string, method: string, params?: string, body?: string): string;
43
56
  /** Removes an event method by name. */
44
57
  export declare function remove_method(src: string, method: string): string;
58
+ /**
59
+ * Returns one event method's body as an editable, de-indented buffer, or null if absent/bodyless.
60
+ * @param method - The on_* method to extract (e.g. 'on_step')
61
+ */
62
+ export declare function get_event_body(src: string, method: string): string | null;
63
+ /**
64
+ * Replaces an event method's body with `body` (an edited, de-indented buffer), re-indenting it to
65
+ * the method's level. Returns the full updated source; a no-op if the method is absent.
66
+ * @param method - The on_* method to update
67
+ * @param body - The new body (column-0 / de-indented, as edited)
68
+ */
69
+ export declare function set_event_body(src: string, method: string, body: string): string;
70
+ /**
71
+ * Normalizes a class-per-object file for display in the full code view: variables hoisted above all
72
+ * events, canonical event order, then consistent indentation. Unchanged if there's no class.
73
+ */
74
+ export declare function normalize_object(src: string): string;
45
75
  /** Generates a minimal class-file source for a new object. Imports are auto-managed (none needed). */
46
76
  export declare function scaffold_object(class_name: string): string;