asphodelos 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.
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +199 -0
- package/dist/core-DS3sAabb.mjs +4018 -0
- package/dist/format-B0W7HIAg.mjs +48 -0
- package/dist/index.d.mts +993 -0
- package/dist/index.mjs +202 -0
- package/dist/openapi-B4aAnx4P.mjs +40 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/shared-CrbXsFUJ.mjs +221 -0
- package/dist/vite-plugin/index.d.mts +11 -0
- package/dist/vite-plugin/index.mjs +336 -0
- package/package.json +71 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Effect, FileSystem, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
//#region src/config/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* The config file is missing, is not a module with a default export, or does not validate.
|
|
7
|
+
*
|
|
8
|
+
* `notFound` separates "there is no config here" from "the config here is wrong": only the first
|
|
9
|
+
* is the caller who ran `asphodelos` with nothing and needs to be told what the command accepts.
|
|
10
|
+
* Everything else already names the field that is wrong.
|
|
11
|
+
*
|
|
12
|
+
* `Schema.TaggedError` rather than `Data.TaggedError`: this is the error a schema decode turns
|
|
13
|
+
* into, which makes the failure a schema in its own right. The errors that never meet a schema
|
|
14
|
+
* (`FormatError`, `GenerateError`, `OpenAPIError`) stay plain `Data.TaggedError`.
|
|
15
|
+
*/
|
|
16
|
+
var ConfigError = class extends Schema.TaggedError()("ConfigError", {
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
notFound: Schema.optionalKey(Schema.Boolean)
|
|
19
|
+
}) {};
|
|
20
|
+
/**
|
|
21
|
+
* A path constrained to a set of extensions.
|
|
22
|
+
*
|
|
23
|
+
* `Schema.TemplateLiteral` carries the literal type but its rejection reads "Expected a string
|
|
24
|
+
* matching template literal parts"; `Schema.declare` over the same guard keeps the type on both
|
|
25
|
+
* sides — so `defineConfig` still rejects a wrong extension while you type — and lets the message
|
|
26
|
+
* say which extensions are meant.
|
|
27
|
+
*/
|
|
28
|
+
const TypeScriptPathSchema = Schema.declare(Schema.is(Schema.TemplateLiteral([Schema.String, ".ts"])), { message: "must be .ts file" });
|
|
29
|
+
const InputSchema = Schema.declare(Schema.is(Schema.TemplateLiteral([Schema.String, Schema.Literals([
|
|
30
|
+
".yaml",
|
|
31
|
+
".json",
|
|
32
|
+
".tsp"
|
|
33
|
+
])])), { message: "must be .yaml | .json | .tsp" });
|
|
34
|
+
/** Milliseconds, bounded so a mock cannot be configured to hang a request. */
|
|
35
|
+
const DelayMsSchema = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(6e4));
|
|
36
|
+
/** A faker seed: faker hashes it with Mersenne Twister, which takes a 32-bit unsigned integer. */
|
|
37
|
+
const SeedSchema = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(4294967295));
|
|
38
|
+
/** How many items a generated array holds when the schema does not say. */
|
|
39
|
+
const ArrayLengthSchema = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1e3));
|
|
40
|
+
/** A directory path normalizes to `<dir>/index.ts`, so single-file mode always names a file. */
|
|
41
|
+
const FileOutputSchema = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({
|
|
42
|
+
decode: (v) => v.endsWith(".ts") ? v : `${v}/index.ts`,
|
|
43
|
+
encode: (v) => v
|
|
44
|
+
})));
|
|
45
|
+
/**
|
|
46
|
+
* Every output target is the same two-branch union: `split: true` writes one file per entry into
|
|
47
|
+
* a directory, anything else writes a single file. Only those two fields differ, so the rest is
|
|
48
|
+
* written once and spread into both branches.
|
|
49
|
+
*
|
|
50
|
+
* `Schema.Union` resolves members in order and each member pins `split` to a literal, so a member
|
|
51
|
+
* is only reachable through its own discriminant — the failure reported is the one inside the
|
|
52
|
+
* matching branch, not a union-wide "no member matched".
|
|
53
|
+
*/
|
|
54
|
+
function splitUnion(shared) {
|
|
55
|
+
return Schema.Union([Schema.Struct({
|
|
56
|
+
split: Schema.Literal(true),
|
|
57
|
+
output: Schema.String.check(Schema.isPattern(/^(?!.*\.ts$).+/u, { message: "split mode requires directory, not .ts file" })),
|
|
58
|
+
...shared
|
|
59
|
+
}), Schema.Struct({
|
|
60
|
+
split: Schema.Literal(false).pipe(Schema.withDecodingDefault(Effect.succeed(false))),
|
|
61
|
+
output: FileOutputSchema,
|
|
62
|
+
...shared
|
|
63
|
+
})]);
|
|
64
|
+
}
|
|
65
|
+
const OutputSchema = splitUnion({ import: Schema.optionalKey(Schema.String) });
|
|
66
|
+
const ExportTypesOutputSchema = splitUnion({
|
|
67
|
+
import: Schema.optionalKey(Schema.String),
|
|
68
|
+
exportTypes: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false)))
|
|
69
|
+
});
|
|
70
|
+
const HooksSchema = splitUnion({
|
|
71
|
+
import: Schema.String,
|
|
72
|
+
client: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("client")))
|
|
73
|
+
});
|
|
74
|
+
const ComponentsSchema = Schema.Struct({
|
|
75
|
+
output: Schema.optionalKey(TypeScriptPathSchema),
|
|
76
|
+
schemas: Schema.optionalKey(ExportTypesOutputSchema),
|
|
77
|
+
responses: Schema.optionalKey(ExportTypesOutputSchema),
|
|
78
|
+
parameters: Schema.optionalKey(ExportTypesOutputSchema),
|
|
79
|
+
examples: Schema.optionalKey(OutputSchema),
|
|
80
|
+
requestBodies: Schema.optionalKey(ExportTypesOutputSchema),
|
|
81
|
+
headers: Schema.optionalKey(ExportTypesOutputSchema),
|
|
82
|
+
securitySchemes: Schema.optionalKey(OutputSchema),
|
|
83
|
+
links: Schema.optionalKey(OutputSchema),
|
|
84
|
+
callbacks: Schema.optionalKey(OutputSchema),
|
|
85
|
+
pathItems: Schema.optionalKey(OutputSchema),
|
|
86
|
+
mediaTypes: Schema.optionalKey(ExportTypesOutputSchema)
|
|
87
|
+
}).check(Schema.makeFilter(({ output, ...perType }) => output === void 0 || Object.keys(perType).length === 0 ? void 0 : "output cannot be combined with per-type component outputs. Use either a single 'output' or per-type configs."));
|
|
88
|
+
const ConfigSchema = Schema.Struct({
|
|
89
|
+
input: InputSchema,
|
|
90
|
+
output: Schema.optionalKey(TypeScriptPathSchema),
|
|
91
|
+
prefix: Schema.optionalKey(Schema.String),
|
|
92
|
+
format: Schema.optionalKey(Schema.declare((u) => typeof u === "object" && u !== null, { message: "must be an oxfmt config object" })),
|
|
93
|
+
port: Schema.optionalKey(Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("3000")))),
|
|
94
|
+
pathAlias: Schema.optionalKey(Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false)))),
|
|
95
|
+
integration: Schema.optionalKey(Schema.Boolean),
|
|
96
|
+
readonly: Schema.optionalKey(Schema.Boolean),
|
|
97
|
+
components: Schema.optionalKey(ComponentsSchema),
|
|
98
|
+
eden: Schema.optionalKey(Schema.Struct({
|
|
99
|
+
output: Schema.String,
|
|
100
|
+
import: Schema.String,
|
|
101
|
+
client: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("client"))),
|
|
102
|
+
docs: Schema.optionalKey(Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))))
|
|
103
|
+
})),
|
|
104
|
+
types: Schema.optionalKey(Schema.Struct({ output: TypeScriptPathSchema })),
|
|
105
|
+
test: Schema.optionalKey(Schema.Union([Schema.Struct({
|
|
106
|
+
split: Schema.Literal(true),
|
|
107
|
+
pathAlias: Schema.optionalKey(Schema.String)
|
|
108
|
+
}), Schema.Struct({
|
|
109
|
+
split: Schema.Literal(false).pipe(Schema.withDecodingDefault(Effect.succeed(false))),
|
|
110
|
+
output: FileOutputSchema,
|
|
111
|
+
pathAlias: Schema.optionalKey(Schema.String)
|
|
112
|
+
})])),
|
|
113
|
+
mock: Schema.optionalKey(Schema.Struct({
|
|
114
|
+
output: FileOutputSchema,
|
|
115
|
+
useExamples: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Literal("all")])),
|
|
116
|
+
seed: Schema.optionalKey(Schema.Union([SeedSchema, Schema.NonEmptyArray(SeedSchema)])),
|
|
117
|
+
locale: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^[A-Za-z_]{1,40}$/u, { message: "must be a faker locale code such as 'ja', 'en' or 'zh_CN'" }))),
|
|
118
|
+
delay: Schema.optionalKey(Schema.Union([
|
|
119
|
+
DelayMsSchema,
|
|
120
|
+
Schema.Literal(false),
|
|
121
|
+
Schema.Struct({
|
|
122
|
+
min: DelayMsSchema,
|
|
123
|
+
max: DelayMsSchema
|
|
124
|
+
}).check(Schema.makeFilter(({ min, max }) => min <= max ? void 0 : "delay.min must be <= delay.max"))
|
|
125
|
+
])),
|
|
126
|
+
arrayMin: Schema.optionalKey(ArrayLengthSchema),
|
|
127
|
+
arrayMax: Schema.optionalKey(ArrayLengthSchema)
|
|
128
|
+
}).check(Schema.makeFilter(({ arrayMin, arrayMax }) => arrayMin === void 0 || arrayMax === void 0 || arrayMin <= arrayMax ? void 0 : "arrayMin must be <= arrayMax. Swap the values or remove one."))),
|
|
129
|
+
swr: Schema.optionalKey(HooksSchema),
|
|
130
|
+
"tanstack-query": Schema.optionalKey(HooksSchema),
|
|
131
|
+
"preact-query": Schema.optionalKey(HooksSchema),
|
|
132
|
+
"solid-query": Schema.optionalKey(HooksSchema),
|
|
133
|
+
"vue-query": Schema.optionalKey(HooksSchema),
|
|
134
|
+
"svelte-query": Schema.optionalKey(HooksSchema),
|
|
135
|
+
"angular-query": Schema.optionalKey(HooksSchema)
|
|
136
|
+
});
|
|
137
|
+
const decodeConfig = Schema.decodeUnknownEffect(ConfigSchema);
|
|
138
|
+
const formatIssue = SchemaIssue.makeFormatterStandardSchemaV1();
|
|
139
|
+
/**
|
|
140
|
+
* Validates an already-loaded config object.
|
|
141
|
+
*
|
|
142
|
+
* The first issue is reported as `<a.b.c>: <message>`: a config file is written by hand, so
|
|
143
|
+
* naming the field that is wrong matters more than listing every consequence of it.
|
|
144
|
+
*/
|
|
145
|
+
function parseConfig(config) {
|
|
146
|
+
return decodeConfig(config).pipe(Effect.mapError((error) => {
|
|
147
|
+
const issue = formatIssue(error.issue).issues[0];
|
|
148
|
+
const path = (issue?.path ?? []).map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
149
|
+
return new ConfigError({ message: `Invalid config: ${path === "" ? "" : `${path}: `}${issue?.message ?? ""}` });
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
const reloads = { count: 0 };
|
|
153
|
+
/**
|
|
154
|
+
* Imports the config module, bypassing the loader cache when asked.
|
|
155
|
+
*
|
|
156
|
+
* Node busts its cache with a `?reload=n` query on the specifier. Bun — the runtime this
|
|
157
|
+
* generator targets — resolves `file:///x.ts?reload=1` back to the module it already has, so the
|
|
158
|
+
* query buys nothing there. What both honour is a path they have not seen, so a reload copies the
|
|
159
|
+
* config next to itself and imports the copy.
|
|
160
|
+
*
|
|
161
|
+
* A sibling rather than a temp directory: the config's own imports are relative to where it sits,
|
|
162
|
+
* and a copy anywhere else would fail to resolve them. The copy is removed however the import
|
|
163
|
+
* ends, and its name carries the pid so two processes watching one project cannot collide.
|
|
164
|
+
*/
|
|
165
|
+
function importConfigModule(abs, reload) {
|
|
166
|
+
return Effect.gen(function* () {
|
|
167
|
+
const importModule = (specifier) => Effect.tryPromise({
|
|
168
|
+
try: () => import(specifier),
|
|
169
|
+
catch: (error) => new ConfigError({ message: error instanceof Error ? error.message : String(error) })
|
|
170
|
+
});
|
|
171
|
+
if (!reload) return yield* importModule(pathToFileURL(abs).href);
|
|
172
|
+
const fs = yield* FileSystem.FileSystem;
|
|
173
|
+
const copy = resolve(abs, "..", `.asphodelos.config.${String(process.pid)}.${String(reloads.count += 1)}.ts`);
|
|
174
|
+
yield* fs.copyFile(abs, copy).pipe(Effect.mapError((error) => new ConfigError({ message: `Config reload failed: ${error.message}` })));
|
|
175
|
+
return yield* importModule(pathToFileURL(copy).href).pipe(Effect.ensuring(fs.remove(copy, { force: true }).pipe(Effect.orElseSucceed(() => void 0))));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Loads and validates a config file, resolved against the current directory.
|
|
180
|
+
*
|
|
181
|
+
* `reload` re-reads a config that has already been imported — what `--watch` needs after the file
|
|
182
|
+
* changes, and nothing else should ask for, since every reload leaves another copy of the module
|
|
183
|
+
* behind.
|
|
184
|
+
*/
|
|
185
|
+
function readConfig(configPath, reload = false) {
|
|
186
|
+
return Effect.gen(function* () {
|
|
187
|
+
const fs = yield* FileSystem.FileSystem;
|
|
188
|
+
const abs = resolve(process.cwd(), configPath ?? "asphodelos.config.ts");
|
|
189
|
+
if (!(yield* fs.exists(abs).pipe(Effect.catchTag("PlatformError", () => Effect.succeed(false))))) return yield* new ConfigError({
|
|
190
|
+
message: `Config not found: ${abs}`,
|
|
191
|
+
notFound: true
|
|
192
|
+
});
|
|
193
|
+
const mod = yield* importConfigModule(abs, reload);
|
|
194
|
+
if (typeof mod !== "object" || mod === null || !("default" in mod) || mod.default === void 0) return yield* new ConfigError({ message: "Config must export default object" });
|
|
195
|
+
return yield* parseConfig(mod.default);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function defineConfig(config) {
|
|
199
|
+
return config;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { ConfigError, defineConfig, parseConfig, readConfig };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { Data, Effect } from "effect";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
5
|
+
import { NodeHost, compile } from "@typespec/compiler";
|
|
6
|
+
import { getOpenAPI3 } from "@typespec/openapi3";
|
|
7
|
+
//#region src/openapi/index.ts
|
|
8
|
+
var openapi_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
OpenAPIError: () => OpenAPIError,
|
|
10
|
+
parseOpenAPI: () => parseOpenAPI,
|
|
11
|
+
pathEntries: () => pathEntries
|
|
12
|
+
});
|
|
13
|
+
/** The document could not be read, compiled or parsed into an OpenAPI object. */
|
|
14
|
+
var OpenAPIError = class extends Data.TaggedError("OpenAPIError") {};
|
|
15
|
+
/** Compiles a TypeSpec entry point into the plain document `SwaggerParser` expects. */
|
|
16
|
+
async function readTypeSpec(input) {
|
|
17
|
+
const program = await compile(NodeHost, path.resolve(input), { noEmit: true });
|
|
18
|
+
if (program.diagnostics.length > 0) throw new Error(`TypeSpec compile failed:\n${program.diagnostics.map((d) => d.message).join("\n")}`);
|
|
19
|
+
const [record] = await getOpenAPI3(program);
|
|
20
|
+
const tsp = record && ("document" in record ? record.document : record.versions[0]?.document);
|
|
21
|
+
return JSON.parse(JSON.stringify(tsp));
|
|
22
|
+
}
|
|
23
|
+
/** Parses `input` into an OpenAPI document. */
|
|
24
|
+
function parseOpenAPI(input) {
|
|
25
|
+
return Effect.tryPromise({
|
|
26
|
+
try: async () => await SwaggerParser.bundle(input.endsWith(".tsp") ? await readTypeSpec(input) : input),
|
|
27
|
+
catch: (error) => new OpenAPIError({ message: error instanceof Error ? error.message : String(error) })
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `Object.entries(openAPI.paths)` on its own yields `any` values: swagger-parser types the
|
|
32
|
+
* document as a union of the OpenAPI 2 / 3.0 / 3.1 shapes, and inference over that union drops
|
|
33
|
+
* the value type. Going through the annotation below is what keeps every caller on `PathItem`.
|
|
34
|
+
*/
|
|
35
|
+
function pathEntries(openAPI) {
|
|
36
|
+
const paths = openAPI.paths;
|
|
37
|
+
return Object.entries(paths);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { parseOpenAPI as n, pathEntries as r, openapi_exports as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
+
import { S as unlink, _ as responses, a as hooks, c as mediaTypes, d as links, f as securitySchemes, g as parameters, h as examples, i as mock, l as pathItems, m as requestBodies, n as types, o as elysia, p as headers, r as test, s as eden, u as callbacks, v as schemas, x as readdir, y as components } from "./core-DS3sAabb.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import path, { posix } from "node:path";
|
|
5
|
+
//#region src/shared/index.ts
|
|
6
|
+
var shared_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
+
cleanSplitOutputs: () => cleanSplitOutputs,
|
|
8
|
+
isUserCodeJob: () => isUserCodeJob,
|
|
9
|
+
jobTargets: () => jobTargets,
|
|
10
|
+
makeJob: () => makeJob
|
|
11
|
+
});
|
|
12
|
+
function edenJob(openAPI, edenConfig, prefix) {
|
|
13
|
+
return {
|
|
14
|
+
name: "eden",
|
|
15
|
+
output: edenConfig.output,
|
|
16
|
+
split: false,
|
|
17
|
+
run: (output) => eden(openAPI, output, edenConfig.import, edenConfig.client, prefix, edenConfig.docs)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function makeJob(openAPI, config) {
|
|
21
|
+
const baseDir = posix.normalize(posix.dirname(config.output ?? "src/index.ts"));
|
|
22
|
+
const { output: componentsOutput, ...componentTargets } = config.components ?? {};
|
|
23
|
+
const componentJobs = componentsOutput ? [{
|
|
24
|
+
name: "components",
|
|
25
|
+
output: componentsOutput,
|
|
26
|
+
split: false,
|
|
27
|
+
run: (output) => components(openAPI.components, output, config.readonly)
|
|
28
|
+
}] : perTypeComponentJobs(openAPI, componentTargets, baseDir, config.readonly);
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: "elysia",
|
|
32
|
+
output: config.output ?? "src/index.ts",
|
|
33
|
+
split: false,
|
|
34
|
+
run: (output) => elysia(openAPI, {
|
|
35
|
+
output,
|
|
36
|
+
prefix: config.prefix,
|
|
37
|
+
port: config.port,
|
|
38
|
+
integration: config.integration === true,
|
|
39
|
+
readonly: config.readonly === true,
|
|
40
|
+
pathAlias: config.pathAlias === true ? `@/${baseDir.replace(/^\.?\/?/u, "")}` : void 0,
|
|
41
|
+
components: componentTargets,
|
|
42
|
+
componentsOutput
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
...componentJobs,
|
|
46
|
+
config.eden ? edenJob(openAPI, config.eden, config.prefix) : void 0,
|
|
47
|
+
config.types ? {
|
|
48
|
+
name: "types",
|
|
49
|
+
output: config.types.output,
|
|
50
|
+
split: false,
|
|
51
|
+
run: (output) => types(openAPI, output, config.prefix)
|
|
52
|
+
} : void 0,
|
|
53
|
+
config.test ? {
|
|
54
|
+
name: "test",
|
|
55
|
+
output: config.test.split ? `${baseDir}/modules` : config.test.output,
|
|
56
|
+
split: config.test.split,
|
|
57
|
+
run: (output) => test(openAPI, config.test?.split === true ? void 0 : output, {
|
|
58
|
+
appOutput: config.output ?? "src/index.ts",
|
|
59
|
+
split: config.test?.split === true,
|
|
60
|
+
pathAlias: config.test?.pathAlias,
|
|
61
|
+
prefix: config.prefix
|
|
62
|
+
})
|
|
63
|
+
} : void 0,
|
|
64
|
+
config.mock ? {
|
|
65
|
+
name: "mock",
|
|
66
|
+
output: config.mock.output,
|
|
67
|
+
split: false,
|
|
68
|
+
run: (output) => mock(openAPI, output, {
|
|
69
|
+
...config.mock,
|
|
70
|
+
prefix: config.prefix,
|
|
71
|
+
port: config.port
|
|
72
|
+
})
|
|
73
|
+
} : void 0,
|
|
74
|
+
...[
|
|
75
|
+
"swr",
|
|
76
|
+
"tanstack-query",
|
|
77
|
+
"preact-query",
|
|
78
|
+
"vue-query",
|
|
79
|
+
"svelte-query",
|
|
80
|
+
"solid-query",
|
|
81
|
+
"angular-query"
|
|
82
|
+
].map((library) => {
|
|
83
|
+
const cfg = config[library];
|
|
84
|
+
return cfg ? {
|
|
85
|
+
name: library,
|
|
86
|
+
output: cfg.output,
|
|
87
|
+
split: cfg.split,
|
|
88
|
+
run: (output) => hooks(openAPI, output, cfg.import, library, {
|
|
89
|
+
client: cfg.client,
|
|
90
|
+
basePath: config.prefix,
|
|
91
|
+
split: cfg.split
|
|
92
|
+
})
|
|
93
|
+
} : void 0;
|
|
94
|
+
})
|
|
95
|
+
].filter((job) => job !== void 0);
|
|
96
|
+
}
|
|
97
|
+
function perTypeComponentJobs(openAPI, componentTargets, baseDir, readonly) {
|
|
98
|
+
return [
|
|
99
|
+
openAPI.components?.schemas ? {
|
|
100
|
+
name: "schemas",
|
|
101
|
+
output: componentTargets.schemas?.output ?? `${baseDir}/components/schemas.ts`,
|
|
102
|
+
split: componentTargets.schemas?.split ?? false,
|
|
103
|
+
run: (output) => schemas(openAPI.components?.schemas, output, componentTargets.schemas?.split ?? false, componentTargets.schemas?.exportTypes ?? false, componentTargets, readonly)
|
|
104
|
+
} : void 0,
|
|
105
|
+
openAPI.components?.responses ? {
|
|
106
|
+
name: "responses",
|
|
107
|
+
output: componentTargets.responses?.output ?? `${baseDir}/components/responses.ts`,
|
|
108
|
+
split: componentTargets.responses?.split ?? false,
|
|
109
|
+
run: (output) => responses(openAPI.components?.responses, output, componentTargets.responses?.split ?? false, componentTargets.responses?.exportTypes ?? false, componentTargets, readonly)
|
|
110
|
+
} : void 0,
|
|
111
|
+
openAPI.components?.parameters ? {
|
|
112
|
+
name: "parameters",
|
|
113
|
+
output: componentTargets.parameters?.output ?? `${baseDir}/components/parameters.ts`,
|
|
114
|
+
split: componentTargets.parameters?.split ?? false,
|
|
115
|
+
run: (output) => parameters(openAPI.components?.parameters, output, componentTargets.parameters?.split ?? false, componentTargets.parameters?.exportTypes ?? false, componentTargets, readonly)
|
|
116
|
+
} : void 0,
|
|
117
|
+
openAPI.components?.examples ? {
|
|
118
|
+
name: "examples",
|
|
119
|
+
output: componentTargets.examples?.output ?? `${baseDir}/components/examples.ts`,
|
|
120
|
+
split: componentTargets.examples?.split ?? false,
|
|
121
|
+
run: (output) => examples(openAPI.components?.examples, output, componentTargets.examples?.split ?? false, componentTargets)
|
|
122
|
+
} : void 0,
|
|
123
|
+
openAPI.components?.requestBodies ? {
|
|
124
|
+
name: "requestBodies",
|
|
125
|
+
output: componentTargets.requestBodies?.output ?? `${baseDir}/components/requestBodies.ts`,
|
|
126
|
+
split: componentTargets.requestBodies?.split ?? false,
|
|
127
|
+
run: (output) => requestBodies(openAPI.components?.requestBodies, output, componentTargets.requestBodies?.split ?? false, componentTargets.requestBodies?.exportTypes ?? false, componentTargets, readonly)
|
|
128
|
+
} : void 0,
|
|
129
|
+
openAPI.components?.headers ? {
|
|
130
|
+
name: "headers",
|
|
131
|
+
output: componentTargets.headers?.output ?? `${baseDir}/components/headers.ts`,
|
|
132
|
+
split: componentTargets.headers?.split ?? false,
|
|
133
|
+
run: (output) => headers(openAPI.components?.headers, output, componentTargets.headers?.split ?? false, componentTargets.headers?.exportTypes ?? false, componentTargets, readonly)
|
|
134
|
+
} : void 0,
|
|
135
|
+
openAPI.components?.securitySchemes ? {
|
|
136
|
+
name: "securitySchemes",
|
|
137
|
+
output: componentTargets.securitySchemes?.output ?? `${baseDir}/components/securitySchemes.ts`,
|
|
138
|
+
split: componentTargets.securitySchemes?.split ?? false,
|
|
139
|
+
run: (output) => securitySchemes(openAPI.components?.securitySchemes, output, componentTargets.securitySchemes?.split ?? false, componentTargets)
|
|
140
|
+
} : void 0,
|
|
141
|
+
openAPI.components?.links ? {
|
|
142
|
+
name: "links",
|
|
143
|
+
output: componentTargets.links?.output ?? `${baseDir}/components/links.ts`,
|
|
144
|
+
split: componentTargets.links?.split ?? false,
|
|
145
|
+
run: (output) => links(openAPI.components?.links, output, componentTargets.links?.split ?? false, componentTargets)
|
|
146
|
+
} : void 0,
|
|
147
|
+
openAPI.components?.callbacks ? {
|
|
148
|
+
name: "callbacks",
|
|
149
|
+
output: componentTargets.callbacks?.output ?? `${baseDir}/components/callbacks.ts`,
|
|
150
|
+
split: componentTargets.callbacks?.split ?? false,
|
|
151
|
+
run: (output) => callbacks(openAPI.components?.callbacks, output, componentTargets.callbacks?.split ?? false, componentTargets)
|
|
152
|
+
} : void 0,
|
|
153
|
+
openAPI.components?.pathItems ? {
|
|
154
|
+
name: "pathItems",
|
|
155
|
+
output: componentTargets.pathItems?.output ?? `${baseDir}/components/pathItems.ts`,
|
|
156
|
+
split: componentTargets.pathItems?.split ?? false,
|
|
157
|
+
run: (output) => pathItems(openAPI.components?.pathItems, output, componentTargets.pathItems?.split ?? false, componentTargets)
|
|
158
|
+
} : void 0,
|
|
159
|
+
openAPI.components?.mediaTypes ? {
|
|
160
|
+
name: "mediaTypes",
|
|
161
|
+
output: componentTargets.mediaTypes?.output ?? `${baseDir}/components/mediaTypes.ts`,
|
|
162
|
+
split: componentTargets.mediaTypes?.split ?? false,
|
|
163
|
+
run: (output) => mediaTypes(openAPI.components?.mediaTypes, output, componentTargets.mediaTypes?.split ?? false, componentTargets.mediaTypes?.exportTypes ?? false, componentTargets, readonly)
|
|
164
|
+
} : void 0
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generators that merge into what is already at their output rather than overwrite it.
|
|
169
|
+
*
|
|
170
|
+
* `elysia` and `test` read the file back and keep whatever the user wrote there (`mergeSource`),
|
|
171
|
+
* so what they write holds the user's code as much as the generator's. Nothing that deletes — the
|
|
172
|
+
* split clean below, the Vite plugin's stale-output cleanup — may touch one.
|
|
173
|
+
*/
|
|
174
|
+
const USER_CODE_JOBS = new Set(["elysia", "test"]);
|
|
175
|
+
function isUserCodeJob(job) {
|
|
176
|
+
return USER_CODE_JOBS.has(job.name);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Every absolute path a job writes under.
|
|
180
|
+
*
|
|
181
|
+
* The output itself, and for `elysia` the `modules/` directory it fills beside the app entry —
|
|
182
|
+
* one file per resource, which a caller that watches for changed output has to see as well.
|
|
183
|
+
*/
|
|
184
|
+
function jobTargets(job) {
|
|
185
|
+
const output = path.resolve(process.cwd(), job.output);
|
|
186
|
+
return job.name === "elysia" ? [output, path.join(path.dirname(output), "modules")] : [output];
|
|
187
|
+
}
|
|
188
|
+
function cleanSplitDirectory(directory, keep) {
|
|
189
|
+
return Effect.gen(function* () {
|
|
190
|
+
const stale = (yield* readdir(directory)).filter((name) => name.endsWith(".ts")).map((name) => path.join(directory, name)).filter((file) => !keep.has(file));
|
|
191
|
+
yield* Effect.all(stale.map(unlink), { concurrency: "unbounded" });
|
|
192
|
+
return stale;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Empties every split output directory before the generators refill them, answering with what
|
|
197
|
+
* was removed.
|
|
198
|
+
*
|
|
199
|
+
* A split generator writes one file per entry plus a barrel beside them, and knows only what it
|
|
200
|
+
* writes — so an entry that leaves the document leaves its file behind, orphaned and still
|
|
201
|
+
* importing names the document no longer defines. A split directory is therefore the generator's,
|
|
202
|
+
* not a place to keep anything by hand.
|
|
203
|
+
*
|
|
204
|
+
* Only the direct `.ts` children are removed, never a subdirectory, and never a file another job
|
|
205
|
+
* writes on its own: a single-file output that lives inside a split directory is left where it is
|
|
206
|
+
* rather than deleted and rewritten. A split job that merges into the user's code is never
|
|
207
|
+
* cleaned at all.
|
|
208
|
+
*
|
|
209
|
+
* This runs before any job writes, never per job as it goes: two jobs can be aimed at one
|
|
210
|
+
* directory, and a clean that lands after a sibling has filled it would take the fresh files with
|
|
211
|
+
* it.
|
|
212
|
+
*/
|
|
213
|
+
function cleanSplitOutputs(jobs) {
|
|
214
|
+
return Effect.gen(function* () {
|
|
215
|
+
const keep = new Set(jobs.filter((job) => !job.split).map((job) => path.resolve(process.cwd(), job.output)));
|
|
216
|
+
const directories = new Set(jobs.filter((job) => job.split && !isUserCodeJob(job)).map((job) => path.resolve(process.cwd(), job.output)));
|
|
217
|
+
return (yield* Effect.all([...directories].map((directory) => cleanSplitDirectory(directory, keep)), { concurrency: "unbounded" })).flat();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
export { shared_exports as a, makeJob as i, isUserCodeJob as n, jobTargets as r, cleanSplitOutputs as t };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/vite-plugin/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The Vite plugin: regenerates on every change to `asphodelos.config.ts` or to the documents it
|
|
4
|
+
* names, and reloads the browser when the output actually changed.
|
|
5
|
+
*
|
|
6
|
+
* Every pass — the first one, a config edit, a document edit — goes through one queue, so no two
|
|
7
|
+
* passes ever interleave their cleanup with each other's writes.
|
|
8
|
+
*/
|
|
9
|
+
declare function asphodelosVite(): any;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { asphodelosVite };
|