cluaupp 0.1.4 → 0.1.5

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.
@@ -2,17 +2,23 @@
2
2
 
3
3
  const path = require("path");
4
4
  const { emit } = require("./emit");
5
- const { collectLibraries, insertRequires, requireCluauppLib } = require("./libs");
5
+ const { requireCluauppLib } = require("./libs");
6
6
  const { organizeDecls, janitorNames, emitSection, joinBlocks, SECTION, ENTRY_FN } = require("./layout");
7
7
  const {
8
8
  VALUE_CLASSES,
9
9
  analyze,
10
10
  toPascalServiceName,
11
11
  legacyOutName,
12
+ modernScriptOutName,
12
13
  } = require("./understand");
14
+ const { implOutName } = require("./preprocess");
13
15
 
14
16
  function header(plan) {
15
- const lines = ["--!strict", "-- Compiled by Cluaupp — C++ × Luau"];
17
+ const lines = [];
18
+ if (plan && plan.strict) {
19
+ lines.push("--!strict");
20
+ }
21
+ lines.push("-- Compiled by Cluaupp — C++ × Luau");
16
22
  if (plan && plan.reasoning) {
17
23
  for (const line of plan.reasoning) {
18
24
  lines.push(`-- ${line}`);
@@ -328,9 +334,7 @@ function emitDomainController(plan, ast, options) {
328
334
  return decl.type === "decl" || decl.type === "proto" || decl.type === "function" || decl.type === "expr";
329
335
  });
330
336
  const groups = organizeDecls(keep);
331
- const subset = { type: "program", body: keep };
332
337
  const name = plan.roles.domain;
333
- const libs = collectLibraries(options.source || "", subset);
334
338
  const entry = keep.find((decl) => decl.type === "function" && ENTRY_FN.test(decl.name || ""));
335
339
  const bootExprs = keep.filter((decl) => decl.type === "expr");
336
340
  const janitors = janitorNames(keep);
@@ -356,7 +360,7 @@ function emitDomainController(plan, ast, options) {
356
360
  const startFn = `function ${name}.Start()\n ${name}.Stop()\n${boot}\nend`;
357
361
  const stopFn = `function ${name}.Stop()\n${stopLines.join("\n")}\nend`;
358
362
  const body = `${header(plan).trimEnd()}\n\n${emitSection(SECTION.api, emitDecls(groups.api, options))}${emitSection(SECTION.constants, emitDecls(groups.constants, options))}${emitSection(SECTION.variables, joinBlocks([emitDecls(groups.variables, options), `local ${name} = {}`]))}${emitSection(SECTION.support, emitDecls(groups.support, options))}${emitSection(SECTION.principal, joinBlocks([emitDecls(groups.principal, options), startFn]))}${emitSection(SECTION.cleanup, joinBlocks([emitDecls(groups.cleanup, options), stopFn]))}${emitSection(SECTION.returns, `return ${name}`)}`;
359
- return insertRequires(body, libs);
363
+ return body;
360
364
  }
361
365
 
362
366
  function emitModule(plan, ast, options) {
@@ -366,7 +370,6 @@ function emitModule(plan, ast, options) {
366
370
  function emitScriptMeta(runContext) {
367
371
  return `${JSON.stringify(
368
372
  {
369
- className: "Script",
370
373
  properties: {
371
374
  RunContext: `Enum.RunContext.${runContext}`,
372
375
  },
@@ -380,7 +383,7 @@ function serviceBootName(plan) {
380
383
  if (plan.isClient) {
381
384
  return "init.client.luau";
382
385
  }
383
- return "init.luau";
386
+ return "init.server.luau";
384
387
  }
385
388
 
386
389
  function serviceFiles(plan, ast, options, dir) {
@@ -404,33 +407,75 @@ function serviceFiles(plan, ast, options, dir) {
404
407
  return files;
405
408
  }
406
409
 
410
+ function serviceFolderStale(plan, rel) {
411
+ const dir = path.posix.dirname(rel).replace(/^\.$/, "");
412
+ const serviceDir = dir && dir !== "." ? dir : plan.isClient ? "client" : "server";
413
+ const folder = `${serviceDir}/${plan.serviceName}`;
414
+ return [
415
+ folder,
416
+ `${folder}/init.luau`,
417
+ `${folder}/init.server.luau`,
418
+ `${folder}/init.client.luau`,
419
+ `${folder}/init.meta.json`,
420
+ `${folder}/Main.luau`,
421
+ `${folder}/PlayersManager.luau`,
422
+ `${folder}/CacheController.luau`,
423
+ `${folder}/${plan.typesName}.luau`,
424
+ plan.roles && plan.roles.domain ? `${folder}/${plan.roles.domain}.luau` : null,
425
+ ].filter(Boolean);
426
+ }
427
+
407
428
  function planOutput(ast, fileName, options = {}) {
408
429
  const plan = analyze(ast, fileName);
430
+ plan.strict = options.strict === true;
409
431
  const rel = (options.relativeName || fileName).replace(/\\/g, "/");
410
432
  const dir = path.dirname(rel).replace(/^\.$/, "");
411
433
  const outDir = dir && dir !== "." ? dir : "";
412
434
  const prefix = outDir ? `${outDir}/` : "";
435
+ const stale = serviceFolderStale(plan, rel);
413
436
 
414
- if (options.architecture === false) {
415
- return { kind: "flat", plan, files: null };
437
+ if (options.siblingHeader) {
438
+ return {
439
+ kind: "module",
440
+ plan,
441
+ files: [{ name: implOutName(rel), contents: emitModule(plan, ast, { ...options, skipInit: true }) }],
442
+ stale: [],
443
+ };
416
444
  }
417
445
 
418
- if (plan.kind === "legacy") {
419
- return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
446
+ if (options.architecture === true) {
447
+ if (plan.kind === "legacy") {
448
+ return { kind: "legacy", plan, files: null, stale: [], outName: legacyOutName(rel) };
449
+ }
450
+
451
+ if (plan.kind === "service") {
452
+ const serviceDir = outDir || (plan.isClient ? "client" : "server");
453
+ const folder = `${serviceDir}/${plan.serviceName}`;
454
+ const serviceStale = plan.isClient
455
+ ? [`${folder}/init.luau`, `${folder}/init.server.luau`, `${folder}/init.meta.json`]
456
+ : [`${folder}/init.luau`, `${folder}/init.client.luau`];
457
+ return {
458
+ kind: "service",
459
+ plan,
460
+ files: serviceFiles(plan, ast, options, serviceDir),
461
+ stale: serviceStale,
462
+ };
463
+ }
464
+
465
+ if (plan.kind === "config" || plan.kind === "module") {
466
+ return {
467
+ kind: plan.kind,
468
+ plan,
469
+ files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
470
+ stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
471
+ };
472
+ }
473
+
474
+ return { kind: "flat", plan, files: null, stale: [], outName: modernScriptOutName(rel) };
420
475
  }
421
476
 
422
- if (plan.kind === "service") {
423
- const serviceDir = outDir || (plan.isClient ? "client" : "server");
424
- const folder = `${serviceDir}/${plan.serviceName}`;
425
- const stale = plan.isClient
426
- ? [`${folder}/init.luau`, `${folder}/init.server.luau`, `${folder}/init.meta.json`]
427
- : [`${folder}/init.server.luau`, `${folder}/init.client.luau`];
428
- return {
429
- kind: "service",
430
- plan,
431
- files: serviceFiles(plan, ast, options, serviceDir),
432
- stale,
433
- };
477
+ if (plan.kind === "legacy") {
478
+ return { kind: "legacy", plan, files: null, stale, outName: legacyOutName(rel) };
434
479
  }
435
480
 
436
481
  if (plan.kind === "config" || plan.kind === "module") {
@@ -438,11 +483,14 @@ function planOutput(ast, fileName, options = {}) {
438
483
  kind: plan.kind,
439
484
  plan,
440
485
  files: [{ name: `${prefix}${plan.serviceName}.luau`, contents: emitModule(plan, ast, options) }],
441
- stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau")],
486
+ stale: [rel.replace(/\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau"), ...stale],
442
487
  };
443
488
  }
444
489
 
445
- return { kind: "flat", plan, files: null, stale: [] };
490
+ const outName = modernScriptOutName(rel);
491
+ const stripped = rel.replace(/\.(server|client|plugin)\.(cpp|cc|cxx|c|h|hpp|hh)$/i, ".luau");
492
+ const extraStale = stripped !== outName ? [stripped] : [];
493
+ return { kind: "flat", plan, files: null, stale: [...stale, ...extraStale], outName };
446
494
  }
447
495
 
448
496
  module.exports = {
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { compileService } = require("./compile");
6
- const { isSourceFile, isHeaderFile, toLuauPath, siblingImplementation } = require("./preprocess");
6
+ const { isSourceFile, toLuauPath } = require("./preprocess");
7
7
  const { syncEditorSupport, installEditorSupport } = require("./intellisense");
8
8
  const pkg = require("../package.json");
9
9
 
@@ -12,7 +12,7 @@ function printHelp() {
12
12
 
13
13
  Usage:
14
14
  cluaupp init [folder] create a game (src/server, src/client, src/shared)
15
- cluaupp build [folder] transpile src → out (PascalCase services)
15
+ cluaupp build [folder] transpile src → out (one file per .cpp)
16
16
  cluaupp watch [folder] rebuild on save
17
17
  cluaupp lsp [folder] language server (stdio JSON-RPC)
18
18
  cluaupp intellisense install Cursor/VS Code IntelliSense
@@ -110,10 +110,10 @@ function loadConfig(root) {
110
110
  for (const name of configNames) {
111
111
  const file = path.join(root, name);
112
112
  if (fs.existsSync(file)) {
113
- return { rootDir: "src", outDir: "out", strict: true, ...JSON.parse(fs.readFileSync(file, "utf8")) };
113
+ return { rootDir: "src", outDir: "out", strict: false, architecture: false, ...JSON.parse(fs.readFileSync(file, "utf8")) };
114
114
  }
115
115
  }
116
- return { rootDir: "src", outDir: "out", strict: true };
116
+ return { rootDir: "src", outDir: "out", strict: false, architecture: false };
117
117
  }
118
118
 
119
119
  function collectCpp(dir, files = []) {
@@ -146,10 +146,6 @@ function collectFiles(dir, files = []) {
146
146
  return files;
147
147
  }
148
148
 
149
- function siblingCpp(file) {
150
- return siblingImplementation(file) || file.replace(/\.(h|hpp|hh)$/i, ".cpp");
151
- }
152
-
153
149
  function resolveKey(file) {
154
150
  const resolved = path.resolve(file);
155
151
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -223,14 +219,10 @@ function pruneOut(root, config, written, failedPrefixes) {
223
219
  function compileProject(root, config) {
224
220
  const srcDir = path.join(root, config.rootDir);
225
221
  const files = collectCpp(srcDir);
226
- const fileSet = new Set(files.map((file) => path.resolve(file)));
227
222
  const jobs = [];
228
223
  const errors = [];
229
224
 
230
225
  for (const file of files) {
231
- if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
232
- continue;
233
- }
234
226
  const source = fs.readFileSync(file, "utf8");
235
227
  const rel = posixRel(srcDir, file);
236
228
  try {
@@ -239,7 +231,7 @@ function compileProject(root, config) {
239
231
  filePath: file,
240
232
  relativeName: rel,
241
233
  outName: toLuauPath(rel),
242
- architecture: config.architecture !== false,
234
+ architecture: config.architecture === true,
243
235
  includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
244
236
  srcDir,
245
237
  });
package/src/compile.js CHANGED
@@ -1,35 +1,89 @@
1
1
  "use strict";
2
2
 
3
+ const path = require("path");
3
4
  const { parse } = require("./parse");
4
5
  const { emit } = require("./emit");
5
- const { preprocess } = require("./preprocess");
6
- const { collectLibraries, insertRequires, insertModuleRequires } = require("./libs");
6
+ const { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHeader, implOutName } = require("./preprocess");
7
+ const { collectLibraries, insertPreamble } = require("./libs");
7
8
  const { planOutput } = require("./architecture");
9
+ const { emitHeaderModule, primaryHeaderName } = require("./headers");
10
+
11
+ function shouldAttachRequires(name) {
12
+ return !/\.(json)$/i.test(String(name || ""));
13
+ }
8
14
 
9
15
  function attachRequires(contents, source, ast, options, outName) {
10
- let next = contents;
11
- if (!next.includes("CluauppLibs")) {
12
- next = insertRequires(next, collectLibraries(source, ast));
16
+ if (!shouldAttachRequires(outName)) {
17
+ return contents;
18
+ }
19
+ const libraries = String(contents).includes("CluauppLibs") ? [] : collectLibraries(source, ast);
20
+ return insertPreamble(contents, options.moduleIncludes || [], libraries, outName);
21
+ }
22
+
23
+ function posixRel(from, file) {
24
+ return path.relative(from, file).replace(/\\/g, "/");
25
+ }
26
+
27
+ function wireHeaderImpl(ast, fileName, options) {
28
+ const headerPath = options.filePath || null;
29
+ const impl = headerPath ? siblingImplementation(headerPath) : null;
30
+ if (!impl) {
31
+ return;
32
+ }
33
+ const typeName = primaryHeaderName(ast, options.relativeName || fileName);
34
+ const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
35
+ const implOut = implOutName(implRel);
36
+ options.headerImplName = `${typeName}Module`;
37
+ options.moduleIncludes = options.moduleIncludes || [];
38
+ if (!options.moduleIncludes.some((spec) => spec.name === options.headerImplName)) {
39
+ options.moduleIncludes.push({
40
+ name: options.headerImplName,
41
+ outRel: implOut,
42
+ exports: { consts: [], structs: [] },
43
+ });
13
44
  }
14
- return insertModuleRequires(next, options.moduleIncludes || [], outName);
45
+ }
46
+
47
+ function compileHeader(source, fileName, options, prepared, ast) {
48
+ wireHeaderImpl(ast, fileName, options);
49
+ const rel = (options.relativeName || fileName || "header.h").replace(/\\/g, "/");
50
+ const outName = toLuauPath(rel);
51
+ const luau = attachRequires(emitHeaderModule(ast, options), source, ast, options, outName);
52
+ return {
53
+ kind: "module",
54
+ plan: { kind: "module", strict: options.strict === true },
55
+ files: [{ name: outName, contents: luau }],
56
+ stale: [],
57
+ };
15
58
  }
16
59
 
17
60
  function compileSource(source, fileName, options = {}) {
18
61
  options.moduleIncludes = options.moduleIncludes || [];
19
- const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
62
+ const prepared = preprocess(source, options.filePath || null, options);
20
63
  const ast = parse(prepared, fileName || "input.cpp");
64
+ if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "")) {
65
+ return compileHeader(source, fileName, options, prepared, ast).files[0].contents;
66
+ }
21
67
  const luau = emit(ast, options);
22
68
  return attachRequires(luau, source, ast, options, options.outName || fileName);
23
69
  }
24
70
 
25
71
  function compileService(source, fileName, options = {}) {
26
72
  options.moduleIncludes = options.moduleIncludes || [];
27
- const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
73
+ const prepared = preprocess(source, options.filePath || null, options);
28
74
  const ast = parse(prepared, fileName || "input.cpp");
75
+ if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "") || isHeaderFile(options.relativeName || "")) {
76
+ return compileHeader(source, fileName, options, prepared, ast);
77
+ }
78
+ const implHeader = options.filePath ? siblingHeader(options.filePath) : null;
79
+ if (implHeader) {
80
+ options.skipInit = true;
81
+ }
29
82
  const planned = planOutput(ast, fileName || "input.cpp", {
30
83
  ...options,
31
84
  relativeName: options.relativeName || fileName,
32
85
  source: prepared,
86
+ siblingHeader: implHeader,
33
87
  });
34
88
  if (planned.files) {
35
89
  return {
package/src/emit.js CHANGED
@@ -4,7 +4,7 @@ const { isInstanceType, isMethod, isDatatype, luauType } = require("./api");
4
4
  const { isLibraryType, isLibraryMethod, MODULE_COLON } = require("./libs");
5
5
 
6
6
  function emit(ast, options = {}) {
7
- const strict = options.strict !== false;
7
+ const strict = options.strict === true;
8
8
  const lines = [];
9
9
  if (!options.skipHeader) {
10
10
  if (strict) {
package/src/headers.js ADDED
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const { luauType } = require("./api");
5
+
6
+ function fileStem(fileName) {
7
+ return path.basename(String(fileName || "module")).replace(/\.(h|hpp|hh|cpp|cc|cxx|c)$/i, "");
8
+ }
9
+
10
+ function emitValue(node) {
11
+ if (!node) {
12
+ return "nil";
13
+ }
14
+ if (node.type === "number") {
15
+ return node.value;
16
+ }
17
+ if (node.type === "string") {
18
+ return `"${node.value}"`;
19
+ }
20
+ if (node.type === "bool") {
21
+ return node.value ? "true" : "false";
22
+ }
23
+ if (node.type === "null") {
24
+ return "nil";
25
+ }
26
+ if (node.type === "initlist") {
27
+ const inner = (node.fields || []).map((field) => `${field.name} = ${emitValue(field.value)}`).join(", ");
28
+ return `{ ${inner} }`;
29
+ }
30
+ return "nil";
31
+ }
32
+
33
+ function collectHeaderShape(ast, fileName) {
34
+ const owners = new Map();
35
+ const ensure = (name) => {
36
+ if (!owners.has(name)) {
37
+ owners.set(name, { fields: [], methods: [] });
38
+ }
39
+ return owners.get(name);
40
+ };
41
+ const freeProtos = [];
42
+ const consts = [];
43
+ for (const decl of ast.body || []) {
44
+ if (decl.type === "decl" && decl.owner) {
45
+ ensure(decl.owner).fields.push(decl);
46
+ continue;
47
+ }
48
+ if (decl.type === "decl") {
49
+ consts.push(decl);
50
+ continue;
51
+ }
52
+ if ((decl.type === "proto" || decl.type === "function") && decl.owner) {
53
+ ensure(decl.owner).methods.push(decl);
54
+ continue;
55
+ }
56
+ if (decl.type === "proto") {
57
+ freeProtos.push(decl);
58
+ }
59
+ }
60
+ const nested = new Set();
61
+ for (const [ownerName, spec] of owners) {
62
+ for (const field of spec.fields) {
63
+ if (field.valueType && owners.has(field.valueType) && field.valueType !== ownerName) {
64
+ nested.add(field.valueType);
65
+ }
66
+ }
67
+ }
68
+ const roots = [...owners.keys()].filter((name) => !nested.has(name));
69
+ const withMethods = roots.filter((name) => ensure(name).methods.length > 0);
70
+ const stem = fileStem(fileName);
71
+ let primary = withMethods[0] || (freeProtos.length ? stem : roots[0] || stem);
72
+ return { owners, nested, roots, freeProtos, consts, primary, stem };
73
+ }
74
+
75
+ function primaryHeaderName(ast, fileName) {
76
+ return collectHeaderShape(ast, fileName).primary;
77
+ }
78
+
79
+ function methodType(fn, owner) {
80
+ const params = [];
81
+ if (owner) {
82
+ params.push(`self: ${owner}`);
83
+ }
84
+ for (const param of fn.params || []) {
85
+ if (typeof param === "string") {
86
+ params.push(param);
87
+ continue;
88
+ }
89
+ const typeAnn = luauType(param.valueType);
90
+ params.push(typeAnn ? `${param.name}: ${typeAnn}` : param.name);
91
+ }
92
+ const ret = luauType(fn.returnType);
93
+ const retAnn = !ret || ret === "()" ? "()" : ret;
94
+ return `(${params.join(", ")}) -> ${retAnn}`;
95
+ }
96
+
97
+ function fieldType(field, owners) {
98
+ if (field.valueType && owners.has(field.valueType)) {
99
+ return field.valueType;
100
+ }
101
+ return luauType(field.valueType) || "any";
102
+ }
103
+
104
+ function emitExportType(lines, name, spec, owners, extraMethods, extraFields) {
105
+ lines.push(`export type ${name} = {`);
106
+ const seen = new Set();
107
+ for (const field of [...((spec && spec.fields) || []), ...(extraFields || [])]) {
108
+ if (!field.name || seen.has(field.name)) {
109
+ continue;
110
+ }
111
+ seen.add(field.name);
112
+ lines.push(` ${field.name}: ${fieldType(field, owners)},`);
113
+ }
114
+ for (const method of (spec && spec.methods) || []) {
115
+ if (!method.name || seen.has(method.name)) {
116
+ continue;
117
+ }
118
+ seen.add(method.name);
119
+ lines.push(` ${method.name}: ${methodType(method, name)},`);
120
+ }
121
+ for (const method of extraMethods || []) {
122
+ if (!method.name || seen.has(method.name)) {
123
+ continue;
124
+ }
125
+ seen.add(method.name);
126
+ lines.push(` ${method.name}: ${methodType(method, null)},`);
127
+ }
128
+ lines.push("}");
129
+ lines.push("");
130
+ }
131
+
132
+ function emitDataConstructor(lines, name, spec, owners) {
133
+ const emitField = (field) => {
134
+ if (field.valueType && owners.has(field.valueType)) {
135
+ const nested = owners.get(field.valueType);
136
+ const inner = nested.fields.map((item) => `${item.name} = ${emitValue(item.value)}`).join(", ");
137
+ return `{ ${inner} }`;
138
+ }
139
+ if (field.value && field.value.type === "initlist") {
140
+ return emitValue(field.value);
141
+ }
142
+ return emitValue(field.value);
143
+ };
144
+ lines.push(`const function ${name}(): ${name}`);
145
+ lines.push(" return {");
146
+ for (const field of spec.fields) {
147
+ lines.push(` ${field.name} = ${emitField(field)},`);
148
+ }
149
+ lines.push(" }");
150
+ lines.push("end");
151
+ lines.push("");
152
+ }
153
+
154
+ function emitHeaderModule(ast, options = {}) {
155
+ const strict = options.strict === true;
156
+ const fileName = options.relativeName || options.filePath || "header.h";
157
+ const shape = collectHeaderShape(ast, fileName);
158
+ const lines = [];
159
+ if (strict) {
160
+ lines.push("--!strict");
161
+ }
162
+ lines.push("-- Compiled by Cluaupp — C++ × Luau");
163
+ lines.push("");
164
+
165
+ const typeOrder = [...shape.nested, ...shape.roots];
166
+ for (const name of typeOrder) {
167
+ const extra = name === shape.primary ? shape.freeProtos : [];
168
+ emitExportType(lines, name, shape.owners.get(name), shape.owners, extra);
169
+ }
170
+ if (shape.freeProtos.length && !shape.owners.has(shape.primary)) {
171
+ emitExportType(lines, shape.primary, { fields: [], methods: [] }, shape.owners, shape.freeProtos, shape.consts);
172
+ }
173
+
174
+ for (const decl of shape.consts) {
175
+ const typeAnn = luauType(decl.valueType);
176
+ const typed = typeAnn ? `: ${typeAnn}` : "";
177
+ lines.push(`const ${decl.name}${typed} = ${emitValue(decl.value)}`);
178
+ }
179
+ if (shape.consts.length) {
180
+ lines.push("");
181
+ }
182
+
183
+ const hasMethods = shape.roots.some((name) => shape.owners.get(name).methods.length > 0) || shape.freeProtos.length > 0;
184
+ const implName = hasMethods ? options.headerImplName : null;
185
+
186
+ if (!hasMethods && shape.roots.length > 0) {
187
+ for (const name of shape.roots) {
188
+ emitDataConstructor(lines, name, shape.owners.get(name), shape.owners);
189
+ }
190
+ lines.push(`return ${shape.primary}`);
191
+ lines.push("");
192
+ return lines.join("\n");
193
+ }
194
+
195
+ if (!hasMethods && shape.consts.length > 0) {
196
+ lines.push("return {");
197
+ for (const decl of shape.consts) {
198
+ lines.push(` ${decl.name} = ${decl.name},`);
199
+ }
200
+ lines.push("}");
201
+ lines.push("");
202
+ return lines.join("\n");
203
+ }
204
+
205
+ const tableName = shape.primary;
206
+ const spec = shape.owners.get(tableName) || { fields: [], methods: [] };
207
+ const methods = [...spec.methods, ...shape.freeProtos];
208
+ lines.push(`const ${tableName}: ${tableName} = {`);
209
+ if (implName) {
210
+ for (const field of spec.fields) {
211
+ if (field.valueType && shape.owners.has(field.valueType) && field.valueType !== tableName) {
212
+ continue;
213
+ }
214
+ lines.push(` ${field.name} = ${implName}.${field.name},`);
215
+ }
216
+ for (const method of methods) {
217
+ lines.push(` ${method.name} = ${implName}.${method.name},`);
218
+ }
219
+ }
220
+ for (const decl of shape.consts) {
221
+ lines.push(` ${decl.name} = ${decl.name},`);
222
+ }
223
+ lines.push("}");
224
+ lines.push("");
225
+ lines.push(`return ${tableName}`);
226
+ lines.push("");
227
+ return lines.join("\n");
228
+ }
229
+
230
+ module.exports = {
231
+ emitHeaderModule,
232
+ primaryHeaderName,
233
+ collectHeaderShape,
234
+ };