@lark-apaas/fullstack-cli 1.1.59-alpha.2 → 1.1.59-alpha.20260717162925
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +557 -12
- package/package.json +4 -1
- package/templates/nest-cli.json +5 -1
- package/templates/scripts/dev.js +228 -34
- package/templates/scripts/preview-startup-timing.cjs +278 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs30 from "fs";
|
|
3
|
+
import path26 from "path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5
5
|
import { config as dotenvConfig } from "dotenv";
|
|
6
6
|
|
|
@@ -4921,7 +4921,7 @@ var PROMPT_PATTERNS = [
|
|
|
4921
4921
|
{ pattern: /proceed\?/i, answer: "y\n" }
|
|
4922
4922
|
];
|
|
4923
4923
|
async function executeShadcnAdd(registryItemPath) {
|
|
4924
|
-
return new Promise((
|
|
4924
|
+
return new Promise((resolve3) => {
|
|
4925
4925
|
let output = "";
|
|
4926
4926
|
const args = ["--yes", "shadcn@3.8.2", "add", registryItemPath];
|
|
4927
4927
|
const ptyProcess = pty.spawn("npx", args, {
|
|
@@ -4947,7 +4947,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4947
4947
|
});
|
|
4948
4948
|
const timeoutId = setTimeout(() => {
|
|
4949
4949
|
ptyProcess.kill();
|
|
4950
|
-
|
|
4950
|
+
resolve3({
|
|
4951
4951
|
success: false,
|
|
4952
4952
|
files: [],
|
|
4953
4953
|
error: "\u6267\u884C\u8D85\u65F6"
|
|
@@ -4958,7 +4958,7 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4958
4958
|
const success = exitCode === 0;
|
|
4959
4959
|
const filePaths = parseOutput(output);
|
|
4960
4960
|
const files = filePaths.map(toFileInfo);
|
|
4961
|
-
|
|
4961
|
+
resolve3({
|
|
4962
4962
|
success,
|
|
4963
4963
|
files,
|
|
4964
4964
|
error: success ? void 0 : output || `Process exited with code ${exitCode}`
|
|
@@ -4969,12 +4969,12 @@ async function executeShadcnAdd(registryItemPath) {
|
|
|
4969
4969
|
|
|
4970
4970
|
// src/commands/component/add.handler.ts
|
|
4971
4971
|
function runActionPluginInit() {
|
|
4972
|
-
return new Promise((
|
|
4972
|
+
return new Promise((resolve3) => {
|
|
4973
4973
|
execFile("fullstack-cli", ["action-plugin", "init"], { cwd: process.cwd(), stdio: "ignore" }, (error) => {
|
|
4974
4974
|
if (error) {
|
|
4975
4975
|
debug("action-plugin init \u5931\u8D25: %s", error.message);
|
|
4976
4976
|
}
|
|
4977
|
-
|
|
4977
|
+
resolve3();
|
|
4978
4978
|
});
|
|
4979
4979
|
});
|
|
4980
4980
|
}
|
|
@@ -8098,6 +8098,527 @@ async function preUploadStatic(options) {
|
|
|
8098
8098
|
}
|
|
8099
8099
|
}
|
|
8100
8100
|
|
|
8101
|
+
// src/commands/build/preview-server-artifact.handler.ts
|
|
8102
|
+
import * as fs29 from "fs";
|
|
8103
|
+
import * as os3 from "os";
|
|
8104
|
+
import * as path25 from "path";
|
|
8105
|
+
import { builtinModules, createRequire as createRequire3 } from "module";
|
|
8106
|
+
import { build } from "esbuild";
|
|
8107
|
+
import { parse as parse2 } from "acorn";
|
|
8108
|
+
import { simple as walkSimple } from "acorn-walk";
|
|
8109
|
+
var STATIC_PLUGIN_REGISTRY_SYMBOL = "@lark-apaas/nestjs-capability/static-plugin-registry";
|
|
8110
|
+
var STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION = 1;
|
|
8111
|
+
var OPTIONAL_DEPENDENCY_STUBS = [
|
|
8112
|
+
"@nestjs/microservices",
|
|
8113
|
+
"@nestjs/microservices/microservices-module",
|
|
8114
|
+
"@nestjs/websockets/socket-module",
|
|
8115
|
+
"fsevents"
|
|
8116
|
+
];
|
|
8117
|
+
var BUILTIN_MODULES = /* @__PURE__ */ new Set([
|
|
8118
|
+
...builtinModules,
|
|
8119
|
+
...builtinModules.map((moduleName) => `node:${moduleName}`)
|
|
8120
|
+
]);
|
|
8121
|
+
function toAbsolutePath(projectRoot, candidate) {
|
|
8122
|
+
return path25.isAbsolute(candidate) ? candidate : path25.resolve(projectRoot, candidate);
|
|
8123
|
+
}
|
|
8124
|
+
function readJson(filePath) {
|
|
8125
|
+
return JSON.parse(fs29.readFileSync(filePath, "utf8"));
|
|
8126
|
+
}
|
|
8127
|
+
function findPackageRoot(entryPath) {
|
|
8128
|
+
let current = path25.dirname(entryPath);
|
|
8129
|
+
const filesystemRoot = path25.parse(current).root;
|
|
8130
|
+
while (current !== filesystemRoot) {
|
|
8131
|
+
if (fs29.existsSync(path25.join(current, "package.json"))) {
|
|
8132
|
+
return current;
|
|
8133
|
+
}
|
|
8134
|
+
current = path25.dirname(current);
|
|
8135
|
+
}
|
|
8136
|
+
return null;
|
|
8137
|
+
}
|
|
8138
|
+
function findNativeAddons(root) {
|
|
8139
|
+
const nativeAddons = [];
|
|
8140
|
+
const pending = [root];
|
|
8141
|
+
while (pending.length > 0) {
|
|
8142
|
+
const current = pending.pop();
|
|
8143
|
+
if (!current) {
|
|
8144
|
+
continue;
|
|
8145
|
+
}
|
|
8146
|
+
for (const entry of fs29.readdirSync(current, { withFileTypes: true })) {
|
|
8147
|
+
const entryPath = path25.join(current, entry.name);
|
|
8148
|
+
if (entry.isSymbolicLink()) {
|
|
8149
|
+
continue;
|
|
8150
|
+
}
|
|
8151
|
+
if (entry.isDirectory()) {
|
|
8152
|
+
pending.push(entryPath);
|
|
8153
|
+
} else if (entry.isFile() && entry.name.endsWith(".node")) {
|
|
8154
|
+
nativeAddons.push(entryPath);
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
8158
|
+
return nativeAddons.sort();
|
|
8159
|
+
}
|
|
8160
|
+
function resolveActionPlugins(projectRoot, packageJson) {
|
|
8161
|
+
const configuredPlugins = packageJson.actionPlugins;
|
|
8162
|
+
if (!configuredPlugins || typeof configuredPlugins !== "object") {
|
|
8163
|
+
return { plugins: [], reasons: [] };
|
|
8164
|
+
}
|
|
8165
|
+
const projectRequire = createRequire3(path25.join(projectRoot, "package.json"));
|
|
8166
|
+
const plugins = [];
|
|
8167
|
+
const reasons = [];
|
|
8168
|
+
for (const name of Object.keys(configuredPlugins).sort()) {
|
|
8169
|
+
let entryPath;
|
|
8170
|
+
try {
|
|
8171
|
+
entryPath = projectRequire.resolve(name);
|
|
8172
|
+
} catch (error) {
|
|
8173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8174
|
+
reasons.push(`Action Plugin ${name} \u65E0\u6CD5\u89E3\u6790: ${message}`);
|
|
8175
|
+
continue;
|
|
8176
|
+
}
|
|
8177
|
+
const packageRoot = findPackageRoot(entryPath);
|
|
8178
|
+
if (!packageRoot) {
|
|
8179
|
+
reasons.push(`Action Plugin ${name} \u65E0\u6CD5\u5B9A\u4F4D package.json: ${entryPath}`);
|
|
8180
|
+
continue;
|
|
8181
|
+
}
|
|
8182
|
+
const nativeAddons = findNativeAddons(packageRoot);
|
|
8183
|
+
if (nativeAddons.length > 0) {
|
|
8184
|
+
const relativeAddons = nativeAddons.map(
|
|
8185
|
+
(filePath) => path25.relative(packageRoot, filePath)
|
|
8186
|
+
);
|
|
8187
|
+
reasons.push(
|
|
8188
|
+
`Action Plugin ${name} \u5305\u542B\u539F\u751F\u6269\u5C55\uFF0C\u4E0D\u80FD\u751F\u6210\u8DE8\u6C99\u7BB1\u5355 bundle: ${relativeAddons.join(", ")}`
|
|
8189
|
+
);
|
|
8190
|
+
continue;
|
|
8191
|
+
}
|
|
8192
|
+
const manifestPath = path25.join(packageRoot, "manifest.json");
|
|
8193
|
+
let manifest;
|
|
8194
|
+
if (fs29.existsSync(manifestPath)) {
|
|
8195
|
+
try {
|
|
8196
|
+
manifest = readJson(manifestPath);
|
|
8197
|
+
} catch (error) {
|
|
8198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8199
|
+
reasons.push(
|
|
8200
|
+
`Action Plugin ${name} \u7684 manifest.json \u65E0\u6CD5\u89E3\u6790: ${message}`
|
|
8201
|
+
);
|
|
8202
|
+
continue;
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
plugins.push({ name, entryPath, manifest });
|
|
8206
|
+
}
|
|
8207
|
+
if (reasons.length === 0 && plugins.length > 0) {
|
|
8208
|
+
try {
|
|
8209
|
+
const capabilityRuntime = projectRequire(
|
|
8210
|
+
"@lark-apaas/nestjs-capability"
|
|
8211
|
+
);
|
|
8212
|
+
if (capabilityRuntime.STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION !== STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION) {
|
|
8213
|
+
reasons.push(
|
|
8214
|
+
`@lark-apaas/nestjs-capability \u672A\u63D0\u4F9B\u517C\u5BB9\u7684 STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION=${STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION}`
|
|
8215
|
+
);
|
|
8216
|
+
}
|
|
8217
|
+
} catch (error) {
|
|
8218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8219
|
+
reasons.push(
|
|
8220
|
+
`@lark-apaas/nestjs-capability \u65E0\u6CD5\u52A0\u8F7D\uFF0C\u7F3A\u5C11 STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION=${STATIC_PLUGIN_REGISTRY_PROTOCOL_VERSION}: ${message}`
|
|
8221
|
+
);
|
|
8222
|
+
}
|
|
8223
|
+
}
|
|
8224
|
+
return { plugins, reasons };
|
|
8225
|
+
}
|
|
8226
|
+
function generateBootstrap(entryPath, plugins) {
|
|
8227
|
+
const lines = [`'use strict';`, `const __staticPluginRegistry = new Map();`];
|
|
8228
|
+
plugins.forEach((plugin, index) => {
|
|
8229
|
+
const moduleVariable = `__staticPlugin${index}`;
|
|
8230
|
+
lines.push(
|
|
8231
|
+
`const ${moduleVariable} = require(${JSON.stringify(plugin.entryPath)});`
|
|
8232
|
+
);
|
|
8233
|
+
lines.push(
|
|
8234
|
+
`__staticPluginRegistry.set(${JSON.stringify(plugin.name)}, { module: ${moduleVariable}, manifest: ${JSON.stringify(plugin.manifest ?? null)} });`
|
|
8235
|
+
);
|
|
8236
|
+
});
|
|
8237
|
+
lines.push(
|
|
8238
|
+
`globalThis[Symbol.for(${JSON.stringify(STATIC_PLUGIN_REGISTRY_SYMBOL)})] = __staticPluginRegistry;`,
|
|
8239
|
+
`require(${JSON.stringify(entryPath)});`,
|
|
8240
|
+
""
|
|
8241
|
+
);
|
|
8242
|
+
return lines.join("\n");
|
|
8243
|
+
}
|
|
8244
|
+
function classTransformerStorageResolver(projectRoot) {
|
|
8245
|
+
const projectRequire = createRequire3(path25.join(projectRoot, "package.json"));
|
|
8246
|
+
return {
|
|
8247
|
+
name: "class-transformer-storage-resolver",
|
|
8248
|
+
setup(esbuild) {
|
|
8249
|
+
esbuild.onResolve({ filter: /^class-transformer\/storage$/ }, () => {
|
|
8250
|
+
try {
|
|
8251
|
+
return {
|
|
8252
|
+
path: projectRequire.resolve("class-transformer/cjs/storage.js")
|
|
8253
|
+
};
|
|
8254
|
+
} catch {
|
|
8255
|
+
return void 0;
|
|
8256
|
+
}
|
|
8257
|
+
});
|
|
8258
|
+
}
|
|
8259
|
+
};
|
|
8260
|
+
}
|
|
8261
|
+
function optionalDependencyStubPlugin() {
|
|
8262
|
+
const stubbedDependencies = /* @__PURE__ */ new Set();
|
|
8263
|
+
const escapedNames = OPTIONAL_DEPENDENCY_STUBS.map(
|
|
8264
|
+
(name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
8265
|
+
);
|
|
8266
|
+
const filter = new RegExp(`^(?:${escapedNames.join("|")})$`);
|
|
8267
|
+
return {
|
|
8268
|
+
stubbedDependencies,
|
|
8269
|
+
plugin: {
|
|
8270
|
+
name: "preview-optional-dependency-stubs",
|
|
8271
|
+
setup(esbuild) {
|
|
8272
|
+
esbuild.onResolve({ filter }, (args) => {
|
|
8273
|
+
stubbedDependencies.add(args.path);
|
|
8274
|
+
return {
|
|
8275
|
+
path: args.path,
|
|
8276
|
+
namespace: "preview-optional-dependency"
|
|
8277
|
+
};
|
|
8278
|
+
});
|
|
8279
|
+
esbuild.onLoad(
|
|
8280
|
+
{ filter: /.*/, namespace: "preview-optional-dependency" },
|
|
8281
|
+
(args) => ({
|
|
8282
|
+
contents: `
|
|
8283
|
+
const error = new Error(${JSON.stringify(
|
|
8284
|
+
`Optional dependency is unavailable in Preview Artifact: ${args.path}`
|
|
8285
|
+
)});
|
|
8286
|
+
error.code = 'MODULE_NOT_FOUND';
|
|
8287
|
+
throw error;
|
|
8288
|
+
`,
|
|
8289
|
+
loader: "js"
|
|
8290
|
+
})
|
|
8291
|
+
);
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
};
|
|
8295
|
+
}
|
|
8296
|
+
function isStaticString(node) {
|
|
8297
|
+
if (!node) {
|
|
8298
|
+
return false;
|
|
8299
|
+
}
|
|
8300
|
+
if (node.type === "Literal") {
|
|
8301
|
+
return typeof node.value === "string";
|
|
8302
|
+
}
|
|
8303
|
+
return node.type === "TemplateLiteral" && node.expressions.length === 0;
|
|
8304
|
+
}
|
|
8305
|
+
function staticPropertyName(node) {
|
|
8306
|
+
if (node.type !== "MemberExpression") {
|
|
8307
|
+
return void 0;
|
|
8308
|
+
}
|
|
8309
|
+
if (!node.computed && node.property.type === "Identifier") {
|
|
8310
|
+
return node.property.name;
|
|
8311
|
+
}
|
|
8312
|
+
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") {
|
|
8313
|
+
return node.property.value;
|
|
8314
|
+
}
|
|
8315
|
+
return void 0;
|
|
8316
|
+
}
|
|
8317
|
+
function auditBundleClosure(outfile, stubbedDependencies) {
|
|
8318
|
+
const ast = parse2(fs29.readFileSync(outfile, "utf8"), {
|
|
8319
|
+
ecmaVersion: "latest",
|
|
8320
|
+
sourceType: "script",
|
|
8321
|
+
allowHashBang: true
|
|
8322
|
+
});
|
|
8323
|
+
let dynamicRequireCount = 0;
|
|
8324
|
+
let dynamicImportCount = 0;
|
|
8325
|
+
let createRequireCount = 0;
|
|
8326
|
+
let requireResolveCount = 0;
|
|
8327
|
+
let runtimeFileReadCount = 0;
|
|
8328
|
+
const runtimeFileReads = /* @__PURE__ */ new Set([
|
|
8329
|
+
"access",
|
|
8330
|
+
"accessSync",
|
|
8331
|
+
"createReadStream",
|
|
8332
|
+
"open",
|
|
8333
|
+
"openSync",
|
|
8334
|
+
"readFile",
|
|
8335
|
+
"readFileSync",
|
|
8336
|
+
"readdir",
|
|
8337
|
+
"readdirSync",
|
|
8338
|
+
"stat",
|
|
8339
|
+
"statSync"
|
|
8340
|
+
]);
|
|
8341
|
+
walkSimple(ast, {
|
|
8342
|
+
CallExpression(node) {
|
|
8343
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require" && !isStaticString(node.arguments[0])) {
|
|
8344
|
+
dynamicRequireCount += 1;
|
|
8345
|
+
}
|
|
8346
|
+
if (node.callee.type === "Identifier" && node.callee.name === "createRequire" || staticPropertyName(node.callee) === "createRequire") {
|
|
8347
|
+
createRequireCount += 1;
|
|
8348
|
+
}
|
|
8349
|
+
const propertyName = staticPropertyName(node.callee);
|
|
8350
|
+
if (propertyName === "resolve" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "require") {
|
|
8351
|
+
requireResolveCount += 1;
|
|
8352
|
+
}
|
|
8353
|
+
if (propertyName && runtimeFileReads.has(propertyName)) {
|
|
8354
|
+
runtimeFileReadCount += 1;
|
|
8355
|
+
}
|
|
8356
|
+
},
|
|
8357
|
+
ImportExpression(node) {
|
|
8358
|
+
if (!isStaticString(node.source)) {
|
|
8359
|
+
dynamicImportCount += 1;
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
});
|
|
8363
|
+
const risks = [];
|
|
8364
|
+
if (dynamicRequireCount > 0) {
|
|
8365
|
+
risks.push(`bundle retains ${dynamicRequireCount} dynamic require call(s)`);
|
|
8366
|
+
}
|
|
8367
|
+
if (dynamicImportCount > 0) {
|
|
8368
|
+
risks.push(`bundle retains ${dynamicImportCount} dynamic import call(s)`);
|
|
8369
|
+
}
|
|
8370
|
+
if (createRequireCount > 0 || requireResolveCount > 0) {
|
|
8371
|
+
risks.push(
|
|
8372
|
+
`bundle retains runtime module resolution (createRequire=${createRequireCount}, require.resolve=${requireResolveCount})`
|
|
8373
|
+
);
|
|
8374
|
+
}
|
|
8375
|
+
if (runtimeFileReadCount > 0) {
|
|
8376
|
+
risks.push(
|
|
8377
|
+
`bundle retains ${runtimeFileReadCount} runtime filesystem read call(s)`
|
|
8378
|
+
);
|
|
8379
|
+
}
|
|
8380
|
+
if (stubbedDependencies.length > 0) {
|
|
8381
|
+
risks.push(
|
|
8382
|
+
`bundle stubs optional dependencies: ${stubbedDependencies.join(", ")}`
|
|
8383
|
+
);
|
|
8384
|
+
}
|
|
8385
|
+
return risks;
|
|
8386
|
+
}
|
|
8387
|
+
function collectExternalImports(metafile, outfile) {
|
|
8388
|
+
const normalizedOutfile = path25.resolve(outfile);
|
|
8389
|
+
const output = Object.entries(metafile.outputs).find(
|
|
8390
|
+
([outputPath]) => path25.resolve(outputPath) === normalizedOutfile
|
|
8391
|
+
)?.[1];
|
|
8392
|
+
if (!output) {
|
|
8393
|
+
return [];
|
|
8394
|
+
}
|
|
8395
|
+
return Array.from(
|
|
8396
|
+
new Set(
|
|
8397
|
+
output.imports.filter((importRecord) => importRecord.external).map((importRecord) => importRecord.path).filter(
|
|
8398
|
+
(importPath) => !BUILTIN_MODULES.has(importPath) && !importPath.startsWith("node:")
|
|
8399
|
+
)
|
|
8400
|
+
)
|
|
8401
|
+
).sort();
|
|
8402
|
+
}
|
|
8403
|
+
function formatBuildError(error) {
|
|
8404
|
+
if (!error || typeof error !== "object" || !("errors" in error)) {
|
|
8405
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
8406
|
+
}
|
|
8407
|
+
const errors = error.errors;
|
|
8408
|
+
if (!errors || errors.length === 0) {
|
|
8409
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
8410
|
+
}
|
|
8411
|
+
return errors.map((item) => {
|
|
8412
|
+
const location = item.location?.file ? `${item.location.file}${item.location.line ? `:${item.location.line}` : ""}: ` : "";
|
|
8413
|
+
return `${location}${item.text ?? "unknown esbuild error"}`;
|
|
8414
|
+
});
|
|
8415
|
+
}
|
|
8416
|
+
function removePublishedArtifact(outfile, metadataFile) {
|
|
8417
|
+
fs29.rmSync(outfile, { force: true });
|
|
8418
|
+
fs29.rmSync(metadataFile, { force: true });
|
|
8419
|
+
}
|
|
8420
|
+
function bestEffortRemove(targetPath, options = {}) {
|
|
8421
|
+
if (!targetPath) {
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
8424
|
+
try {
|
|
8425
|
+
fs29.rmSync(targetPath, { force: true, recursive: options.recursive });
|
|
8426
|
+
} catch {
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
function outputPathConflicts(entryPath, packageJsonPath, outfile, metadataFile) {
|
|
8430
|
+
const reasons = [];
|
|
8431
|
+
if (outfile === metadataFile) {
|
|
8432
|
+
reasons.push("bundle \u4E0E metadata \u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u76F8\u540C");
|
|
8433
|
+
}
|
|
8434
|
+
const protectedInputs = /* @__PURE__ */ new Map([
|
|
8435
|
+
[entryPath, "Nest \u7F16\u8BD1\u5165\u53E3"],
|
|
8436
|
+
[packageJsonPath, "package.json"]
|
|
8437
|
+
]);
|
|
8438
|
+
for (const [outputPath, outputName] of [
|
|
8439
|
+
[outfile, "bundle"],
|
|
8440
|
+
[metadataFile, "metadata"]
|
|
8441
|
+
]) {
|
|
8442
|
+
const protectedName = protectedInputs.get(outputPath);
|
|
8443
|
+
if (protectedName) {
|
|
8444
|
+
reasons.push(
|
|
8445
|
+
`${outputName} \u8F93\u51FA\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6 ${protectedName}: ${outputPath}`
|
|
8446
|
+
);
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
8449
|
+
return reasons;
|
|
8450
|
+
}
|
|
8451
|
+
async function buildPreviewServerArtifact(options) {
|
|
8452
|
+
const startedAt = Date.now();
|
|
8453
|
+
const projectRoot = path25.resolve(options.projectRoot);
|
|
8454
|
+
const entryPath = toAbsolutePath(
|
|
8455
|
+
projectRoot,
|
|
8456
|
+
options.entry ?? "dist/main.js"
|
|
8457
|
+
);
|
|
8458
|
+
const outfile = toAbsolutePath(
|
|
8459
|
+
projectRoot,
|
|
8460
|
+
options.outfile ?? ".preview-artifact/server.bundle.cjs"
|
|
8461
|
+
);
|
|
8462
|
+
const metadataFile = toAbsolutePath(
|
|
8463
|
+
projectRoot,
|
|
8464
|
+
options.metadataFile ?? `${outfile}.meta.json`
|
|
8465
|
+
);
|
|
8466
|
+
const packageJsonPath = path25.join(projectRoot, "package.json");
|
|
8467
|
+
let pluginCount = 0;
|
|
8468
|
+
let outputCleared = false;
|
|
8469
|
+
let phase = "\u6821\u9A8C\u8F93\u5165";
|
|
8470
|
+
let tempRoot;
|
|
8471
|
+
let tempOutputRoot;
|
|
8472
|
+
let tempMetadataFile;
|
|
8473
|
+
const failed = (reasons) => ({
|
|
8474
|
+
built: false,
|
|
8475
|
+
elapsedMs: Date.now() - startedAt,
|
|
8476
|
+
pluginCount,
|
|
8477
|
+
reasons
|
|
8478
|
+
});
|
|
8479
|
+
const pathConflicts = outputPathConflicts(
|
|
8480
|
+
entryPath,
|
|
8481
|
+
packageJsonPath,
|
|
8482
|
+
outfile,
|
|
8483
|
+
metadataFile
|
|
8484
|
+
);
|
|
8485
|
+
if (pathConflicts.length > 0) {
|
|
8486
|
+
return failed(pathConflicts);
|
|
8487
|
+
}
|
|
8488
|
+
if (!fs29.existsSync(entryPath)) {
|
|
8489
|
+
return failed([`Nest \u7F16\u8BD1\u5165\u53E3\u4E0D\u5B58\u5728: ${entryPath}`]);
|
|
8490
|
+
}
|
|
8491
|
+
if (!fs29.existsSync(packageJsonPath)) {
|
|
8492
|
+
return failed([`package.json \u4E0D\u5B58\u5728: ${packageJsonPath}`]);
|
|
8493
|
+
}
|
|
8494
|
+
try {
|
|
8495
|
+
phase = `\u89E3\u6790 ${packageJsonPath}`;
|
|
8496
|
+
const packageJson = readJson(packageJsonPath);
|
|
8497
|
+
phase = "\u89E3\u6790 Action Plugin";
|
|
8498
|
+
const { plugins, reasons } = resolveActionPlugins(projectRoot, packageJson);
|
|
8499
|
+
pluginCount = plugins.length;
|
|
8500
|
+
phase = "\u6E05\u7406\u65E7 Preview Artifact";
|
|
8501
|
+
removePublishedArtifact(outfile, metadataFile);
|
|
8502
|
+
outputCleared = true;
|
|
8503
|
+
if (reasons.length > 0) {
|
|
8504
|
+
return failed(reasons);
|
|
8505
|
+
}
|
|
8506
|
+
phase = "\u521B\u5EFA Preview Artifact \u4E34\u65F6\u76EE\u5F55";
|
|
8507
|
+
fs29.mkdirSync(path25.dirname(outfile), { recursive: true });
|
|
8508
|
+
fs29.mkdirSync(path25.dirname(metadataFile), { recursive: true });
|
|
8509
|
+
tempRoot = fs29.mkdtempSync(
|
|
8510
|
+
path25.join(os3.tmpdir(), "preview-server-artifact-")
|
|
8511
|
+
);
|
|
8512
|
+
tempOutputRoot = fs29.mkdtempSync(
|
|
8513
|
+
path25.join(path25.dirname(outfile), ".preview-server-artifact-")
|
|
8514
|
+
);
|
|
8515
|
+
const bootstrapPath = path25.join(tempRoot, "bootstrap.cjs");
|
|
8516
|
+
const tempOutfile = path25.join(tempOutputRoot, path25.basename(outfile));
|
|
8517
|
+
tempMetadataFile = path25.join(
|
|
8518
|
+
path25.dirname(metadataFile),
|
|
8519
|
+
`.${path25.basename(metadataFile)}.${process.pid}.${Date.now()}.tmp`
|
|
8520
|
+
);
|
|
8521
|
+
fs29.writeFileSync(bootstrapPath, generateBootstrap(entryPath, plugins));
|
|
8522
|
+
const optionalDependencies = optionalDependencyStubPlugin();
|
|
8523
|
+
const buildOptions = {
|
|
8524
|
+
absWorkingDir: projectRoot,
|
|
8525
|
+
entryPoints: [bootstrapPath],
|
|
8526
|
+
outfile: tempOutfile,
|
|
8527
|
+
bundle: true,
|
|
8528
|
+
platform: "node",
|
|
8529
|
+
format: "cjs",
|
|
8530
|
+
target: "node22",
|
|
8531
|
+
treeShaking: true,
|
|
8532
|
+
keepNames: true,
|
|
8533
|
+
legalComments: "none",
|
|
8534
|
+
sourcemap: false,
|
|
8535
|
+
metafile: true,
|
|
8536
|
+
logLevel: "silent",
|
|
8537
|
+
plugins: [
|
|
8538
|
+
classTransformerStorageResolver(projectRoot),
|
|
8539
|
+
optionalDependencies.plugin
|
|
8540
|
+
]
|
|
8541
|
+
};
|
|
8542
|
+
phase = "\u6267\u884C esbuild";
|
|
8543
|
+
const buildResult2 = await build(buildOptions);
|
|
8544
|
+
if (!buildResult2.metafile) {
|
|
8545
|
+
throw new Error("esbuild \u672A\u8FD4\u56DE metafile");
|
|
8546
|
+
}
|
|
8547
|
+
const externalImports = collectExternalImports(
|
|
8548
|
+
buildResult2.metafile,
|
|
8549
|
+
tempOutfile
|
|
8550
|
+
);
|
|
8551
|
+
if (externalImports.length > 0) {
|
|
8552
|
+
return failed([`bundle \u6B8B\u7559\u8FD0\u884C\u65F6\u4F9D\u8D56: ${externalImports.join(", ")}`]);
|
|
8553
|
+
}
|
|
8554
|
+
phase = "\u5BA1\u8BA1 bundle \u8FD0\u884C\u65F6\u95ED\u5305";
|
|
8555
|
+
const stubbedOptionalDependencies = Array.from(
|
|
8556
|
+
optionalDependencies.stubbedDependencies
|
|
8557
|
+
).sort();
|
|
8558
|
+
const closureRisks = auditBundleClosure(
|
|
8559
|
+
tempOutfile,
|
|
8560
|
+
stubbedOptionalDependencies
|
|
8561
|
+
);
|
|
8562
|
+
const closureStatus = closureRisks.length === 0 ? "static-audit-clean" : "runtime-validation-required";
|
|
8563
|
+
const metadata = {
|
|
8564
|
+
version: 1,
|
|
8565
|
+
entry: entryPath,
|
|
8566
|
+
outfile,
|
|
8567
|
+
bundleBytes: fs29.statSync(tempOutfile).size,
|
|
8568
|
+
inputFiles: Object.keys(buildResult2.metafile.inputs).length,
|
|
8569
|
+
externalImports,
|
|
8570
|
+
actionPlugins: plugins.map((plugin) => plugin.name),
|
|
8571
|
+
closureStatus,
|
|
8572
|
+
closureRisks,
|
|
8573
|
+
stubbedOptionalDependencies,
|
|
8574
|
+
runtimeProbeStatus: "not-run",
|
|
8575
|
+
consumable: false,
|
|
8576
|
+
elapsedMs: Date.now() - startedAt
|
|
8577
|
+
};
|
|
8578
|
+
phase = "\u53D1\u5E03 bundle \u4E0E metadata";
|
|
8579
|
+
fs29.writeFileSync(
|
|
8580
|
+
tempMetadataFile,
|
|
8581
|
+
`${JSON.stringify(metadata, null, 2)}
|
|
8582
|
+
`
|
|
8583
|
+
);
|
|
8584
|
+
fs29.renameSync(tempOutfile, outfile);
|
|
8585
|
+
fs29.renameSync(tempMetadataFile, metadataFile);
|
|
8586
|
+
return {
|
|
8587
|
+
built: true,
|
|
8588
|
+
outfile,
|
|
8589
|
+
metadataFile,
|
|
8590
|
+
bundleBytes: metadata.bundleBytes,
|
|
8591
|
+
inputFiles: metadata.inputFiles,
|
|
8592
|
+
externalImports,
|
|
8593
|
+
pluginCount,
|
|
8594
|
+
closureStatus,
|
|
8595
|
+
closureRisks,
|
|
8596
|
+
stubbedOptionalDependencies,
|
|
8597
|
+
runtimeProbeStatus: "not-run",
|
|
8598
|
+
consumable: false,
|
|
8599
|
+
elapsedMs: metadata.elapsedMs
|
|
8600
|
+
};
|
|
8601
|
+
} catch (error) {
|
|
8602
|
+
const reasons = formatBuildError(error).map(
|
|
8603
|
+
(reason) => `${phase}: ${reason}`
|
|
8604
|
+
);
|
|
8605
|
+
if (outputCleared) {
|
|
8606
|
+
try {
|
|
8607
|
+
removePublishedArtifact(outfile, metadataFile);
|
|
8608
|
+
} catch (cleanupError) {
|
|
8609
|
+
reasons.push(
|
|
8610
|
+
`\u6E05\u7406\u5931\u8D25\u4EA7\u7269: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
8611
|
+
);
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
return failed(reasons);
|
|
8615
|
+
} finally {
|
|
8616
|
+
bestEffortRemove(tempRoot, { recursive: true });
|
|
8617
|
+
bestEffortRemove(tempOutputRoot, { recursive: true });
|
|
8618
|
+
bestEffortRemove(tempMetadataFile);
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8101
8622
|
// src/commands/build/index.ts
|
|
8102
8623
|
var getTokenCommand = {
|
|
8103
8624
|
name: "get-token",
|
|
@@ -8126,10 +8647,34 @@ var preUploadStaticCommand = {
|
|
|
8126
8647
|
});
|
|
8127
8648
|
}
|
|
8128
8649
|
};
|
|
8650
|
+
var previewServerArtifactCommand = {
|
|
8651
|
+
name: "preview-server-artifact",
|
|
8652
|
+
description: "Build and audit a single-file Nest preview server artifact",
|
|
8653
|
+
register(program) {
|
|
8654
|
+
program.command(this.name).description(this.description).option("--project-root <dir>", "Application project root", process.cwd()).option("--entry <file>", "Compiled Nest entry", "dist/main.js").option(
|
|
8655
|
+
"--outfile <file>",
|
|
8656
|
+
"Output bundle",
|
|
8657
|
+
".preview-artifact/server.bundle.cjs"
|
|
8658
|
+
).option("--metadata-file <file>", "Output build metadata file").action(
|
|
8659
|
+
async (options) => {
|
|
8660
|
+
const result = await buildPreviewServerArtifact(options);
|
|
8661
|
+
console.log(JSON.stringify(result));
|
|
8662
|
+
if (!result.built) {
|
|
8663
|
+
process.exitCode = 2;
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
);
|
|
8667
|
+
}
|
|
8668
|
+
};
|
|
8129
8669
|
var buildCommandGroup = {
|
|
8130
8670
|
name: "build",
|
|
8131
8671
|
description: "Build related commands",
|
|
8132
|
-
commands: [
|
|
8672
|
+
commands: [
|
|
8673
|
+
getTokenCommand,
|
|
8674
|
+
uploadStaticCommand,
|
|
8675
|
+
preUploadStaticCommand,
|
|
8676
|
+
previewServerArtifactCommand
|
|
8677
|
+
]
|
|
8133
8678
|
};
|
|
8134
8679
|
|
|
8135
8680
|
// src/commands/index.ts
|
|
@@ -8146,12 +8691,12 @@ var commands = [
|
|
|
8146
8691
|
];
|
|
8147
8692
|
|
|
8148
8693
|
// src/index.ts
|
|
8149
|
-
var envPath =
|
|
8150
|
-
if (
|
|
8694
|
+
var envPath = path26.join(process.cwd(), ".env");
|
|
8695
|
+
if (fs30.existsSync(envPath)) {
|
|
8151
8696
|
dotenvConfig({ path: envPath });
|
|
8152
8697
|
}
|
|
8153
|
-
var __dirname =
|
|
8154
|
-
var pkg = JSON.parse(
|
|
8698
|
+
var __dirname = path26.dirname(fileURLToPath5(import.meta.url));
|
|
8699
|
+
var pkg = JSON.parse(fs30.readFileSync(path26.join(__dirname, "../package.json"), "utf-8"));
|
|
8155
8700
|
var cli = new FullstackCLI(pkg.version);
|
|
8156
8701
|
cli.useAll(commands);
|
|
8157
8702
|
cli.run();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-cli",
|
|
3
|
-
"version": "1.1.59-alpha.
|
|
3
|
+
"version": "1.1.59-alpha.20260717162925",
|
|
4
4
|
"description": "CLI tool for fullstack template management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,12 +34,15 @@
|
|
|
34
34
|
"@lark-apaas/http-client": "^0.1.7",
|
|
35
35
|
"@lydell/node-pty": "1.1.0",
|
|
36
36
|
"@vercel/nft": "^0.30.3",
|
|
37
|
+
"acorn": "^8.16.0",
|
|
38
|
+
"acorn-walk": "^8.3.5",
|
|
37
39
|
"commander": "^13.0.0",
|
|
38
40
|
"debug": "^4.4.3",
|
|
39
41
|
"dotenv": "^16.0.0",
|
|
40
42
|
"drizzle-kit": "0.31.5",
|
|
41
43
|
"drizzle-orm": "0.44.6",
|
|
42
44
|
"es-toolkit": "^1.44.0",
|
|
45
|
+
"esbuild": "^0.27.0",
|
|
43
46
|
"inflection": "^3.0.2",
|
|
44
47
|
"pinyin-pro": "^3.27.0",
|
|
45
48
|
"postgres": "^3.4.3",
|
package/templates/nest-cli.json
CHANGED
package/templates/scripts/dev.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { spawn, execSync } = require('child_process');
|
|
6
|
+
const { spawn, execFileSync, execSync } = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
const {
|
|
9
|
+
buildListeningPortLookupArgs,
|
|
10
|
+
createDeferredTypecheckGate,
|
|
9
11
|
createPreviewPhaseReporter,
|
|
12
|
+
normalizeTcpPort,
|
|
13
|
+
parseTscWatchSummary,
|
|
14
|
+
resolvePreviewServerCommand,
|
|
10
15
|
waitForTcpReady,
|
|
11
16
|
} = require('./preview-startup-timing.cjs');
|
|
12
17
|
|
|
@@ -40,8 +45,12 @@ const MAX_RESTART_COUNT = process.env.MAX_RESTART_COUNT != null && process.env.M
|
|
|
40
45
|
: Infinity;
|
|
41
46
|
const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
|
|
42
47
|
const MAX_DELAY = 8;
|
|
43
|
-
const SERVER_PORT = process.env.SERVER_PORT
|
|
44
|
-
const CLIENT_DEV_PORT =
|
|
48
|
+
const SERVER_PORT = normalizeTcpPort(process.env.SERVER_PORT, 3000, 'SERVER_PORT');
|
|
49
|
+
const CLIENT_DEV_PORT = normalizeTcpPort(
|
|
50
|
+
process.env.CLIENT_DEV_PORT,
|
|
51
|
+
8080,
|
|
52
|
+
'CLIENT_DEV_PORT'
|
|
53
|
+
);
|
|
45
54
|
|
|
46
55
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
47
56
|
|
|
@@ -102,16 +111,38 @@ function killProcessGroup(pid, signal) {
|
|
|
102
111
|
|
|
103
112
|
function killOrphansByPort(port) {
|
|
104
113
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
const output = execFileSync('lsof', buildListeningPortLookupArgs(port), {
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
timeout: 5000,
|
|
117
|
+
}).trim();
|
|
118
|
+
const killedPids = [];
|
|
119
|
+
const failures = [];
|
|
120
|
+
if (output) {
|
|
121
|
+
for (const pidText of output.split('\n').filter(Boolean)) {
|
|
122
|
+
const pid = Number(pidText);
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, 'SIGKILL');
|
|
125
|
+
killedPids.push(pidText);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code !== 'ESRCH') {
|
|
128
|
+
failures.push(
|
|
129
|
+
`${pidText}: ${error instanceof Error ? error.message : String(error)}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
110
133
|
}
|
|
111
|
-
return pidList;
|
|
112
134
|
}
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
return {
|
|
136
|
+
pids: killedPids,
|
|
137
|
+
error: failures.length > 0 ? failures.join('; ') : null,
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error?.status === 1) return { pids: [], error: null };
|
|
141
|
+
return {
|
|
142
|
+
pids: [],
|
|
143
|
+
error: error instanceof Error ? error.message : String(error),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
// ── Process supervision ───────────────────────────────────────────────────────
|
|
@@ -126,7 +157,7 @@ function sleep(ms) {
|
|
|
126
157
|
* Start and supervise a process with auto-restart and log piping.
|
|
127
158
|
* Returns a promise that resolves when the process loop ends.
|
|
128
159
|
*/
|
|
129
|
-
function startProcess({ name, command, args, cleanupPort }) {
|
|
160
|
+
function startProcess({ name, command, args, cleanupPort, phaseDetail = {}, onReady, onOutputLine }) {
|
|
130
161
|
const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
|
|
131
162
|
const logFd = fs.openSync(logFilePath, 'a');
|
|
132
163
|
|
|
@@ -136,6 +167,30 @@ function startProcess({ name, command, args, cleanupPort }) {
|
|
|
136
167
|
const run = async () => {
|
|
137
168
|
let restartCount = 0;
|
|
138
169
|
|
|
170
|
+
if (cleanupPort) {
|
|
171
|
+
const portCleanup = killOrphansByPort(cleanupPort);
|
|
172
|
+
const stalePids = portCleanup.pids;
|
|
173
|
+
logEvent(
|
|
174
|
+
portCleanup.error ? 'ERROR' : stalePids.length > 0 ? 'WARN' : 'INFO',
|
|
175
|
+
name,
|
|
176
|
+
portCleanup.error
|
|
177
|
+
? `Port ${cleanupPort} preflight failed: ${portCleanup.error}`
|
|
178
|
+
: stalePids.length > 0
|
|
179
|
+
? `Killed stale processes on port ${cleanupPort} before first spawn: ${stalePids.join(' ')}`
|
|
180
|
+
: `Port ${cleanupPort} is clear before first spawn`
|
|
181
|
+
);
|
|
182
|
+
previewPhaseReporter.emit(
|
|
183
|
+
name === 'server' ? 'backend_port_preflight' : 'client_port_preflight',
|
|
184
|
+
portCleanup.error ? 'error' : 'success',
|
|
185
|
+
{
|
|
186
|
+
exact: true,
|
|
187
|
+
cleanup_count: stalePids.length,
|
|
188
|
+
...(portCleanup.error ? { error: portCleanup.error } : {}),
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (stalePids.length > 0) await sleep(500);
|
|
192
|
+
}
|
|
193
|
+
|
|
139
194
|
while (!stopping) {
|
|
140
195
|
const child = spawn(command, args, {
|
|
141
196
|
detached: true,
|
|
@@ -151,11 +206,17 @@ function startProcess({ name, command, args, cleanupPort }) {
|
|
|
151
206
|
const startTime = Date.now();
|
|
152
207
|
const attempt = restartCount + 1;
|
|
153
208
|
logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
|
|
154
|
-
const processPhase =
|
|
209
|
+
const processPhase =
|
|
210
|
+
name === 'server'
|
|
211
|
+
? 'backend_process_spawn'
|
|
212
|
+
: name === 'server-typecheck'
|
|
213
|
+
? 'backend_typecheck_process_spawn'
|
|
214
|
+
: 'client_process_spawn';
|
|
155
215
|
previewPhaseReporter.emit(processPhase, 'success', {
|
|
156
216
|
at_ms: startTime,
|
|
157
217
|
attempt,
|
|
158
218
|
exact: true,
|
|
219
|
+
...phaseDetail,
|
|
159
220
|
});
|
|
160
221
|
if (name === 'server') {
|
|
161
222
|
void waitForTcpReady({
|
|
@@ -164,29 +225,60 @@ function startProcess({ name, command, args, cleanupPort }) {
|
|
|
164
225
|
interval_ms: 50,
|
|
165
226
|
timeout_ms: 120000,
|
|
166
227
|
should_continue: () => entry.child === child && !stopping,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
})
|
|
229
|
+
.then(result => {
|
|
230
|
+
if (result.cancelled) return;
|
|
231
|
+
previewPhaseReporter.emit(
|
|
232
|
+
'backend_tcp_ready',
|
|
233
|
+
result.ready ? 'success' : 'error',
|
|
234
|
+
{ ...result, attempt, exact: false }
|
|
235
|
+
);
|
|
236
|
+
if (result.ready && onReady) {
|
|
237
|
+
try {
|
|
238
|
+
onReady(result);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logEvent(
|
|
241
|
+
'ERROR',
|
|
242
|
+
name,
|
|
243
|
+
`Backend ready callback failed: ${error instanceof Error ? error.message : String(error)}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.catch(error => {
|
|
249
|
+
if (entry.child !== child || stopping) return;
|
|
250
|
+
previewPhaseReporter.emit('backend_tcp_ready', 'error', {
|
|
251
|
+
duration_ms: Date.now() - startTime,
|
|
252
|
+
attempt,
|
|
253
|
+
exact: false,
|
|
254
|
+
precision_ms: 50,
|
|
255
|
+
error: error instanceof Error ? error.message : String(error),
|
|
256
|
+
});
|
|
182
257
|
});
|
|
183
|
-
});
|
|
184
258
|
}
|
|
185
259
|
|
|
186
260
|
// Pipe stdout and stderr through readline for timestamped logging
|
|
261
|
+
let typecheckSummaryReported = false;
|
|
187
262
|
const pipeLines = (stream) => {
|
|
188
263
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
189
264
|
rl.on('line', (line) => {
|
|
265
|
+
if (onOutputLine) onOutputLine(line);
|
|
266
|
+
if (name === 'server-typecheck' && !typecheckSummaryReported) {
|
|
267
|
+
const summary = parseTscWatchSummary(line);
|
|
268
|
+
if (summary) {
|
|
269
|
+
typecheckSummaryReported = true;
|
|
270
|
+
previewPhaseReporter.emit(
|
|
271
|
+
'backend_typecheck_ready',
|
|
272
|
+
summary.passed ? 'success' : 'error',
|
|
273
|
+
{
|
|
274
|
+
duration_ms: Date.now() - startTime,
|
|
275
|
+
attempt,
|
|
276
|
+
exact: true,
|
|
277
|
+
...summary,
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
190
282
|
const msg = `[${timestamp()}] [${name}] ${line}\n`;
|
|
191
283
|
try { fs.writeSync(logFd, msg); } catch {}
|
|
192
284
|
writeOutput(msg);
|
|
@@ -215,7 +307,15 @@ function startProcess({ name, command, args, cleanupPort }) {
|
|
|
215
307
|
|
|
216
308
|
// Port cleanup fallback
|
|
217
309
|
if (cleanupPort) {
|
|
218
|
-
const
|
|
310
|
+
const portCleanup = killOrphansByPort(cleanupPort);
|
|
311
|
+
const orphans = portCleanup.pids;
|
|
312
|
+
if (portCleanup.error) {
|
|
313
|
+
logEvent(
|
|
314
|
+
'ERROR',
|
|
315
|
+
name,
|
|
316
|
+
`Port ${cleanupPort} cleanup failed: ${portCleanup.error}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
219
319
|
if (orphans.length > 0) {
|
|
220
320
|
logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
|
|
221
321
|
await sleep(500);
|
|
@@ -277,8 +377,12 @@ async function cleanup() {
|
|
|
277
377
|
}
|
|
278
378
|
|
|
279
379
|
// Port cleanup fallback
|
|
280
|
-
|
|
281
|
-
|
|
380
|
+
for (const port of [SERVER_PORT, CLIENT_DEV_PORT]) {
|
|
381
|
+
const portCleanup = killOrphansByPort(port);
|
|
382
|
+
if (portCleanup.error) {
|
|
383
|
+
logEvent('ERROR', 'main', `Port ${port} cleanup failed: ${portCleanup.error}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
282
386
|
|
|
283
387
|
logEvent('INFO', 'main', 'All processes stopped');
|
|
284
388
|
|
|
@@ -332,11 +436,101 @@ async function main() {
|
|
|
332
436
|
}
|
|
333
437
|
|
|
334
438
|
// Start server and client
|
|
439
|
+
const serverCommand = resolvePreviewServerCommand({
|
|
440
|
+
project_root: PROJECT_ROOT,
|
|
441
|
+
});
|
|
442
|
+
logEvent(
|
|
443
|
+
'INFO',
|
|
444
|
+
'server',
|
|
445
|
+
`Selected ${serverCommand.compiler} compiler (${serverCommand.reason})`
|
|
446
|
+
);
|
|
447
|
+
previewPhaseReporter.emit('backend_compiler_selected', 'success', {
|
|
448
|
+
exact: true,
|
|
449
|
+
compiler: serverCommand.compiler,
|
|
450
|
+
type_check: serverCommand.type_check,
|
|
451
|
+
type_check_mode: serverCommand.type_check_mode,
|
|
452
|
+
reason: serverCommand.reason,
|
|
453
|
+
missing_modules: serverCommand.missing_modules,
|
|
454
|
+
});
|
|
455
|
+
const typecheckGate = createDeferredTypecheckGate();
|
|
456
|
+
let serverTypecheckStarted = false;
|
|
457
|
+
const startServerTypecheck = trigger => {
|
|
458
|
+
if (serverTypecheckStarted || !serverCommand.type_check_command) return;
|
|
459
|
+
try {
|
|
460
|
+
logEvent(
|
|
461
|
+
'INFO',
|
|
462
|
+
'server-typecheck',
|
|
463
|
+
`Starting deferred server type checker after ${trigger}`
|
|
464
|
+
);
|
|
465
|
+
previewPhaseReporter.emit('backend_typecheck_deferred_start', 'success', {
|
|
466
|
+
exact: true,
|
|
467
|
+
type_check_mode: serverCommand.type_check_mode,
|
|
468
|
+
trigger,
|
|
469
|
+
});
|
|
470
|
+
const typecheckPromise = startProcess({
|
|
471
|
+
name: 'server-typecheck',
|
|
472
|
+
command: serverCommand.type_check_command.command,
|
|
473
|
+
args: serverCommand.type_check_command.args,
|
|
474
|
+
phaseDetail: { type_check_mode: serverCommand.type_check_mode },
|
|
475
|
+
});
|
|
476
|
+
serverTypecheckStarted = true;
|
|
477
|
+
void typecheckPromise.catch(error => {
|
|
478
|
+
logEvent(
|
|
479
|
+
'ERROR',
|
|
480
|
+
'server-typecheck',
|
|
481
|
+
`Deferred server type checker failed: ${error instanceof Error ? error.message : String(error)}`
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
} catch (error) {
|
|
485
|
+
previewPhaseReporter.emit('backend_typecheck_deferred_start', 'error', {
|
|
486
|
+
exact: true,
|
|
487
|
+
type_check_mode: serverCommand.type_check_mode,
|
|
488
|
+
error: error instanceof Error ? error.message : String(error),
|
|
489
|
+
});
|
|
490
|
+
logEvent(
|
|
491
|
+
'ERROR',
|
|
492
|
+
'server-typecheck',
|
|
493
|
+
`Failed to start deferred server type checker: ${error instanceof Error ? error.message : String(error)}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const handleBackendReady = () => {
|
|
498
|
+
if (!serverCommand.type_check_command) return;
|
|
499
|
+
const decision = typecheckGate.onBackendReady();
|
|
500
|
+
if (decision.should_start) {
|
|
501
|
+
startServerTypecheck(decision.reason);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
logEvent(
|
|
505
|
+
'INFO',
|
|
506
|
+
'server-typecheck',
|
|
507
|
+
'Skipped type checking during initial preview startup'
|
|
508
|
+
);
|
|
509
|
+
previewPhaseReporter.emit('backend_typecheck_skipped', 'success', {
|
|
510
|
+
exact: true,
|
|
511
|
+
type_check_mode: serverCommand.type_check_mode,
|
|
512
|
+
reason: decision.reason,
|
|
513
|
+
next_trigger: 'source_change_or_backend_restart',
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
const handleServerOutput = line => {
|
|
517
|
+
if (!serverCommand.type_check_command || serverTypecheckStarted) return;
|
|
518
|
+
const decision = typecheckGate.onCompilerOutput(line);
|
|
519
|
+
if (decision?.should_start) startServerTypecheck(decision.reason);
|
|
520
|
+
};
|
|
335
521
|
const serverPromise = startProcess({
|
|
336
522
|
name: 'server',
|
|
337
|
-
command:
|
|
338
|
-
args:
|
|
523
|
+
command: serverCommand.command,
|
|
524
|
+
args: serverCommand.args,
|
|
339
525
|
cleanupPort: SERVER_PORT,
|
|
526
|
+
phaseDetail: {
|
|
527
|
+
compiler: serverCommand.compiler,
|
|
528
|
+
type_check: serverCommand.type_check,
|
|
529
|
+
type_check_mode: serverCommand.type_check_mode,
|
|
530
|
+
compiler_selection_reason: serverCommand.reason,
|
|
531
|
+
},
|
|
532
|
+
onReady: handleBackendReady,
|
|
533
|
+
onOutputLine: handleServerOutput,
|
|
340
534
|
});
|
|
341
535
|
|
|
342
536
|
const clientPromise = startProcess({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const net = require('net');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
|
|
5
7
|
const PREFIX = '[MiaodaPreviewPhase] ';
|
|
6
8
|
const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -37,6 +39,277 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
37
39
|
return { emit };
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function createTscServerCommand(reason, missingModules = []) {
|
|
43
|
+
return {
|
|
44
|
+
command: 'npm',
|
|
45
|
+
args: ['run', 'dev:server', '--', '--builder', 'tsc'],
|
|
46
|
+
compiler: 'tsc',
|
|
47
|
+
type_check: true,
|
|
48
|
+
type_check_mode: 'compiler_integrated',
|
|
49
|
+
type_check_command: null,
|
|
50
|
+
reason,
|
|
51
|
+
missing_modules: missingModules,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeTcpPort(value, fallback, name = 'TCP port') {
|
|
56
|
+
const candidate = value == null || value === '' ? fallback : value;
|
|
57
|
+
const serialized = String(candidate);
|
|
58
|
+
if (!/^\d+$/.test(serialized)) {
|
|
59
|
+
throw new Error(`${name} must be a valid TCP port`);
|
|
60
|
+
}
|
|
61
|
+
const port = Number(serialized);
|
|
62
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
63
|
+
throw new Error(`${name} must be a valid TCP port`);
|
|
64
|
+
}
|
|
65
|
+
return port;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildListeningPortLookupArgs(port) {
|
|
69
|
+
const validatedPort = normalizeTcpPort(port, 0);
|
|
70
|
+
return [`-tiTCP:${validatedPort}`, '-sTCP:LISTEN'];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasRuntimeCompilerPluginConsumer(projectRoot, sourceRoot) {
|
|
74
|
+
const root = path.resolve(projectRoot, sourceRoot || 'src');
|
|
75
|
+
const stack = [root];
|
|
76
|
+
let scannedFiles = 0;
|
|
77
|
+
let scannedBytes = 0;
|
|
78
|
+
const maxFiles = 2000;
|
|
79
|
+
const maxBytes = 16 * 1024 * 1024;
|
|
80
|
+
const consumerPatterns = [
|
|
81
|
+
/\bSwaggerModule\s*\.\s*(?:createDocument|loadPluginMetadata)\b/,
|
|
82
|
+
/\bcreateDocument\s*\(/,
|
|
83
|
+
/\bloadPluginMetadata\s*\(/,
|
|
84
|
+
/\bDevTools(?:V2)?Module\s*\.\s*mount\b/,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(root)) return true;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const current = stack.pop();
|
|
92
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
if (!['node_modules', 'dist', '.git'].includes(entry.name)) {
|
|
95
|
+
stack.push(path.join(current, entry.name));
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!entry.isFile() || !/\.(?:[cm]?js|tsx?)$/.test(entry.name)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
scannedFiles += 1;
|
|
103
|
+
const filePath = path.join(current, entry.name);
|
|
104
|
+
const stat = fs.statSync(filePath);
|
|
105
|
+
scannedBytes += stat.size;
|
|
106
|
+
if (scannedFiles > maxFiles || scannedBytes > maxBytes) return true;
|
|
107
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
108
|
+
if (consumerPatterns.some(pattern => pattern.test(source))) return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// If we cannot prove the compiler plugin is unobservable at runtime, keep TSC.
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolvePreviewServerCommand(options = {}) {
|
|
119
|
+
const projectRoot = options.project_root || path.resolve(__dirname, '..');
|
|
120
|
+
let packageJson = options.package_json;
|
|
121
|
+
if (!packageJson) {
|
|
122
|
+
try {
|
|
123
|
+
packageJson = JSON.parse(
|
|
124
|
+
fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
|
|
125
|
+
);
|
|
126
|
+
} catch {
|
|
127
|
+
packageJson = {};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let nestCliConfig = options.nest_cli_config;
|
|
132
|
+
if (!nestCliConfig) {
|
|
133
|
+
try {
|
|
134
|
+
nestCliConfig = JSON.parse(
|
|
135
|
+
fs.readFileSync(path.join(projectRoot, 'nest-cli.json'), 'utf8')
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
nestCliConfig = {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const serverScript = packageJson?.scripts?.['dev:server'] || '';
|
|
143
|
+
const normalizedServerScript = serverScript
|
|
144
|
+
.trim()
|
|
145
|
+
.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '')
|
|
146
|
+
.replace(/\s+/g, ' ');
|
|
147
|
+
const isNestWatchScript = normalizedServerScript === 'nest start --watch';
|
|
148
|
+
if (!isNestWatchScript) {
|
|
149
|
+
return {
|
|
150
|
+
command: 'npm',
|
|
151
|
+
args: ['run', 'dev:server'],
|
|
152
|
+
compiler: 'configured',
|
|
153
|
+
type_check: false,
|
|
154
|
+
type_check_mode: 'configured',
|
|
155
|
+
type_check_command: null,
|
|
156
|
+
reason: 'custom_server_script',
|
|
157
|
+
missing_modules: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const configuredBuilder = nestCliConfig?.compilerOptions?.builder;
|
|
162
|
+
if (configuredBuilder && configuredBuilder !== 'tsc') {
|
|
163
|
+
return {
|
|
164
|
+
command: 'npm',
|
|
165
|
+
args: ['run', 'dev:server'],
|
|
166
|
+
compiler: 'configured',
|
|
167
|
+
type_check: false,
|
|
168
|
+
type_check_mode: 'configured',
|
|
169
|
+
type_check_command: null,
|
|
170
|
+
reason: 'custom_nest_builder',
|
|
171
|
+
missing_modules: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const serverTypecheckScript =
|
|
176
|
+
packageJson?.scripts?.['type:check:server'] || '';
|
|
177
|
+
const normalizedTypecheckScript = serverTypecheckScript
|
|
178
|
+
.trim()
|
|
179
|
+
.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '')
|
|
180
|
+
.replace(/\s+/g, ' ');
|
|
181
|
+
const isNonEmittingTscScript =
|
|
182
|
+
/^(?:npx )?tsc --noEmit --project [A-Za-z0-9_./-]+$/.test(
|
|
183
|
+
normalizedTypecheckScript
|
|
184
|
+
);
|
|
185
|
+
if (!isNonEmittingTscScript) {
|
|
186
|
+
return createTscServerCommand('server_typecheck_script_unavailable');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const previewCapability = nestCliConfig?.['x-miaoda-preview'];
|
|
190
|
+
if (
|
|
191
|
+
previewCapability?.compiler !== 'swc' ||
|
|
192
|
+
previewCapability?.typeCheck !== 'deferred'
|
|
193
|
+
) {
|
|
194
|
+
return createTscServerCommand('preview_swc_capability_not_declared');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const compilerPlugins = Array.isArray(nestCliConfig?.compilerOptions?.plugins)
|
|
198
|
+
? nestCliConfig.compilerOptions.plugins
|
|
199
|
+
: [];
|
|
200
|
+
const compilerPluginNames = compilerPlugins.map(plugin =>
|
|
201
|
+
typeof plugin === 'string' ? plugin : plugin?.name || ''
|
|
202
|
+
);
|
|
203
|
+
if (
|
|
204
|
+
compilerPluginNames.some(pluginName => pluginName !== '@nestjs/swagger')
|
|
205
|
+
) {
|
|
206
|
+
return createTscServerCommand('unsupported_nest_compiler_plugins');
|
|
207
|
+
}
|
|
208
|
+
if (compilerPluginNames.includes('@nestjs/swagger')) {
|
|
209
|
+
const runtimeConsumer =
|
|
210
|
+
typeof options.has_runtime_compiler_plugin_consumer === 'boolean'
|
|
211
|
+
? options.has_runtime_compiler_plugin_consumer
|
|
212
|
+
: hasRuntimeCompilerPluginConsumer(
|
|
213
|
+
projectRoot,
|
|
214
|
+
nestCliConfig?.sourceRoot || 'src'
|
|
215
|
+
);
|
|
216
|
+
if (runtimeConsumer) {
|
|
217
|
+
return createTscServerCommand(
|
|
218
|
+
'runtime_compiler_plugin_consumer_detected'
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const resolveModule =
|
|
224
|
+
options.resolve_module ||
|
|
225
|
+
(moduleName => {
|
|
226
|
+
const resolved = require.resolve(moduleName, { paths: [projectRoot] });
|
|
227
|
+
if (moduleName === '@swc/core') {
|
|
228
|
+
const swc = require(resolved);
|
|
229
|
+
if (typeof swc.transformSync !== 'function') {
|
|
230
|
+
throw new Error('@swc/core native binding is unavailable');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return resolved;
|
|
234
|
+
});
|
|
235
|
+
const requiredModules = ['@swc/cli', '@swc/core'];
|
|
236
|
+
const missingModules = requiredModules.filter(moduleName => {
|
|
237
|
+
try {
|
|
238
|
+
resolveModule(moduleName);
|
|
239
|
+
return false;
|
|
240
|
+
} catch {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (missingModules.length > 0) {
|
|
246
|
+
return createTscServerCommand('swc_dependencies_missing', missingModules);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
command: 'npm',
|
|
251
|
+
args: ['run', 'dev:server', '--', '--builder', 'swc'],
|
|
252
|
+
compiler: 'swc',
|
|
253
|
+
type_check: true,
|
|
254
|
+
type_check_mode: 'deferred_tsc_watch',
|
|
255
|
+
type_check_command: {
|
|
256
|
+
command: 'npm',
|
|
257
|
+
args: [
|
|
258
|
+
'run',
|
|
259
|
+
'type:check:server',
|
|
260
|
+
'--',
|
|
261
|
+
'--watch',
|
|
262
|
+
'--preserveWatchOutput',
|
|
263
|
+
'--locale',
|
|
264
|
+
'en',
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
reason: 'swc_dependencies_ready',
|
|
268
|
+
missing_modules: [],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createDeferredTypecheckGate() {
|
|
273
|
+
let initialBackendReadySeen = false;
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
onBackendReady() {
|
|
277
|
+
if (!initialBackendReadySeen) {
|
|
278
|
+
initialBackendReadySeen = true;
|
|
279
|
+
return {
|
|
280
|
+
should_start: false,
|
|
281
|
+
reason: 'initial_preview_startup',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
should_start: true,
|
|
286
|
+
reason: 'backend_restart',
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
onCompilerOutput(line) {
|
|
290
|
+
if (
|
|
291
|
+
!initialBackendReadySeen ||
|
|
292
|
+
!/Successfully compiled:\s+\d+\s+files?\s+with\s+swc\b/i.test(line)
|
|
293
|
+
) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
should_start: true,
|
|
298
|
+
reason: 'source_change',
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parseTscWatchSummary(line) {
|
|
305
|
+
const match = /Found\s+(\d+)\s+errors?\.\s+Watching for file changes\./i.exec(
|
|
306
|
+
line
|
|
307
|
+
);
|
|
308
|
+
if (!match) return null;
|
|
309
|
+
const errorCount = Number(match[1]);
|
|
310
|
+
return { error_count: errorCount, passed: errorCount === 0 };
|
|
311
|
+
}
|
|
312
|
+
|
|
40
313
|
function connectTcpOnce({ host, port, timeout_ms }) {
|
|
41
314
|
return new Promise(resolve => {
|
|
42
315
|
const socket = net.createConnection({ host, port });
|
|
@@ -127,6 +400,11 @@ async function waitForTcpReady(options) {
|
|
|
127
400
|
}
|
|
128
401
|
|
|
129
402
|
module.exports = {
|
|
403
|
+
buildListeningPortLookupArgs,
|
|
404
|
+
createDeferredTypecheckGate,
|
|
130
405
|
createPreviewPhaseReporter,
|
|
406
|
+
normalizeTcpPort,
|
|
407
|
+
parseTscWatchSummary,
|
|
408
|
+
resolvePreviewServerCommand,
|
|
131
409
|
waitForTcpReady,
|
|
132
410
|
};
|