@avleon/cli 0.0.47 → 0.4.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 +21 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +10 -0
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +65 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +96 -0
- package/dist/commands/new.d.ts +19 -0
- package/dist/commands/new.js +306 -0
- package/dist/commands/route-list.d.ts +19 -0
- package/dist/commands/route-list.js +160 -0
- package/dist/commands/serve.d.ts +17 -0
- package/dist/commands/serve.js +238 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/start.js +43 -0
- package/dist/compiler.d.ts +24 -0
- package/dist/compiler.js +83 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +16 -0
- package/dist/generators/make.d.ts +24 -0
- package/dist/generators/make.js +157 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +40 -15
- package/dist/program.d.ts +22 -0
- package/dist/program.js +178 -0
- package/dist/utils/entry.d.ts +15 -0
- package/dist/utils/entry.js +55 -0
- package/dist/utils/env-types.d.ts +8 -0
- package/dist/utils/env-types.js +19 -0
- package/dist/utils/log.d.ts +25 -0
- package/dist/utils/log.js +39 -0
- package/dist/utils/names.d.ts +27 -0
- package/dist/utils/names.js +58 -0
- package/dist/utils/process.d.ts +18 -0
- package/dist/utils/process.js +84 -0
- package/dist/utils/project.d.ts +17 -0
- package/dist/utils/project.js +91 -0
- package/dist/utils/stubs.d.ts +15 -0
- package/dist/utils/stubs.js +52 -0
- package/dist/utils/tsconfig.d.ts +13 -0
- package/dist/utils/tsconfig.js +102 -0
- package/package.json +28 -27
- package/readme.md +50 -11
- package/stubs/config/config.stub +9 -0
- package/stubs/controllers/basic-controller.stub +3 -3
- package/stubs/controllers/resource-controller.stub +45 -0
- package/stubs/controllers/rest-controller.stub +2 -2
- package/stubs/dtos/dto.stub +11 -0
- package/stubs/middlewares/middleware.stub +9 -0
- package/stubs/models/basic-model.stub +2 -2
- package/stubs/models/entity-model.stub +6 -8
- package/stubs/{app → project}/app-minimal.stub +1 -1
- package/stubs/{app/app-controller.stub → project/app.stub} +2 -7
- package/stubs/project/env.stub +3 -0
- package/stubs/project/gitignore.stub +5 -0
- package/stubs/project/home.controller.spec.stub +19 -0
- package/stubs/project/home.controller.stub +12 -0
- package/stubs/project/home.service.stub +8 -0
- package/stubs/{config.stub → project/openapi.config.stub} +4 -4
- package/stubs/project/readme.stub +26 -0
- package/stubs/{serve.stub → project/serve.stub} +1 -1
- package/stubs/project/tsconfig.build.stub +4 -0
- package/stubs/project/tsconfig.stub +18 -0
- package/stubs/project/vitest-e2e.config.stub +10 -0
- package/stubs/project/vitest.config.stub +17 -0
- package/stubs/services/basic-service.stub +1 -1
- package/stubs/services/orm-rest-service.stub +15 -14
- package/stubs/services/rest-service.stub +9 -9
- package/dist/av-cli.js +0 -60
- package/dist/avcli.js +0 -26
- package/dist/cli.interface.js +0 -2
- package/dist/cli.js +0 -332
- package/dist/commands.js +0 -63
- package/dist/controller.js +0 -96
- package/dist/create.js +0 -512
- package/dist/helpers/file-helper.js +0 -30
- package/dist/helpers/path-helper.js +0 -36
- package/dist/paths.js +0 -32
- package/dist/scripts/serve.js +0 -290
- package/dist/serve.js +0 -311
- package/dist/service.js +0 -69
- package/stubs/controllers/orm-rest-controller.stub +0 -45
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.joinRoute = joinRoute;
|
|
7
|
+
exports.runtimeRoutes = runtimeRoutes;
|
|
8
|
+
exports.scanRoutes = scanRoutes;
|
|
9
|
+
exports.staticRoutes = staticRoutes;
|
|
10
|
+
exports.formatRoutes = formatRoutes;
|
|
11
|
+
exports.routeList = routeList;
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const compiler_1 = require("../compiler");
|
|
16
|
+
const errors_1 = require("../errors");
|
|
17
|
+
const entry_1 = require("../utils/entry");
|
|
18
|
+
const log_1 = require("../utils/log");
|
|
19
|
+
const project_1 = require("../utils/project");
|
|
20
|
+
function joinRoute(...parts) {
|
|
21
|
+
const joined = "/" + parts.join("/");
|
|
22
|
+
const normalized = joined.replace(/\/{2,}/g, "/");
|
|
23
|
+
return normalized.length > 1 ? normalized.replace(/\/$/, "") : normalized;
|
|
24
|
+
}
|
|
25
|
+
function controllerFiles(srcDir) {
|
|
26
|
+
if (!fs_1.default.existsSync(srcDir))
|
|
27
|
+
return [];
|
|
28
|
+
return fs_1.default
|
|
29
|
+
.readdirSync(srcDir, { withFileTypes: true, recursive: true })
|
|
30
|
+
.filter((e) => e.isFile() && e.name.endsWith(".controller.ts"))
|
|
31
|
+
.map((e) => path_1.default.join(e.parentPath ?? e.path, e.name))
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
// ── Runtime: compile, require, read Avleon's route metadata ───────────────
|
|
35
|
+
const MARKER = "__AVLEON_ROUTES__";
|
|
36
|
+
/**
|
|
37
|
+
* Runs in a child process (so the app's import side effects cannot affect the
|
|
38
|
+
* CLI). Requires each compiled controller and reads the metadata written by
|
|
39
|
+
* `@ApiController` (symbol "icontroller:options") and `@Get`/`@Post`/...
|
|
40
|
+
* ("route:method", "route:path").
|
|
41
|
+
*/
|
|
42
|
+
const WORKER = `
|
|
43
|
+
const files = JSON.parse(process.argv[process.argv.length - 1]);
|
|
44
|
+
const out = [];
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const [src, compiled] of files) {
|
|
47
|
+
let mod;
|
|
48
|
+
try { mod = require(compiled); } catch (e) { errors.push(src + ": " + (e && e.message || e)); continue; }
|
|
49
|
+
if (typeof Reflect.getOwnMetadataKeys !== "function") {
|
|
50
|
+
errors.push("reflect-metadata is not loaded; is @avleon/core installed?");
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
for (const value of Object.values(mod)) {
|
|
54
|
+
if (typeof value !== "function") continue;
|
|
55
|
+
const key = Reflect.getOwnMetadataKeys(value).find(
|
|
56
|
+
(k) => typeof k === "symbol" && k.description === "icontroller:options");
|
|
57
|
+
if (!key) continue;
|
|
58
|
+
const meta = Reflect.getOwnMetadata(key, value) || {};
|
|
59
|
+
const proto = value.prototype;
|
|
60
|
+
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
61
|
+
if (name === "constructor") continue;
|
|
62
|
+
const method = Reflect.getMetadata("route:method", proto, name);
|
|
63
|
+
if (!method) continue;
|
|
64
|
+
out.push({ controllerPath: meta.path || "/", method, path: Reflect.getMetadata("route:path", proto, name) || "/",
|
|
65
|
+
controller: value.name, handler: name, file: src });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log(${JSON.stringify(MARKER)} + JSON.stringify({ routes: out, errors }));
|
|
70
|
+
process.exit(0);
|
|
71
|
+
`;
|
|
72
|
+
function runtimeRoutes(root) {
|
|
73
|
+
const layout = (0, entry_1.projectLayout)(root);
|
|
74
|
+
const cacheDir = path_1.default.join(root, "node_modules", ".cache", "avleon", "route-list");
|
|
75
|
+
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
76
|
+
const compiler = new compiler_1.SwcCompiler(layout.srcDir, cacheDir, layout.compilerOptions);
|
|
77
|
+
const failures = compiler.compileAll();
|
|
78
|
+
if (failures.size) {
|
|
79
|
+
const list = [...failures].map(([f, e]) => `${path_1.default.relative(root, f)}: ${e.split("\n")[0]}`).join("\n ");
|
|
80
|
+
throw new errors_1.CliError(`Compilation failed:\n ${list}`);
|
|
81
|
+
}
|
|
82
|
+
const files = controllerFiles(layout.srcDir).map((f) => [path_1.default.relative(root, f), compiler.outFileFor(f)]);
|
|
83
|
+
if (!files.length)
|
|
84
|
+
return [];
|
|
85
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, ["-e", WORKER, JSON.stringify(files)], {
|
|
86
|
+
cwd: root,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "development" },
|
|
89
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
90
|
+
});
|
|
91
|
+
const line = (result.stdout ?? "").split(/\r?\n/).find((l) => l.startsWith(MARKER));
|
|
92
|
+
if (!line) {
|
|
93
|
+
throw new errors_1.CliError(`Could not load controllers:\n${result.stderr || result.stdout || result.error?.message}`);
|
|
94
|
+
}
|
|
95
|
+
const { routes, errors } = JSON.parse(line.slice(MARKER.length));
|
|
96
|
+
if (errors.length) {
|
|
97
|
+
throw new errors_1.CliError(`Could not load controllers:\n ${errors.join("\n ")}\nTry --static to scan the source instead.`);
|
|
98
|
+
}
|
|
99
|
+
return routes.map((r) => ({
|
|
100
|
+
method: String(r.method).toUpperCase(),
|
|
101
|
+
path: joinRoute(r.controllerPath, r.path),
|
|
102
|
+
controller: r.controller,
|
|
103
|
+
handler: r.handler,
|
|
104
|
+
file: r.file.split(path_1.default.sep).join("/"),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
// ── Static: scan decorators in the source ─────────────────────────────────
|
|
108
|
+
const STRING = /^\s*(['"`])((?:(?!\1).)*)\1/;
|
|
109
|
+
function decoratorPath(args) {
|
|
110
|
+
if (!args)
|
|
111
|
+
return "/";
|
|
112
|
+
const literal = args.match(STRING);
|
|
113
|
+
if (literal)
|
|
114
|
+
return literal[2] || "/";
|
|
115
|
+
const objPath = args.match(/path\s*:\s*(['"`])((?:(?!\1).)*)\1/);
|
|
116
|
+
return objPath ? objPath[2] : "/";
|
|
117
|
+
}
|
|
118
|
+
/** Scan `@ApiController` classes and their `@Get`/`@Post`/... methods without running any code. */
|
|
119
|
+
function scanRoutes(source, file) {
|
|
120
|
+
const routes = [];
|
|
121
|
+
const ctrlRe = /@ApiController\b(?:\s*\(([^)]*)\))?[\s\S]*?\bclass\s+([A-Za-z_$][\w$]*)/g;
|
|
122
|
+
const ctrls = [...source.matchAll(ctrlRe)];
|
|
123
|
+
ctrls.forEach((m, i) => {
|
|
124
|
+
const start = m.index + m[0].length;
|
|
125
|
+
const end = i + 1 < ctrls.length ? ctrls[i + 1].index : source.length;
|
|
126
|
+
const body = source.slice(start, end);
|
|
127
|
+
const base = decoratorPath(m[1]);
|
|
128
|
+
const methodRe = /@(Get|Post|Put|Patch|Delete|Options|All)\s*\(([^)]*)\)[\s\S]*?(?<![@\w$])([A-Za-z_$][\w$]*)\s*\(/g;
|
|
129
|
+
for (const r of body.matchAll(methodRe)) {
|
|
130
|
+
routes.push({
|
|
131
|
+
method: r[1].toUpperCase(),
|
|
132
|
+
path: joinRoute(base, decoratorPath(r[2])),
|
|
133
|
+
controller: m[2],
|
|
134
|
+
handler: r[3],
|
|
135
|
+
file,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return routes;
|
|
140
|
+
}
|
|
141
|
+
function staticRoutes(root) {
|
|
142
|
+
const layout = (0, entry_1.projectLayout)(root);
|
|
143
|
+
return controllerFiles(layout.srcDir).flatMap((f) => scanRoutes(fs_1.default.readFileSync(f, "utf8"), path_1.default.relative(root, f).split(path_1.default.sep).join("/")));
|
|
144
|
+
}
|
|
145
|
+
// ── Command ───────────────────────────────────────────────────────────────
|
|
146
|
+
function formatRoutes(routes) {
|
|
147
|
+
if (!routes.length)
|
|
148
|
+
return "No routes found.";
|
|
149
|
+
const rows = routes.map((r) => [r.method, r.path, `${r.controller}.${r.handler}`]);
|
|
150
|
+
const head = ["METHOD", "PATH", "HANDLER"];
|
|
151
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
152
|
+
const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
|
|
153
|
+
return [log_1.c.bold(fmt(head)), ...rows.map(fmt)].join("\n");
|
|
154
|
+
}
|
|
155
|
+
function routeList(opts = {}) {
|
|
156
|
+
const root = (0, project_1.requireProjectRoot)(opts.cwd ?? process.cwd());
|
|
157
|
+
const routes = (opts.static ? staticRoutes(root) : runtimeRoutes(root)).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
158
|
+
console.log(opts.json ? JSON.stringify(routes, null, 2) : formatRoutes(routes));
|
|
159
|
+
return routes;
|
|
160
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ServeOptions = {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
/** Entry file (relative to the project root), overriding package.json. */
|
|
4
|
+
entry?: string;
|
|
5
|
+
/** Run `tsc --noEmit --watch` alongside the server. Default true. */
|
|
6
|
+
typeCheck?: boolean;
|
|
7
|
+
/** Milliseconds to wait for more file events before rebuilding. Default 200. */
|
|
8
|
+
debounce?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Development server: transpiles `src/` with SWC into the output dir, copies
|
|
12
|
+
* assets, runs the entry file, and restarts it on changes to sources, assets
|
|
13
|
+
* or `.env*` files. Type errors are reported by a parallel `tsc --watch` and
|
|
14
|
+
* never block a reload; syntax errors do, and stop the running app so stale
|
|
15
|
+
* JS never keeps serving.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serve(opts?: ServeOptions): Promise<void>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.serve = serve;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const compiler_1 = require("../compiler");
|
|
11
|
+
const errors_1 = require("../errors");
|
|
12
|
+
const entry_1 = require("../utils/entry");
|
|
13
|
+
const log_1 = require("../utils/log");
|
|
14
|
+
const process_1 = require("../utils/process");
|
|
15
|
+
const project_1 = require("../utils/project");
|
|
16
|
+
const logServe = (0, log_1.tagged)("SERVE", log_1.c.cyan);
|
|
17
|
+
const logApp = (0, log_1.tagged)("AVLEON", log_1.c.green);
|
|
18
|
+
const logWatch = (0, log_1.tagged)("WATCH", log_1.c.yellow);
|
|
19
|
+
const logErr = (category) => (0, log_1.tagged)(category, log_1.c.red);
|
|
20
|
+
/**
|
|
21
|
+
* Development server: transpiles `src/` with SWC into the output dir, copies
|
|
22
|
+
* assets, runs the entry file, and restarts it on changes to sources, assets
|
|
23
|
+
* or `.env*` files. Type errors are reported by a parallel `tsc --watch` and
|
|
24
|
+
* never block a reload; syntax errors do, and stop the running app so stale
|
|
25
|
+
* JS never keeps serving.
|
|
26
|
+
*/
|
|
27
|
+
async function serve(opts = {}) {
|
|
28
|
+
const root = (0, project_1.requireProjectRoot)(opts.cwd ?? process.cwd());
|
|
29
|
+
const layout = (0, entry_1.projectLayout)(root);
|
|
30
|
+
const pkg = (0, project_1.readPackageJson)(root);
|
|
31
|
+
if (!fs_1.default.existsSync(layout.srcDir))
|
|
32
|
+
throw new errors_1.CliError(`Source directory ${layout.srcDir} does not exist.`);
|
|
33
|
+
const compiler = new compiler_1.SwcCompiler(layout.srcDir, layout.outDir, layout.compilerOptions);
|
|
34
|
+
const candidates = (0, entry_1.entryCandidates)(layout, pkg, opts.entry);
|
|
35
|
+
const debounceMs = opts.debounce ?? 200;
|
|
36
|
+
const failures = new Map();
|
|
37
|
+
let app = null;
|
|
38
|
+
let stopping = false;
|
|
39
|
+
let typeChecker = null;
|
|
40
|
+
const watchers = [];
|
|
41
|
+
console.log(log_1.c.bold(log_1.c.cyan("Avleon development server")));
|
|
42
|
+
logServe(`Project: ${root}`);
|
|
43
|
+
// ── Initial build ───────────────────────────────────────────────────────
|
|
44
|
+
const started = Date.now();
|
|
45
|
+
for (const [file, error] of compiler.compileAll())
|
|
46
|
+
failures.set(file, error);
|
|
47
|
+
(0, project_1.copyAssets)(layout.srcDir, layout.outDir);
|
|
48
|
+
logServe(`Compiled in ${Date.now() - started}ms`);
|
|
49
|
+
// ── App process ─────────────────────────────────────────────────────────
|
|
50
|
+
async function stopApp() {
|
|
51
|
+
if (!app)
|
|
52
|
+
return;
|
|
53
|
+
const child = app;
|
|
54
|
+
app = null;
|
|
55
|
+
stopping = true;
|
|
56
|
+
await (0, process_1.killTree)(child);
|
|
57
|
+
stopping = false;
|
|
58
|
+
}
|
|
59
|
+
function startApp() {
|
|
60
|
+
const entry = (0, entry_1.resolveEntry)(candidates, opts.entry);
|
|
61
|
+
if (!entry) {
|
|
62
|
+
logErr("AVLEON")(`No entry file found. Tried:\n ${candidates.map((p) => path_1.default.relative(root, p)).join("\n ")}\n` +
|
|
63
|
+
`Set "avleon": { "entry": "dist/serve.js" } or "main" in package.json, or pass --entry.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
logApp(`Starting ${path_1.default.relative(root, entry)}`);
|
|
67
|
+
const child = (0, child_process_1.spawn)(process.execPath, ["--enable-source-maps", entry], {
|
|
68
|
+
cwd: root,
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "development" },
|
|
71
|
+
...process_1.treeSpawnOptions,
|
|
72
|
+
});
|
|
73
|
+
app = child;
|
|
74
|
+
child.on("exit", (code, signal) => {
|
|
75
|
+
if (app === child)
|
|
76
|
+
app = null;
|
|
77
|
+
if (stopping)
|
|
78
|
+
return;
|
|
79
|
+
if (code)
|
|
80
|
+
logErr("AVLEON")(`Process exited with code ${code}. Waiting for changes...`);
|
|
81
|
+
else if (signal)
|
|
82
|
+
logApp(`Process terminated by ${signal}. Waiting for changes...`);
|
|
83
|
+
});
|
|
84
|
+
child.on("error", (err) => logErr("AVLEON")(`Failed to start: ${err.message}`));
|
|
85
|
+
}
|
|
86
|
+
function reportFailures() {
|
|
87
|
+
for (const [file, error] of failures) {
|
|
88
|
+
logErr("COMPILE")(`${path_1.default.relative(root, file)}\n${error}`);
|
|
89
|
+
}
|
|
90
|
+
logErr("COMPILE")(`${failures.size} file(s) failed to compile. The app is stopped until they are fixed.`);
|
|
91
|
+
}
|
|
92
|
+
// Serialize restarts so overlapping events never race.
|
|
93
|
+
let chain = Promise.resolve();
|
|
94
|
+
const enqueue = (task) => {
|
|
95
|
+
chain = chain.then(task).catch((err) => logErr("SERVE")(String(err?.stack ?? err)));
|
|
96
|
+
return chain;
|
|
97
|
+
};
|
|
98
|
+
async function reload() {
|
|
99
|
+
await stopApp();
|
|
100
|
+
if (failures.size)
|
|
101
|
+
return reportFailures();
|
|
102
|
+
startApp();
|
|
103
|
+
}
|
|
104
|
+
// ── Change handling (debounced) ─────────────────────────────────────────
|
|
105
|
+
const pending = new Map();
|
|
106
|
+
let timer = null;
|
|
107
|
+
function onEvent(event, file) {
|
|
108
|
+
pending.set(file, event);
|
|
109
|
+
if (timer)
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
timer = setTimeout(() => {
|
|
112
|
+
timer = null;
|
|
113
|
+
const batch = new Map(pending);
|
|
114
|
+
pending.clear();
|
|
115
|
+
enqueue(() => applyBatch(batch));
|
|
116
|
+
}, debounceMs);
|
|
117
|
+
}
|
|
118
|
+
const outPath = (file) => path_1.default.join(layout.outDir, path_1.default.relative(layout.srcDir, file));
|
|
119
|
+
const inSrc = (file) => {
|
|
120
|
+
const rel = path_1.default.relative(layout.srcDir, file);
|
|
121
|
+
return !!rel && !rel.startsWith("..") && !path_1.default.isAbsolute(rel);
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Mirror one file event into the output dir. Returns true when the app
|
|
125
|
+
* should restart; test files, `.d.ts` and dotfiles are ignored.
|
|
126
|
+
*/
|
|
127
|
+
function applyChange(event, file) {
|
|
128
|
+
if (path_1.default.dirname(file) === root && path_1.default.basename(file).startsWith(".env"))
|
|
129
|
+
return true;
|
|
130
|
+
if (!inSrc(file))
|
|
131
|
+
return false;
|
|
132
|
+
if (event === "unlinkDir") {
|
|
133
|
+
fs_1.default.rmSync(outPath(file), { recursive: true, force: true });
|
|
134
|
+
for (const f of failures.keys())
|
|
135
|
+
if (f.startsWith(file + path_1.default.sep))
|
|
136
|
+
failures.delete(f);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (event === "addDir")
|
|
140
|
+
return false;
|
|
141
|
+
if ((0, compiler_1.isTsSource)(file)) {
|
|
142
|
+
if ((0, compiler_1.isTestFile)(file))
|
|
143
|
+
return false;
|
|
144
|
+
if (event === "unlink") {
|
|
145
|
+
fs_1.default.rmSync(compiler.outFileFor(file), { force: true });
|
|
146
|
+
failures.delete(file);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const result = compiler.compileFile(file);
|
|
150
|
+
if (result.ok)
|
|
151
|
+
failures.delete(file);
|
|
152
|
+
else
|
|
153
|
+
failures.set(file, result.error);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (!(0, project_1.isAsset)(file))
|
|
157
|
+
return false;
|
|
158
|
+
if (event === "unlink") {
|
|
159
|
+
fs_1.default.rmSync(outPath(file), { force: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
fs_1.default.mkdirSync(path_1.default.dirname(outPath(file)), { recursive: true });
|
|
163
|
+
fs_1.default.copyFileSync(file, outPath(file));
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
async function applyBatch(batch) {
|
|
168
|
+
let relevant = false;
|
|
169
|
+
for (const [file, event] of batch) {
|
|
170
|
+
if (applyChange(event, file)) {
|
|
171
|
+
relevant = true;
|
|
172
|
+
logWatch(`${event} ${path_1.default.relative(root, file)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (relevant)
|
|
176
|
+
await reload();
|
|
177
|
+
}
|
|
178
|
+
// ── Watchers ────────────────────────────────────────────────────────────
|
|
179
|
+
const chokidar = require("chokidar");
|
|
180
|
+
const srcWatcher = chokidar.watch(layout.srcDir, {
|
|
181
|
+
ignoreInitial: true,
|
|
182
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
183
|
+
});
|
|
184
|
+
const envWatcher = chokidar.watch(root, {
|
|
185
|
+
ignoreInitial: true,
|
|
186
|
+
depth: 0,
|
|
187
|
+
ignored: (p) => p !== root && !path_1.default.basename(p).startsWith(".env"),
|
|
188
|
+
});
|
|
189
|
+
for (const w of [srcWatcher, envWatcher]) {
|
|
190
|
+
w.on("all", (event, file) => onEvent(event, file));
|
|
191
|
+
w.on("error", (err) => logErr("WATCH")(String(err?.message ?? err)));
|
|
192
|
+
watchers.push(w);
|
|
193
|
+
}
|
|
194
|
+
// ── Type checking ───────────────────────────────────────────────────────
|
|
195
|
+
if (opts.typeCheck !== false) {
|
|
196
|
+
const tsc = (0, process_1.findTsc)(root);
|
|
197
|
+
if (!tsc || !layout.tsconfig) {
|
|
198
|
+
logServe(log_1.c.yellow("Type checking disabled: TypeScript or tsconfig.json not found in the project."));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
typeChecker = (0, child_process_1.spawn)(process.execPath, [tsc, "--noEmit", "--watch", "--preserveWatchOutput", "--pretty", "-p", layout.tsconfig], { cwd: root, stdio: ["ignore", "pipe", "pipe"], ...process_1.treeSpawnOptions });
|
|
202
|
+
const forward = (data) => {
|
|
203
|
+
for (const line of data.toString().split(/\r?\n/)) {
|
|
204
|
+
// Skip tsc's per-cycle "Starting compilation" / "File change detected" chatter.
|
|
205
|
+
if (!line.trim() || /Starting (incremental )?compilation|File change detected/.test(line))
|
|
206
|
+
continue;
|
|
207
|
+
console.log(`${log_1.c.dim("[TSC]")} ${/Found 0 errors/.test(line) ? log_1.c.green(line) : line}`);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
typeChecker.stdout?.on("data", forward);
|
|
211
|
+
typeChecker.stderr?.on("data", forward);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ── Shutdown ────────────────────────────────────────────────────────────
|
|
215
|
+
let shuttingDown = false;
|
|
216
|
+
const shutdown = async (signal) => {
|
|
217
|
+
if (shuttingDown)
|
|
218
|
+
return;
|
|
219
|
+
shuttingDown = true;
|
|
220
|
+
logServe(`Received ${signal}, shutting down...`);
|
|
221
|
+
if (timer)
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
await Promise.allSettled(watchers.map((w) => w.close()));
|
|
224
|
+
await Promise.allSettled([stopApp(), (0, process_1.killTree)(typeChecker, 1000)]);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
};
|
|
227
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
228
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
229
|
+
if (process.platform === "win32")
|
|
230
|
+
process.on("SIGBREAK", () => void shutdown("SIGBREAK"));
|
|
231
|
+
if (failures.size)
|
|
232
|
+
reportFailures();
|
|
233
|
+
else
|
|
234
|
+
startApp();
|
|
235
|
+
logWatch("Watching for changes. Press Ctrl+C to stop.");
|
|
236
|
+
// Keep the promise pending for the lifetime of the server.
|
|
237
|
+
await new Promise(() => { });
|
|
238
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type StartOptions = {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
entry?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Run the compiled app from the output dir in production mode
|
|
7
|
+
* (`NODE_ENV=production` unless already set). Resolves to the app's exit code.
|
|
8
|
+
* SIGINT/SIGTERM are forwarded so the app can shut down gracefully.
|
|
9
|
+
*/
|
|
10
|
+
export declare function start(opts?: StartOptions): Promise<number>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.start = start;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const errors_1 = require("../errors");
|
|
10
|
+
const entry_1 = require("../utils/entry");
|
|
11
|
+
const log_1 = require("../utils/log");
|
|
12
|
+
const project_1 = require("../utils/project");
|
|
13
|
+
/**
|
|
14
|
+
* Run the compiled app from the output dir in production mode
|
|
15
|
+
* (`NODE_ENV=production` unless already set). Resolves to the app's exit code.
|
|
16
|
+
* SIGINT/SIGTERM are forwarded so the app can shut down gracefully.
|
|
17
|
+
*/
|
|
18
|
+
async function start(opts = {}) {
|
|
19
|
+
const root = (0, project_1.requireProjectRoot)(opts.cwd ?? process.cwd());
|
|
20
|
+
const layout = (0, entry_1.projectLayout)(root, true);
|
|
21
|
+
const candidates = (0, entry_1.entryCandidates)(layout, (0, project_1.readPackageJson)(root), opts.entry);
|
|
22
|
+
const entry = (0, entry_1.resolveEntry)(candidates, opts.entry);
|
|
23
|
+
if (!entry) {
|
|
24
|
+
throw new errors_1.CliError(`No compiled entry file found (tried ${candidates.map((p) => path_1.default.relative(root, p)).join(", ")}). ` +
|
|
25
|
+
"Run `avleon build` first, or pass --entry.");
|
|
26
|
+
}
|
|
27
|
+
const env = { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "production" };
|
|
28
|
+
log_1.log.info(`Starting ${path_1.default.relative(root, entry)} (NODE_ENV=${env.NODE_ENV})`);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = (0, child_process_1.spawn)(process.execPath, ["--enable-source-maps", entry], { cwd: root, env, stdio: "inherit" });
|
|
31
|
+
const forward = (signal) => () => child.kill(signal);
|
|
32
|
+
const onInt = forward("SIGINT");
|
|
33
|
+
const onTerm = forward("SIGTERM");
|
|
34
|
+
process.on("SIGINT", onInt);
|
|
35
|
+
process.on("SIGTERM", onTerm);
|
|
36
|
+
child.on("error", reject);
|
|
37
|
+
child.on("exit", (code, signal) => {
|
|
38
|
+
process.off("SIGINT", onInt);
|
|
39
|
+
process.off("SIGTERM", onTerm);
|
|
40
|
+
resolve(code ?? (signal === "SIGINT" || signal === "SIGTERM" ? 0 : 1));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CompileResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
} | {
|
|
4
|
+
ok: false;
|
|
5
|
+
error: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function isTestFile(file: string): boolean;
|
|
8
|
+
export declare function isTsSource(file: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Fast per-file TypeScript -> CommonJS transpiler (SWC). It does not type-check;
|
|
11
|
+
* `serve` runs `tsc --noEmit` alongside it for that. tsconfig `paths` are
|
|
12
|
+
* rewritten to relative imports at compile time, so no runtime hook is needed.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SwcCompiler {
|
|
15
|
+
readonly srcDir: string;
|
|
16
|
+
readonly outDir: string;
|
|
17
|
+
private readonly options;
|
|
18
|
+
constructor(srcDir: string, outDir: string, compilerOptions?: Record<string, any>);
|
|
19
|
+
outFileFor(file: string): string;
|
|
20
|
+
compileFile(file: string): CompileResult;
|
|
21
|
+
/** All non-test `.ts` sources under `srcDir`. */
|
|
22
|
+
sources(): string[];
|
|
23
|
+
compileAll(): Map<string, string>;
|
|
24
|
+
}
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SwcCompiler = void 0;
|
|
7
|
+
exports.isTestFile = isTestFile;
|
|
8
|
+
exports.isTsSource = isTsSource;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
function isTestFile(file) {
|
|
12
|
+
return /\.(test|spec)\.[cm]?[tj]s$/.test(path_1.default.basename(file));
|
|
13
|
+
}
|
|
14
|
+
function isTsSource(file) {
|
|
15
|
+
return /\.ts$/.test(file) && !/\.d\.ts$/.test(file);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fast per-file TypeScript -> CommonJS transpiler (SWC). It does not type-check;
|
|
19
|
+
* `serve` runs `tsc --noEmit` alongside it for that. tsconfig `paths` are
|
|
20
|
+
* rewritten to relative imports at compile time, so no runtime hook is needed.
|
|
21
|
+
*/
|
|
22
|
+
class SwcCompiler {
|
|
23
|
+
srcDir;
|
|
24
|
+
outDir;
|
|
25
|
+
options;
|
|
26
|
+
constructor(srcDir, outDir, compilerOptions = {}) {
|
|
27
|
+
this.srcDir = srcDir;
|
|
28
|
+
this.outDir = outDir;
|
|
29
|
+
const baseUrl = compilerOptions.baseUrl ?? compilerOptions.pathsBasePath;
|
|
30
|
+
const paths = compilerOptions.paths;
|
|
31
|
+
this.options = {
|
|
32
|
+
swcrc: false,
|
|
33
|
+
configFile: false,
|
|
34
|
+
sourceMaps: "inline",
|
|
35
|
+
module: { type: "commonjs" },
|
|
36
|
+
jsc: {
|
|
37
|
+
parser: { syntax: "typescript", decorators: true },
|
|
38
|
+
target: "es2022",
|
|
39
|
+
keepClassNames: true,
|
|
40
|
+
transform: { legacyDecorator: true, decoratorMetadata: true },
|
|
41
|
+
...(paths && baseUrl ? { baseUrl, paths } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
outFileFor(file) {
|
|
46
|
+
return path_1.default.join(this.outDir, path_1.default.relative(this.srcDir, file)).replace(/\.ts$/, ".js");
|
|
47
|
+
}
|
|
48
|
+
compileFile(file) {
|
|
49
|
+
try {
|
|
50
|
+
// Loaded lazily: the native binding is only needed by serve and route:list.
|
|
51
|
+
const { transformFileSync } = require("@swc/core");
|
|
52
|
+
const result = transformFileSync(file, { ...this.options, filename: file });
|
|
53
|
+
const out = this.outFileFor(file);
|
|
54
|
+
fs_1.default.mkdirSync(path_1.default.dirname(out), { recursive: true });
|
|
55
|
+
fs_1.default.writeFileSync(out, result.code, "utf8");
|
|
56
|
+
return { ok: true };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// Never leave stale JS behind for a file that no longer compiles.
|
|
60
|
+
fs_1.default.rmSync(this.outFileFor(file), { force: true });
|
|
61
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** All non-test `.ts` sources under `srcDir`. */
|
|
65
|
+
sources() {
|
|
66
|
+
if (!fs_1.default.existsSync(this.srcDir))
|
|
67
|
+
return [];
|
|
68
|
+
return fs_1.default
|
|
69
|
+
.readdirSync(this.srcDir, { withFileTypes: true, recursive: true })
|
|
70
|
+
.filter((e) => e.isFile() && isTsSource(e.name) && !isTestFile(e.name))
|
|
71
|
+
.map((e) => path_1.default.join(e.parentPath ?? e.path, e.name));
|
|
72
|
+
}
|
|
73
|
+
compileAll() {
|
|
74
|
+
const failures = new Map();
|
|
75
|
+
for (const file of this.sources()) {
|
|
76
|
+
const r = this.compileFile(file);
|
|
77
|
+
if (!r.ok)
|
|
78
|
+
failures.set(file, r.error);
|
|
79
|
+
}
|
|
80
|
+
return failures;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.SwcCompiler = SwcCompiler;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An expected failure: printed as a one-line message (no stack trace) and
|
|
3
|
+
* turned into a non-zero exit code by the bin entry.
|
|
4
|
+
*/
|
|
5
|
+
export declare class CliError extends Error {
|
|
6
|
+
readonly exitCode: number;
|
|
7
|
+
constructor(message: string, exitCode?: number);
|
|
8
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CliError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* An expected failure: printed as a one-line message (no stack trace) and
|
|
6
|
+
* turned into a non-zero exit code by the bin entry.
|
|
7
|
+
*/
|
|
8
|
+
class CliError extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
constructor(message, exitCode = 1) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
this.name = "CliError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.CliError = CliError;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type MakeBaseOptions = {
|
|
2
|
+
/** Directory to start the project-root search from. Defaults to `process.cwd()`. */
|
|
3
|
+
cwd?: string;
|
|
4
|
+
force?: boolean;
|
|
5
|
+
/** Folder under `src/` to write to instead of the default one. */
|
|
6
|
+
root?: string;
|
|
7
|
+
};
|
|
8
|
+
export type MakeModelOptions = MakeBaseOptions & {
|
|
9
|
+
orm?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type MakeServiceOptions = MakeBaseOptions & {
|
|
12
|
+
resource?: boolean;
|
|
13
|
+
model?: string;
|
|
14
|
+
orm?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type MakeControllerOptions = MakeServiceOptions & {
|
|
17
|
+
basePath?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function makeModel(name: string, opts?: MakeModelOptions): string;
|
|
20
|
+
export declare function makeService(name: string, opts?: MakeServiceOptions): string;
|
|
21
|
+
export declare function makeController(name: string, opts?: MakeControllerOptions): string;
|
|
22
|
+
export declare function makeConfig(name: string, opts?: MakeBaseOptions): string;
|
|
23
|
+
export declare function makeMiddleware(name: string, opts?: MakeBaseOptions): string;
|
|
24
|
+
export declare function makeDto(name: string, opts?: MakeBaseOptions): string;
|