@qse/edu-scripts 2.0.4 → 2.1.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/dist/cli.mjs CHANGED
@@ -1,35 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ import chalk from "chalk";
3
4
  import fs from "fs-extra";
4
- import path from "path";
5
+ import semver from "semver";
5
6
  import { globby, globbySync } from "globby";
6
7
  import { fileURLToPath } from "node:url";
7
- import chalk from "chalk";
8
- import semver from "semver";
8
+ import path from "path";
9
9
  import yargs from "yargs";
10
+ import { checkbox, input, select } from "@inquirer/prompts";
11
+ import ora from "ora";
10
12
  import { rspack } from "@rspack/core";
11
- import { RspackDevServer } from "@rspack/dev-server";
13
+ import filesize from "filesize";
14
+ import { gzipSizeSync } from "gzip-size";
15
+ import recursive from "recursive-readdir";
16
+ import stripAnsi from "strip-ansi";
12
17
  import { RsdoctorRspackPlugin } from "@rsdoctor/rspack-plugin";
13
18
  import ReactRefreshPlugin from "@rspack/plugin-react-refresh";
14
19
  import HtmlWebpackPlugin from "html-webpack-plugin";
15
20
  import path$1 from "node:path";
16
21
  import chokidar from "chokidar";
22
+ import cookieParser from "cookie-parser";
17
23
  import { debounce, memoize } from "es-toolkit";
18
24
  import express from "express";
19
- import cookieParser from "cookie-parser";
20
25
  import multer from "multer";
21
26
  import { pathToRegexp } from "path-to-regexp";
22
- import filesize from "filesize";
23
- import recursive from "recursive-readdir";
24
- import stripAnsi from "strip-ansi";
25
- import { gzipSizeSync } from "gzip-size";
26
- import { sshSftp } from "@qse/ssh-sftp";
27
- import { parse, transformFromAstSync, traverse, types } from "@babel/core";
28
- import ora from "ora";
29
- import { format } from "prettier";
30
- import { checkbox, input, select } from "@inquirer/prompts";
31
27
  import cp from "child_process";
32
28
  import tmp from "tmp";
29
+ import { sshSftp } from "@qse/ssh-sftp";
30
+ import { format } from "prettier";
31
+ import { parse, transformFromAstSync, traverse, types } from "@babel/core";
32
+ import { RspackDevServer } from "@rspack/dev-server";
33
33
 
34
34
  //#region src/utils/esm-register.ts
35
35
  const { register } = createRequire(import.meta.url)("@swc-node/register/register");
@@ -155,6 +155,228 @@ if (appPkg$2.eslintConfig) {
155
155
  console.log(chalk.yellow("export default config"));
156
156
  }
157
157
 
