@genapi/pipeline 3.7.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-PRESENT Hairyf <https://github.com/hairyf>
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Hairyf <https://github.com/hairyf>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @genapi/pipeline
2
+
3
+ <!-- automd:badges -->
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@genapi/pipeline)](https://npmjs.com/package/@genapi/pipeline)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@genapi/pipeline)](https://npm.chart.dev/@genapi/pipeline)
7
+
8
+ <!-- /automd -->
9
+
10
+ GenAPI pipeline core: config reading, data source fetching, parsing, compilation, generation, and output. Composable for custom generation flows.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @genapi/pipeline
16
+ ```
17
+
18
+ ## API
19
+
20
+ <!-- automd:jsdocs src=src/index.ts -->
21
+
22
+ ## Pipeline
23
+
24
+ ### `compiler(configRead)`
25
+
26
+ Compiles graphs to code string: request and typings for each output.
27
+
28
+ **Example:**
29
+
30
+ ```ts
31
+ compiler(configRead)
32
+ configRead.outputs.forEach(o => console.log(o.code))
33
+ ```
34
+
35
+ ### `config(userConfig)`
36
+
37
+ Normalizes pipeline config: output paths, responseType, baseURL, and builds ConfigRead with inputs/outputs.
38
+
39
+ **Example:**
40
+
41
+ ```ts
42
+ const configRead = config(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
43
+ ```
44
+
45
+ ### `default(config, original, parser, compiler, generate, dest)`
46
+
47
+ Builds a GenAPI pipeline from five steps: config → original → parser → compiler → generate → dest.
48
+
49
+ **Example:**
50
+
51
+ ```ts
52
+ const run = pipeline(config, original, parser, compiler, generate, dest)
53
+ await run(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
54
+ ```
55
+
56
+ ### `dest(configRead)`
57
+
58
+ Writes output files from configRead.outputs (code to path).
59
+
60
+ **Example:**
61
+
62
+ ```ts
63
+ await dest(configRead)
64
+ // Writes configRead.outputs[].code to configRead.outputs[].path
65
+ ```
66
+
67
+ ### `generate(configRead, options?)`
68
+
69
+ Formats code for each output with Prettier.
70
+
71
+ **Example:**
72
+
73
+ ```ts
74
+ await generate(configRead)
75
+ await generate(configRead, { printWidth: 100 })
76
+ ```
77
+
78
+ ### `original(configRead)`
79
+
80
+ Fetches source: resolves uri/http/json from configRead.inputs and sets configRead.source. Transforms the source based on parser configuration (wpapi -> swagger2, swagger -> unchanged). Supports YAML source URLs (e.g. .yaml / .yml); uses confbox for parsing (same as undocs).
81
+
82
+ **Example:**
83
+
84
+ ```ts
85
+ await original(configRead)
86
+ // configRead.source is set from uri/http/json; wpapi transformed to swagger2 if parser is 'wpapi'
87
+ ```
88
+
89
+ ### `formatTypescript(code, options?)`
90
+
91
+ Formats TypeScript/JavaScript code string with Prettier.
92
+
93
+ **Example:**
94
+
95
+ ```ts
96
+ const formatted = await formatTypescript('const x=1')
97
+ ```
98
+
99
+ <!-- /automd -->
100
+
101
+ ## License
102
+
103
+ [MIT](https://github.com/hairyf/genapi/blob/main/LICENSE)
104
+
105
+ <!-- automd:with-automd -->
106
+
107
+ ---
108
+
109
+ _🤖 auto updated with [automd](https://automd.unjs.io)_
110
+
111
+ <!-- /automd -->
package/dist/index.d.mts CHANGED
@@ -2,55 +2,133 @@ import { ApiPipeline } from "@genapi/shared";
2
2
 
3
3
  //#region src/pipeline/index.d.ts
4
4
  /**
5
- * Pipeline readinput)function
5
+ * Pipeline read (input) step: turns raw config into internal config + inputs.
6
+ * @description First step: normalizes user config and builds ConfigRead (inputs, outputs, graphs).
6
7
  */
7
8
  type PipelineRead<Config, ConfigRead> = (config: Config) => ConfigRead | Promise<ConfigRead>;
8
9
  /**
9
- * Transfer data in pipeline
10
+ * Middle step that receives and returns config read; used for original, parser, compiler, generate.
11
+ * @description Each step may mutate configRead (e.g. set source, graphs, output.code).
10
12
  */
11
13
  type PipelineFlow<ConfigRead> = (configRead: ConfigRead) => ConfigRead | Promise<ConfigRead>;
12
14
  /**
13
- * Pipeline destoutput)function
15
+ * Pipeline dest (output) step: writes files from config read.
16
+ * @description Last step: writes configRead.outputs[].code to configRead.outputs[].path.
14
17
  */
15
- type PipelineDest<ConfigRead> = (configRead: ApiPipeline.ConfigRead) => void;
18
+ type PipelineDest<ConfigRead> = (configRead: ApiPipeline.ConfigRead) => void | Promise<void>;
16
19
  /**
17
- * create genapi pipeline process
18
- * @param config read config pa
19
- * @param original get the source according to config
20
- * @param parser resolve source as available data
21
- * @param compiler compile parse info conversion AST tree
22
- * @param generate generate code
23
- * @param dest dest file
20
+ * Builds a GenAPI pipeline from five steps: config → original → parser → compiler → generate → dest.
21
+ *
22
+ * @param config - Reads config and returns ConfigRead
23
+ * @param original - Fetches source (e.g. OpenAPI JSON)
24
+ * @param parser - Parses source into graphs
25
+ * @param compiler - Compiles graphs to AST
26
+ * @param generate - Generates code string
27
+ * @param dest - Writes output files
28
+ * @returns A function that runs the pipeline for a given config
29
+ * @group Pipeline
30
+ * @example
31
+ * ```ts
32
+ * const run = pipeline(config, original, parser, compiler, generate, dest)
33
+ * await run(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
34
+ * ```
24
35
  */
25
36
  declare function pipeline<Config = ApiPipeline.Config, ConfigRead = ApiPipeline.ConfigRead>(config: PipelineRead<Config, ConfigRead>, original: PipelineFlow<ConfigRead>, parser: PipelineFlow<ConfigRead>, compiler: PipelineFlow<ConfigRead>, generate: PipelineFlow<ConfigRead>, dest: PipelineDest<ConfigRead>): ApiPipeline.Pipeline;
26
37
  declare namespace pipeline {
27
- var config: typeof config;
28
- var original: typeof original;
29
- var parser: typeof original;
30
- var compiler: typeof compiler;
31
- var generate: typeof generate;
32
- var dest: typeof dest;
38
+ var config: typeof config;
39
+ var original: typeof original;
40
+ var parser: typeof original;
41
+ var compiler: typeof compiler;
42
+ var generate: typeof generate;
43
+ var dest: typeof dest;
33
44
  }
34
-
45
+ //#endregion
46
+ //#region src/compiler/scope.d.ts
47
+ /**
48
+ * 统一编译:按 scope 从 graphs.scopes[scope] 生成一段完整 TS 代码(comments → imports → variables → functions → typings → interfaces)。
49
+ */
50
+ declare function compile(configRead: ApiPipeline.ConfigRead, scope: string): string;
35
51
  //#endregion
36
52
  //#region src/compiler/index.d.ts
53
+ /**
54
+ * Compiles each output from graphs.scopes[output.type]; onlyDeclaration 时仅编译 type 为 "type" 的 output。
55
+ *
56
+ * @param configRead - ConfigRead with graphs.scopes and outputs
57
+ * @returns Same configRead with output.code set
58
+ * @group Pipeline
59
+ */
37
60
  declare function compiler(configRead: ApiPipeline.ConfigRead): ApiPipeline.ConfigRead<ApiPipeline.Config>;
38
-
39
61
  //#endregion
40
62
  //#region src/config/index.d.ts
63
+ /**
64
+ * Normalizes pipeline config: output paths, responseType, baseURL, and builds ConfigRead with inputs/outputs.
65
+ *
66
+ * @param userConfig - Raw config from defineConfig
67
+ * @returns ConfigRead (config + inputs + outputs)
68
+ * @group Pipeline
69
+ * @example
70
+ * ```ts
71
+ * const configRead = config(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
72
+ * ```
73
+ */
41
74
  declare function config(userConfig: ApiPipeline.Config): ApiPipeline.ConfigRead<Required<ApiPipeline.Config>>;
42
-
43
75
  //#endregion
44
76
  //#region src/dest/index.d.ts
45
- declare function dest(configRead: ApiPipeline.ConfigRead): void;
46
-
77
+ /**
78
+ * Writes output files from configRead.outputs (code to path).
79
+ *
80
+ * @param configRead - ConfigRead with outputs[].path and outputs[].code
81
+ * @group Pipeline
82
+ * @example
83
+ * ```ts
84
+ * await dest(configRead)
85
+ * // Writes configRead.outputs[].code to configRead.outputs[].path
86
+ * ```
87
+ */
88
+ declare function dest(configRead: ApiPipeline.ConfigRead): Promise<void>;
47
89
  //#endregion
48
90
  //#region src/generate/index.d.ts
49
- declare function generate(configRead: ApiPipeline.ConfigRead): ApiPipeline.ConfigRead<ApiPipeline.Config>;
50
-
91
+ /**
92
+ * Formats code for each output with Prettier.
93
+ *
94
+ * @param configRead - ConfigRead with outputs[].code
95
+ * @returns Same configRead with outputs[].code formatted
96
+ * @group Pipeline
97
+ * @example
98
+ * ```ts
99
+ * await generate(configRead)
100
+ * await generate(configRead, { printWidth: 100 })
101
+ * ```
102
+ */
103
+ declare function generate(configRead: ApiPipeline.ConfigRead, options?: any): Promise<ApiPipeline.ConfigRead<ApiPipeline.Config>>;
104
+ /**
105
+ * Formats TypeScript/JavaScript code string with Prettier.
106
+ *
107
+ * @param code - Source code to format
108
+ * @param options - Prettier options (merged with parser: 'typescript', printWidth: 800)
109
+ * @returns Formatted code string
110
+ * @example
111
+ * ```ts
112
+ * const formatted = await formatTypescript('const x=1')
113
+ * ```
114
+ */
115
+ declare function formatTypescript(code: string, options?: any): Promise<string>;
51
116
  //#endregion
52
117
  //#region src/original/index.d.ts
118
+ /**
119
+ * Fetches source: resolves uri/http/json from configRead.inputs and sets configRead.source.
120
+ * Transforms the source based on parser configuration (wpapi -> swagger2, swagger -> unchanged).
121
+ * Supports YAML source URLs (e.g. .yaml / .yml); uses confbox for parsing (same as undocs).
122
+ *
123
+ * @param configRead - ConfigRead with inputs (uri, http, or json)
124
+ * @returns Same configRead with source set and transformed if needed
125
+ * @group Pipeline
126
+ * @example
127
+ * ```ts
128
+ * await original(configRead)
129
+ * // configRead.source is set from uri/http/json; wpapi transformed to swagger2 if parser is 'wpapi'
130
+ * ```
131
+ */
53
132
  declare function original(configRead: ApiPipeline.ConfigRead): Promise<ApiPipeline.ConfigRead<ApiPipeline.Config>>;
54
-
55
133
  //#endregion
56
- export { PipelineDest, PipelineFlow, PipelineRead, compiler, config, pipeline as default, dest, generate, original };
134
+ export { type PipelineDest, type PipelineFlow, type PipelineRead, compile, compiler, config, pipeline as default, dest, formatTypescript, generate, original };
package/dist/index.mjs CHANGED
@@ -1,136 +1,211 @@
1
1
  import pPipe from "p-pipe";
2
- import { astNodeToCode, codeToAstNode, createComment, createFunction, createImport, createInterface, createTypeAlias, createVariable } from "ts-factory-extra";
3
- import { NodeFlags, factory } from "typescript";
2
+ import { inject, provide } from "@genapi/shared";
3
+ import { genComment, genFunction, genImport, genInterface, genTypeAlias, genVariable } from "knitwork-x";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
- import { provide } from "@genapi/shared";
7
6
  import fs from "fs-extra";
8
7
  import { format } from "prettier";
9
- import got from "got";
8
+ import { wpapiToSwagger2 } from "@genapi/transform";
9
+ import { parseYAML } from "confbox";
10
+ import { ofetch } from "ofetch";
10
11
 
11
- //#region src/compiler/typings.ts
12
- function compilerTsTypingsDeclaration(configRead, comment = true) {
13
- configRead.graphs.comments = configRead.graphs.comments || [];
14
- configRead.graphs.typings = configRead.graphs.typings || [];
15
- configRead.graphs.interfaces = configRead.graphs.interfaces || [];
16
- const typings = configRead.graphs.typings.map((item) => {
17
- return createTypeAlias(item.export, item.name, item.value);
18
- });
19
- const interfaces = configRead.graphs.interfaces.map((item) => {
20
- return createInterface({
21
- export: item.export,
22
- name: item.name,
23
- properties: item.properties || []
24
- });
12
+ //#region src/compiler/scope.ts
13
+ const RE_NAMESPACE = /^(Types\.|import\(['"][^'"]+['"]\)\.)/g;
14
+ const RE_ARRAY_TYPE = /^(.+)\[\]$/;
15
+ function generateMockTemplate(typeName, interfaceMap, visited = /* @__PURE__ */ new Set()) {
16
+ const cleanTypeName = typeName.replace(RE_NAMESPACE, "").trim();
17
+ const arrayMatch = cleanTypeName.match(RE_ARRAY_TYPE);
18
+ if (arrayMatch) return `[${generateMockTemplate(arrayMatch[1], interfaceMap, visited)}]`;
19
+ const primitiveMap = {
20
+ string: "'@string'",
21
+ number: "'@integer'",
22
+ boolean: "'@boolean'",
23
+ Date: "'@datetime'",
24
+ void: "null",
25
+ any: "'@string'"
26
+ };
27
+ if (primitiveMap[cleanTypeName]) return primitiveMap[cleanTypeName];
28
+ if (cleanTypeName.includes("|")) return "{}";
29
+ const interfaceDef = interfaceMap.get(cleanTypeName);
30
+ if (!interfaceDef?.properties || visited.has(cleanTypeName)) return "{}";
31
+ visited.add(cleanTypeName);
32
+ const properties = interfaceDef.properties.filter((prop) => prop.type).map((prop) => {
33
+ const propType = prop.type.replace(RE_NAMESPACE, "").trim();
34
+ const nextVisited = new Set(visited);
35
+ if (propType.endsWith("[]")) {
36
+ const itemTemplate = generateMockTemplate(propType.slice(0, -2), interfaceMap, nextVisited);
37
+ return `'${prop.name}|1-5': ${itemTemplate}`;
38
+ }
39
+ return `'${prop.name}': ${generateMockTemplate(propType, interfaceMap, nextVisited)}`;
25
40
  });
26
- const nodes = [
27
- factory.createIdentifier(""),
28
- ...typings,
29
- factory.createIdentifier(""),
30
- ...interfaces
31
- ];
32
- if (comment) nodes.unshift(createComment("multi", configRead.graphs.comments));
33
- return nodes;
41
+ visited.delete(cleanTypeName);
42
+ return properties.length > 0 ? `{\n ${properties.join(",\n ")}\n }` : "{}";
34
43
  }
35
-
36
- //#endregion
37
- //#region src/compiler/request.ts
38
- const varFlags = {
39
- let: NodeFlags.Let,
40
- const: NodeFlags.Const,
41
- var: NodeFlags.None
42
- };
43
- function compilerTsRequestDeclaration(configRead) {
44
- configRead.graphs.imports = configRead.graphs.imports || [];
45
- configRead.graphs.comments = configRead.graphs.comments || [];
46
- configRead.graphs.variables = configRead.graphs.variables || [];
47
- configRead.graphs.functions = configRead.graphs.functions || [];
48
- const isGenerateType = configRead.outputs.some((v) => v.type === "typings");
49
- const isTypescript = configRead.outputs.some((v) => v.type === "request" && v.path.endsWith(".ts"));
50
- const comments = [createComment("multi", configRead.graphs.comments)];
51
- const imports = configRead.graphs.imports?.map((item) => {
52
- return createImport(item.name, item.names, item.value, item.namespace);
53
- });
54
- const variables = configRead.graphs.variables.map((item) => {
55
- return createVariable(item.export, varFlags[item.flag], item.name, item.value);
56
- });
57
- const functions = configRead.graphs.functions.flatMap((item) => {
58
- return createFunction({
44
+ function genFunctionsWithMock(functions, interfaceMap, config) {
45
+ const functionBlocks = [];
46
+ for (const item of functions || []) {
47
+ functionBlocks.push(genFunction({
59
48
  export: true,
60
- comment: item.description,
61
49
  name: item.name,
62
- parameters: item.parameters,
63
- body: item.body?.map(codeToAstNode),
50
+ parameters: (item.parameters || []).map((p) => ({
51
+ name: p.name,
52
+ type: p.type,
53
+ optional: !p.required
54
+ })),
55
+ body: item.body || [],
64
56
  async: item.async,
65
57
  returnType: item.returnType,
66
58
  generics: item.generics,
67
- generator: item.generator
68
- });
69
- });
70
- const nodes = [
71
- ...comments,
72
- factory.createIdentifier(""),
73
- ...imports,
74
- factory.createIdentifier(""),
75
- ...variables,
76
- factory.createIdentifier(""),
77
- ...functions
78
- ];
79
- if (!isGenerateType && isTypescript) {
80
- nodes.push(factory.createIdentifier(""));
81
- nodes.push(factory.createIdentifier(""));
82
- nodes.push(...compilerTsTypingsDeclaration(configRead, false));
59
+ jsdoc: item.description
60
+ }));
61
+ const responseType = inject(item.name)?.responseType;
62
+ if (config.meta?.mockjs && responseType && responseType !== "void" && responseType !== "any") {
63
+ const mockTemplate = generateMockTemplate(responseType, interfaceMap);
64
+ functionBlocks.push(`${item.name}.mock = () => Mock.mock(${mockTemplate});`);
65
+ }
83
66
  }
84
- return nodes;
67
+ return functionBlocks;
68
+ }
69
+ function emptySlice() {
70
+ return {
71
+ comments: [],
72
+ functions: [],
73
+ imports: [],
74
+ variables: [],
75
+ typings: [],
76
+ interfaces: []
77
+ };
78
+ }
79
+ function varFiledToRaw(name) {
80
+ if (name.length >= 2 && name.startsWith("'") && name.endsWith("'")) return name.slice(1, -1);
81
+ return name;
82
+ }
83
+ /**
84
+ * 统一编译:按 scope 从 graphs.scopes[scope] 生成一段完整 TS 代码(comments → imports → variables → functions → typings → interfaces)。
85
+ */
86
+ function compile(configRead, scope) {
87
+ const slice = configRead.graphs.scopes[scope] ?? emptySlice();
88
+ const { config } = configRead;
89
+ const sections = [];
90
+ if (slice.comments?.length) sections.push(genComment(slice.comments.join("\n"), { block: true }));
91
+ const importLines = (slice.imports || []).map((item) => {
92
+ const isType = !!item.type;
93
+ if (item.namespace) return genImport(item.value, {
94
+ name: "*",
95
+ as: item.name
96
+ }, { type: isType });
97
+ if (item.name && !item.names) return genImport(item.value, item.name, { type: isType });
98
+ const names = item.names || [];
99
+ const imports = item.name ? [{
100
+ name: "default",
101
+ as: item.name
102
+ }, ...names.map((n) => ({ name: n }))] : names;
103
+ return genImport(item.value, imports, { type: isType });
104
+ });
105
+ if (config.meta?.mockjs) importLines.push(genImport("better-mock", { name: "Mock" }));
106
+ if (importLines.length) sections.push(importLines.join("\n"));
107
+ const variables = (slice.variables || []).map((item) => genVariable(item.name, item.value ?? "", {
108
+ export: !!item.export,
109
+ kind: item.flag
110
+ }));
111
+ if (variables.length) sections.push(variables.join("\n"));
112
+ const interfaceMap = new Map((slice.interfaces || []).map((i) => [i.name, i]));
113
+ const functions = genFunctionsWithMock(slice.functions || [], interfaceMap, config);
114
+ if (functions.length) sections.push(functions.join("\n\n"));
115
+ const typings = (slice.typings || []).map((item) => genTypeAlias(item.name, item.value, { export: !!item.export }));
116
+ if (typings.length) sections.push(typings.join("\n"));
117
+ const interfaces = (slice.interfaces || []).map((item) => {
118
+ const properties = (item.properties || []).map((p) => ({
119
+ name: varFiledToRaw(p.name),
120
+ type: p.type ?? "any",
121
+ optional: !p.required,
122
+ jsdoc: p.description
123
+ }));
124
+ return genInterface(item.name, properties, { export: !!item.export });
125
+ });
126
+ if (interfaces.length) sections.push(interfaces.join("\n"));
127
+ return sections.filter(Boolean).join("\n\n");
85
128
  }
86
129
 
87
130
  //#endregion
88
131
  //#region src/compiler/index.ts
132
+ /**
133
+ * Compiles each output from graphs.scopes[output.type]; onlyDeclaration 时仅编译 type 为 "type" 的 output。
134
+ *
135
+ * @param configRead - ConfigRead with graphs.scopes and outputs
136
+ * @returns Same configRead with output.code set
137
+ * @group Pipeline
138
+ */
89
139
  function compiler(configRead) {
140
+ const onlyDeclaration = !!configRead.config.meta?.onlyDeclaration;
90
141
  for (const output of configRead.outputs) {
91
- if (output.type === "request" && !configRead.config.onlyDeclaration) output.ast = compilerTsRequestDeclaration(configRead);
92
- if (output.type === "typings") output.ast = compilerTsTypingsDeclaration(configRead);
142
+ if (onlyDeclaration && output.type !== "type") continue;
143
+ output.code = compile(configRead, output.type);
93
144
  }
94
145
  return configRead;
95
146
  }
96
147
 
97
148
  //#endregion
98
149
  //#region src/config/index.ts
150
+ /**
151
+ * Normalizes pipeline config: output paths, responseType, baseURL, and builds ConfigRead with inputs/outputs.
152
+ *
153
+ * @param userConfig - Raw config from defineConfig
154
+ * @returns ConfigRead (config + inputs + outputs)
155
+ * @group Pipeline
156
+ * @example
157
+ * ```ts
158
+ * const configRead = config(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
159
+ * ```
160
+ */
99
161
  function config(userConfig) {
100
- userConfig.import = userConfig.import || {};
101
- userConfig.responseType = userConfig.responseType || {};
162
+ userConfig.meta = userConfig.meta || {};
163
+ userConfig.meta.import = userConfig.meta.import || {};
164
+ userConfig.meta.responseType = userConfig.meta.responseType || {};
102
165
  if (typeof userConfig.output === "string") userConfig.output = { main: userConfig.output };
103
166
  userConfig.output = userConfig.output || {};
104
167
  userConfig.output.main = userConfig.output.main || "src/api/index.ts";
105
- if (typeof userConfig.baseURL === "string") userConfig.baseURL = userConfig.baseURL.endsWith("/\"") ? userConfig.baseURL = `${userConfig.baseURL.slice(0, userConfig.baseURL.length - 2)}"` : userConfig.baseURL;
168
+ if (typeof userConfig.meta.baseURL === "string") userConfig.meta.baseURL = userConfig.meta.baseURL.endsWith("/\"") ? userConfig.meta.baseURL = `${userConfig.meta.baseURL.slice(0, userConfig.meta.baseURL.length - 2)}"` : userConfig.meta.baseURL;
106
169
  if (userConfig.output?.type !== false) userConfig.output.type = userConfig.output.type || userConfig.output.main.replace(/\.ts|\.js/g, ".type.ts");
107
- if (typeof userConfig.responseType === "string") userConfig.responseType = { infer: userConfig.responseType };
170
+ if (typeof userConfig.meta.responseType === "string") userConfig.meta.responseType = { infer: userConfig.meta.responseType };
108
171
  const userRoot = process.cwd();
109
172
  const isTypescript = userConfig.output.main.endsWith(".ts");
110
173
  const isGenerateType = userConfig.output?.type !== false;
111
- const importTypePath = userConfig.import.type || getImportTypePath(userConfig.output.main, userConfig.output.type || "");
112
- const imports = [isTypescript && isGenerateType && {
113
- name: "Types",
114
- value: importTypePath,
115
- type: true,
116
- namespace: true
117
- }];
118
- const outputs = [{
119
- type: "request",
120
- root: path.join(userRoot, path.dirname(userConfig.output.main)),
121
- path: path.join(userRoot, userConfig.output.main)
122
- }];
123
- const typings = [!!userConfig.responseType.infer && {
124
- export: true,
125
- name: "Infer<T>",
126
- value: userConfig.responseType.infer
127
- }];
128
- if (userConfig.output.type !== false) outputs.push({
129
- type: "typings",
130
- root: path.join(userRoot, path.dirname(userConfig.output.type)),
131
- import: importTypePath,
132
- path: path.join(userRoot, userConfig.output.type)
133
- });
174
+ const importTypePath = userConfig.meta.import.type || getImportTypePath(userConfig.output.main, userConfig.output.type || "");
175
+ const outputs = [];
176
+ const scopes = {};
177
+ const outputKeys = [
178
+ "main",
179
+ ...userConfig.output.type !== false ? ["type"] : [],
180
+ ...objectOutputKeys(userConfig.output)
181
+ ];
182
+ for (const key of outputKeys) {
183
+ const outPath = key === "main" ? userConfig.output.main : key === "type" ? userConfig.output.type : userConfig.output[key];
184
+ if (typeof outPath !== "string") continue;
185
+ outputs.push({
186
+ type: key,
187
+ root: path.join(userRoot, path.dirname(outPath)),
188
+ path: path.join(userRoot, outPath),
189
+ ...key === "type" ? { import: importTypePath } : {}
190
+ });
191
+ scopes[key] = {
192
+ comments: [],
193
+ functions: [],
194
+ imports: key === "main" && isTypescript && isGenerateType ? [{
195
+ name: "Types",
196
+ value: importTypePath,
197
+ type: true,
198
+ namespace: true
199
+ }] : [],
200
+ variables: [],
201
+ typings: key === "type" && userConfig.meta.responseType?.infer ? [{
202
+ export: true,
203
+ name: "Infer<T>",
204
+ value: userConfig.meta.responseType.infer
205
+ }] : [],
206
+ interfaces: []
207
+ };
208
+ }
134
209
  const inputs = {};
135
210
  if (typeof userConfig.input === "string") inputs.uri = userConfig.input;
136
211
  if (typeof userConfig.input === "object") Object.assign(inputs, userConfig.input);
@@ -139,23 +214,45 @@ function config(userConfig) {
139
214
  inputs,
140
215
  outputs,
141
216
  graphs: {
142
- imports: imports.filter(Boolean),
143
- variables: [],
144
- comments: [],
145
- functions: [],
146
- interfaces: [],
147
- typings: typings.filter(Boolean),
148
- response: userConfig.responseType
217
+ scopes,
218
+ response: userConfig.meta.responseType
149
219
  }
150
220
  };
151
221
  provide({
152
222
  config: userConfig,
153
- configRead
223
+ configRead,
224
+ imports: block(configRead, "imports")
154
225
  });
155
226
  return configRead;
156
227
  }
157
- function prefix(path$1) {
158
- return path$1.startsWith(".") ? path$1 : `./${path$1}`;
228
+ function block(configRead, key) {
229
+ return {
230
+ add(scope, item) {
231
+ let slice = configRead.graphs.scopes[scope];
232
+ if (!slice) {
233
+ slice = {
234
+ comments: [],
235
+ functions: [],
236
+ imports: [],
237
+ variables: [],
238
+ typings: [],
239
+ interfaces: []
240
+ };
241
+ configRead.graphs.scopes[scope] = slice;
242
+ }
243
+ if (Array.isArray(slice[key])) slice[key].push(item);
244
+ },
245
+ values(scope) {
246
+ const slice = configRead.graphs.scopes[scope];
247
+ return slice && Array.isArray(slice[key]) ? slice[key] : [];
248
+ },
249
+ all() {
250
+ return Object.values(configRead.graphs.scopes).flatMap((s) => s[key] || []);
251
+ }
252
+ };
253
+ }
254
+ function prefix(path) {
255
+ return path.startsWith(".") ? path : `./${path}`;
159
256
  }
160
257
  function getImportTypePath(main, type) {
161
258
  let importTypePath = path.dirname(main);
@@ -163,64 +260,148 @@ function getImportTypePath(main, type) {
163
260
  importTypePath = prefix(importTypePath).replace(".ts", "");
164
261
  return importTypePath;
165
262
  }
263
+ /** 从 output 对象中取除 main/type 外的 key(如 api),且值为 string 的 */
264
+ function objectOutputKeys(output) {
265
+ if (typeof output !== "object" || output == null) return [];
266
+ return Object.keys(output).filter((k) => k !== "main" && k !== "type" && typeof output[k] === "string");
267
+ }
166
268
 
167
269
  //#endregion
168
270
  //#region src/dest/index.ts
169
- function dest(configRead) {
170
- configRead.outputs.map(async (output) => {
271
+ /**
272
+ * Writes output files from configRead.outputs (code to path).
273
+ *
274
+ * @param configRead - ConfigRead with outputs[].path and outputs[].code
275
+ * @group Pipeline
276
+ * @example
277
+ * ```ts
278
+ * await dest(configRead)
279
+ * // Writes configRead.outputs[].code to configRead.outputs[].path
280
+ * ```
281
+ */
282
+ async function dest(configRead) {
283
+ await Promise.all(configRead.outputs.map(async (output) => {
171
284
  await fs.ensureDir(output.root);
172
285
  await fs.writeFile(output.path, output.code || "", { flag: "w" });
173
- });
286
+ }));
174
287
  }
175
288
 
176
289
  //#endregion
177
290
  //#region src/generate/index.ts
178
- function generate(configRead) {
179
- for (const output of configRead.outputs || []) {
180
- if (output.ast) output.code = astNodeToCode(output.ast);
181
- if (output.code) output.code = formatTypescript(output.code);
182
- }
291
+ /**
292
+ * Formats code for each output with Prettier.
293
+ *
294
+ * @param configRead - ConfigRead with outputs[].code
295
+ * @returns Same configRead with outputs[].code formatted
296
+ * @group Pipeline
297
+ * @example
298
+ * ```ts
299
+ * await generate(configRead)
300
+ * await generate(configRead, { printWidth: 100 })
301
+ * ```
302
+ */
303
+ async function generate(configRead, options) {
304
+ for (const output of configRead.outputs || []) if (output.code) output.code = await formatTypescript(output.code, options);
183
305
  return configRead;
184
306
  }
185
- function formatTypescript(code) {
186
- return format(code, {
307
+ /**
308
+ * Formats TypeScript/JavaScript code string with Prettier.
309
+ *
310
+ * @param code - Source code to format
311
+ * @param options - Prettier options (merged with parser: 'typescript', printWidth: 800)
312
+ * @returns Formatted code string
313
+ * @example
314
+ * ```ts
315
+ * const formatted = await formatTypescript('const x=1')
316
+ * ```
317
+ */
318
+ async function formatTypescript(code, options) {
319
+ return await format(code, {
187
320
  printWidth: 800,
321
+ ...options,
188
322
  parser: "typescript"
189
323
  });
190
324
  }
191
325
 
192
326
  //#endregion
193
327
  //#region src/original/index.ts
328
+ function isYamlUrl(url) {
329
+ try {
330
+ const pathname = new URL(url).pathname;
331
+ return pathname.endsWith(".yaml") || pathname.endsWith(".yml");
332
+ } catch {
333
+ return false;
334
+ }
335
+ }
336
+ async function fetchSource(url, options) {
337
+ if (isYamlUrl(url)) return parseYAML(await ofetch(url, {
338
+ ...options,
339
+ responseType: "text"
340
+ }));
341
+ if (options && Object.keys(options).length > 0) return await ofetch(url, options);
342
+ return await ofetch(url);
343
+ }
344
+ /**
345
+ * Fetches source: resolves uri/http/json from configRead.inputs and sets configRead.source.
346
+ * Transforms the source based on parser configuration (wpapi -> swagger2, swagger -> unchanged).
347
+ * Supports YAML source URLs (e.g. .yaml / .yml); uses confbox for parsing (same as undocs).
348
+ *
349
+ * @param configRead - ConfigRead with inputs (uri, http, or json)
350
+ * @returns Same configRead with source set and transformed if needed
351
+ * @group Pipeline
352
+ * @example
353
+ * ```ts
354
+ * await original(configRead)
355
+ * // configRead.source is set from uri/http/json; wpapi transformed to swagger2 if parser is 'wpapi'
356
+ * ```
357
+ */
194
358
  async function original(configRead) {
195
- if (configRead.inputs.uri) configRead.source = await got({
196
- url: configRead.inputs.uri,
197
- responseType: "json"
198
- }).json();
199
- if (configRead.inputs.http) configRead.source = await got({
200
- ...configRead.inputs.http,
201
- responseType: "json"
202
- }).json();
359
+ if (configRead.inputs.uri) configRead.source = await fetchSource(configRead.inputs.uri);
360
+ if (configRead.inputs.http) {
361
+ const { url, ...options } = configRead.inputs.http;
362
+ configRead.source = await fetchSource(url || configRead.inputs.uri || "", options);
363
+ }
203
364
  if (configRead.inputs.json) configRead.source = await readJsonSource(configRead.inputs.json);
204
365
  if (!configRead.source) throw new Error("No source found, please check your input config.");
366
+ if ((configRead.config.parser || "swagger") === "wpapi") configRead.source = wpapiToSwagger2(configRead.source);
205
367
  if (!configRead.source.schemes?.length) {
368
+ const effectiveUrl = configRead.inputs.http?.url || configRead.inputs.uri || "";
206
369
  const schemes = [];
207
- if (configRead.inputs.uri?.startsWith("https://")) schemes.push("https", "http");
208
- if (configRead.inputs.uri?.startsWith("http://")) schemes.push("http");
209
- configRead.source.schemes = schemes;
370
+ if (effectiveUrl.startsWith("https://")) schemes.push("https", "http");
371
+ if (effectiveUrl.startsWith("http://")) schemes.push("http");
372
+ if (schemes.length > 0) configRead.source.schemes = schemes;
210
373
  }
211
374
  return configRead;
212
375
  }
213
376
  function readJsonSource(json) {
214
- if (!json) return;
215
- if (typeof json === "object") return json;
216
- else return import(json).then((mod) => mod.default);
377
+ if (!json) return Promise.resolve(void 0);
378
+ if (typeof json === "object") return Promise.resolve(json);
379
+ const trimmed = String(json).trim();
380
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return Promise.resolve(JSON.parse(json));
381
+ return import(json).then((mod) => mod.default);
217
382
  }
218
383
 
219
384
  //#endregion
220
385
  //#region src/pipeline/index.ts
221
- function pipeline(config$1, original$1, parser, compiler$1, generate$1, dest$1) {
222
- const pipe = pPipe(config$1, original$1, parser, compiler$1, generate$1, dest$1);
223
- return pipe;
386
+ /**
387
+ * Builds a GenAPI pipeline from five steps: config original parser compiler generate dest.
388
+ *
389
+ * @param config - Reads config and returns ConfigRead
390
+ * @param original - Fetches source (e.g. OpenAPI JSON)
391
+ * @param parser - Parses source into graphs
392
+ * @param compiler - Compiles graphs to AST
393
+ * @param generate - Generates code string
394
+ * @param dest - Writes output files
395
+ * @returns A function that runs the pipeline for a given config
396
+ * @group Pipeline
397
+ * @example
398
+ * ```ts
399
+ * const run = pipeline(config, original, parser, compiler, generate, dest)
400
+ * await run(defineConfig({ input: 'openapi.json', output: { main: 'src/api.ts' } }))
401
+ * ```
402
+ */
403
+ function pipeline(config, original, parser, compiler, generate, dest) {
404
+ return pPipe(config, original, parser, compiler, generate, dest);
224
405
  }
225
406
  pipeline.config = config;
226
407
  pipeline.original = original;
@@ -234,4 +415,4 @@ pipeline.dest = dest;
234
415
  var src_default = pipeline;
235
416
 
236
417
  //#endregion
237
- export { compiler, config, src_default as default, dest, generate, original };
418
+ export { compile, compiler, config, src_default as default, dest, formatTypescript, generate, original };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genapi/pipeline",
3
3
  "type": "module",
4
- "version": "3.7.1",
4
+ "version": "4.1.0",
5
5
  "author": "Hairyf <wwu710632@gmail.com>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/hairyf/genapi#readme",
@@ -15,29 +15,32 @@
15
15
  "shared"
16
16
  ],
17
17
  "sideEffects": false,
18
- "main": "./dist/index.mjs",
18
+ "exports": {
19
+ ".": "./dist/index.mjs",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "types": "./dist/index.d.mts",
19
23
  "files": [
20
24
  "dist"
21
25
  ],
22
26
  "dependencies": {
23
- "fs-extra": "^11.1.0",
24
- "got": "11.8.6",
25
- "p-pipe": "^3",
26
- "prettier": "^2.8.4",
27
- "ts-factory-extra": "^0.0.5",
28
- "typescript": "^5.0.0",
29
- "@genapi/shared": "3.7.1"
27
+ "confbox": "^0.2.4",
28
+ "fs-extra": "^11.3.3",
29
+ "knitwork-x": "^0.3.1",
30
+ "ofetch": "^1.5.1",
31
+ "p-pipe": "^4.0.0",
32
+ "prettier": "^3.8.1",
33
+ "@genapi/shared": "4.1.0",
34
+ "@genapi/transform": "4.1.0"
30
35
  },
31
36
  "devDependencies": {
32
- "@types/fs-extra": "^11.0.1"
37
+ "@types/fs-extra": "^11.0.4",
38
+ "@types/prettier": "^2.7.3"
33
39
  },
34
40
  "scripts": {
35
41
  "build": "tsdown",
36
- "start": "tsx src/index.ts"
37
- },
38
- "exports": {
39
- ".": "./dist/index.mjs"
40
- },
41
- "module": "./dist/index.mjs",
42
- "types": "./dist/index.d.mts"
42
+ "dev": "tsdown --watch",
43
+ "typecheck": "tsc --noEmit",
44
+ "automd": "automd"
45
+ }
43
46
  }