@cms0/cms0 0.0.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.
Files changed (70) hide show
  1. package/bin/cms0.js +30 -0
  2. package/dist/cjs/cli.cjs +7 -0
  3. package/dist/cjs/config.cjs +6 -0
  4. package/dist/cjs/generated/schema-descriptor.cjs +523 -0
  5. package/dist/cjs/index.cjs +48 -0
  6. package/dist/cjs/libs/cli/build.cjs +14 -0
  7. package/dist/cjs/libs/cli/cli.cjs +64 -0
  8. package/dist/cjs/libs/cli/config-loader.cjs +211 -0
  9. package/dist/cjs/libs/cli/descriptor-builder-alt.cjs +221 -0
  10. package/dist/cjs/libs/cli/descriptor-builder.cjs +232 -0
  11. package/dist/cjs/libs/cli/descriptor-writer.cjs +23 -0
  12. package/dist/cjs/libs/cli/index.cjs +10 -0
  13. package/dist/cjs/libs/cli/paths.cjs +28 -0
  14. package/dist/cjs/libs/cli/publisher.cjs +103 -0
  15. package/dist/cjs/libs/cli/types.cjs +3 -0
  16. package/dist/cjs/libs/cli/watcher.cjs +34 -0
  17. package/dist/cjs/schema-descriptors.cjs +5 -0
  18. package/dist/cjs/utils/index.cjs +2 -0
  19. package/dist/esm/cli.js +5 -0
  20. package/dist/esm/config.js +3 -0
  21. package/dist/esm/generated/schema-descriptor.js +520 -0
  22. package/dist/esm/index.js +45 -0
  23. package/dist/esm/libs/cli/build.js +12 -0
  24. package/dist/esm/libs/cli/cli.js +62 -0
  25. package/dist/esm/libs/cli/config-loader.js +168 -0
  26. package/dist/esm/libs/cli/descriptor-builder-alt.js +216 -0
  27. package/dist/esm/libs/cli/descriptor-builder.js +227 -0
  28. package/dist/esm/libs/cli/descriptor-writer.js +18 -0
  29. package/dist/esm/libs/cli/index.js +4 -0
  30. package/dist/esm/libs/cli/paths.js +21 -0
  31. package/dist/esm/libs/cli/publisher.js +101 -0
  32. package/dist/esm/libs/cli/types.js +2 -0
  33. package/dist/esm/libs/cli/watcher.js +29 -0
  34. package/dist/esm/schema-descriptors.js +1 -0
  35. package/dist/esm/utils/index.js +1 -0
  36. package/dist/types/cli.d.ts +3 -0
  37. package/dist/types/cli.d.ts.map +1 -0
  38. package/dist/types/config.d.ts +22 -0
  39. package/dist/types/config.d.ts.map +1 -0
  40. package/dist/types/generated/schema-descriptor.d.ts +520 -0
  41. package/dist/types/generated/schema-descriptor.d.ts.map +1 -0
  42. package/dist/types/index.d.ts +14 -0
  43. package/dist/types/index.d.ts.map +1 -0
  44. package/dist/types/libs/cli/build.d.ts +5 -0
  45. package/dist/types/libs/cli/build.d.ts.map +1 -0
  46. package/dist/types/libs/cli/cli.d.ts +3 -0
  47. package/dist/types/libs/cli/cli.d.ts.map +1 -0
  48. package/dist/types/libs/cli/config-loader.d.ts +12 -0
  49. package/dist/types/libs/cli/config-loader.d.ts.map +1 -0
  50. package/dist/types/libs/cli/descriptor-builder-alt.d.ts +5 -0
  51. package/dist/types/libs/cli/descriptor-builder-alt.d.ts.map +1 -0
  52. package/dist/types/libs/cli/descriptor-builder.d.ts +5 -0
  53. package/dist/types/libs/cli/descriptor-builder.d.ts.map +1 -0
  54. package/dist/types/libs/cli/descriptor-writer.d.ts +4 -0
  55. package/dist/types/libs/cli/descriptor-writer.d.ts.map +1 -0
  56. package/dist/types/libs/cli/index.d.ts +4 -0
  57. package/dist/types/libs/cli/index.d.ts.map +1 -0
  58. package/dist/types/libs/cli/paths.d.ts +4 -0
  59. package/dist/types/libs/cli/paths.d.ts.map +1 -0
  60. package/dist/types/libs/cli/publisher.d.ts +5 -0
  61. package/dist/types/libs/cli/publisher.d.ts.map +1 -0
  62. package/dist/types/libs/cli/types.d.ts +8 -0
  63. package/dist/types/libs/cli/types.d.ts.map +1 -0
  64. package/dist/types/libs/cli/watcher.d.ts +5 -0
  65. package/dist/types/libs/cli/watcher.d.ts.map +1 -0
  66. package/dist/types/schema-descriptors.d.ts +2 -0
  67. package/dist/types/schema-descriptors.d.ts.map +1 -0
  68. package/dist/types/utils/index.d.ts +2 -0
  69. package/dist/types/utils/index.d.ts.map +1 -0
  70. package/package.json +74 -0
