@rexeus/typeweaver 0.10.3 → 0.10.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.
- package/dist/{cli-Dz7saa33.mjs → cli-BVUW7VcY.mjs} +236 -46
- package/dist/cli.cjs +236 -46
- package/dist/cli.mjs +236 -46
- package/dist/cli.mjs.map +1 -1
- package/dist/entry.mjs +1 -1
- package/dist/templates/Index.ejs +2 -2
- package/package.json +8 -8
|
@@ -7,42 +7,83 @@ import { createPluginContextBuilder, createPluginRegistry, normalizeSpec, render
|
|
|
7
7
|
import { TypesPlugin } from "@rexeus/typeweaver-types";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { build } from "rolldown";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import { HttpMethod, HttpStatusCode } from "@rexeus/typeweaver-core";
|
|
12
|
+
//#region src/errors/InvalidConfigExportError.ts
|
|
13
|
+
var InvalidConfigExportError = class extends Error {
|
|
14
|
+
name = "InvalidConfigExportError";
|
|
15
|
+
constructor(configPath, reason) {
|
|
16
|
+
super(getInvalidConfigExportMessage(configPath, reason));
|
|
17
|
+
this.configPath = configPath;
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const getInvalidConfigExportMessage = (configPath, reason) => {
|
|
22
|
+
switch (reason) {
|
|
23
|
+
case "both-default-and-named-config": return `Configuration file '${configPath}' must choose one export style: use either 'export default' or 'export const config = ...', but not both.`;
|
|
24
|
+
case "default-namespace-wrapper": return `Configuration file '${configPath}' default export must be the config object itself, not a module namespace-like wrapper. Export the config directly with 'export default { ... }' or use 'export const config = ...'.`;
|
|
25
|
+
case "missing-config-export": return `Configuration file '${configPath}' must export its config via 'export default' or 'export const config = ...'.`;
|
|
26
|
+
case "non-object-config": return `Configuration file '${configPath}' must export a config object via 'export default' or 'export const config = ...'.`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/errors/UnsupportedConfigExtensionError.ts
|
|
31
|
+
var UnsupportedConfigExtensionError = class extends Error {
|
|
32
|
+
name = "UnsupportedConfigExtensionError";
|
|
33
|
+
constructor(configPath, extension, supportedExtensions) {
|
|
34
|
+
super(`Unsupported config file extension '${extension}' for '${configPath}'. TypeWeaver accepts only these config extensions: ${supportedExtensions.join(", ")}.`);
|
|
35
|
+
this.configPath = configPath;
|
|
36
|
+
this.extension = extension;
|
|
37
|
+
this.supportedExtensions = supportedExtensions;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/errors/UnsupportedTypeScriptConfigError.ts
|
|
42
|
+
var UnsupportedTypeScriptConfigError = class extends Error {
|
|
43
|
+
name = "UnsupportedTypeScriptConfigError";
|
|
44
|
+
constructor(configPath, extension) {
|
|
45
|
+
super(`TypeScript config files are not supported: '${configPath}' uses '${extension}'. Use a JavaScript config file with one of these extensions: .js, .mjs, or .cjs.`);
|
|
46
|
+
this.configPath = configPath;
|
|
47
|
+
this.extension = extension;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
11
51
|
//#region src/configLoader.ts
|
|
12
|
-
const SUPPORTED_CONFIG_EXTENSIONS =
|
|
52
|
+
const SUPPORTED_CONFIG_EXTENSIONS = [
|
|
13
53
|
".js",
|
|
14
54
|
".mjs",
|
|
15
55
|
".cjs"
|
|
16
|
-
]
|
|
56
|
+
];
|
|
57
|
+
const SUPPORTED_CONFIG_EXTENSION_SET = new Set(SUPPORTED_CONFIG_EXTENSIONS);
|
|
17
58
|
const UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS = new Set([
|
|
18
59
|
".ts",
|
|
19
60
|
".mts",
|
|
20
61
|
".cts"
|
|
21
62
|
]);
|
|
22
63
|
const getResolvedConfigPath = (configPath, currentWorkingDirectory = process.cwd()) => {
|
|
23
|
-
return path.isAbsolute(configPath) ? configPath : path.
|
|
64
|
+
return path.isAbsolute(configPath) ? configPath : path.resolve(currentWorkingDirectory, configPath);
|
|
24
65
|
};
|
|
25
66
|
const assertSupportedConfigPath = (configPath) => {
|
|
26
67
|
const extension = path.extname(configPath).toLowerCase();
|
|
27
|
-
if (UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS.has(extension)) throw new
|
|
28
|
-
if (!
|
|
68
|
+
if (UNSUPPORTED_TYPESCRIPT_CONFIG_EXTENSIONS.has(extension)) throw new UnsupportedTypeScriptConfigError(configPath, extension);
|
|
69
|
+
if (!SUPPORTED_CONFIG_EXTENSION_SET.has(extension)) throw new UnsupportedConfigExtensionError(configPath, extension, SUPPORTED_CONFIG_EXTENSIONS);
|
|
29
70
|
};
|
|
30
71
|
const loadConfig = async (configPath) => {
|
|
31
72
|
assertSupportedConfigPath(configPath);
|
|
32
73
|
const loadedConfig = getConfigExport(await import(pathToFileURL(path.resolve(configPath)).toString()), configPath);
|
|
33
|
-
if (!isConfigObject(loadedConfig)) throw new
|
|
74
|
+
if (!isConfigObject(loadedConfig)) throw new InvalidConfigExportError(configPath, "non-object-config");
|
|
34
75
|
return loadedConfig;
|
|
35
76
|
};
|
|
36
77
|
const getConfigExport = (configModule, configPath) => {
|
|
37
78
|
const hasDefaultExport = Object.hasOwn(configModule, "default");
|
|
38
79
|
const hasNamedConfigExport = Object.hasOwn(configModule, "config");
|
|
39
|
-
if (hasDefaultExport && hasNamedConfigExport) throw new
|
|
80
|
+
if (hasDefaultExport && hasNamedConfigExport) throw new InvalidConfigExportError(configPath, "both-default-and-named-config");
|
|
40
81
|
if (hasDefaultExport) {
|
|
41
|
-
if (isNamespaceLikeConfigExport(configModule.default)) throw new
|
|
82
|
+
if (isNamespaceLikeConfigExport(configModule.default)) throw new InvalidConfigExportError(configPath, "default-namespace-wrapper");
|
|
42
83
|
return configModule.default;
|
|
43
84
|
}
|
|
44
85
|
if (hasNamedConfigExport) return configModule.config;
|
|
45
|
-
throw new
|
|
86
|
+
throw new InvalidConfigExportError(configPath, "missing-config-export");
|
|
46
87
|
};
|
|
47
88
|
const isConfigObject = (value) => {
|
|
48
89
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -52,6 +93,35 @@ const isNamespaceLikeConfigExport = (value) => {
|
|
|
52
93
|
return Object.hasOwn(value, "default") || Object.hasOwn(value, "config");
|
|
53
94
|
};
|
|
54
95
|
//#endregion
|
|
96
|
+
//#region src/generators/errors/UnsafeCleanTargetError.ts
|
|
97
|
+
var UnsafeCleanTargetError = class extends Error {
|
|
98
|
+
name = "UnsafeCleanTargetError";
|
|
99
|
+
resolvedOutputDir;
|
|
100
|
+
currentWorkingDirectory;
|
|
101
|
+
protectedWorkspaceRoot;
|
|
102
|
+
filesystemRoot;
|
|
103
|
+
constructor(outputDir, reason, diagnostics = {}) {
|
|
104
|
+
super(getUnsafeCleanTargetMessage(outputDir, reason, diagnostics));
|
|
105
|
+
this.outputDir = outputDir;
|
|
106
|
+
this.reason = reason;
|
|
107
|
+
this.resolvedOutputDir = diagnostics.resolvedOutputDir;
|
|
108
|
+
this.currentWorkingDirectory = diagnostics.currentWorkingDirectory;
|
|
109
|
+
this.protectedWorkspaceRoot = diagnostics.protectedWorkspaceRoot;
|
|
110
|
+
this.filesystemRoot = diagnostics.filesystemRoot;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const getUnsafeCleanTargetMessage = (outputDir, reason, diagnostics) => {
|
|
114
|
+
const targetDescription = `Refusing to clean '${outputDir}'`;
|
|
115
|
+
const suffix = "Use a dedicated generated output directory instead.";
|
|
116
|
+
switch (reason) {
|
|
117
|
+
case "empty-path": return `Refusing to clean an empty output directory path. ${suffix}`;
|
|
118
|
+
case "filesystem-root": return `${targetDescription} because it resolves to the filesystem root '${diagnostics.filesystemRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
|
|
119
|
+
case "current-working-directory": return `${targetDescription} because it resolves to the current working directory '${diagnostics.currentWorkingDirectory ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
|
|
120
|
+
case "workspace-root": return `${targetDescription} because it resolves to the protected workspace root '${diagnostics.protectedWorkspaceRoot ?? diagnostics.resolvedOutputDir ?? outputDir}'. ${suffix}`;
|
|
121
|
+
case "ancestor-of-current-working-directory": return `${targetDescription} because it resolves to an ancestor directory of the current working directory '${diagnostics.currentWorkingDirectory ?? ""}'. ${suffix}`;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
55
125
|
//#region src/generators/formatter.ts
|
|
56
126
|
async function formatCode(outputDir, startDir) {
|
|
57
127
|
const format = await loadFormatter();
|
|
@@ -88,11 +158,16 @@ function generateIndexFiles(templateDir, context) {
|
|
|
88
158
|
const withJsExt = normalizedFile.replace(/\.ts$/, ".js");
|
|
89
159
|
const stripped = normalizedFile.replace(/\.ts$/, "");
|
|
90
160
|
const firstSlash = stripped.indexOf("/");
|
|
161
|
+
if (stripped === "index") continue;
|
|
91
162
|
if (firstSlash === -1) {
|
|
92
163
|
rootFiles.add(`./${withJsExt}`);
|
|
93
164
|
continue;
|
|
94
165
|
}
|
|
95
166
|
const firstSegment = stripped.slice(0, firstSlash);
|
|
167
|
+
if (stripped === "lib/index") {
|
|
168
|
+
existingBarrels.add("lib");
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
96
171
|
if (firstSegment === "lib") {
|
|
97
172
|
const secondSlash = stripped.indexOf("/", firstSlash + 1);
|
|
98
173
|
const groupKey = secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
|
|
@@ -141,30 +216,29 @@ async function loadPlugins(registry, requiredPlugins, strategies, config) {
|
|
|
141
216
|
if (!config?.plugins) return;
|
|
142
217
|
const successful = [];
|
|
143
218
|
for (const plugin of config.plugins) {
|
|
144
|
-
|
|
145
|
-
if (typeof plugin === "string") result = await loadPlugin(plugin, strategies);
|
|
146
|
-
else result = await loadPlugin(plugin[0], strategies);
|
|
219
|
+
const result = await loadPlugin(typeof plugin === "string" ? plugin : plugin[0], strategies, typeof plugin === "string" ? void 0 : plugin[1]);
|
|
147
220
|
if (result.success === false) throw new PluginLoadingFailure(result.error.pluginName, result.error.attempts);
|
|
148
221
|
successful.push(result.value);
|
|
149
|
-
registry.register(result.value.plugin);
|
|
222
|
+
registry.register(result.value.plugin, result.value.config);
|
|
150
223
|
}
|
|
151
224
|
reportSuccessfulLoads(successful);
|
|
152
225
|
}
|
|
153
|
-
async function loadPlugin(pluginName, strategies) {
|
|
226
|
+
async function loadPlugin(pluginName, strategies, pluginConfig) {
|
|
154
227
|
const possiblePaths = generatePluginPaths(pluginName, strategies);
|
|
155
228
|
const attempts = [];
|
|
156
229
|
for (const possiblePath of possiblePaths) try {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
230
|
+
const plugin = createPluginInstance(await import(possiblePath), pluginConfig);
|
|
231
|
+
if (plugin.success) return {
|
|
159
232
|
success: true,
|
|
160
233
|
value: {
|
|
161
|
-
plugin:
|
|
162
|
-
source: possiblePath
|
|
234
|
+
plugin: plugin.value,
|
|
235
|
+
source: possiblePath,
|
|
236
|
+
config: pluginConfig
|
|
163
237
|
}
|
|
164
238
|
};
|
|
165
239
|
attempts.push({
|
|
166
240
|
path: possiblePath,
|
|
167
|
-
error:
|
|
241
|
+
error: plugin.error
|
|
168
242
|
});
|
|
169
243
|
} catch (error) {
|
|
170
244
|
attempts.push({
|
|
@@ -180,10 +254,46 @@ async function loadPlugin(pluginName, strategies) {
|
|
|
180
254
|
}
|
|
181
255
|
};
|
|
182
256
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
257
|
+
function createPluginInstance(pluginModule, pluginConfig) {
|
|
258
|
+
const candidates = findPluginConstructorCandidates(pluginModule);
|
|
259
|
+
if (candidates.length === 0) return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: "No plugin constructor export found"
|
|
262
|
+
};
|
|
263
|
+
const errors = [];
|
|
264
|
+
for (const candidate of candidates) try {
|
|
265
|
+
const plugin = new candidate.constructor(pluginConfig);
|
|
266
|
+
if (isTypeweaverPlugin(plugin)) return {
|
|
267
|
+
success: true,
|
|
268
|
+
value: plugin
|
|
269
|
+
};
|
|
270
|
+
errors.push(`Export '${candidate.exportName}' did not produce a valid plugin with a string name`);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
errors.push(`Export '${candidate.exportName}' could not be instantiated: ${formatError(error)}`);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
success: false,
|
|
276
|
+
error: errors.join("; ")
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function findPluginConstructorCandidates(pluginModule) {
|
|
280
|
+
const candidates = [];
|
|
281
|
+
for (const [key, value] of Object.entries(pluginModule)) if (key !== "default" && typeof value === "function") candidates.push({
|
|
282
|
+
exportName: key,
|
|
283
|
+
constructor: value
|
|
284
|
+
});
|
|
185
285
|
const defaultExport = pluginModule.default;
|
|
186
|
-
if (typeof defaultExport === "function")
|
|
286
|
+
if (typeof defaultExport === "function") candidates.push({
|
|
287
|
+
exportName: "default",
|
|
288
|
+
constructor: defaultExport
|
|
289
|
+
});
|
|
290
|
+
return candidates;
|
|
291
|
+
}
|
|
292
|
+
function isTypeweaverPlugin(value) {
|
|
293
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && value.name.length > 0;
|
|
294
|
+
}
|
|
295
|
+
function formatError(error) {
|
|
296
|
+
return error instanceof Error ? error.message : String(error);
|
|
187
297
|
}
|
|
188
298
|
function generatePluginPaths(pluginName, strategies) {
|
|
189
299
|
const paths = [];
|
|
@@ -193,11 +303,19 @@ function generatePluginPaths(pluginName, strategies) {
|
|
|
193
303
|
paths.push(`@rexeus/${pluginName}`);
|
|
194
304
|
break;
|
|
195
305
|
case "local":
|
|
306
|
+
paths.push(toLocalImportSpecifier(pluginName));
|
|
307
|
+
break;
|
|
308
|
+
case "scoped":
|
|
196
309
|
paths.push(pluginName);
|
|
197
310
|
break;
|
|
198
311
|
}
|
|
199
312
|
return paths;
|
|
200
313
|
}
|
|
314
|
+
function toLocalImportSpecifier(pluginName) {
|
|
315
|
+
if (pluginName.startsWith("file:")) return pluginName;
|
|
316
|
+
if (path.isAbsolute(pluginName)) return pathToFileURL(pluginName).href;
|
|
317
|
+
return pluginName;
|
|
318
|
+
}
|
|
201
319
|
function reportSuccessfulLoads(successful) {
|
|
202
320
|
if (successful.length > 0) {
|
|
203
321
|
console.info(`Successfully loaded ${successful.length} plugin(s):`);
|
|
@@ -205,6 +323,17 @@ function reportSuccessfulLoads(successful) {
|
|
|
205
323
|
}
|
|
206
324
|
}
|
|
207
325
|
//#endregion
|
|
326
|
+
//#region src/generators/spec/errors/SpecBundleOutputMissingError.ts
|
|
327
|
+
var SpecBundleOutputMissingError = class extends Error {
|
|
328
|
+
name = "SpecBundleOutputMissingError";
|
|
329
|
+
constructor(inputFile, bundledSpecFile, specOutputDir) {
|
|
330
|
+
super(`Spec bundling completed but did not create the expected output '${bundledSpecFile}' for entrypoint '${inputFile}'. Expected the bundle inside '${specOutputDir}'.`);
|
|
331
|
+
this.inputFile = inputFile;
|
|
332
|
+
this.bundledSpecFile = bundledSpecFile;
|
|
333
|
+
this.specOutputDir = specOutputDir;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
208
337
|
//#region src/generators/spec/specBundler.ts
|
|
209
338
|
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
|
210
339
|
const WINDOWS_UNC_PATH_PATTERN = /^\\\\/;
|
|
@@ -218,7 +347,7 @@ function createWrapperImportSpecifier(wrapperFile, inputFile) {
|
|
|
218
347
|
if (relativeInputFile.startsWith(".") || relativeInputFile.startsWith("..")) return relativeInputFile;
|
|
219
348
|
return `./${relativeInputFile}`;
|
|
220
349
|
}
|
|
221
|
-
async function bundle(config) {
|
|
350
|
+
async function bundle(config, deps = {}) {
|
|
222
351
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "typeweaver-spec-loader-"));
|
|
223
352
|
const wrapperFile = path.join(tempDir, "spec-entrypoint.ts");
|
|
224
353
|
const bundledSpecFile = path.join(config.specOutputDir, "spec.js");
|
|
@@ -234,7 +363,7 @@ async function bundle(config) {
|
|
|
234
363
|
""
|
|
235
364
|
].join("\n"));
|
|
236
365
|
try {
|
|
237
|
-
await build({
|
|
366
|
+
await (deps.build ?? build)({
|
|
238
367
|
cwd: tempDir,
|
|
239
368
|
input: wrapperFile,
|
|
240
369
|
treeshake: true,
|
|
@@ -254,7 +383,7 @@ async function bundle(config) {
|
|
|
254
383
|
force: true
|
|
255
384
|
});
|
|
256
385
|
}
|
|
257
|
-
if (!fs.existsSync(bundledSpecFile)) throw new
|
|
386
|
+
if (!(deps.existsSync ?? fs.existsSync)(bundledSpecFile)) throw new SpecBundleOutputMissingError(config.inputFile, bundledSpecFile, config.specOutputDir);
|
|
258
387
|
return bundledSpecFile;
|
|
259
388
|
}
|
|
260
389
|
function resolveBundledInputFile(inputFile) {
|
|
@@ -304,7 +433,10 @@ const isSpecDefinition = (value) => {
|
|
|
304
433
|
//#endregion
|
|
305
434
|
//#region src/generators/spec/specImporter.ts
|
|
306
435
|
async function importDefinition(bundledSpecFile) {
|
|
307
|
-
const
|
|
436
|
+
const contentHash = createHash("sha256").update(fs.readFileSync(bundledSpecFile)).digest("hex");
|
|
437
|
+
const moduleUrl = pathToFileURL(bundledSpecFile);
|
|
438
|
+
moduleUrl.searchParams.set("content", contentHash);
|
|
439
|
+
const specModule = await import(moduleUrl.toString());
|
|
308
440
|
const definition = specModule.spec ?? specModule.default ?? specModule;
|
|
309
441
|
if (!isSpecDefinition(definition)) throw new InvalidSpecEntrypointError(bundledSpecFile);
|
|
310
442
|
return definition;
|
|
@@ -333,13 +465,32 @@ function writeSpecDeclarationFile(specOutputDir) {
|
|
|
333
465
|
const moduleDir$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
334
466
|
const assertSafeCleanTarget = (outputDir, currentWorkingDirectory) => {
|
|
335
467
|
const trimmedOutputDir = outputDir.trim();
|
|
336
|
-
if (trimmedOutputDir.length === 0) throw new
|
|
468
|
+
if (trimmedOutputDir.length === 0) throw new UnsafeCleanTargetError(outputDir, "empty-path");
|
|
337
469
|
const resolvedWorkingDirectory = path.resolve(currentWorkingDirectory);
|
|
470
|
+
const canonicalWorkingDirectory = fs.realpathSync.native(resolvedWorkingDirectory);
|
|
338
471
|
const resolvedOutputDir = path.resolve(resolvedWorkingDirectory, trimmedOutputDir);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
472
|
+
const canonicalOutputDir = canonicalizePathForContainment(resolvedOutputDir);
|
|
473
|
+
const filesystemRoot = path.parse(canonicalOutputDir).root;
|
|
474
|
+
if (canonicalOutputDir === filesystemRoot) throw new UnsafeCleanTargetError(outputDir, "filesystem-root", {
|
|
475
|
+
resolvedOutputDir,
|
|
476
|
+
currentWorkingDirectory: resolvedWorkingDirectory,
|
|
477
|
+
filesystemRoot
|
|
478
|
+
});
|
|
479
|
+
if (resolvedOutputDir === resolvedWorkingDirectory || canonicalOutputDir === canonicalWorkingDirectory) throw new UnsafeCleanTargetError(outputDir, "current-working-directory", {
|
|
480
|
+
resolvedOutputDir,
|
|
481
|
+
currentWorkingDirectory: resolvedWorkingDirectory
|
|
482
|
+
});
|
|
483
|
+
const protectedWorkspaceRoots = [findProtectedWorkspaceRoot(resolvedWorkingDirectory), findProtectedWorkspaceRoot(canonicalWorkingDirectory)].filter((root) => root !== void 0);
|
|
484
|
+
const protectedWorkspaceRootTarget = protectedWorkspaceRoots.find((protectedWorkspaceRoot) => resolvedOutputDir === protectedWorkspaceRoot || canonicalOutputDir === fs.realpathSync.native(protectedWorkspaceRoot));
|
|
485
|
+
if (protectedWorkspaceRootTarget !== void 0) throw new UnsafeCleanTargetError(outputDir, "workspace-root", {
|
|
486
|
+
resolvedOutputDir,
|
|
487
|
+
currentWorkingDirectory: resolvedWorkingDirectory,
|
|
488
|
+
protectedWorkspaceRoot: protectedWorkspaceRootTarget
|
|
489
|
+
});
|
|
490
|
+
if (protectedWorkspaceRoots.length > 0 && (isSameOrDescendantOf(resolvedWorkingDirectory, resolvedOutputDir) || isSameOrDescendantOf(canonicalWorkingDirectory, canonicalOutputDir))) throw new UnsafeCleanTargetError(outputDir, "ancestor-of-current-working-directory", {
|
|
491
|
+
resolvedOutputDir,
|
|
492
|
+
currentWorkingDirectory: resolvedWorkingDirectory
|
|
493
|
+
});
|
|
343
494
|
};
|
|
344
495
|
/**
|
|
345
496
|
* Main generator for typeweaver
|
|
@@ -433,6 +584,58 @@ const findProtectedWorkspaceRoot = (startDirectory) => {
|
|
|
433
584
|
const hasWorkspaceMarker = (directory) => {
|
|
434
585
|
return ["pnpm-workspace.yaml", ".git"].some((marker) => fs.existsSync(path.join(directory, marker)));
|
|
435
586
|
};
|
|
587
|
+
const canonicalizePathForContainment = (targetPath) => {
|
|
588
|
+
const remainingSegments = [];
|
|
589
|
+
let nearestExistingPath = path.resolve(targetPath);
|
|
590
|
+
while (!fs.existsSync(nearestExistingPath)) {
|
|
591
|
+
const parentPath = path.dirname(nearestExistingPath);
|
|
592
|
+
if (parentPath === nearestExistingPath) break;
|
|
593
|
+
remainingSegments.unshift(path.basename(nearestExistingPath));
|
|
594
|
+
nearestExistingPath = parentPath;
|
|
595
|
+
}
|
|
596
|
+
const canonicalExistingPath = fs.realpathSync.native(nearestExistingPath);
|
|
597
|
+
return path.join(canonicalExistingPath, ...remainingSegments);
|
|
598
|
+
};
|
|
599
|
+
const isSameOrDescendantOf = (directory, ancestor) => {
|
|
600
|
+
const relativePath = path.relative(ancestor, directory);
|
|
601
|
+
const parentTraversalPrefix = `..${path.sep}`;
|
|
602
|
+
const escapesAncestor = relativePath === ".." || relativePath.startsWith(parentTraversalPrefix);
|
|
603
|
+
return relativePath === "" || !escapesAncestor && !path.isAbsolute(relativePath);
|
|
604
|
+
};
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/errors/MissingGenerateOptionError.ts
|
|
607
|
+
var MissingGenerateOptionError = class extends Error {
|
|
608
|
+
name = "MissingGenerateOptionError";
|
|
609
|
+
constructor(optionName, flag, configKey) {
|
|
610
|
+
super(`Missing required generate option '${optionName}'. Pass ${flag} or set '${configKey}' in the TypeWeaver config file.`);
|
|
611
|
+
this.optionName = optionName;
|
|
612
|
+
this.flag = flag;
|
|
613
|
+
this.configKey = configKey;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/resolveGenerateOptions.ts
|
|
618
|
+
const resolveGenerateOptions = (options, config, currentWorkingDirectory) => {
|
|
619
|
+
const inputPath = options.input ?? config.input;
|
|
620
|
+
const outputDir = options.output ?? config.output;
|
|
621
|
+
if (!inputPath) throw new MissingGenerateOptionError("input", "--input", "input");
|
|
622
|
+
if (!outputDir) throw new MissingGenerateOptionError("output", "--output", "output");
|
|
623
|
+
const resolvedInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(currentWorkingDirectory, inputPath);
|
|
624
|
+
const resolvedOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(currentWorkingDirectory, outputDir);
|
|
625
|
+
const finalConfig = {
|
|
626
|
+
input: resolvedInputPath,
|
|
627
|
+
output: resolvedOutputDir,
|
|
628
|
+
format: options.format ?? config.format ?? true,
|
|
629
|
+
clean: options.clean ?? config.clean ?? true
|
|
630
|
+
};
|
|
631
|
+
if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((plugin) => plugin.trim());
|
|
632
|
+
else if (config.plugins) finalConfig.plugins = config.plugins;
|
|
633
|
+
return {
|
|
634
|
+
inputPath: resolvedInputPath,
|
|
635
|
+
outputDir: resolvedOutputDir,
|
|
636
|
+
config: finalConfig
|
|
637
|
+
};
|
|
638
|
+
};
|
|
436
639
|
//#endregion
|
|
437
640
|
//#region src/cli.ts
|
|
438
641
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -453,21 +656,8 @@ program.command("generate").description("Generate types, validators, and clients
|
|
|
453
656
|
process.exit(1);
|
|
454
657
|
}
|
|
455
658
|
}
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
if (!inputPath) throw new Error("No input spec entrypoint provided. Use --input or specify in config file.");
|
|
459
|
-
if (!outputDir) throw new Error("No output directory provided. Use --output or specify in config file.");
|
|
460
|
-
const resolvedInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(execDir, inputPath);
|
|
461
|
-
const resolvedOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(execDir, outputDir);
|
|
462
|
-
const finalConfig = {
|
|
463
|
-
input: resolvedInputPath,
|
|
464
|
-
output: resolvedOutputDir,
|
|
465
|
-
format: options.format ?? config.format ?? true,
|
|
466
|
-
clean: options.clean ?? config.clean ?? true
|
|
467
|
-
};
|
|
468
|
-
if (options.plugins) finalConfig.plugins = options.plugins.split(",").map((p) => p.trim());
|
|
469
|
-
else if (config.plugins) finalConfig.plugins = config.plugins;
|
|
470
|
-
return new Generator().generate(resolvedInputPath, resolvedOutputDir, finalConfig, execDir);
|
|
659
|
+
const resolvedGenerateOptions = resolveGenerateOptions(options, config, execDir);
|
|
660
|
+
return new Generator().generate(resolvedGenerateOptions.inputPath, resolvedGenerateOptions.outputDir, resolvedGenerateOptions.config, execDir);
|
|
471
661
|
});
|
|
472
662
|
program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
|
|
473
663
|
console.log("The init command is coming soon!");
|