158
+ //#endregion
159
+ //#region src/auto-refactor.ts
160
+ const pkg$1 = fs.readJsonSync(paths.resolveOwn("package.json"));
161
+ async function step(msg, callback) {
162
+ const spinner = ora(msg).start();
163
+ try {
164
+ await callback(spinner);
165
+ spinner.succeed();
166
+ } catch (error) {
167
+ spinner.fail();
168
+ throw error;
169
+ }
170
+ }
171
+ async function autoRefactor() {
172
+ if (await fs.pathExists(paths.public)) {
173
+ console.log(chalk.green("已完成改造,不需要重复执行,如果运行报错请查看文档"));
174
+ console.log(`文档: ${chalk.underline(pkg$1.homepage)}`);
175
+ process.exit(0);
176
+ }
177
+ const appPkg = fs.readJsonSync(paths.package);
178
+ const name = await input({
179
+ message: "请输入模块名称,要求唯一,不能与其他工程相同",
180
+ default: appPkg.name,
181
+ validate: (v) => /^[a-z][a-z0-9_-]+$/.test(v) || "请按格式输入 /^[a-z][a-z0-9_-]+$/ 只能使用小写字母、连字符、下划线"
182
+ });
183
+ const version = await input({
184
+ message: "版本号",
185
+ default: "1.0.0",
186
+ validate: (v) => /^\d+\.\d+\.\d+$/.test(v) || "请按格式输入 /^\\d+\\.\\d+\\.\\d+$/ 例如: 1.0.0"
187
+ });
188
+ const mode = await select({
189
+ message: "项目使用的哪种模式",
190
+ choices: [
191
+ {
192
+ name: "教育子工程模式",
193
+ value: ""
194
+ },
195
+ {
196
+ name: "教育主工程模式",
197
+ value: "main"
198
+ },
199
+ {
200
+ name: "独立模式",
201
+ value: "single"
202
+ }
203
+ ]
204
+ });
205
+ let deploy = [];
206
+ if (mode !== "single") deploy = await checkbox({
207
+ message: "项目需要部署到哪里",
208
+ choices: [
209
+ {
210
+ name: "校端",
211
+ value: "-s"
212
+ },
213
+ {
214
+ name: "局端",
215
+ value: "-b"
216
+ },
217
+ {
218
+ name: "公文端",
219
+ value: "-d"
220
+ }
221
+ ],
222
+ validate: (v) => !!v && v.length > 0 || "必须选一个"
223
+ });
224
+ const answers = {
225
+ name,
226
+ version,
227
+ mode,
228
+ deploy
229
+ };
230
+ appPkg.name = answers.name;
231
+ appPkg.version = answers.version;
232
+ await step("创建 public 文件夹", async () => {
233
+ await fs.mkdir(paths.public);
234
+ });
235
+ await step("移动 js 文件夹", async () => {
236
+ if (await fs.pathExists(path.resolve(paths.src, "js"))) await fs.move(path.resolve(paths.src, "js"), path.resolve(paths.public, "js"));
237
+ });
238
+ await step("移动 html 文件", async () => {
239
+ const HTMLFiles = await globby("*.html", { cwd: paths.src });
240
+ for (const file of HTMLFiles) await fs.move(path.resolve(paths.src, file), path.resolve(paths.public, file));
241
+ });
242
+ await step("删除 dll 文件夹", async () => {
243
+ if (await fs.pathExists(path.resolve(paths.src, "dll"))) await fs.remove(path.resolve(paths.src, "dll"));
244
+ });
245
+ await step("删除没用的 babel 和 webpack 配置", async () => {
246
+ const deleteFiles = [
247
+ ...await globby("{.,*}babel*"),
248
+ ...await globby("*webpack*"),
249
+ ...await globby("package-lock.json"),
250
+ paths.nodeModules,
251
+ paths.sshSftp
252
+ ];
253
+ for (const filePath of deleteFiles) await fs.remove(filePath);
254
+ });
255
+ await step("设置项目模式", () => {
256
+ if (answers.mode) appPkg.edu = { mode: answers.mode };
257
+ });
258
+ await step("修改 package.json 的 scripts", () => {
259
+ const scripts = appPkg.scripts;
260
+ scripts.start = "edu-scripts start";
261
+ scripts.build = "edu-scripts build";
262
+ scripts.analyze = "edu-scripts build --analyze";
263
+ if (answers.mode !== "single") scripts.deploy = `edu-scripts deploy ${answers.deploy.join(" ")}`;
264
+ else {
265
+ scripts.deploy = `edu-scripts deploy`;
266
+ scripts["commit-dist"] = "edu-scripts commit-dist --rm-local";
267
+ }
268
+ scripts["one-key-deploy"] = "npm version patch";
269
+ scripts.postversion = "npm run build && npm run deploy";
270
+ });
271
+ await step("删除 babel/webpack 相关依赖", (spinner) => {
272
+ const deleteRe = /(babel|autoprefixer|webpack|loader|less|css|sass|hmr|ssh-sftp|regenerator-runtime|nowa|prettier)/i;
273
+ const deletePkgs = {
274
+ dependencies: [],
275
+ devDependencies: []
276
+ };
277
+ Object.entries(deletePkgs).forEach(([scope, pkgs]) => {
278
+ for (const key in appPkg[scope]) if (Object.hasOwnProperty.call(appPkg[scope], key)) {
279
+ if (deleteRe.test(key)) {
280
+ pkgs.push(key);
281
+ delete appPkg[scope][key];
282
+ }
283
+ }
284
+ });
285
+ spinner.clear();
286
+ console.log(`删除的pkgs\n${chalk.green(JSON.stringify(deletePkgs, null, 2))}\n`);
287
+ appPkg.devDependencies["@qse/edu-scripts"] = "^" + pkg$1.version;
288
+ });
289
+ await fs.writeFile(paths.package, JSON.stringify(appPkg, null, 2), "utf-8");
290
+ console.log(chalk.green(`
291
+ 改造还未完成,剩余步骤请查看文档
292
+ ${pkg$1.homepage}#/refactor
293
+
294
+ 运行 npm i 安装依赖
295
+ 运行 edu-scripts start 启动服务
296
+ `));
297
+ }
298
+
299
+ //#endregion
300
+ //#region src/utils/FileSizeReporter.ts
301
+ function canReadAsset(asset) {
302
+ return /\.(js|css)$/.test(asset) && !/service-worker\.js/.test(asset) && !/precache-manifest\.[0-9a-f]+\.js/.test(asset);
303
+ }
304
+ function printFileSizesAfterBuild(webpackStats, previousSizeMap, buildFolder, maxBundleGzipSize, maxChunkGzipSize) {
305
+ const root = previousSizeMap.root;
306
+ const sizes = previousSizeMap.sizes;
307
+ const assets = (webpackStats.stats || [webpackStats]).map((stats) => stats.toJson({
308
+ all: false,
309
+ assets: true
310
+ }).assets.filter((asset) => canReadAsset(asset.name)).map((asset) => {
311
+ const size = gzipSizeSync(fs.readFileSync(path.join(root, asset.name)));
312
+ const previousSize = sizes[removeFileNameHash(root, asset.name)];
313
+ const difference = getDifferenceLabel(size, previousSize);
314
+ return {
315
+ folder: path.join(path.basename(buildFolder), path.dirname(asset.name)),
316
+ name: path.basename(asset.name),
317
+ size,
318
+ sizeLabel: filesize(size) + (difference ? " (" + difference + ")" : "")
319
+ };
320
+ })).reduce((single, all) => all.concat(single), []);
321
+ if (assets.length === 0) return;
322
+ console.log("\ngzip 后文件大小:\n");
323
+ assets.sort((a, b) => b.size - a.size);
324
+ const mainAssetIdx = assets.findIndex((asset) => /_\d+\.\d+\.\d+/.test(asset.name));
325
+ assets.unshift(assets.splice(mainAssetIdx, 1)[0]);
326
+ const longestSizeLabelLength = Math.max.apply(null, assets.map((a) => stripAnsi(a.sizeLabel).length));
327
+ let suggestBundleSplitting = false;
328
+ assets.forEach((asset) => {
329
+ let sizeLabel = asset.sizeLabel;
330
+ const sizeLength = stripAnsi(sizeLabel).length;
331
+ if (sizeLength < longestSizeLabelLength) {
332
+ const rightPadding = " ".repeat(longestSizeLabelLength - sizeLength);
333
+ sizeLabel += rightPadding;
334
+ }
335
+ const isMainBundle = /_\d+\.\d+\.\d+/.test(asset.name);
336
+ const maxRecommendedSize = isMainBundle ? maxBundleGzipSize : maxChunkGzipSize;
337
+ const isLarge = maxRecommendedSize && asset.size > maxRecommendedSize;
338
+ if (isLarge && path.extname(asset.name) === ".js") suggestBundleSplitting = true;
339
+ console.log(" " + (isLarge ? chalk.yellow(sizeLabel) : sizeLabel) + " " + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name));
340
+ if (isMainBundle) console.log("");
341
+ });
342
+ if (suggestBundleSplitting) {
343
+ console.log();
344
+ console.log(chalk.yellow(`产物大小明显大于推荐的大小 (主文件 ${filesize(maxBundleGzipSize)}, chunk ${filesize(maxChunkGzipSize)}, 黄色标注为偏大)`));
345
+ console.log(chalk.yellow("考虑下使用代码分割解决"));
346
+ console.log(chalk.yellow("也可以使用 npm run analyze 命令分析产物"));
347
+ }
348
+ console.log();
349
+ }
350
+ function removeFileNameHash(buildFolder, fileName) {
351
+ return fileName.replace(buildFolder, "").replace(/\\/g, "/").replace(/\/\d+\.\d+\.\d+\//, "/").replace(/\/?(.*)(\.[0-9a-f]+)(\.chunk)?(\.js|\.css)/, (match, p1, p2, p3, p4) => p1 + p4);
352
+ }
353
+ function getDifferenceLabel(currentSize, previousSize) {
354
+ const FIFTY_KILOBYTES = 1024 * 50;
355
+ const difference = currentSize - previousSize;
356
+ const fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
357
+ if (difference >= FIFTY_KILOBYTES) return chalk.red("+" + fileSize);
358
+ else if (difference < FIFTY_KILOBYTES && difference > 0) return chalk.yellow("+" + fileSize);
359
+ else if (difference < 0) return chalk.green(fileSize);
360
+ else return "";
361
+ }
362
+ function measureFileSizesBeforeBuild(buildFolder) {
363
+ return new Promise((resolve) => {
364
+ recursive(buildFolder, (err, fileNames) => {
365
+ let sizes;
366
+ if (!err && fileNames) sizes = fileNames.filter(canReadAsset).reduce((memo, fileName) => {
367
+ const contents = fs.readFileSync(fileName);
368
+ const key = removeFileNameHash(buildFolder, fileName);
369
+ memo[key] = gzipSizeSync(contents);
370
+ return memo;
371
+ }, {});
372
+ resolve({
373
+ root: buildFolder,
374
+ sizes: sizes || {}
375
+ });
376
+ });
377
+ });
378
+ }
379
+
158
380
  //#endregion
159
381
  //#region src/utils/resolveModule.ts
160
382
  function resolveModule(mod) {
@@ -342,10 +564,10 @@ function getWebpackConfig(args, override) {
342
564
  ".json",
343
565
  ".wasm"
344
566
  ],
345
- tsConfig: {
567
+ tsConfig: fs.existsSync(paths.tsconfig) ? {
346
568
  configFile: paths.tsconfig,
347
569
  references: "auto"
348
- }
570
+ } : void 0
349
571
  },
350
572
  stats: false,
351
573
  devtool: isDev ? "cheap-module-source-map" : false,
@@ -790,133 +1012,8 @@ function getConfig(args) {
790
1012
  return webpackConfig;
791
1013
  }
792
1014
 
793
- //#endregion
794
- //#region src/start.ts
795
- async function start(args) {
796
- process.env.NODE_ENV = "development";
797
- process.env.BABEL_ENV = "development";
798
- process.env.BROWSERSLIST = "chrome >= 70";
799
- process.env.WEBPACK_DEV_SERVER_BASE_PORT = "3000";
800
- process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
801
- const basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT;
802
- const port = await RspackDevServer.getFreePort(args.port || process.env.PORT);
803
- if (!(args.port || process.env.PORT) && +port !== +basePort) console.log(chalk.bgYellow(`${basePort} 端口已被占用,现切换到 ${port} 端口运行`));
804
- args.port = port;
805
- process.env.PORT = port;
806
- const config = getConfig(args);
807
- const compiler = rspack(config);
808
- const middleware = rspack.lazyCompilationMiddleware(compiler);
809
- const oldSetupMiddlewares = config.devServer.setupMiddlewares;
810
- config.devServer.setupMiddlewares = (middlewares, devServer) => {
811
- if (oldSetupMiddlewares) middlewares = oldSetupMiddlewares(middlewares, devServer);
812
- middlewares.unshift(middleware);
813
- return middlewares;
814
- };
815
- const devServer = new RspackDevServer(config.devServer, compiler);
816
- devServer.start();
817
- ["SIGINT", "SIGTERM"].forEach(function(sig) {
818
- process.on(sig, function() {
819
- devServer.stop();
820
- process.exit();
821
- });
822
- });
823
- if (process.env.CI !== "true") process.stdin.on("end", function() {
824
- devServer.stop();
825
- process.exit();
826
- });
827
- }
828
-
829
- //#endregion
830
- //#region src/utils/FileSizeReporter.ts
831
- /**
832
- * Copyright (c) 2015-present, Facebook, Inc.
833
- *
834
- * This source code is licensed under the MIT license found in the
835
- * LICENSE file in the root directory of this source tree.
836
- */
837
- function canReadAsset(asset) {
838
- return /\.(js|css)$/.test(asset) && !/service-worker\.js/.test(asset) && !/precache-manifest\.[0-9a-f]+\.js/.test(asset);
839
- }
840
- function printFileSizesAfterBuild(webpackStats, previousSizeMap, buildFolder, maxBundleGzipSize, maxChunkGzipSize) {
841
- const root = previousSizeMap.root;
842
- const sizes = previousSizeMap.sizes;
843
- const assets = (webpackStats.stats || [webpackStats]).map((stats) => stats.toJson({
844
- all: false,
845
- assets: true
846
- }).assets.filter((asset) => canReadAsset(asset.name)).map((asset) => {
847
- const size = gzipSizeSync(fs.readFileSync(path.join(root, asset.name)));
848
- const previousSize = sizes[removeFileNameHash(root, asset.name)];
849
- const difference = getDifferenceLabel(size, previousSize);
850
- return {
851
- folder: path.join(path.basename(buildFolder), path.dirname(asset.name)),
852
- name: path.basename(asset.name),
853
- size,
854
- sizeLabel: filesize(size) + (difference ? " (" + difference + ")" : "")
855
- };
856
- })).reduce((single, all) => all.concat(single), []);
857
- if (assets.length === 0) return;
858
- console.log("\ngzip 后文件大小:\n");
859
- assets.sort((a, b) => b.size - a.size);
860
- const mainAssetIdx = assets.findIndex((asset) => /_\d+\.\d+\.\d+/.test(asset.name));
861
- assets.unshift(assets.splice(mainAssetIdx, 1)[0]);
862
- const longestSizeLabelLength = Math.max.apply(null, assets.map((a) => stripAnsi(a.sizeLabel).length));
863
- let suggestBundleSplitting = false;
864
- assets.forEach((asset) => {
865
- let sizeLabel = asset.sizeLabel;
866
- const sizeLength = stripAnsi(sizeLabel).length;
867
- if (sizeLength < longestSizeLabelLength) {
868
- const rightPadding = " ".repeat(longestSizeLabelLength - sizeLength);
869
- sizeLabel += rightPadding;
870
- }
871
- const isMainBundle = /_\d+\.\d+\.\d+/.test(asset.name);
872
- const maxRecommendedSize = isMainBundle ? maxBundleGzipSize : maxChunkGzipSize;
873
- const isLarge = maxRecommendedSize && asset.size > maxRecommendedSize;
874
- if (isLarge && path.extname(asset.name) === ".js") suggestBundleSplitting = true;
875
- console.log(" " + (isLarge ? chalk.yellow(sizeLabel) : sizeLabel) + " " + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name));
876
- if (isMainBundle) console.log("");
877
- });
878
- if (suggestBundleSplitting) {
879
- console.log();
880
- console.log(chalk.yellow(`产物大小明显大于推荐的大小 (主文件 ${filesize(maxBundleGzipSize)}, chunk ${filesize(maxChunkGzipSize)}, 黄色标注为偏大)`));
881
- console.log(chalk.yellow("考虑下使用代码分割解决"));
882
- console.log(chalk.yellow("也可以使用 npm run analyze 命令分析产物"));
883
- }
884
- console.log();
885
- }
886
- function removeFileNameHash(buildFolder, fileName) {
887
- return fileName.replace(buildFolder, "").replace(/\\/g, "/").replace(/\/\d+\.\d+\.\d+\//, "/").replace(/\/?(.*)(\.[0-9a-f]+)(\.chunk)?(\.js|\.css)/, (match, p1, p2, p3, p4) => p1 + p4);
888
- }
889
- function getDifferenceLabel(currentSize, previousSize) {
890
- const FIFTY_KILOBYTES = 1024 * 50;
891
- const difference = currentSize - previousSize;
892
- const fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
893
- if (difference >= FIFTY_KILOBYTES) return chalk.red("+" + fileSize);
894
- else if (difference < FIFTY_KILOBYTES && difference > 0) return chalk.yellow("+" + fileSize);
895
- else if (difference < 0) return chalk.green(fileSize);
896
- else return "";
897
- }
898
- function measureFileSizesBeforeBuild(buildFolder) {
899
- return new Promise((resolve) => {
900
- recursive(buildFolder, (err, fileNames) => {
901
- let sizes;
902
- if (!err && fileNames) sizes = fileNames.filter(canReadAsset).reduce((memo, fileName) => {
903
- const contents = fs.readFileSync(fileName);
904
- const key = removeFileNameHash(buildFolder, fileName);
905
- memo[key] = gzipSizeSync(contents);
906
- return memo;
907
- }, {});
908
- resolve({
909
- root: buildFolder,
910
- sizes: sizes || {}
911
- });
912
- });
913
- });
914
- }
915
-
916
1015
  //#endregion
917
1016
  //#region src/build.ts
918
- const WARN_AFTER_BUNDLE_GZIP_SIZE = appConfig.single ? 1024 * 1024 : 30 * 1024;
919
- const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
920
1017
  async function build(args) {
921
1018
  process.env.NODE_ENV = "production";
922
1019
  process.env.BABEL_ENV = "production";
@@ -928,7 +1025,8 @@ async function build(args) {
928
1025
  fs.emptyDirSync(paths.dist);
929
1026
  if (appConfig.single) fs.copySync(paths.public, paths.resolveApp("dist"));
930
1027
  if (appConfig.mainProject && fs.existsSync(paths.static)) fs.copySync(paths.static, paths.resolveApp("dist", "static"));
931
- rspack(getConfig(args), (error, stats) => {
1028
+ const config = getConfig(args);
1029
+ rspack(config, (error, stats) => {
932
1030
  if (error) {
933
1031
  console.log(chalk.red("编译失败"));
934
1032
  console.log(chalk.red(error.message || error));
@@ -944,7 +1042,15 @@ async function build(args) {
944
1042
  }));
945
1043
  process.exit(1);
946
1044
  }
947
- printFileSizesAfterBuild(stats, previousSizeMap, paths.dist, WARN_AFTER_BUNDLE_GZIP_SIZE, WARN_AFTER_CHUNK_GZIP_SIZE);
1045
+ const size = {
1046
+ maxEntrypointSize: 25e4,
1047
+ maxAssetSize: 25e4
1048
+ };
1049
+ if (typeof config.performance === "object") {
1050
+ config.performance.maxEntrypointSize = size.maxEntrypointSize;
1051
+ config.performance.maxAssetSize = size.maxAssetSize;
1052
+ }
1053
+ printFileSizesAfterBuild(stats, previousSizeMap, paths.dist, size.maxEntrypointSize, size.maxAssetSize);
948
1054
  if (appConfig.single) console.log(`打包完成,可以使用 ${chalk.green("@qse/ssh-sftp")} 自动部署代码到 v1`);
949
1055
  else console.log(`打包完成,可以运行 ${chalk.green("npx edu-scripts deploy")} 部署代码到 v1`);
950
1056
  console.log();
@@ -957,6 +1063,82 @@ async function build(args) {
957
1063
  });
958
1064
  }
959
1065
 
1066
+ //#endregion
1067
+ //#region src/commit-dist.ts
1068
+ const exec = (cmd, opts) => cp.execSync(cmd, {
1069
+ encoding: "utf-8",
1070
+ stdio: "pipe",
1071
+ ...opts
1072
+ });
1073
+ function validateSVNRoot(root) {
1074
+ const ls = exec(`svn ls ${root}`);
1075
+ return ["trunk", "branches"].every((s) => ls.includes(s));
1076
+ }
1077
+ function getWorkingCopyInfo() {
1078
+ exec(`svn up`);
1079
+ const url = exec(`svn info --show-item url`).trim();
1080
+ const revision = exec(`svn info --show-item last-changed-revision`).trim();
1081
+ const author = exec(`svn info --show-item last-changed-author`).trim();
1082
+ let branch = "trunk";
1083
+ let root = url.replace(/\/trunk$/, "");
1084
+ if (url.includes("/branches/")) {
1085
+ branch = url.split("/").pop();
1086
+ root = url.replace(/\/branches\/[^/]+$/, "");
1087
+ }
1088
+ let distBranchURL = root + "/branches/dist";
1089
+ let distBranchDirURL = distBranchURL + "/" + branch;
1090
+ if (!validateSVNRoot(root)) {
1091
+ console.log(chalk.red("SVN目录不符合规则,必须包含 trunk branches"));
1092
+ process.exit(1);
1093
+ }
1094
+ return {
1095
+ url,
1096
+ branch,
1097
+ revision,
1098
+ author,
1099
+ distBranchURL,
1100
+ distBranchDirURL,
1101
+ root
1102
+ };
1103
+ }
1104
+ function copyDistToRepo(info) {
1105
+ const tmpdir = tmp.dirSync().name;
1106
+ try {
1107
+ exec(`svn ls ${info.distBranchDirURL} --depth empty`);
1108
+ } catch (error) {
1109
+ if (error.message.includes("non-existent")) exec(`svn mkdir ${info.distBranchDirURL} --parents -m "[edu-scripts] create ${info.branch} dist"`);
1110
+ else throw error;
1111
+ }
1112
+ exec(`svn co ${info.distBranchDirURL} ${tmpdir}`);
1113
+ try {
1114
+ exec(`svn rm * --force -q`, { cwd: tmpdir });
1115
+ } catch {}
1116
+ fs.copySync(paths.dist, tmpdir);
1117
+ exec(`svn add * --force --auto-props --parents --depth infinity -q`, { cwd: tmpdir });
1118
+ exec(`svn ci -m "${`[edu-scripts] commit ${info.branch} dist #${info.revision} @${info.author}`}"`, { cwd: tmpdir });
1119
+ fs.removeSync(tmpdir);
1120
+ }
1121
+ async function commitDist(args) {
1122
+ if (!fs.existsSync(paths.dist)) {
1123
+ console.log(chalk.red("未找到 dist 文件夹,请先 edu-scpirts build"));
1124
+ process.exit(1);
1125
+ }
1126
+ if (exec("svn st").trim().length) {
1127
+ console.log(chalk.red("似乎存在未提交的代码,请提交后重试。运行 svn st 查看具体信息"));
1128
+ process.exit(1);
1129
+ }
1130
+ const info = getWorkingCopyInfo();
1131
+ console.log(chalk.green([
1132
+ `分支: ${info.branch}`,
1133
+ `版本: ${info.revision}`,
1134
+ `作者: ${info.author}`,
1135
+ `地址: ${info.distBranchDirURL}`
1136
+ ].join("\n")));
1137
+ copyDistToRepo(info);
1138
+ if (args.rmLocal) fs.removeSync(paths.dist);
1139
+ console.log(chalk.green("提交完成"));
1140
+ }
1141
+
960
1142
  //#endregion
961
1143
  //#region src/utils/changeDeployVersion.ts
962
1144
  const TARGET_IDENTIFIER_NAME = "project_apiArr";
@@ -1134,12 +1316,16 @@ async function normalDeploy(args) {
1134
1316
  if (args.documentshelves) uploadSftpConfigs.push(presetConfig.d);
1135
1317
  if (args.compositionshelves) uploadSftpConfigs.push(presetConfig.c);
1136
1318
  if (args.compositionshelvesDingtalk) uploadSftpConfigs.push(presetConfig.cd);
1319
+ if (uploadSftpConfigs.length === 0 && fs.existsSync(paths.sshSftp)) {
1320
+ const config = fs.readJsonSync(paths.sshSftp);
1321
+ uploadSftpConfigs.push(config);
1322
+ }
1137
1323
  if (uploadSftpConfigs.length === 0) {
1138
1324
  console.log(`
1139
1325
  ${chalk.red("指定 deploy 部署范围")}
1140
1326
  执行 ${chalk.green("npx edu-scripts deploy -h")} 查看具体用法
1141
1327
  `);
1142
- process.exit();
1328
+ process.exit(1);
1143
1329
  }
1144
1330
  const uploadConfig = {
1145
1331
  ...baseConfig,
@@ -1166,223 +1352,6 @@ function deploy(args) {
1166
1352
  else normalDeploy(args);
1167
1353
  }
1168
1354
 
1169
- //#endregion
1170
- //#region src/auto-refactor.ts
1171
- const pkg$1 = fs.readJsonSync(paths.resolveOwn("package.json"));
1172
- async function step(msg, callback) {
1173
- const spinner = ora(msg).start();
1174
- try {
1175
- await callback(spinner);
1176
- spinner.succeed();
1177
- } catch (error) {
1178
- spinner.fail();
1179
- throw error;
1180
- }
1181
- }
1182
- async function autoRefactor() {
1183
- if (await fs.pathExists(paths.public)) {
1184
- console.log(chalk.green("已完成改造,不需要重复执行,如果运行报错请查看文档"));
1185
- console.log(`文档: ${chalk.underline(pkg$1.homepage)}`);
1186
- process.exit(0);
1187
- }
1188
- const appPkg = fs.readJsonSync(paths.package);
1189
- const name = await input({
1190
- message: "请输入模块名称,要求唯一,不能与其他工程相同",
1191
- default: appPkg.name,
1192
- validate: (v) => /^[a-z][a-z0-9_-]+$/.test(v) || "请按格式输入 /^[a-z][a-z0-9_-]+$/ 只能使用小写字母、连字符、下划线"
1193
- });
1194
- const version = await input({
1195
- message: "版本号",
1196
- default: "1.0.0",
1197
- validate: (v) => /^\d+\.\d+\.\d+$/.test(v) || "请按格式输入 /^\\d+\\.\\d+\\.\\d+$/ 例如: 1.0.0"
1198
- });
1199
- const mode = await select({
1200
- message: "项目使用的哪种模式",
1201
- choices: [
1202
- {
1203
- name: "教育子工程模式",
1204
- value: ""
1205
- },
1206
- {
1207
- name: "教育主工程模式",
1208
- value: "main"
1209
- },
1210
- {
1211
- name: "独立模式",
1212
- value: "single"
1213
- }
1214
- ]
1215
- });
1216
- let deploy = [];
1217
- if (mode !== "single") deploy = await checkbox({
1218
- message: "项目需要部署到哪里",
1219
- choices: [
1220
- {
1221
- name: "校端",
1222
- value: "-s"
1223
- },
1224
- {
1225
- name: "局端",
1226
- value: "-b"
1227
- },
1228
- {
1229
- name: "公文端",
1230
- value: "-d"
1231
- }
1232
- ],
1233
- validate: (v) => !!v && v.length > 0 || "必须选一个"
1234
- });
1235
- const answers = {
1236
- name,
1237
- version,
1238
- mode,
1239
- deploy
1240
- };
1241
- appPkg.name = answers.name;
1242
- appPkg.version = answers.version;
1243
- await step("创建 public 文件夹", async () => {
1244
- await fs.mkdir(paths.public);
1245
- });
1246
- await step("移动 js 文件夹", async () => {
1247
- if (await fs.pathExists(path.resolve(paths.src, "js"))) await fs.move(path.resolve(paths.src, "js"), path.resolve(paths.public, "js"));
1248
- });
1249
- await step("移动 html 文件", async () => {
1250
- const HTMLFiles = await globby("*.html", { cwd: paths.src });
1251
- for (const file of HTMLFiles) await fs.move(path.resolve(paths.src, file), path.resolve(paths.public, file));
1252
- });
1253
- await step("删除 dll 文件夹", async () => {
1254
- if (await fs.pathExists(path.resolve(paths.src, "dll"))) await fs.remove(path.resolve(paths.src, "dll"));
1255
- });
1256
- await step("删除没用的 babel 和 webpack 配置", async () => {
1257
- const deleteFiles = [
1258
- ...await globby("{.,*}babel*"),
1259
- ...await globby("*webpack*"),
1260
- ...await globby("package-lock.json"),
1261
- paths.nodeModules,
1262
- paths.sshSftp
1263
- ];
1264
- for (const filePath of deleteFiles) await fs.remove(filePath);
1265
- });
1266
- await step("设置项目模式", () => {
1267
- if (answers.mode) appPkg.edu = { mode: answers.mode };
1268
- });
1269
- await step("修改 package.json 的 scripts", () => {
1270
- const scripts = appPkg.scripts;
1271
- scripts.start = "edu-scripts start";
1272
- scripts.build = "edu-scripts build";
1273
- scripts.analyze = "edu-scripts build --analyze";
1274
- if (answers.mode !== "single") scripts.deploy = `edu-scripts deploy ${answers.deploy.join(" ")}`;
1275
- else {
1276
- scripts.deploy = `edu-scripts deploy`;
1277
- scripts["commit-dist"] = "edu-scripts commit-dist --rm-local";
1278
- }
1279
- scripts["one-key-deploy"] = "npm version patch";
1280
- scripts.postversion = "npm run build && npm run deploy";
1281
- });
1282
- await step("删除 babel/webpack 相关依赖", (spinner) => {
1283
- const deleteRe = /(babel|autoprefixer|webpack|loader|less|css|sass|hmr|ssh-sftp|regenerator-runtime|nowa|prettier)/i;
1284
- const deletePkgs = {
1285
- dependencies: [],
1286
- devDependencies: []
1287
- };
1288
- Object.entries(deletePkgs).forEach(([scope, pkgs]) => {
1289
- for (const key in appPkg[scope]) if (Object.hasOwnProperty.call(appPkg[scope], key)) {
1290
- if (deleteRe.test(key)) {
1291
- pkgs.push(key);
1292
- delete appPkg[scope][key];
1293
- }
1294
- }
1295
- });
1296
- spinner.clear();
1297
- console.log(`删除的pkgs\n${chalk.green(JSON.stringify(deletePkgs, null, 2))}\n`);
1298
- appPkg.devDependencies["@qse/edu-scripts"] = "^" + pkg$1.version;
1299
- });
1300
- await fs.writeFile(paths.package, JSON.stringify(appPkg, null, 2), "utf-8");
1301
- console.log(chalk.green(`
1302
- 改造还未完成,剩余步骤请查看文档
1303
- ${pkg$1.homepage}#/refactor
1304
-
1305
- 运行 npm i 安装依赖
1306
- 运行 edu-scripts start 启动服务
1307
- `));
1308
- }
1309
-
1310
- //#endregion
1311
- //#region src/commit-dist.ts
1312
- const exec = (cmd, opts) => cp.execSync(cmd, {
1313
- encoding: "utf-8",
1314
- stdio: "pipe",
1315
- ...opts
1316
- });
1317
- function validateSVNRoot(root) {
1318
- const ls = exec(`svn ls ${root}`);
1319
- return ["trunk", "branches"].every((s) => ls.includes(s));
1320
- }
1321
- function getWorkingCopyInfo() {
1322
- exec(`svn up`);
1323
- const url = exec(`svn info --show-item url`).trim();
1324
- const revision = exec(`svn info --show-item last-changed-revision`).trim();
1325
- const author = exec(`svn info --show-item last-changed-author`).trim();
1326
- let branch = "trunk";
1327
- let root = url.replace(/\/trunk$/, "");
1328
- if (url.includes("/branches/")) {
1329
- branch = url.split("/").pop();
1330
- root = url.replace(/\/branches\/[^/]+$/, "");
1331
- }
1332
- let distBranchURL = root + "/branches/dist";
1333
- let distBranchDirURL = distBranchURL + "/" + branch;
1334
- if (!validateSVNRoot(root)) {
1335
- console.log(chalk.red("SVN目录不符合规则,必须包含 trunk branches"));
1336
- process.exit(1);
1337
- }
1338
- return {
1339
- url,
1340
- branch,
1341
- revision,
1342
- author,
1343
- distBranchURL,
1344
- distBranchDirURL,
1345
- root
1346
- };
1347
- }
1348
- function copyDistToRepo(info) {
1349
- const tmpdir = tmp.dirSync().name;
1350
- try {
1351
- exec(`svn ls ${info.distBranchDirURL} --depth empty`);
1352
- } catch (error) {
1353
- if (error.message.includes("non-existent")) exec(`svn mkdir ${info.distBranchDirURL} --parents -m "[edu-scripts] create ${info.branch} dist"`);
1354
- else throw error;
1355
- }
1356
- exec(`svn co ${info.distBranchDirURL} ${tmpdir}`);
1357
- try {
1358
- exec(`svn rm * --force -q`, { cwd: tmpdir });
1359
- } catch {}
1360
- fs.copySync(paths.dist, tmpdir);
1361
- exec(`svn add * --force --auto-props --parents --depth infinity -q`, { cwd: tmpdir });
1362
- exec(`svn ci -m "${`[edu-scripts] commit ${info.branch} dist #${info.revision} @${info.author}`}"`, { cwd: tmpdir });
1363
- fs.removeSync(tmpdir);
1364
- }
1365
- async function commitDist(args) {
1366
- if (!fs.existsSync(paths.dist)) {
1367
- console.log(chalk.red("未找到 dist 文件夹,请先 edu-scpirts build"));
1368
- process.exit(1);
1369
- }
1370
- if (exec("svn st").trim().length) {
1371
- console.log(chalk.red("似乎存在未提交的代码,请提交后重试。运行 svn st 查看具体信息"));
1372
- process.exit(1);
1373
- }
1374
- const info = getWorkingCopyInfo();
1375
- console.log(chalk.green([
1376
- `分支: ${info.branch}`,
1377
- `版本: ${info.revision}`,
1378
- `作者: ${info.author}`,
1379
- `地址: ${info.distBranchDirURL}`
1380
- ].join("\n")));
1381
- copyDistToRepo(info);
1382
- if (args.rmLocal) fs.removeSync(paths.dist);
1383
- console.log(chalk.green("提交完成"));
1384
- }
1385
-
1386
1355
  //#endregion
1387
1356
  //#region src/generator.ts
1388
1357
  const getTmpPath = (...args) => paths.resolveOwn("asset", "template", ...args);
@@ -1429,6 +1398,42 @@ async function generatorTailwind() {
1429
1398
  ].join("\n")));
1430
1399
  }
1431
1400
 
1401
+ //#endregion
1402
+ //#region src/start.ts
1403
+ async function start(args) {
1404
+ process.env.NODE_ENV = "development";
1405
+ process.env.BABEL_ENV = "development";
1406
+ process.env.BROWSERSLIST = "chrome >= 70";
1407
+ process.env.WEBPACK_DEV_SERVER_BASE_PORT = "3000";
1408
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
1409
+ const basePort = process.env.WEBPACK_DEV_SERVER_BASE_PORT;
1410
+ const port = await RspackDevServer.getFreePort(args.port || process.env.PORT);
1411
+ if (!(args.port || process.env.PORT) && +port !== +basePort) console.log(chalk.bgYellow(`${basePort} 端口已被占用,现切换到 ${port} 端口运行`));
1412
+ args.port = port;
1413
+ process.env.PORT = port;
1414
+ const config = getConfig(args);
1415
+ const compiler = rspack(config);
1416
+ const middleware = rspack.lazyCompilationMiddleware(compiler);
1417
+ const oldSetupMiddlewares = config.devServer.setupMiddlewares;
1418
+ config.devServer.setupMiddlewares = (middlewares, devServer) => {
1419
+ if (oldSetupMiddlewares) middlewares = oldSetupMiddlewares(middlewares, devServer);
1420
+ middlewares.unshift(middleware);
1421
+ return middlewares;
1422
+ };
1423
+ const devServer = new RspackDevServer(config.devServer, compiler);
1424
+ devServer.start();
1425
+ ["SIGINT", "SIGTERM"].forEach(function(sig) {
1426
+ process.on(sig, function() {
1427
+ devServer.stop();
1428
+ process.exit();
1429
+ });
1430
+ });
1431
+ if (process.env.CI !== "true") process.stdin.on("end", function() {
1432
+ devServer.stop();
1433
+ process.exit();
1434
+ });
1435
+ }
1436
+
1432
1437
  //#endregion
1433
1438
  //#region src/cli.ts
1434
1439
  const pkg = fs.readJsonSync(paths.resolveOwn("package.json"));
@@ -1446,7 +1451,7 @@ yargs(process.argv.slice(2)).usage(`教育工程化 webpack5 基础框架\n文
1446
1451
  desc: "输出 html 文件",
1447
1452
  default: false,
1448
1453
  boolean: true
1449
- }), (args) => build(args)).command("deploy", "自动部署 dist 到 v1 服务器", (yargs) => yargs.option("school", {
1454
+ }), (args) => build(args)).command("deploy", "自动部署 dist 到 v1 服务器,可以使用 @qse/ssh-sftp 工具生成.sftprc.json文件,或者直接使用命令行参数指定上传目标", (yargs) => yargs.option("school", {
1450
1455
  alias: "s",
1451
1456
  desc: "上传到校端",
1452
1457
  default: false,