@@ -0,0 +1,168 @@
1
+ // Load cms0.config files and resolve relevant paths for the CLI.
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { pathToFileURL, fileURLToPath } from "url";
5
+ import crypto from "crypto";
6
+ const DEFAULT_CONFIG_BASENAMES = [
7
+ "cms0.config.ts",
8
+ "cms0.config.js",
9
+ "cms0.config.mjs",
10
+ "cms0.config.cjs",
11
+ "cms0.config.json",
12
+ ];
13
+ function findConfigPath(provided) {
14
+ if (provided) {
15
+ const resolved = path.isAbsolute(provided)
16
+ ? provided
17
+ : path.resolve(process.cwd(), provided);
18
+ return fs.existsSync(resolved) ? resolved : undefined;
19
+ }
20
+ let dir = process.cwd();
21
+ while (true) {
22
+ for (const name of DEFAULT_CONFIG_BASENAMES) {
23
+ const candidate = path.join(dir, name);
24
+ if (fs.existsSync(candidate))
25
+ return candidate;
26
+ }
27
+ const parent = path.dirname(dir);
28
+ if (parent === dir)
29
+ break;
30
+ dir = parent;
31
+ }
32
+ return undefined;
33
+ }
34
+ function findNearestPackageJson(startPath) {
35
+ let dir = path.dirname(startPath);
36
+ while (true) {
37
+ const candidate = path.join(dir, "package.json");
38
+ if (fs.existsSync(candidate))
39
+ return candidate;
40
+ const parent = path.dirname(dir);
41
+ if (parent === dir)
42
+ break;
43
+ dir = parent;
44
+ }
45
+ return undefined;
46
+ }
47
+ function readPackageType(pkgJsonPath) {
48
+ try {
49
+ const raw = fs.readFileSync(pkgJsonPath, "utf8");
50
+ const parsed = JSON.parse(raw);
51
+ return parsed?.type === "module"
52
+ ? "module"
53
+ : parsed?.type === "commonjs"
54
+ ? "commonjs"
55
+ : undefined;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ async function loadUserConfig(configPath) {
62
+ const resolvedPath = findConfigPath(configPath);
63
+ if (!resolvedPath) {
64
+ console.warn("cms0: no cms0.config.* file found; provide --config");
65
+ return undefined;
66
+ }
67
+ const ext = path.extname(resolvedPath).toLowerCase();
68
+ if (ext === ".json") {
69
+ const json = fs.readFileSync(resolvedPath, "utf8");
70
+ return { path: resolvedPath, config: JSON.parse(json) };
71
+ }
72
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts" || ext === ".tsx") {
73
+ // If the enclosing package is ESM, use dynamic import instead of require.
74
+ const pkgJsonPath = findNearestPackageJson(resolvedPath);
75
+ const pkgType = pkgJsonPath ? readPackageType(pkgJsonPath) : undefined;
76
+ const isEsmPkg = pkgType === "module";
77
+ if (isEsmPkg) {
78
+ const compiledHref = await transpileTsModuleToTemp(resolvedPath);
79
+ const imported = (await import(compiledHref));
80
+ const config = imported?.default ?? imported?.config ?? imported ?? {};
81
+ cleanupTempModule(compiledHref);
82
+ return { path: resolvedPath, config };
83
+ }
84
+ const previous = process.env.TS_NODE_COMPILER_OPTIONS;
85
+ // register ts-node on demand so TypeScript configs can be required
86
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
87
+ require("ts-node/register/transpile-only");
88
+ try {
89
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
90
+ const required = require(resolvedPath);
91
+ const config = required?.default ?? required?.config ?? required ?? {};
92
+ return { path: resolvedPath, config };
93
+ }
94
+ finally {
95
+ if (previous === undefined) {
96
+ delete process.env.TS_NODE_COMPILER_OPTIONS;
97
+ }
98
+ else {
99
+ process.env.TS_NODE_COMPILER_OPTIONS = previous;
100
+ }
101
+ }
102
+ }
103
+ const imported = (await import(pathToFileURL(resolvedPath).href));
104
+ const config = imported?.default ?? imported?.config ?? imported ?? {};
105
+ return { path: resolvedPath, config };
106
+ }
107
+ function resolvePaths(cfgPath, config) {
108
+ if (!config.entry) {
109
+ throw new Error("cms0: config.entry is required");
110
+ }
111
+ const baseDir = path.dirname(cfgPath);
112
+ const entryFile = path.resolve(baseDir, config.entry);
113
+ const tsconfigPath = config.tsconfig
114
+ ? path.resolve(baseDir, config.tsconfig)
115
+ : undefined;
116
+ const apiBaseUrl = config.api?.baseUrl;
117
+ const apiKey = config.api?.key;
118
+ return {
119
+ configPath: cfgPath,
120
+ entryFile,
121
+ tsconfigPath,
122
+ apiBaseUrl,
123
+ apiKey,
124
+ };
125
+ }
126
+ function findTsConfig(entryFile) {
127
+ let dir = path.dirname(path.resolve(entryFile));
128
+ while (true) {
129
+ const candidate = path.join(dir, "tsconfig.json");
130
+ if (fs.existsSync(candidate)) {
131
+ return candidate;
132
+ }
133
+ const parent = path.dirname(dir);
134
+ if (parent === dir)
135
+ break;
136
+ dir = parent;
137
+ }
138
+ return undefined;
139
+ }
140
+ async function transpileTsModuleToTemp(tsPath) {
141
+ const ts = await import("typescript");
142
+ const source = fs.readFileSync(tsPath, "utf8");
143
+ const transpiled = ts.transpileModule(source, {
144
+ compilerOptions: {
145
+ module: ts.ModuleKind.ESNext,
146
+ target: ts.ScriptTarget.ES2022,
147
+ esModuleInterop: true,
148
+ },
149
+ fileName: tsPath,
150
+ });
151
+ const hash = crypto
152
+ .createHash("sha1")
153
+ .update(tsPath + source + Date.now().toString())
154
+ .digest("hex");
155
+ const outFile = path.join(path.dirname(tsPath), `.cms0-config.${hash}.mjs`);
156
+ fs.writeFileSync(outFile, transpiled.outputText, "utf8");
157
+ return pathToFileURL(outFile).href;
158
+ }
159
+ function cleanupTempModule(tempHref) {
160
+ try {
161
+ const filePath = fileURLToPath(tempHref);
162
+ fs.unlinkSync(filePath);
163
+ }
164
+ catch {
165
+ // ignore cleanup failures
166
+ }
167
+ }
168
+ export { DEFAULT_CONFIG_BASENAMES, findConfigPath, loadUserConfig, resolvePaths, findTsConfig, };
@@ -0,0 +1,216 @@
1
+ // Fresh descriptor builder using tree-based traversal from cms0<T>.
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { Project, SyntaxKind, } from "ts-morph";
5
+ import { findTsConfig } from "./config-loader.js";
6
+ function unwrapOptional(type) {
7
+ let optional = false;
8
+ let nullable = false;
9
+ if (type.isUnion()) {
10
+ const unionTypes = type.getUnionTypes();
11
+ const filtered = unionTypes.filter((t) => {
12
+ if (t.isNull()) {
13
+ nullable = true;
14
+ return false;
15
+ }
16
+ if (t.isUndefined()) {
17
+ optional = true;
18
+ return false;
19
+ }
20
+ return true;
21
+ });
22
+ if (filtered.length === 1) {
23
+ return { base: filtered[0], optional, nullable };
24
+ }
25
+ }
26
+ return { base: type, optional, nullable };
27
+ }
28
+ function collectModelNames(sourceFiles) {
29
+ const names = new Set();
30
+ sourceFiles.forEach((sf) => {
31
+ sf.getTypeAliases().forEach((alias) => {
32
+ if (!alias.hasExportKeyword() && !alias.isDefaultExport())
33
+ return;
34
+ if (alias.getType().isObject())
35
+ names.add(alias.getName());
36
+ });
37
+ sf.getInterfaces().forEach((iface) => {
38
+ if (!iface.hasExportKeyword() && !iface.isDefaultExport())
39
+ return;
40
+ if (iface.getType().isObject())
41
+ names.add(iface.getName());
42
+ });
43
+ });
44
+ return names;
45
+ }
46
+ function collectReferencedModelNames(type, names, visited, ctx) {
47
+ const { base } = unwrapOptional(type);
48
+ const key = `${ctx}:${base.getText()}`;
49
+ if (visited.has(key))
50
+ return;
51
+ visited.add(key);
52
+ if (base.isUnion()) {
53
+ base.getUnionTypes().forEach((t, idx) => collectReferencedModelNames(t, names, visited, `${ctx}|${idx}`));
54
+ return;
55
+ }
56
+ if (base.isArray()) {
57
+ const elem = base.getArrayElementTypeOrThrow();
58
+ collectReferencedModelNames(elem, names, visited, `${ctx}[]`);
59
+ return;
60
+ }
61
+ const aliasSymbol = base.getAliasSymbol();
62
+ const symbol = base.getSymbol();
63
+ const name = aliasSymbol?.getName() ?? symbol?.getName();
64
+ if (name && base.isObject()) {
65
+ names.add(name);
66
+ }
67
+ if (base.isObject()) {
68
+ base.getProperties().forEach((prop) => {
69
+ const decl = prop.getDeclarations()[0];
70
+ if (!decl)
71
+ return;
72
+ collectReferencedModelNames(decl.getType(), names, visited, `${ctx}.${prop.getName()}`);
73
+ });
74
+ }
75
+ }
76
+ function buildProperties(type, modelNames, warnings, ctx) {
77
+ const props = {};
78
+ type.getProperties().forEach((prop) => {
79
+ const decl = prop.getDeclarations()[0];
80
+ if (!decl)
81
+ return;
82
+ const analyzed = unwrapOptional(decl.getType());
83
+ props[prop.getName()] = descriptorFromType(analyzed.base, modelNames, warnings, `${ctx}.${prop.getName()}`, { optional: prop.isOptional() || analyzed.optional, nullable: analyzed.nullable });
84
+ });
85
+ return props;
86
+ }
87
+ function descriptorFromType(type, modelNames, warnings, ctx, opts) {
88
+ const { base, optional, nullable } = unwrapOptional(type);
89
+ const isOptional = !!opts?.optional || optional;
90
+ const isNullable = !!opts?.nullable || nullable;
91
+ if (base.isString()) {
92
+ return { kind: "primitive", type: "string", optional: isOptional, nullable: isNullable };
93
+ }
94
+ if (base.isNumber()) {
95
+ return { kind: "primitive", type: "number", optional: isOptional, nullable: isNullable };
96
+ }
97
+ if (base.isBoolean()) {
98
+ return { kind: "primitive", type: "boolean", optional: isOptional, nullable: isNullable };
99
+ }
100
+ if (base.isArray()) {
101
+ const elem = base.getArrayElementTypeOrThrow();
102
+ return {
103
+ type: "array",
104
+ items: descriptorFromType(elem, modelNames, warnings, `${ctx}[]`),
105
+ optional: isOptional,
106
+ nullable: isNullable,
107
+ };
108
+ }
109
+ const aliasSymbol = base.getAliasSymbol();
110
+ const symbol = base.getSymbol();
111
+ const name = aliasSymbol?.getName() ?? symbol?.getName();
112
+ if (name && modelNames.has(name)) {
113
+ return { kind: "modelRef", model: name, optional: isOptional, nullable: isNullable };
114
+ }
115
+ if (base.isObject()) {
116
+ return {
117
+ type: "object",
118
+ properties: buildProperties(base, modelNames, warnings, ctx),
119
+ optional: isOptional,
120
+ nullable: isNullable,
121
+ };
122
+ }
123
+ warnings.add(`cms0: unsupported type at ${ctx}; falling back to string`);
124
+ return { kind: "primitive", type: "string", optional: isOptional, nullable: isNullable };
125
+ }
126
+ function collectModels(sourceFiles, modelNames, modelMap, warnings) {
127
+ const handleShape = (name, type, ctx) => {
128
+ if (modelMap[name])
129
+ return;
130
+ if (!modelNames.has(name))
131
+ return;
132
+ if (!type.isObject())
133
+ return;
134
+ modelMap[name] = { kind: "model", properties: buildProperties(type, modelNames, warnings, ctx) };
135
+ };
136
+ sourceFiles.forEach((sf) => {
137
+ sf.getTypeAliases().forEach((alias) => {
138
+ if (!alias.hasExportKeyword() && !alias.isDefaultExport())
139
+ return;
140
+ handleShape(alias.getName(), alias.getType(), `model ${alias.getName()}`);
141
+ });
142
+ sf.getInterfaces().forEach((iface) => {
143
+ if (!iface.hasExportKeyword() && !iface.isDefaultExport())
144
+ return;
145
+ handleShape(iface.getName(), iface.getType(), `model ${iface.getName()}`);
146
+ });
147
+ });
148
+ }
149
+ function findRootType(sourceFiles) {
150
+ for (const sf of sourceFiles) {
151
+ const invoc = sf
152
+ ?.getDescendantsOfKind(SyntaxKind.CallExpression)
153
+ .find((call) => call.getExpression().getText() === "cms0");
154
+ if (invoc) {
155
+ const typeArgs = invoc.getTypeArguments();
156
+ if (typeArgs.length > 0)
157
+ return typeArgs[0].getType();
158
+ }
159
+ }
160
+ return undefined;
161
+ }
162
+ function buildDescriptorAlt(resolved) {
163
+ const tsconfig = resolved.tsconfigPath ?? findTsConfig(resolved.entryFile);
164
+ const project = tsconfig ? new Project({ tsConfigFilePath: tsconfig }) : new Project();
165
+ const warnings = new Set();
166
+ if (!project.getSourceFile(resolved.entryFile) && fs.existsSync(resolved.entryFile)) {
167
+ project.addSourceFileAtPath(resolved.entryFile);
168
+ }
169
+ const sourceFiles = project.getSourceFiles().filter((sf) => {
170
+ const filePath = path.resolve(sf.getFilePath());
171
+ if (filePath.includes("node_modules"))
172
+ return false;
173
+ if (filePath.endsWith(".d.ts"))
174
+ return false;
175
+ const rel = path.relative(path.dirname(resolved.entryFile), filePath);
176
+ return rel && !rel.startsWith("..") && !path.isAbsolute(rel);
177
+ });
178
+ const rootType = findRootType(sourceFiles);
179
+ if (!rootType)
180
+ throw new Error("Could not locate cms0<T>() invocation in project sources.");
181
+ // Restrict models to only those referenced by the root type tree and exported in scope.
182
+ const exportedModelNames = collectModelNames(sourceFiles);
183
+ const referencedModelNames = new Set();
184
+ collectReferencedModelNames(rootType, referencedModelNames, new Set(), "root");
185
+ const modelNames = new Set(Array.from(referencedModelNames).filter((n) => exportedModelNames.has(n)));
186
+ const modelMap = {};
187
+ collectModels(sourceFiles, modelNames, modelMap, warnings);
188
+ const roots = {};
189
+ for (const prop of rootType.getProperties()) {
190
+ const name = prop.getName();
191
+ const decl = prop.getDeclarations()[0];
192
+ if (!decl)
193
+ continue;
194
+ const analyzed = unwrapOptional(decl.getType());
195
+ const propType = analyzed.base;
196
+ const propOptional = prop.isOptional() || analyzed.optional;
197
+ const propNullable = analyzed.nullable;
198
+ if (propType.isArray()) {
199
+ const elem = propType.getArrayElementTypeOrThrow();
200
+ const itemDesc = descriptorFromType(elem, modelNames, warnings, `root ${name}[]`, { optional: false, nullable: false });
201
+ roots[name] = {
202
+ type: "array",
203
+ items: itemDesc,
204
+ optional: propOptional,
205
+ nullable: propNullable,
206
+ };
207
+ continue;
208
+ }
209
+ const fieldDesc = descriptorFromType(propType, modelNames, warnings, `root ${name}`, { optional: propOptional, nullable: propNullable });
210
+ roots[name] = fieldDesc;
211
+ }
212
+ if (warnings.size)
213
+ warnings.forEach((w) => console.warn(w));
214
+ return { models: modelMap, roots };
215
+ }
216
+ export { buildDescriptorAlt };
@@ -0,0 +1,227 @@
1
+ // Build a schema descriptor by inspecting user types with ts-morph.
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { Project, SyntaxKind, } from "ts-morph";
5
+ import { findTsConfig } from "./config-loader.js";
6
+ function unwrapOptional(type) {
7
+ let optional = false;
8
+ let nullable = false;
9
+ if (type.isUnion()) {
10
+ const unionTypes = type.getUnionTypes();
11
+ const filtered = unionTypes.filter((t) => {
12
+ if (t.isNull()) {
13
+ nullable = true;
14
+ return false;
15
+ }
16
+ if (t.isUndefined()) {
17
+ optional = true;
18
+ return false;
19
+ }
20
+ return true;
21
+ });
22
+ if (filtered.length === 1) {
23
+ return { base: filtered[0], optional, nullable };
24
+ }
25
+ }
26
+ return { base: type, optional, nullable };
27
+ }
28
+ function getDescriptorForType(type, modelMap, warnings, ctx, opts) {
29
+ const { base, optional, nullable } = unwrapOptional(type);
30
+ const isOptional = !!opts?.optional || optional;
31
+ const isNullable = !!opts?.nullable || nullable;
32
+ if (base.isString()) {
33
+ return {
34
+ kind: "primitive",
35
+ type: "string",
36
+ optional: isOptional,
37
+ nullable: isNullable,
38
+ };
39
+ }
40
+ if (base.isNumber()) {
41
+ return {
42
+ kind: "primitive",
43
+ type: "number",
44
+ optional: isOptional,
45
+ nullable: isNullable,
46
+ };
47
+ }
48
+ if (base.isBoolean()) {
49
+ return {
50
+ kind: "primitive",
51
+ type: "boolean",
52
+ optional: isOptional,
53
+ nullable: isNullable,
54
+ };
55
+ }
56
+ if (base.isArray()) {
57
+ const elem = base.getArrayElementTypeOrThrow();
58
+ return {
59
+ kind: "array",
60
+ type: "array",
61
+ items: getDescriptorForType(elem, modelMap, warnings, `${ctx}[]`),
62
+ optional: isOptional,
63
+ nullable: isNullable,
64
+ };
65
+ }
66
+ const aliasSymbol = base.getAliasSymbol();
67
+ if (aliasSymbol) {
68
+ const name = aliasSymbol.getName();
69
+ if (modelMap[name]) {
70
+ return {
71
+ kind: "modelRef",
72
+ model: name,
73
+ optional: isOptional,
74
+ nullable: isNullable,
75
+ };
76
+ }
77
+ }
78
+ if (base.isObject() && !base.isInterface()) {
79
+ const props = {};
80
+ for (const prop of base.getProperties()) {
81
+ const propDecl = prop.getDeclarations()[0];
82
+ const analyzed = unwrapOptional(propDecl.getType());
83
+ props[prop.getName()] = getDescriptorForType(analyzed.base, modelMap, warnings, `${ctx}.${prop.getName()}`, {
84
+ optional: prop.isOptional() || analyzed.optional,
85
+ nullable: analyzed.nullable,
86
+ });
87
+ }
88
+ return {
89
+ kind: "object",
90
+ type: "object",
91
+ properties: props,
92
+ optional: isOptional,
93
+ nullable: isNullable,
94
+ };
95
+ }
96
+ // unsupported or unknown type; fall back to string and record warning
97
+ warnings.add(`cms0: unsupported type at ${ctx}; falling back to string`);
98
+ return {
99
+ kind: "primitive",
100
+ type: "string",
101
+ optional: isOptional,
102
+ nullable: isNullable,
103
+ };
104
+ }
105
+ function collectModels(sourceFiles, modelMap, warnings) {
106
+ sourceFiles.forEach((sf) => {
107
+ sf.getTypeAliases().forEach((alias) => {
108
+ if (!alias.hasExportKeyword())
109
+ return;
110
+ const name = alias.getName();
111
+ const type = alias.getType();
112
+ if (type.isObject()) {
113
+ const props = {};
114
+ for (const prop of type.getProperties()) {
115
+ const decl = prop.getDeclarations()[0];
116
+ const propType = decl.getType();
117
+ props[prop.getName()] = getDescriptorForType(propType, modelMap, warnings, `model ${name}.${prop.getName()}`);
118
+ }
119
+ modelMap[name] = { kind: "model", properties: props };
120
+ }
121
+ });
122
+ });
123
+ }
124
+ function findRootType(sourceFiles) {
125
+ for (const sf of sourceFiles) {
126
+ const invoc = sf
127
+ ?.getDescendantsOfKind(SyntaxKind.CallExpression)
128
+ .find((call) => call.getExpression().getText() === "cms0");
129
+ if (invoc) {
130
+ const typeArgs = invoc.getTypeArguments();
131
+ if (typeArgs.length > 0) {
132
+ return typeArgs[0].getType();
133
+ }
134
+ }
135
+ }
136
+ return undefined;
137
+ }
138
+ function buildDescriptor(resolved) {
139
+ const tsconfig = resolved.tsconfigPath ?? findTsConfig(resolved.entryFile);
140
+ const project = tsconfig
141
+ ? new Project({ tsConfigFilePath: tsconfig })
142
+ : new Project();
143
+ const warnings = new Set();
144
+ const entryDir = path.dirname(path.resolve(resolved.entryFile));
145
+ if (!project.getSourceFile(resolved.entryFile) &&
146
+ fs.existsSync(resolved.entryFile)) {
147
+ project.addSourceFileAtPath(resolved.entryFile);
148
+ }
149
+ const sourceFiles = project.getSourceFiles().filter((sf) => {
150
+ const filePath = path.resolve(sf.getFilePath());
151
+ const rel = path.relative(entryDir, filePath);
152
+ return rel && !rel.startsWith("..") && !path.isAbsolute(rel);
153
+ });
154
+ const modelMap = {};
155
+ collectModels(sourceFiles, modelMap, warnings);
156
+ const rootType = findRootType(sourceFiles);
157
+ if (!rootType) {
158
+ throw new Error("Could not locate cms0<T>() invocation in project sources.");
159
+ }
160
+ const roots = {};
161
+ for (const prop of rootType.getProperties()) {
162
+ const name = prop.getName();
163
+ const decl = prop.getDeclarations()[0];
164
+ const analyzed = unwrapOptional(decl.getType());
165
+ const propType = analyzed.base;
166
+ const propOptional = prop.isOptional() || analyzed.optional;
167
+ const propNullable = analyzed.nullable;
168
+ if (propType.isArray()) {
169
+ const elem = propType.getArrayElementTypeOrThrow();
170
+ const itemDesc = getDescriptorForType(elem, modelMap, warnings, `root ${name}[]`);
171
+ roots[name] = {
172
+ type: "array",
173
+ items: itemDesc,
174
+ optional: propOptional,
175
+ nullable: propNullable,
176
+ };
177
+ }
178
+ else if (propType.isObject() && !propType.isArray()) {
179
+ const objDesc = getDescriptorForType(propType, modelMap, warnings, `root ${name}`, {
180
+ optional: propOptional,
181
+ nullable: propNullable,
182
+ });
183
+ if (objDesc.kind === "object") {
184
+ roots[name] = {
185
+ type: "object",
186
+ properties: objDesc.properties,
187
+ optional: propOptional,
188
+ nullable: propNullable,
189
+ };
190
+ }
191
+ else if (objDesc.kind === "modelRef") {
192
+ roots[name] = {
193
+ type: "object",
194
+ properties: {},
195
+ optional: propOptional,
196
+ nullable: propNullable,
197
+ };
198
+ }
199
+ else {
200
+ roots[name] = {
201
+ type: "object",
202
+ properties: {},
203
+ optional: propOptional,
204
+ nullable: propNullable,
205
+ };
206
+ }
207
+ }
208
+ else {
209
+ roots[name] = {
210
+ type: "object",
211
+ properties: {
212
+ [name]: getDescriptorForType(propType, modelMap, warnings, `root ${name}.${name}`, {
213
+ optional: propOptional,
214
+ nullable: propNullable,
215
+ }),
216
+ },
217
+ optional: propOptional,
218
+ nullable: propNullable,
219
+ };
220
+ }
221
+ }
222
+ if (warnings.size) {
223
+ warnings.forEach((w) => console.warn(w));
224
+ }
225
+ return { models: modelMap, roots };
226
+ }
227
+ export { buildDescriptor };
@@ -0,0 +1,18 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ function writeDescriptorFile(descriptor, outputPath) {
4
+ const output = `// Auto-generated schema descriptor\nexport const schemaDescriptor = ${JSON.stringify(descriptor, null, 2)} as const;\n`;
5
+ try {
6
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
7
+ fs.writeFileSync(outputPath, output, "utf8");
8
+ }
9
+ catch (err) {
10
+ if (err?.code === "EPERM" || err?.code === "EACCES") {
11
+ console.warn(`cms0: skipped writing descriptor (no write permission): ${outputPath}`);
12
+ }
13
+ else {
14
+ throw err;
15
+ }
16
+ }
17
+ }
18
+ export { writeDescriptorFile };
@@ -0,0 +1,4 @@
1
+ // Public surface for cms0 CLI utilities.
2
+ export { buildOnce } from "./build.js";
3
+ export { startWatcher } from "./watcher.js";
4
+ export { runFromCli } from "./cli.js";
@@ -0,0 +1,21 @@
1
+ // Path utilities for locating package-relative outputs.
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ const importMetaUrl = (() => {
5
+ try {
6
+ // Avoid syntax that breaks CommonJS compilation; evaluate only if supported
7
+ // eslint-disable-next-line no-new-func
8
+ return Function("return import.meta.url")();
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ })();
14
+ const here = typeof __dirname === "string"
15
+ ? __dirname
16
+ : importMetaUrl
17
+ ? path.dirname(fileURLToPath(importMetaUrl))
18
+ : process.cwd();
19
+ const packageRoot = path.resolve(here, "../../..");
20
+ const descriptorOutPath = path.resolve(packageRoot, "src/generated/schema-descriptor.ts");
21
+ export { descriptorOutPath, packageRoot };