@faasjs/dev 8.0.0-beta.10
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 +141 -0
- package/configs/oxlint.base.json +5 -0
- package/dist/chunk-D8kEL_kv.mjs +38 -0
- package/dist/cli/index.cjs +292 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.mjs +290 -0
- package/dist/index.cjs +318 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.mjs +272 -0
- package/dist/typegen-BNWmP5Qp.mjs +165 -0
- package/dist/typegen-HX5QyuhP.cjs +182 -0
- package/faas.mjs +7 -0
- package/package.json +59 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { n as __reExport, t as __exportAll } from "./chunk-D8kEL_kv.mjs";
|
|
2
|
+
import { n as isTypegenSourceFile, r as resolveServerConfig, t as generateFaasTypes } from "./typegen-BNWmP5Qp.mjs";
|
|
3
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
4
|
+
import { Http, Server } from "@faasjs/core";
|
|
5
|
+
import { Logger, deepMerge, loadConfig, streamToObject, streamToString, streamToText } from "@faasjs/node-utils";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export * from "@faasjs/core"
|
|
9
|
+
|
|
10
|
+
//#region src/test.ts
|
|
11
|
+
/**
|
|
12
|
+
* Test wrapper for a function.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { FuncWarper } from '@faasjs/dev'
|
|
16
|
+
* import Func from '../demo.func.ts'
|
|
17
|
+
*
|
|
18
|
+
* const func = new FuncWarper(Func)
|
|
19
|
+
*
|
|
20
|
+
* expect(await func.handler()).toEqual('Hello, world')
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
var FuncWarper = class {
|
|
24
|
+
file;
|
|
25
|
+
staging;
|
|
26
|
+
logger;
|
|
27
|
+
func;
|
|
28
|
+
config;
|
|
29
|
+
plugins;
|
|
30
|
+
_handler;
|
|
31
|
+
/**
|
|
32
|
+
* @param initBy {Func} A FaasJS function
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { FuncWarper } from '@faasjs/dev'
|
|
35
|
+
*
|
|
36
|
+
* new FuncWarper(__dirname + '/../demo.func.ts')
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
constructor(initBy) {
|
|
40
|
+
this.staging = process.env.FaasEnv ?? "default";
|
|
41
|
+
this.logger = new Logger("TestCase");
|
|
42
|
+
this.func = initBy.default ? initBy.default : initBy;
|
|
43
|
+
if (this.func.filename) this.func.config = deepMerge(loadConfig(process.cwd(), this.func.filename, this.staging, this.logger), this.func.config);
|
|
44
|
+
this.file = this.func.filename || "";
|
|
45
|
+
this.config = this.func.config;
|
|
46
|
+
this.plugins = this.func.plugins || [];
|
|
47
|
+
for (const plugin of this.plugins) {
|
|
48
|
+
if ([
|
|
49
|
+
"handler",
|
|
50
|
+
"config",
|
|
51
|
+
"plugins",
|
|
52
|
+
"logger",
|
|
53
|
+
"mount"
|
|
54
|
+
].includes(plugin.type)) continue;
|
|
55
|
+
this[plugin.type] = plugin;
|
|
56
|
+
}
|
|
57
|
+
this._handler = this.func.export().handler;
|
|
58
|
+
}
|
|
59
|
+
async mount(handler) {
|
|
60
|
+
if (!this.func.mounted) await this.func.mount();
|
|
61
|
+
if (handler) await handler(this);
|
|
62
|
+
}
|
|
63
|
+
async handler(event = Object.create(null), context = Object.create(null)) {
|
|
64
|
+
await this.mount();
|
|
65
|
+
const response = await this._handler(event, context);
|
|
66
|
+
this.logger.debug("response: %j", response);
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
async JSONhandler(body, options = Object.create(null)) {
|
|
70
|
+
await this.mount();
|
|
71
|
+
const headers = options.headers || Object.create(null);
|
|
72
|
+
if (this.http && this.http instanceof Http) {
|
|
73
|
+
if (options.cookie) for (const key in options.cookie) this.http.cookie.write(key, options.cookie[key]);
|
|
74
|
+
if (options.session) {
|
|
75
|
+
for (const key in options.session) this.http.session.write(key, options.session[key]);
|
|
76
|
+
this.http.session.update();
|
|
77
|
+
}
|
|
78
|
+
const cookie = this.http.cookie.headers()["Set-Cookie"]?.map((c) => c.split(";")[0]).join(";");
|
|
79
|
+
if (cookie) if (headers.cookie) headers.cookie += `;${cookie}`;
|
|
80
|
+
else headers.cookie = cookie;
|
|
81
|
+
}
|
|
82
|
+
const response = await this._handler({
|
|
83
|
+
httpMethod: "POST",
|
|
84
|
+
headers: Object.assign({ "content-type": "application/json" }, headers),
|
|
85
|
+
body: typeof body === "string" ? body : JSON.stringify(body)
|
|
86
|
+
});
|
|
87
|
+
if (response?.body instanceof ReadableStream) {
|
|
88
|
+
let stream = response.body;
|
|
89
|
+
const encoding = response.headers?.["Content-Encoding"] || response.headers?.["content-encoding"];
|
|
90
|
+
if (encoding) {
|
|
91
|
+
const chunks = [];
|
|
92
|
+
const reader = stream.getReader();
|
|
93
|
+
try {
|
|
94
|
+
while (true) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done) break;
|
|
97
|
+
if (value) chunks.push(value);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.logger.error("Failed to read ReadableStream: %s", error);
|
|
101
|
+
response.body = JSON.stringify({ error: { message: error.message } });
|
|
102
|
+
response.error = { message: error.message };
|
|
103
|
+
response.statusCode = 500;
|
|
104
|
+
reader.releaseLock();
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
reader.releaseLock();
|
|
108
|
+
const compressedBuffer = Buffer.concat(chunks);
|
|
109
|
+
try {
|
|
110
|
+
let decompressed;
|
|
111
|
+
if (encoding === "br") decompressed = brotliDecompressSync(compressedBuffer);
|
|
112
|
+
else if (encoding === "gzip") decompressed = gunzipSync(compressedBuffer);
|
|
113
|
+
else if (encoding === "deflate") decompressed = inflateSync(compressedBuffer);
|
|
114
|
+
else throw new Error(`Unsupported encoding: ${encoding}`);
|
|
115
|
+
stream = new ReadableStream({ start(controller) {
|
|
116
|
+
controller.enqueue(new Uint8Array(decompressed));
|
|
117
|
+
controller.close();
|
|
118
|
+
} });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.logger.error("Failed to decompress: %s", error);
|
|
121
|
+
response.body = JSON.stringify({ error: { message: error.message } });
|
|
122
|
+
response.error = { message: error.message };
|
|
123
|
+
response.statusCode = 500;
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
response.body = await streamToText(stream);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.logger.error("Failed to decode ReadableStream: %s", error);
|
|
131
|
+
response.body = JSON.stringify({ error: { message: error.message } });
|
|
132
|
+
response.error = { message: error.message };
|
|
133
|
+
response.statusCode = 500;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (response?.headers && response.body && response.headers["content-type"]?.includes("json")) {
|
|
137
|
+
const parsedBody = JSON.parse(response.body);
|
|
138
|
+
response.data = parsedBody.data;
|
|
139
|
+
response.error = parsedBody.error;
|
|
140
|
+
}
|
|
141
|
+
if (this.http) {
|
|
142
|
+
response.cookie = this.http.cookie.content;
|
|
143
|
+
response.session = this.http.session.content;
|
|
144
|
+
}
|
|
145
|
+
this.logger.debug("response: %j", response);
|
|
146
|
+
return response;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* A simple way to wrap a FaasJS function.
|
|
151
|
+
* @param initBy {Func} Full file path or a FaasJs function
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { test } from '@faasjs/dev'
|
|
155
|
+
* import Func from '../demo.func.ts'
|
|
156
|
+
*
|
|
157
|
+
* const func = test(Func)
|
|
158
|
+
*
|
|
159
|
+
* expect(await func.handler()).toEqual('Hello, world')
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function test(initBy) {
|
|
163
|
+
const warper = new FuncWarper(initBy);
|
|
164
|
+
warper.mount = warper.mount.bind(warper);
|
|
165
|
+
warper.handler = warper.handler.bind(warper);
|
|
166
|
+
warper.JSONhandler = warper.JSONhandler.bind(warper);
|
|
167
|
+
return warper;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/vite.ts
|
|
172
|
+
const TYPEGEN_DEBOUNCE = 120;
|
|
173
|
+
function normalizeBase(base) {
|
|
174
|
+
const normalized = base.startsWith("/") ? base : `/${base}`;
|
|
175
|
+
if (normalized === "/") return "/";
|
|
176
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
177
|
+
}
|
|
178
|
+
function stripBase(url, base) {
|
|
179
|
+
if (base === "/") return url;
|
|
180
|
+
const queryIndex = url.indexOf("?");
|
|
181
|
+
const pathname = queryIndex >= 0 ? url.slice(0, queryIndex) : url;
|
|
182
|
+
const search = queryIndex >= 0 ? url.slice(queryIndex) : "";
|
|
183
|
+
if (pathname === base) return `/${search}`;
|
|
184
|
+
if (pathname.startsWith(`${base}/`)) return `${pathname.slice(base.length)}${search}`;
|
|
185
|
+
return url;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Create a Vite plugin that proxies POST requests to an in-process FaasJS server.
|
|
189
|
+
*
|
|
190
|
+
* It resolves server root/base from `src/faas.yaml` and strips `base` from
|
|
191
|
+
* request URL before forwarding to `@faasjs/core`.
|
|
192
|
+
*/
|
|
193
|
+
function viteFaasJsServer() {
|
|
194
|
+
let config;
|
|
195
|
+
let server = null;
|
|
196
|
+
const logger = new Logger("FaasJs:Vite");
|
|
197
|
+
return {
|
|
198
|
+
name: "vite:faasjs",
|
|
199
|
+
enforce: "pre",
|
|
200
|
+
configResolved(resolvedConfig) {
|
|
201
|
+
const { root, base } = resolveServerConfig(resolvedConfig.root, logger, resolvedConfig.base);
|
|
202
|
+
config = {
|
|
203
|
+
root,
|
|
204
|
+
base: normalizeBase(base)
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
configureServer: async ({ middlewares, watcher }) => {
|
|
208
|
+
if (process.env.VITEST) {
|
|
209
|
+
logger.debug("Skipping faas server in vitest environment");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!config) throw new Error("viteFaasJsServer: config is not resolved");
|
|
213
|
+
server = new Server(join(config.root, "src"));
|
|
214
|
+
const runTypegen = async () => {
|
|
215
|
+
try {
|
|
216
|
+
const result = await generateFaasTypes({ root: config.root });
|
|
217
|
+
logger.debug("[faas types] %s %s (%i routes)", result.changed ? "generated" : "up-to-date", result.output, result.routeCount);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.error("[faas types] %s", error.message);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
let timer;
|
|
223
|
+
let typegenChain = Promise.resolve();
|
|
224
|
+
const scheduleTypegen = () => {
|
|
225
|
+
if (timer) clearTimeout(timer);
|
|
226
|
+
timer = setTimeout(() => {
|
|
227
|
+
typegenChain = typegenChain.then(runTypegen);
|
|
228
|
+
}, TYPEGEN_DEBOUNCE);
|
|
229
|
+
};
|
|
230
|
+
await runTypegen();
|
|
231
|
+
watcher.on("all", (_eventName, filePath) => {
|
|
232
|
+
if (!isTypegenSourceFile(filePath)) return;
|
|
233
|
+
scheduleTypegen();
|
|
234
|
+
});
|
|
235
|
+
middlewares.use(async (req, res, next) => {
|
|
236
|
+
if (!req.url || req.method !== "POST" || !server) return next();
|
|
237
|
+
const originalUrl = req.url;
|
|
238
|
+
req.url = stripBase(req.url, config.base);
|
|
239
|
+
try {
|
|
240
|
+
logger.debug(`Request ${req.url}`);
|
|
241
|
+
await server.handle(req, res, { requestedAt: Date.now() });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
logger.error(error);
|
|
244
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
245
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
246
|
+
res.write(JSON.stringify({ error: { message: "Internal Server Error" } }));
|
|
247
|
+
res.end();
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
req.url = originalUrl;
|
|
251
|
+
}
|
|
252
|
+
if (!res.writableEnded) next();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/index.ts
|
|
260
|
+
var src_exports = /* @__PURE__ */ __exportAll({
|
|
261
|
+
FuncWarper: () => FuncWarper,
|
|
262
|
+
generateFaasTypes: () => generateFaasTypes,
|
|
263
|
+
isTypegenSourceFile: () => isTypegenSourceFile,
|
|
264
|
+
streamToObject: () => streamToObject,
|
|
265
|
+
streamToString: () => streamToString,
|
|
266
|
+
streamToText: () => streamToText,
|
|
267
|
+
test: () => test,
|
|
268
|
+
viteFaasJsServer: () => viteFaasJsServer
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
export { FuncWarper, generateFaasTypes, isTypegenSourceFile, streamToObject, streamToString, streamToText, test, viteFaasJsServer };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Logger, loadConfig } from "@faasjs/node-utils";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/server_config.ts
|
|
7
|
+
function resolveFaasStaging() {
|
|
8
|
+
return process.env.FaasEnv || "development";
|
|
9
|
+
}
|
|
10
|
+
function resolveServerConfig(root, logger, defaultBase = "/") {
|
|
11
|
+
const projectRoot = resolve(root);
|
|
12
|
+
const staging = resolveFaasStaging();
|
|
13
|
+
const srcRoot = join(projectRoot, "src");
|
|
14
|
+
const config = loadConfig(srcRoot, join(srcRoot, "index.func.ts"), staging, logger);
|
|
15
|
+
const server = config && typeof config === "object" ? config.server : void 0;
|
|
16
|
+
return {
|
|
17
|
+
root: typeof server?.root === "string" && server.root.length ? resolve(projectRoot, server.root) : projectRoot,
|
|
18
|
+
base: typeof server?.base === "string" && server.base.length ? server.base : defaultBase,
|
|
19
|
+
staging
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/typegen.ts
|
|
25
|
+
function normalizeRoute(path) {
|
|
26
|
+
const normalized = path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
27
|
+
if (!normalized.length || normalized === "/") return "/";
|
|
28
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
29
|
+
}
|
|
30
|
+
function toTypegenRoute(route) {
|
|
31
|
+
return route === "/" ? "/" : route.replace(/^\/+/, "");
|
|
32
|
+
}
|
|
33
|
+
function toRoute(srcRoot, file) {
|
|
34
|
+
const noTsPath = relative(srcRoot, file).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
35
|
+
if (noTsPath === "index.func") return {
|
|
36
|
+
route: "/",
|
|
37
|
+
priority: 2
|
|
38
|
+
};
|
|
39
|
+
if (noTsPath === "default.func") return {
|
|
40
|
+
route: "/*",
|
|
41
|
+
priority: 1
|
|
42
|
+
};
|
|
43
|
+
if (noTsPath.endsWith("/index.func")) return {
|
|
44
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -11)}`),
|
|
45
|
+
priority: 2
|
|
46
|
+
};
|
|
47
|
+
if (noTsPath.endsWith("/default.func")) return {
|
|
48
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -13)}/*`),
|
|
49
|
+
priority: 1
|
|
50
|
+
};
|
|
51
|
+
if (noTsPath.endsWith(".func")) return {
|
|
52
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -5)}`),
|
|
53
|
+
priority: 3
|
|
54
|
+
};
|
|
55
|
+
throw Error(`[faas types] Invalid func filename: ${file}`);
|
|
56
|
+
}
|
|
57
|
+
function toImportPath(fromFile, targetFile) {
|
|
58
|
+
const importPath = relative(dirname(fromFile), targetFile).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
59
|
+
if (importPath.startsWith(".")) return importPath;
|
|
60
|
+
return `./${importPath}`;
|
|
61
|
+
}
|
|
62
|
+
function parsePluginTypes(config) {
|
|
63
|
+
const pluginConfig = config.plugins;
|
|
64
|
+
if (!pluginConfig || typeof pluginConfig !== "object") return [];
|
|
65
|
+
const pluginTypes = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const key in pluginConfig) {
|
|
67
|
+
const data = pluginConfig[key];
|
|
68
|
+
if (typeof data === "string" && data.length) {
|
|
69
|
+
pluginTypes.add(data);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (data && typeof data === "object") {
|
|
73
|
+
if (typeof data.type === "string" && data.type.length) pluginTypes.add(data.type);
|
|
74
|
+
else pluginTypes.add(key);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
pluginTypes.add(key);
|
|
78
|
+
}
|
|
79
|
+
return Array.from(pluginTypes).sort((a, b) => a.localeCompare(b));
|
|
80
|
+
}
|
|
81
|
+
async function readFuncFiles(dir) {
|
|
82
|
+
const result = [];
|
|
83
|
+
const pendingDirs = [dir];
|
|
84
|
+
while (pendingDirs.length) {
|
|
85
|
+
const currentDir = pendingDirs.pop();
|
|
86
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.name === ".faasjs" || entry.name === "node_modules") continue;
|
|
89
|
+
const filePath = join(currentDir, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
pendingDirs.push(filePath);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (entry.isFile() && entry.name.endsWith(".func.ts")) result.push(filePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
98
|
+
}
|
|
99
|
+
function formatTypes(items) {
|
|
100
|
+
const actionLines = items.map((item) => {
|
|
101
|
+
return ` ${JSON.stringify(toTypegenRoute(item.route))}: InferFaasAction<InferFaasFunc<typeof import(${JSON.stringify(item.importPath)})>>`;
|
|
102
|
+
});
|
|
103
|
+
const eventLines = items.map((item) => {
|
|
104
|
+
const plugins = item.pluginTypes.length ? `[${item.pluginTypes.map((type) => JSON.stringify(type)).join(", ")}]` : "[]";
|
|
105
|
+
return ` ${JSON.stringify(toTypegenRoute(item.route))}: InferPluginEvent<${plugins}>`;
|
|
106
|
+
});
|
|
107
|
+
return `/**
|
|
108
|
+
* Generated by @faasjs/dev.
|
|
109
|
+
*
|
|
110
|
+
* Do not edit this file manually.
|
|
111
|
+
*/
|
|
112
|
+
import type { Func, InferPluginEvent } from '@faasjs/core'
|
|
113
|
+
import type { InferFaasAction, InferFaasFunc } from '@faasjs/types'
|
|
114
|
+
|
|
115
|
+
declare module '@faasjs/types' {
|
|
116
|
+
interface FaasActions {
|
|
117
|
+
${actionLines.length ? `${actionLines.join("\n")}\n` : ""} }
|
|
118
|
+
|
|
119
|
+
interface FaasEvents {
|
|
120
|
+
${eventLines.length ? `${eventLines.join("\n")}\n` : ""} }
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function isTypegenSourceFile(filePath) {
|
|
125
|
+
return filePath.endsWith(".func.ts") || /(^|[\\/])faas\.ya?ml$/.test(filePath);
|
|
126
|
+
}
|
|
127
|
+
async function generateFaasTypes(options = {}) {
|
|
128
|
+
const logger = options.logger ?? new Logger("FaasJs:Typegen");
|
|
129
|
+
const { root: projectRoot, staging } = resolveServerConfig(options.root ?? process.cwd(), logger);
|
|
130
|
+
const srcRoot = join(projectRoot, "src");
|
|
131
|
+
const output = join(srcRoot, ".faasjs", "types.d.ts");
|
|
132
|
+
if (!existsSync(srcRoot)) throw Error(`[faas types] Source directory not found: ${srcRoot}`);
|
|
133
|
+
const files = await readFuncFiles(srcRoot);
|
|
134
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const { route, priority } = toRoute(srcRoot, file);
|
|
137
|
+
const prev = routeMap.get(route);
|
|
138
|
+
if (prev && priority <= prev.priority) continue;
|
|
139
|
+
routeMap.set(route, {
|
|
140
|
+
route,
|
|
141
|
+
importPath: toImportPath(output, file),
|
|
142
|
+
pluginTypes: parsePluginTypes(loadConfig(srcRoot, file, staging, logger)),
|
|
143
|
+
priority
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const items = Array.from(routeMap.values()).sort((a, b) => a.route.localeCompare(b.route));
|
|
147
|
+
const content = formatTypes(items);
|
|
148
|
+
let changed = true;
|
|
149
|
+
try {
|
|
150
|
+
if (await readFile(output, "utf8") === content) changed = false;
|
|
151
|
+
} catch {}
|
|
152
|
+
if (changed) {
|
|
153
|
+
await mkdir(dirname(output), { recursive: true });
|
|
154
|
+
await writeFile(output, content);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
output,
|
|
158
|
+
changed,
|
|
159
|
+
fileCount: files.length,
|
|
160
|
+
routeCount: items.length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { isTypegenSourceFile as n, resolveServerConfig as r, generateFaasTypes as t };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
let _faasjs_node_utils = require("@faasjs/node-utils");
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
let node_fs_promises = require("node:fs/promises");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
|
|
6
|
+
//#region src/server_config.ts
|
|
7
|
+
function resolveFaasStaging() {
|
|
8
|
+
return process.env.FaasEnv || "development";
|
|
9
|
+
}
|
|
10
|
+
function resolveServerConfig(root, logger, defaultBase = "/") {
|
|
11
|
+
const projectRoot = (0, node_path.resolve)(root);
|
|
12
|
+
const staging = resolveFaasStaging();
|
|
13
|
+
const srcRoot = (0, node_path.join)(projectRoot, "src");
|
|
14
|
+
const config = (0, _faasjs_node_utils.loadConfig)(srcRoot, (0, node_path.join)(srcRoot, "index.func.ts"), staging, logger);
|
|
15
|
+
const server = config && typeof config === "object" ? config.server : void 0;
|
|
16
|
+
return {
|
|
17
|
+
root: typeof server?.root === "string" && server.root.length ? (0, node_path.resolve)(projectRoot, server.root) : projectRoot,
|
|
18
|
+
base: typeof server?.base === "string" && server.base.length ? server.base : defaultBase,
|
|
19
|
+
staging
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/typegen.ts
|
|
25
|
+
function normalizeRoute(path) {
|
|
26
|
+
const normalized = path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
27
|
+
if (!normalized.length || normalized === "/") return "/";
|
|
28
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
29
|
+
}
|
|
30
|
+
function toTypegenRoute(route) {
|
|
31
|
+
return route === "/" ? "/" : route.replace(/^\/+/, "");
|
|
32
|
+
}
|
|
33
|
+
function toRoute(srcRoot, file) {
|
|
34
|
+
const noTsPath = (0, node_path.relative)(srcRoot, file).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
35
|
+
if (noTsPath === "index.func") return {
|
|
36
|
+
route: "/",
|
|
37
|
+
priority: 2
|
|
38
|
+
};
|
|
39
|
+
if (noTsPath === "default.func") return {
|
|
40
|
+
route: "/*",
|
|
41
|
+
priority: 1
|
|
42
|
+
};
|
|
43
|
+
if (noTsPath.endsWith("/index.func")) return {
|
|
44
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -11)}`),
|
|
45
|
+
priority: 2
|
|
46
|
+
};
|
|
47
|
+
if (noTsPath.endsWith("/default.func")) return {
|
|
48
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -13)}/*`),
|
|
49
|
+
priority: 1
|
|
50
|
+
};
|
|
51
|
+
if (noTsPath.endsWith(".func")) return {
|
|
52
|
+
route: normalizeRoute(`/${noTsPath.slice(0, -5)}`),
|
|
53
|
+
priority: 3
|
|
54
|
+
};
|
|
55
|
+
throw Error(`[faas types] Invalid func filename: ${file}`);
|
|
56
|
+
}
|
|
57
|
+
function toImportPath(fromFile, targetFile) {
|
|
58
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(fromFile), targetFile).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
59
|
+
if (importPath.startsWith(".")) return importPath;
|
|
60
|
+
return `./${importPath}`;
|
|
61
|
+
}
|
|
62
|
+
function parsePluginTypes(config) {
|
|
63
|
+
const pluginConfig = config.plugins;
|
|
64
|
+
if (!pluginConfig || typeof pluginConfig !== "object") return [];
|
|
65
|
+
const pluginTypes = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const key in pluginConfig) {
|
|
67
|
+
const data = pluginConfig[key];
|
|
68
|
+
if (typeof data === "string" && data.length) {
|
|
69
|
+
pluginTypes.add(data);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (data && typeof data === "object") {
|
|
73
|
+
if (typeof data.type === "string" && data.type.length) pluginTypes.add(data.type);
|
|
74
|
+
else pluginTypes.add(key);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
pluginTypes.add(key);
|
|
78
|
+
}
|
|
79
|
+
return Array.from(pluginTypes).sort((a, b) => a.localeCompare(b));
|
|
80
|
+
}
|
|
81
|
+
async function readFuncFiles(dir) {
|
|
82
|
+
const result = [];
|
|
83
|
+
const pendingDirs = [dir];
|
|
84
|
+
while (pendingDirs.length) {
|
|
85
|
+
const currentDir = pendingDirs.pop();
|
|
86
|
+
const entries = await (0, node_fs_promises.readdir)(currentDir, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.name === ".faasjs" || entry.name === "node_modules") continue;
|
|
89
|
+
const filePath = (0, node_path.join)(currentDir, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
pendingDirs.push(filePath);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (entry.isFile() && entry.name.endsWith(".func.ts")) result.push(filePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
98
|
+
}
|
|
99
|
+
function formatTypes(items) {
|
|
100
|
+
const actionLines = items.map((item) => {
|
|
101
|
+
return ` ${JSON.stringify(toTypegenRoute(item.route))}: InferFaasAction<InferFaasFunc<typeof import(${JSON.stringify(item.importPath)})>>`;
|
|
102
|
+
});
|
|
103
|
+
const eventLines = items.map((item) => {
|
|
104
|
+
const plugins = item.pluginTypes.length ? `[${item.pluginTypes.map((type) => JSON.stringify(type)).join(", ")}]` : "[]";
|
|
105
|
+
return ` ${JSON.stringify(toTypegenRoute(item.route))}: InferPluginEvent<${plugins}>`;
|
|
106
|
+
});
|
|
107
|
+
return `/**
|
|
108
|
+
* Generated by @faasjs/dev.
|
|
109
|
+
*
|
|
110
|
+
* Do not edit this file manually.
|
|
111
|
+
*/
|
|
112
|
+
import type { Func, InferPluginEvent } from '@faasjs/core'
|
|
113
|
+
import type { InferFaasAction, InferFaasFunc } from '@faasjs/types'
|
|
114
|
+
|
|
115
|
+
declare module '@faasjs/types' {
|
|
116
|
+
interface FaasActions {
|
|
117
|
+
${actionLines.length ? `${actionLines.join("\n")}\n` : ""} }
|
|
118
|
+
|
|
119
|
+
interface FaasEvents {
|
|
120
|
+
${eventLines.length ? `${eventLines.join("\n")}\n` : ""} }
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function isTypegenSourceFile(filePath) {
|
|
125
|
+
return filePath.endsWith(".func.ts") || /(^|[\\/])faas\.ya?ml$/.test(filePath);
|
|
126
|
+
}
|
|
127
|
+
async function generateFaasTypes(options = {}) {
|
|
128
|
+
const logger = options.logger ?? new _faasjs_node_utils.Logger("FaasJs:Typegen");
|
|
129
|
+
const { root: projectRoot, staging } = resolveServerConfig(options.root ?? process.cwd(), logger);
|
|
130
|
+
const srcRoot = (0, node_path.join)(projectRoot, "src");
|
|
131
|
+
const output = (0, node_path.join)(srcRoot, ".faasjs", "types.d.ts");
|
|
132
|
+
if (!(0, node_fs.existsSync)(srcRoot)) throw Error(`[faas types] Source directory not found: ${srcRoot}`);
|
|
133
|
+
const files = await readFuncFiles(srcRoot);
|
|
134
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const { route, priority } = toRoute(srcRoot, file);
|
|
137
|
+
const prev = routeMap.get(route);
|
|
138
|
+
if (prev && priority <= prev.priority) continue;
|
|
139
|
+
routeMap.set(route, {
|
|
140
|
+
route,
|
|
141
|
+
importPath: toImportPath(output, file),
|
|
142
|
+
pluginTypes: parsePluginTypes((0, _faasjs_node_utils.loadConfig)(srcRoot, file, staging, logger)),
|
|
143
|
+
priority
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const items = Array.from(routeMap.values()).sort((a, b) => a.route.localeCompare(b.route));
|
|
147
|
+
const content = formatTypes(items);
|
|
148
|
+
let changed = true;
|
|
149
|
+
try {
|
|
150
|
+
if (await (0, node_fs_promises.readFile)(output, "utf8") === content) changed = false;
|
|
151
|
+
} catch {}
|
|
152
|
+
if (changed) {
|
|
153
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(output), { recursive: true });
|
|
154
|
+
await (0, node_fs_promises.writeFile)(output, content);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
output,
|
|
158
|
+
changed,
|
|
159
|
+
fileCount: files.length,
|
|
160
|
+
routeCount: items.length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
Object.defineProperty(exports, 'generateFaasTypes', {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
get: function () {
|
|
168
|
+
return generateFaasTypes;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
Object.defineProperty(exports, 'isTypegenSourceFile', {
|
|
172
|
+
enumerable: true,
|
|
173
|
+
get: function () {
|
|
174
|
+
return isTypegenSourceFile;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
Object.defineProperty(exports, 'resolveServerConfig', {
|
|
178
|
+
enumerable: true,
|
|
179
|
+
get: function () {
|
|
180
|
+
return resolveServerConfig;
|
|
181
|
+
}
|
|
182
|
+
});
|
package/faas.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@faasjs/dev",
|
|
3
|
+
"version": "8.0.0-beta.10",
|
|
4
|
+
"homepage": "https://faasjs.com/doc/dev",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/faasjs/faasjs/issues"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/faasjs/faasjs.git",
|
|
12
|
+
"directory": "packages/dev"
|
|
13
|
+
},
|
|
14
|
+
"funding": "https://github.com/sponsors/faasjs",
|
|
15
|
+
"bin": {
|
|
16
|
+
"faas": "faas.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"faas.mjs",
|
|
21
|
+
"configs"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "dist/index.cjs",
|
|
25
|
+
"module": "dist/index.mjs",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown --entry src/index.ts --entry src/cli/index.ts --config ../../tsdown.config.ts"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@faasjs/core": ">=8.0.0-beta.10",
|
|
39
|
+
"@faasjs/node-utils": ">=8.0.0-beta.10",
|
|
40
|
+
"@types/node": "*",
|
|
41
|
+
"knex": "*",
|
|
42
|
+
"oxlint": "*",
|
|
43
|
+
"vite": "*",
|
|
44
|
+
"vitest": "*"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@faasjs/core": ">=8.0.0-beta.10",
|
|
48
|
+
"@faasjs/node-utils": ">=8.0.0-beta.10",
|
|
49
|
+
"@types/node": "*",
|
|
50
|
+
"knex": "*",
|
|
51
|
+
"oxlint": "*",
|
|
52
|
+
"vite": "*",
|
|
53
|
+
"vitest": "*"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=24.0.0",
|
|
57
|
+
"npm": ">=11.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|