@lark-apaas/fullstack-cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/templates/scripts/build.sh +87 -25
- package/templates/scripts/prune-smart.js +247 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "CLI tool for fullstack template management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@lark-apaas/devtool-kits": "^1.0.0",
|
|
33
|
+
"@vercel/nft": "^0.30.3",
|
|
33
34
|
"cac": "^6.7.14",
|
|
34
35
|
"dotenv": "^16.0.0",
|
|
35
36
|
"drizzle-kit": "0.31.5"
|
|
@@ -1,39 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# This file is auto-generated by @lark-apaas/fullstack-cli
|
|
3
|
-
|
|
4
3
|
set -euo pipefail
|
|
5
4
|
|
|
6
5
|
ROOT_DIR="$(pwd)"
|
|
7
6
|
OUT_DIR="$ROOT_DIR/dist/server"
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
# 记录总开始时间
|
|
9
|
+
TOTAL_START=$(node -e "console.log(Date.now())")
|
|
10
|
+
|
|
11
|
+
# 打印耗时的辅助函数
|
|
12
|
+
print_time() {
|
|
13
|
+
local start=$1
|
|
14
|
+
local end=$(node -e "console.log(Date.now())")
|
|
15
|
+
local elapsed=$((end - start))
|
|
16
|
+
local seconds=$((elapsed / 1000))
|
|
17
|
+
local ms=$((elapsed % 1000))
|
|
18
|
+
echo " ⏱️ 耗时: ${seconds}.$(printf "%03d" $ms)s"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# ==================== 步骤 0 ====================
|
|
22
|
+
echo "📝 [0/4] 更新 openapi 代码"
|
|
23
|
+
STEP_START=$(node -e "console.log(Date.now())")
|
|
12
24
|
npm run gen:openapi
|
|
25
|
+
print_time $STEP_START
|
|
26
|
+
echo ""
|
|
13
27
|
|
|
14
|
-
|
|
15
|
-
echo "1
|
|
16
|
-
|
|
28
|
+
# ==================== 步骤 1 ====================
|
|
29
|
+
echo "🗑️ [1/4] 清理 dist 目录"
|
|
30
|
+
STEP_START=$(node -e "console.log(Date.now())")
|
|
17
31
|
rm -rf "$ROOT_DIR/dist"
|
|
32
|
+
print_time $STEP_START
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# ==================== 步骤 2 ====================
|
|
36
|
+
echo "🔨 [2/4] 并行构建 server 和 client"
|
|
37
|
+
STEP_START=$(node -e "console.log(Date.now())")
|
|
38
|
+
|
|
39
|
+
# 并行构建
|
|
40
|
+
echo " ├─ 启动 server 构建..."
|
|
41
|
+
npm run build:server > /tmp/build-server.log 2>&1 &
|
|
42
|
+
SERVER_PID=$!
|
|
43
|
+
|
|
44
|
+
echo " ├─ 启动 client 构建..."
|
|
45
|
+
npm run build:client > /tmp/build-client.log 2>&1 &
|
|
46
|
+
CLIENT_PID=$!
|
|
47
|
+
|
|
48
|
+
# 等待两个构建完成
|
|
49
|
+
SERVER_EXIT=0
|
|
50
|
+
CLIENT_EXIT=0
|
|
51
|
+
|
|
52
|
+
wait $SERVER_PID || SERVER_EXIT=$?
|
|
53
|
+
wait $CLIENT_PID || CLIENT_EXIT=$?
|
|
18
54
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
echo "
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
# 检查构建结果
|
|
56
|
+
if [ $SERVER_EXIT -ne 0 ]; then
|
|
57
|
+
echo " ❌ Server 构建失败"
|
|
58
|
+
cat /tmp/build-server.log
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ $CLIENT_EXIT -ne 0 ]; then
|
|
63
|
+
echo " ❌ Client 构建失败"
|
|
64
|
+
cat /tmp/build-client.log
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
echo " ✅ Server 构建完成"
|
|
69
|
+
echo " ✅ Client 构建完成"
|
|
70
|
+
print_time $STEP_START
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
# ==================== 步骤 3 ====================
|
|
74
|
+
echo "📦 [3/4] 准备 server 依赖产物"
|
|
75
|
+
STEP_START=$(node -e "console.log(Date.now())")
|
|
24
76
|
|
|
25
|
-
echo "===================="
|
|
26
|
-
echo "3) 准备 server 产物目录"
|
|
27
|
-
echo "===================="
|
|
28
77
|
mkdir -p "$OUT_DIR/dist/client"
|
|
29
78
|
|
|
30
79
|
# 拷贝 HTML
|
|
31
80
|
cp "$ROOT_DIR/dist/client/"*.html "$OUT_DIR/dist/client/" || true
|
|
32
81
|
|
|
33
|
-
# 拷贝 package.json
|
|
34
|
-
cp "$ROOT_DIR/package.json" "$OUT_DIR/"
|
|
35
|
-
[ -f "$ROOT_DIR/pnpm-lock.yaml" ] && cp "$ROOT_DIR/pnpm-lock.yaml" "$OUT_DIR/"
|
|
36
|
-
|
|
37
82
|
# 拷贝 run.sh 文件
|
|
38
83
|
cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
|
|
39
84
|
|
|
@@ -41,11 +86,28 @@ cp "$ROOT_DIR/scripts/run.sh" "$OUT_DIR/"
|
|
|
41
86
|
rm -rf "$ROOT_DIR/dist/scripts"
|
|
42
87
|
rm -rf "$ROOT_DIR/dist/tsconfig.node.tsbuildinfo"
|
|
43
88
|
|
|
44
|
-
|
|
45
|
-
echo "
|
|
46
|
-
|
|
89
|
+
print_time $STEP_START
|
|
90
|
+
echo ""
|
|
91
|
+
|
|
92
|
+
# ==================== 步骤 4 ====================
|
|
93
|
+
echo "✂️ [4/4] 智能依赖裁剪"
|
|
94
|
+
STEP_START=$(node -e "console.log(Date.now())")
|
|
95
|
+
|
|
96
|
+
# 分析实际依赖、复制并裁剪 node_modules、生成精简的 package.json
|
|
97
|
+
node "$ROOT_DIR/scripts/prune-smart.js"
|
|
98
|
+
|
|
99
|
+
print_time $STEP_START
|
|
100
|
+
echo ""
|
|
47
101
|
|
|
48
|
-
|
|
49
|
-
|
|
102
|
+
# 总耗时
|
|
103
|
+
echo "构建完成"
|
|
104
|
+
print_time $TOTAL_START
|
|
50
105
|
|
|
51
|
-
|
|
106
|
+
# 输出产物信息
|
|
107
|
+
DIST_SIZE=$(du -sh "$OUT_DIR" | cut -f1)
|
|
108
|
+
NODE_MODULES_SIZE=$(du -sh "$OUT_DIR/node_modules" | cut -f1)
|
|
109
|
+
echo ""
|
|
110
|
+
echo "📊 构建产物统计:"
|
|
111
|
+
echo " 总大小: $DIST_SIZE"
|
|
112
|
+
echo " node_modules: $NODE_MODULES_SIZE"
|
|
113
|
+
echo ""
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is auto-generated by @lark-apaas/fullstack-cli
|
|
3
|
+
|
|
4
|
+
const { nodeFileTrace } = require('@vercel/nft');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
9
|
+
const DIST_SERVER_DIR = path.join(ROOT_DIR, 'dist/server');
|
|
10
|
+
const ROOT_PACKAGE_JSON = path.join(ROOT_DIR, 'package.json');
|
|
11
|
+
const ROOT_NODE_MODULES = path.join(ROOT_DIR, 'node_modules');
|
|
12
|
+
const OUT_NODE_MODULES = path.join(DIST_SERVER_DIR, 'node_modules');
|
|
13
|
+
const OUT_PACKAGE_JSON = path.join(DIST_SERVER_DIR, 'package.json');
|
|
14
|
+
|
|
15
|
+
// Server 入口文件
|
|
16
|
+
const SERVER_ENTRY = path.join(DIST_SERVER_DIR, 'main.js');
|
|
17
|
+
|
|
18
|
+
// Node.js 内置模块列表
|
|
19
|
+
const BUILTIN_MODULES = new Set([
|
|
20
|
+
'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
|
|
21
|
+
'events', 'fs', 'http', 'http2', 'https', 'net', 'os', 'path', 'stream',
|
|
22
|
+
'string_decoder', 'timers', 'tls', 'url', 'util', 'v8', 'vm', 'zlib',
|
|
23
|
+
'async_hooks', 'perf_hooks', 'worker_threads', 'inspector', 'trace_events'
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从完整路径中提取 npm 包名(优化版)
|
|
28
|
+
*/
|
|
29
|
+
const nodeModulesSep = 'node_modules' + path.sep;
|
|
30
|
+
function extractPackageName(filePath) {
|
|
31
|
+
const idx = filePath.lastIndexOf(nodeModulesSep);
|
|
32
|
+
if (idx === -1) return null;
|
|
33
|
+
|
|
34
|
+
const afterNodeModules = filePath.slice(idx + nodeModulesSep.length);
|
|
35
|
+
const slashIdx = afterNodeModules.indexOf(path.sep);
|
|
36
|
+
|
|
37
|
+
// 处理 scoped package (@xxx/yyy)
|
|
38
|
+
if (afterNodeModules[0] === '@') {
|
|
39
|
+
const secondSlash = afterNodeModules.indexOf(path.sep, slashIdx + 1);
|
|
40
|
+
return secondSlash === -1
|
|
41
|
+
? afterNodeModules
|
|
42
|
+
: afterNodeModules.slice(0, secondSlash);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return slashIdx === -1 ? afterNodeModules : afterNodeModules.slice(0, slashIdx);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 递归删除目录
|
|
50
|
+
*/
|
|
51
|
+
function removeDir(dir) {
|
|
52
|
+
if (fs.existsSync(dir)) {
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 递归复制目录(优化版:优先使用硬链接)
|
|
59
|
+
*/
|
|
60
|
+
function copyDir(src, dest, stats = { hardLinks: 0, copies: 0 }) {
|
|
61
|
+
if (!fs.existsSync(dest)) {
|
|
62
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
66
|
+
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const srcPath = path.join(src, entry.name);
|
|
69
|
+
const destPath = path.join(dest, entry.name);
|
|
70
|
+
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
copyDir(srcPath, destPath, stats);
|
|
73
|
+
} else {
|
|
74
|
+
try {
|
|
75
|
+
// 优先使用硬链接(CI 环境中安全,速度快,节省空间)
|
|
76
|
+
fs.linkSync(srcPath, destPath);
|
|
77
|
+
stats.hardLinks++;
|
|
78
|
+
} catch {
|
|
79
|
+
// 硬链接失败时回退到复制(如跨文件系统)
|
|
80
|
+
fs.copyFileSync(srcPath, destPath);
|
|
81
|
+
stats.copies++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return stats;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 选择性复制包
|
|
91
|
+
*/
|
|
92
|
+
function copyPackagesSelectively(packages, rootNodeModules, outNodeModules) {
|
|
93
|
+
const copiedCount = { success: 0, failed: 0 };
|
|
94
|
+
const fileStats = { hardLinks: 0, copies: 0 };
|
|
95
|
+
|
|
96
|
+
// 确保输出目录存在
|
|
97
|
+
if (fs.existsSync(outNodeModules)) {
|
|
98
|
+
removeDir(outNodeModules);
|
|
99
|
+
}
|
|
100
|
+
fs.mkdirSync(outNodeModules, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// 复制每个包
|
|
103
|
+
for (const pkg of packages) {
|
|
104
|
+
const srcPath = path.join(rootNodeModules, pkg);
|
|
105
|
+
const destPath = path.join(outNodeModules, pkg);
|
|
106
|
+
|
|
107
|
+
if (!fs.existsSync(srcPath)) {
|
|
108
|
+
copiedCount.failed++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// 确保父目录存在(处理 scoped packages)
|
|
114
|
+
const destDir = path.dirname(destPath);
|
|
115
|
+
if (!fs.existsSync(destDir)) {
|
|
116
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 复制包目录,收集统计信息
|
|
120
|
+
copyDir(srcPath, destPath, fileStats);
|
|
121
|
+
copiedCount.success++;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(` ⚠️ 复制失败: ${pkg} - ${err.message}`);
|
|
124
|
+
copiedCount.failed++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { copiedCount, fileStats };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 主函数:智能裁剪
|
|
133
|
+
*/
|
|
134
|
+
async function smartPrune() {
|
|
135
|
+
console.log('🔍 开始智能依赖分析...\n');
|
|
136
|
+
|
|
137
|
+
const totalStartTime = Date.now();
|
|
138
|
+
|
|
139
|
+
// 1. 检查入口文件是否存在
|
|
140
|
+
if (!fs.existsSync(SERVER_ENTRY)) {
|
|
141
|
+
console.error(`❌ 入口文件不存在: ${SERVER_ENTRY}`);
|
|
142
|
+
console.error(' 请先运行 npm run build:server');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`📂 分析入口文件: ${path.relative(ROOT_DIR, SERVER_ENTRY)}`);
|
|
147
|
+
|
|
148
|
+
// 2. 使用 @vercel/nft 追踪依赖
|
|
149
|
+
console.log('🔎 追踪实际依赖...');
|
|
150
|
+
const analyzeStart = Date.now();
|
|
151
|
+
|
|
152
|
+
const { fileList } = await nodeFileTrace([SERVER_ENTRY], {
|
|
153
|
+
base: ROOT_DIR,
|
|
154
|
+
processCwd: ROOT_DIR,
|
|
155
|
+
ts: false, // 禁用 TS 解析
|
|
156
|
+
conditions: ['node', 'production'], // 只解析 Node.js 生产环境的导出
|
|
157
|
+
exportsOnly: true, // 只使用 package.json 的 exports 字段
|
|
158
|
+
analysis: {
|
|
159
|
+
emitGlobs: false, // 禁用 glob 分析
|
|
160
|
+
computeFileReferences: false, // 禁用文件引用计算
|
|
161
|
+
evaluatePureExpressions: false, // 禁用纯表达式求值
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const analyzeElapsed = Date.now() - analyzeStart;
|
|
166
|
+
console.log(` ✅ 分析完成,耗时: ${analyzeElapsed}ms\n`);
|
|
167
|
+
|
|
168
|
+
// 3. 提取 npm 包名(优化:减少字符串操作)
|
|
169
|
+
const requiredPackages = new Set();
|
|
170
|
+
|
|
171
|
+
for (const file of fileList) {
|
|
172
|
+
// 快速跳过非 node_modules 文件
|
|
173
|
+
if (!file.includes('node_modules')) continue;
|
|
174
|
+
|
|
175
|
+
const pkgName = extractPackageName(file);
|
|
176
|
+
if (pkgName && !BUILTIN_MODULES.has(pkgName)) {
|
|
177
|
+
requiredPackages.add(pkgName);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`📦 实际需要 ${requiredPackages.size} 个 npm 包\n`);
|
|
182
|
+
|
|
183
|
+
// 4. 选择性复制包(只复制需要的)
|
|
184
|
+
console.log('📋 选择性复制 node_modules(仅复制需要的包)...');
|
|
185
|
+
const copyStart = Date.now();
|
|
186
|
+
|
|
187
|
+
const sortedPackages = Array.from(requiredPackages).sort();
|
|
188
|
+
const { copiedCount, fileStats } = copyPackagesSelectively(sortedPackages, ROOT_NODE_MODULES, OUT_NODE_MODULES);
|
|
189
|
+
|
|
190
|
+
const copyElapsed = Date.now() - copyStart;
|
|
191
|
+
console.log(` ✅ 复制完成,耗时: ${copyElapsed}ms`);
|
|
192
|
+
console.log(` 成功: ${copiedCount.success} 个包,失败: ${copiedCount.failed} 个`);
|
|
193
|
+
console.log(` 硬链接: ${fileStats.hardLinks} 个文件,复制: ${fileStats.copies} 个文件\n`);
|
|
194
|
+
|
|
195
|
+
// 5. 读取原始 package.json 并生成精简版本
|
|
196
|
+
const originalPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_JSON, 'utf8'));
|
|
197
|
+
|
|
198
|
+
// 优化:直接构建 dependencies,避免多次对象展开
|
|
199
|
+
const prunedDependencies = {};
|
|
200
|
+
const allDeps = originalPackage.dependencies || {};
|
|
201
|
+
const allDevDeps = originalPackage.devDependencies || {};
|
|
202
|
+
|
|
203
|
+
for (const pkg of requiredPackages) {
|
|
204
|
+
const version = allDeps[pkg] || allDevDeps[pkg];
|
|
205
|
+
if (version) {
|
|
206
|
+
prunedDependencies[pkg] = version;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const prunedPackage = {
|
|
211
|
+
name: originalPackage.name,
|
|
212
|
+
version: originalPackage.version,
|
|
213
|
+
private: true,
|
|
214
|
+
dependencies: prunedDependencies,
|
|
215
|
+
scripts: {
|
|
216
|
+
start: originalPackage.scripts?.start || 'node main.js'
|
|
217
|
+
},
|
|
218
|
+
engines: originalPackage.engines
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
fs.writeFileSync(OUT_PACKAGE_JSON, JSON.stringify(prunedPackage, null, 2));
|
|
222
|
+
|
|
223
|
+
const totalElapsed = Date.now() - totalStartTime;
|
|
224
|
+
|
|
225
|
+
// 6. 输出统计信息
|
|
226
|
+
console.log('='.repeat(60));
|
|
227
|
+
console.log('📊 智能裁剪统计:');
|
|
228
|
+
console.log('='.repeat(60));
|
|
229
|
+
console.log(` 需要的包数量: ${requiredPackages.size}`);
|
|
230
|
+
console.log(` 成功复制: ${copiedCount.success} 个包`);
|
|
231
|
+
console.log(` 失败: ${copiedCount.failed} 个包`);
|
|
232
|
+
console.log(` 硬链接文件: ${fileStats.hardLinks} 个`);
|
|
233
|
+
console.log(` 复制文件: ${fileStats.copies} 个`);
|
|
234
|
+
console.log(` 分析耗时: ${analyzeElapsed}ms`);
|
|
235
|
+
console.log(` 复制耗时: ${copyElapsed}ms`);
|
|
236
|
+
console.log(` 总耗时: ${totalElapsed}ms`);
|
|
237
|
+
console.log('='.repeat(60) + '\n');
|
|
238
|
+
|
|
239
|
+
console.log('✅ 智能裁剪完成!');
|
|
240
|
+
console.log(`📍 精简的 package.json 已保存到: ${OUT_PACKAGE_JSON}\n`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 运行智能裁剪
|
|
244
|
+
smartPrune().catch(err => {
|
|
245
|
+
console.error('❌ 智能裁剪失败:', err);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|