@yannick-z/modulo 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/README.md +57 -60
  3. package/bin/modulo.js +29 -19
  4. package/biome.json +34 -0
  5. package/package.json +35 -35
  6. package/rslib.config.ts +19 -19
  7. package/src/args/get-framework-name.ts +7 -7
  8. package/src/args/index.ts +16 -74
  9. package/src/args/preset.ts +16 -16
  10. package/src/cli/init.ts +10 -6
  11. package/src/cli/pack-code.ts +20 -9
  12. package/src/config/example/example-config.ts +50 -39
  13. package/src/config/example/example-externals.ts +21 -37
  14. package/src/config/index.ts +186 -16
  15. package/src/config/presets.ts +100 -0
  16. package/src/config/type.ts +34 -12
  17. package/src/index.ts +102 -10
  18. package/src/initiator/create-config-file.ts +52 -36
  19. package/src/initiator/create-project.ts +249 -0
  20. package/src/initiator/modify-scripts.ts +42 -42
  21. package/src/packer/auto-external-plugin.ts +110 -0
  22. package/src/packer/collect-modules.ts +58 -69
  23. package/src/packer/get-externals-and-tags.ts +78 -55
  24. package/src/packer/lib.ts +76 -69
  25. package/src/packer/page.ts +106 -82
  26. package/src/packer/prepare.ts +63 -57
  27. package/src/tools/cli.ts +21 -0
  28. package/src/tools/file.ts +84 -14
  29. package/src/tools/find-path-root.ts +52 -25
  30. package/src/tools/get-framework-name.ts +7 -7
  31. package/src/tools/get-ui-plugin.ts +27 -13
  32. package/src/tools/json.ts +63 -9
  33. package/src/tools/log.ts +58 -0
  34. package/src/tools/merge-user-config.ts +17 -17
  35. package/src/tools/omit-root-path.ts +17 -7
  36. package/src/tools/panic.ts +12 -8
  37. package/src/tools/string.ts +13 -2
  38. package/src/type/guard.ts +22 -3
  39. package/tsconfig.json +9 -9
  40. package/dist/index.js +0 -773
  41. package/src/args/cmd.ts +0 -16
  42. package/src/args/mode.ts +0 -38
  43. package/src/args/node_env.ts +0 -15
  44. package/src/args/target.ts +0 -44
  45. package/src/config/externals.ts +0 -70
  46. package/src/config/generate_config.ts +0 -105
  47. package/src/config/preset/alias.ts +0 -3
  48. package/src/config/preset/dev-server.ts +0 -6
  49. package/src/config/preset/dirs.ts +0 -12
  50. package/src/config/preset/html.ts +0 -19
  51. package/src/config/preset/index.ts +0 -23
  52. package/src/config/preset/libs.ts +0 -5
  53. package/src/config/preset/minify.ts +0 -24
  54. package/src/config/preset/url.ts +0 -4
  55. package/src/tools/debug-log.ts +0 -37
package/src/index.ts CHANGED
@@ -1,13 +1,105 @@
1
- import { get_args } from "./args/index.ts";
2
- import { init_tool } from "./cli/init.ts";
3
- import { pack_code } from "./cli/pack-code.ts";
1
+ import { cac } from "cac";
2
+ import { logger } from "./tools/log.ts";
3
+ export type { USER_CONFIG as UserConfig } from "./config/type.ts";
4
4
 
5
- export function exec() {
6
- const args = get_args();
5
+ // Create CLI instance
6
+ const cli = cac("modulo");
7
+
8
+ // Define 'init' command
9
+ cli
10
+ .command("init <target>", "Initialize modulo configuration or scripts")
11
+ .option("-f, --force", "Force overwrite existing files")
12
+ .option("--path <path>", "Specify the path to initialize")
13
+ .option("--preset <preset>", "Specify the preset to use")
14
+ .action((target, options) => {
15
+ // We will dynamically import the action handler to keep startup fast
16
+ import("./cli/init.ts").then(({ init_tool }) => {
17
+ init_tool({
18
+ cmd: "init",
19
+ target: target as any,
20
+ init: {
21
+ path: options.path,
22
+ force: options.force,
23
+ preset: options.preset,
24
+ },
25
+ });
26
+ });
27
+ });
28
+
29
+ // Define 'build' command
30
+ cli
31
+ .command("build <target>", "Build the project for production")
32
+ .option("-c, --config <file>", "Use specified config file")
33
+ .option("-w, --watch", "Watch for changes")
34
+ .option("--env <env>", "Specify the environment (dev/prd)")
35
+ .action((target, options) => {
36
+ import("./cli/pack-code.ts").then(({ pack_code }) => {
37
+ pack_code({
38
+ cmd: "build",
39
+ target: target as any,
40
+ pack: {
41
+ config: options.config,
42
+ env: options.env || "prd",
43
+ watch: options.watch,
44
+ esm: true,
45
+ },
46
+ });
47
+ });
48
+ });
49
+
50
+ // Define 'dev' command
51
+ cli
52
+ .command("dev <target>", "Start development server")
53
+ .option("-c, --config <file>", "Use specified config file")
54
+ .option("--env <env>", "Specify the environment (dev/prd)")
55
+ .option("--debug", "Enable debug mode")
56
+ .action((target, options) => {
57
+ if (options.debug) {
58
+ process.env.DEBUG = "true";
59
+ }
60
+ import("./cli/pack-code.ts").then(({ pack_code }) => {
61
+ pack_code({
62
+ cmd: "dev",
63
+ target: target as any,
64
+ pack: {
65
+ config: options.config,
66
+ env: options.env || "dev",
67
+ watch: true, // dev always watches
68
+ esm: true,
69
+ },
70
+ });
71
+ });
72
+ });
7
73
 
8
- if (args.cmd === "init") {
9
- init_tool(args);
10
- } else if (args.cmd === "build" || args.cmd === "dev") {
11
- pack_code(args);
12
- }
74
+ // Define 'preview' command
75
+ cli
76
+ .command("preview <target>", "Preview the production build")
77
+ .option("-c, --config <file>", "Use specified config file")
78
+ .action((target, options) => {
79
+ import("./cli/pack-code.ts").then(({ pack_code }) => {
80
+ pack_code({
81
+ cmd: "preview",
82
+ target: target as any,
83
+ pack: {
84
+ config: options.config,
85
+ env: "prd",
86
+ watch: false,
87
+ esm: true,
88
+ },
89
+ });
90
+ });
91
+ });
92
+
93
+ cli.help();
94
+ cli.version("0.2.0");
95
+
96
+ export function exec() {
97
+ try {
98
+ // Parse arguments
99
+ cli.parse();
100
+ } catch (error) {
101
+ logger.error(`Error: ${(error as Error).message}`);
102
+ cli.outputHelp();
103
+ process.exit(1);
104
+ }
13
105
  }
@@ -1,46 +1,62 @@
1
1
  // 在当前目录创建一份json配置文件
2
2
  import { existsSync, writeFileSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { resolve, extname } from "node:path";
4
4
  import readline from "node:readline";
5
5
  import picocolors from "picocolors";
6
6
  import type { ModuloArgs_Init } from "../args/index.ts";
7
7
  import {
8
- default_config_file_name,
9
- get_example_config,
8
+ default_config_file_name,
9
+ get_example_config,
10
10
  } from "../config/example/example-config.ts";
11
11
 
12
12
  export async function create_config_file(args: ModuloArgs_Init) {
13
- const path = args.init.path || default_config_file_name;
14
- console.log(picocolors.blue("即将创建配置文件"), path);
15
-
16
- const filepath = resolve(process.cwd(), path);
17
-
18
- if (existsSync(filepath)) {
19
- if (args.init.force) {
20
- console.log(picocolors.bgRed(picocolors.white("配置文件已存在,将覆盖")));
21
- } else {
22
- console.log(picocolors.red("配置文件已存在,是否覆盖?"));
23
- const rl = readline.createInterface({
24
- input: process.stdin,
25
- output: process.stdout,
26
- });
27
- const answer = await new Promise<string>((resolve) => {
28
- rl.question("\n请输入(Y/N) ", (answer) => {
29
- rl.close();
30
- resolve(answer);
31
- });
32
- });
33
- if (answer.toLowerCase() !== "y") {
34
- console.log("取消创建");
35
- return;
36
- }
37
- }
38
- }
39
-
40
- writeFileSync(
41
- filepath,
42
- JSON.stringify(get_example_config(args.init.preset), null, 2)
43
- );
44
-
45
- console.log(picocolors.green("创建成功"), filepath);
13
+ const path = args.init.path || default_config_file_name;
14
+ console.log(picocolors.blue("即将创建配置文件"), path);
15
+
16
+ const filepath = resolve(process.cwd(), path);
17
+
18
+ if (existsSync(filepath)) {
19
+ if (args.init.force) {
20
+ console.log(picocolors.bgRed(picocolors.white("配置文件已存在,将覆盖")));
21
+ } else {
22
+ console.log(picocolors.red("配置文件已存在,是否覆盖?"));
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout,
26
+ });
27
+ const answer = await new Promise<string>((resolve) => {
28
+ rl.question("\n请输入(Y/N) ", (answer) => {
29
+ rl.close();
30
+ resolve(answer);
31
+ });
32
+ });
33
+ if (answer.toLowerCase() !== "y") {
34
+ console.log("取消创建");
35
+ return;
36
+ }
37
+ }
38
+ }
39
+
40
+ const config = get_example_config(args.init.preset);
41
+ const ext = extname(path);
42
+ let content = "";
43
+
44
+ if (ext === ".ts") {
45
+ content = `import type { UserConfig } from "@yannick-z/modulo";
46
+
47
+ const config: UserConfig = ${JSON.stringify(config, null, 2)};
48
+
49
+ export default config;
50
+ `;
51
+ } else if (ext === ".js") {
52
+ content = `/** @type {import('@yannick-z/modulo').UserConfig} */
53
+ export default ${JSON.stringify(config, null, 2)};
54
+ `;
55
+ } else {
56
+ content = JSON.stringify(config, null, 2);
57
+ }
58
+
59
+ writeFileSync(filepath, content);
60
+
61
+ console.log(picocolors.green("创建成功"), filepath);
46
62
  }
@@ -0,0 +1,249 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import picocolors from "picocolors";
4
+ import type { ModuloArgs_Init } from "../args/index.ts";
5
+ import { create_config_file } from "./create-config-file.ts";
6
+ import { modify_scripts } from "./modify-scripts.ts";
7
+ import { get_packagejson } from "../config/index.ts";
8
+
9
+ export async function create_project(args: ModuloArgs_Init) {
10
+ const { path, preset } = args.init;
11
+
12
+ if (!path) {
13
+ console.error(
14
+ picocolors.red(
15
+ "请指定项目路径: modulo init project --path <project-path>",
16
+ ),
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ const projectRoot = resolve(process.cwd(), path);
22
+
23
+ if (existsSync(projectRoot)) {
24
+ if (!args.init.force) {
25
+ console.error(
26
+ picocolors.red(
27
+ `目录 ${path} 已存在,请使用 --force 覆盖或选择其他路径`,
28
+ ),
29
+ );
30
+ process.exit(1);
31
+ }
32
+ } else {
33
+ mkdirSync(projectRoot, { recursive: true });
34
+ }
35
+
36
+ console.log(picocolors.blue(`正在初始化项目到: ${projectRoot}`));
37
+
38
+ // 切换工作目录到新项目目录,以便复用现有的 config 和 script 生成逻辑
39
+ const originalCwd = process.cwd();
40
+ process.chdir(projectRoot);
41
+
42
+ try {
43
+ // 1. 创建 package.json
44
+ const packageJson = {
45
+ name: path.split("/").pop() || "modulo-project",
46
+ version: "0.0.0",
47
+ type: "module",
48
+ scripts: {
49
+ lint: "biome lint .",
50
+ format: "biome format --write .",
51
+ check: "biome check --write .",
52
+ },
53
+ dependencies: {},
54
+ devDependencies: {
55
+ "@yannick-z/modulo": "^0.2.0",
56
+ typescript: "^5.0.0",
57
+ "@biomejs/biome": "2.4.4",
58
+ },
59
+ };
60
+
61
+ // 添加依赖
62
+ if (preset === "vue2") {
63
+ packageJson.dependencies = {
64
+ vue: "2.7.16",
65
+ };
66
+ } else if (preset === "react19") {
67
+ packageJson.dependencies = {
68
+ react: "19.2.4",
69
+ "react-dom": "19.2.4",
70
+ };
71
+ }
72
+
73
+ writeFileSync(
74
+ resolve(projectRoot, "package.json"),
75
+ JSON.stringify(packageJson, null, 2),
76
+ );
77
+ console.log(picocolors.green("创建 package.json 成功"));
78
+
79
+ // 手动触发一次 get_packagejson 以缓存新创建的 package.json
80
+ get_packagejson(projectRoot);
81
+
82
+ // 2. 创建 tsconfig.json
83
+ const tsConfig = {
84
+ compilerOptions: {
85
+ target: "ESNext",
86
+ module: "ESNext",
87
+ moduleResolution: "bundler",
88
+ strict: true,
89
+ jsx: preset === "react19" ? "react-jsx" : "preserve",
90
+ esModuleInterop: true,
91
+ skipLibCheck: true,
92
+ forceConsistentCasingInFileNames: true,
93
+ baseUrl: ".",
94
+ paths: {
95
+ "@/*": ["src/*"],
96
+ },
97
+ },
98
+ include: ["src/**/*", "modulo.config.ts"],
99
+ };
100
+ writeFileSync(
101
+ resolve(projectRoot, "tsconfig.json"),
102
+ JSON.stringify(tsConfig, null, 2),
103
+ );
104
+ console.log(picocolors.green("创建 tsconfig.json 成功"));
105
+
106
+ // 3. 创建目录结构
107
+ mkdirSync(resolve(projectRoot, "src/pages/index"), { recursive: true });
108
+
109
+ // 4. 创建示例代码
110
+ if (preset === "vue2") {
111
+ const vueContent = `<template>
112
+ <div id="app">
113
+ <h1>Hello Modulo + Vue 2</h1>
114
+ </div>
115
+ </template>
116
+
117
+ <script lang="ts">
118
+ import Vue from 'vue';
119
+ export default Vue.extend({
120
+ name: 'App'
121
+ });
122
+ </script>
123
+
124
+ <style scoped>
125
+ h1 {
126
+ color: #42b983;
127
+ }
128
+ </style>
129
+ `;
130
+ writeFileSync(
131
+ resolve(projectRoot, "src/pages/index/App.vue"),
132
+ vueContent,
133
+ );
134
+
135
+ const indexContent = `import Vue from 'vue';
136
+ import App from './App.vue';
137
+
138
+ new Vue({
139
+ render: h => h(App)
140
+ }).$mount('#app');
141
+ `;
142
+ writeFileSync(
143
+ resolve(projectRoot, "src/pages/index/index.ts"),
144
+ indexContent,
145
+ );
146
+
147
+ // 创建 shim-vue.d.ts
148
+ const shimContent = `declare module '*.vue' {
149
+ import Vue from 'vue';
150
+ export default Vue;
151
+ }
152
+ `;
153
+ writeFileSync(resolve(projectRoot, "src/shim-vue.d.ts"), shimContent);
154
+ } else if (preset === "react19") {
155
+ const appContent = `import { useState } from 'react';
156
+
157
+ export function App() {
158
+ const [count, setCount] = useState(0);
159
+ return (
160
+ <div>
161
+ <h1>Hello Modulo + React 19</h1>
162
+ <button onClick={() => setCount(count + 1)}>Count: {count}</button>
163
+ </div>
164
+ );
165
+ }
166
+ `;
167
+ writeFileSync(
168
+ resolve(projectRoot, "src/pages/index/App.tsx"),
169
+ appContent,
170
+ );
171
+
172
+ const indexContent = `import { createRoot } from 'react-dom/client';
173
+ import { App } from './App';
174
+
175
+ const root = createRoot(document.getElementById('app')!);
176
+ root.render(<App />);
177
+ `;
178
+ writeFileSync(
179
+ resolve(projectRoot, "src/pages/index/index.tsx"),
180
+ indexContent,
181
+ );
182
+ }
183
+
184
+ // 5. 生成配置文件
185
+ // 将 path 重置为 undefined,否则 create_config_file 会使用项目路径作为配置文件名
186
+ const configArgs = { ...args, init: { ...args.init, path: "" } };
187
+ await create_config_file(configArgs);
188
+
189
+ // 6. 添加 scripts
190
+ modify_scripts();
191
+
192
+ // 7. 生成 biome.json
193
+ const biomeConfig = {
194
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
195
+ vcs: {
196
+ enabled: false,
197
+ clientKind: "git",
198
+ useIgnoreFile: false,
199
+ },
200
+ files: {
201
+ ignoreUnknown: false,
202
+ ignore: [],
203
+ },
204
+ formatter: {
205
+ enabled: true,
206
+ indentStyle: "tab",
207
+ },
208
+ organizeImports: {
209
+ enabled: true,
210
+ },
211
+ linter: {
212
+ enabled: true,
213
+ rules: {
214
+ recommended: true,
215
+ },
216
+ },
217
+ javascript: {
218
+ formatter: {
219
+ quoteStyle: "double",
220
+ },
221
+ },
222
+ };
223
+ writeFileSync(
224
+ resolve(projectRoot, "biome.json"),
225
+ JSON.stringify(biomeConfig, null, 2),
226
+ );
227
+ console.log(picocolors.green("创建 biome.json 成功"));
228
+
229
+ // 8. 创建 .vscode/extensions.json
230
+ const vscodeExtensions = {
231
+ recommendations: ["biomejs.biome"],
232
+ };
233
+ mkdirSync(resolve(projectRoot, ".vscode"), { recursive: true });
234
+ writeFileSync(
235
+ resolve(projectRoot, ".vscode/extensions.json"),
236
+ JSON.stringify(vscodeExtensions, null, 2),
237
+ );
238
+ console.log(picocolors.green("创建 .vscode/extensions.json 成功"));
239
+
240
+ console.log(picocolors.green("\n项目初始化完成!\n"));
241
+ console.log(picocolors.cyan(` cd ${path}`));
242
+ console.log(picocolors.cyan(" npm install"));
243
+ console.log(picocolors.cyan(" npm run dev page\n"));
244
+ } catch (error) {
245
+ console.error(picocolors.red("项目初始化失败:"), error);
246
+ } finally {
247
+ process.chdir(originalCwd);
248
+ }
249
+ }
@@ -1,52 +1,52 @@
1
- import { writeFileSync } from "node:fs";
2
1
  import { resolve } from "node:path";
3
- import readline from "node:readline";
4
2
  import picocolors from "picocolors";
5
3
  import { get_packagejson } from "../config/index.ts";
4
+ import { confirm } from "../tools/cli.ts";
5
+ import { update_json_file } from "../tools/json.ts";
6
6
 
7
7
  export const star_line = "**********************";
8
8
 
9
9
  export async function modify_scripts() {
10
- const packagejson = get_packagejson();
11
- const new_scripts = {
12
- ...(packagejson.scripts || {}),
13
- "build:page": "modulo build page",
14
- "build:module": "modulo build module",
15
- "build:all": "modulo build all",
16
- build: "modulo build all",
17
- "dev:page": "modulo dev page",
18
- "dev:module": "modulo dev module",
19
- "watch:page": "modulo build page --watch=true",
20
- "watch:module": "modulo build module --watch=true",
21
- };
22
- console.log(
23
- picocolors.magentaBright(
24
- `\n${star_line}修改package.json中的scripts\n新的内容修改后如下:\n${JSON.stringify(
25
- new_scripts,
26
- null,
27
- 2
28
- )}\n${star_line}`
29
- )
30
- );
10
+ const packagejson = get_packagejson();
11
+ const new_scripts = {
12
+ ...(packagejson.scripts || {}),
13
+ "build:page": "modulo build page",
14
+ "build:module": "modulo build module",
15
+ "build:all": "modulo build all",
16
+ build: "modulo build all",
17
+ "dev:page": "modulo dev page",
18
+ "dev:module": "modulo dev module",
19
+ "watch:page": "modulo build page --watch=true",
20
+ "watch:module": "modulo build module --watch=true",
21
+ };
22
+ console.log(
23
+ picocolors.magentaBright(
24
+ `\n${star_line}\n修改package.json中的scripts\n新的内容修改后如下:\n${JSON.stringify(
25
+ new_scripts,
26
+ null,
27
+ 2,
28
+ )}\n${star_line}`,
29
+ ),
30
+ );
31
31
 
32
- // 确定是否修改package.json
33
- const rl = readline.createInterface({
34
- input: process.stdin,
35
- output: process.stdout,
36
- });
37
- const answer = await new Promise<string>((resolve) => {
38
- rl.question("\n确定修改吗?请输入(Y/N) ", (answer) => {
39
- rl.close();
40
- resolve(answer);
41
- });
42
- });
43
- if (answer.toLowerCase() !== "y") {
44
- console.log("取消修改");
45
- return;
46
- }
32
+ // 确定是否修改package.json
33
+ const confirmed = await confirm("\n确定修改吗?");
34
+ if (!confirmed) {
35
+ console.log("取消修改");
36
+ return;
37
+ }
47
38
 
48
- packagejson.scripts = new_scripts;
49
- const new_packagejson = JSON.stringify(packagejson, null, 2);
50
- writeFileSync(resolve(process.cwd(), "package.json"), new_packagejson);
51
- console.log(picocolors.green(`\npackage.json修改成功`));
39
+ const success = update_json_file(
40
+ resolve(process.cwd(), "package.json"),
41
+ (data: any) => {
42
+ data.scripts = new_scripts;
43
+ return data;
44
+ },
45
+ );
46
+
47
+ if (success) {
48
+ console.log(picocolors.green(`\npackage.json修改成功`));
49
+ } else {
50
+ console.log(picocolors.red(`\npackage.json修改失败`));
51
+ }
52
52
  }
@@ -0,0 +1,110 @@
1
+ import type { Compiler, Compilation } from "@rspack/core";
2
+ import type { ModuloArgs_Pack } from "../args/index.ts";
3
+ import type { GLOBAL_CONFIG } from "../config/type.ts";
4
+ import { getExternalsAndImportMap } from "./get-externals-and-tags.ts";
5
+
6
+ /**
7
+ * 自动 External 插件
8
+ *
9
+ * 1. 扫描编译过程中使用到的 node_modules 依赖
10
+ * 2. 如果这些依赖在 config.externals 中定义了,则标记为已使用
11
+ * 3. 在 HTML 生成阶段,仅注入已使用的依赖的 importmap 或 script 标签
12
+ */
13
+ export class AutoExternalPlugin {
14
+ private externalLibNames: string[];
15
+ private usedExternals: Set<string>;
16
+ private args: ModuloArgs_Pack;
17
+ private config: GLOBAL_CONFIG;
18
+
19
+ constructor(args: ModuloArgs_Pack, config: GLOBAL_CONFIG) {
20
+ this.args = args;
21
+ this.config = config;
22
+ this.externalLibNames = Object.keys(config.externals);
23
+ this.usedExternals = new Set<string>();
24
+ }
25
+
26
+ apply(compiler: Compiler) {
27
+ compiler.hooks.compilation.tap(
28
+ "AutoExternalPlugin",
29
+ (compilation: Compilation) => {
30
+ // 1. 扫描模块依赖
31
+ compilation.hooks.finishModules.tap("AutoExternalPlugin", (modules) => {
32
+ for (const module of modules) {
33
+ // 检查 module.resource 是否包含 node_modules
34
+ // 并且属于我们在 config.externals 中定义的包
35
+ const resource = (module as any).resource;
36
+ if (resource && resource.includes("node_modules")) {
37
+ for (const libName of this.externalLibNames) {
38
+ // 简单的包含匹配,更严谨的可以使用路径解析
39
+ // 例如: /node_modules/vue/
40
+ // 这里简化处理,匹配 /node_modules/<libName>/
41
+ if (
42
+ resource.includes(`/node_modules/${libName}/`) ||
43
+ resource.includes(`\\node_modules\\${libName}\\`)
44
+ ) {
45
+ this.usedExternals.add(libName);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ });
51
+
52
+ // 2. 注入 HTML
53
+ const HtmlWebpackPlugin =
54
+ compiler.webpack.HtmlWebpackPlugin ||
55
+ compiler.options.plugins.find(
56
+ (p: any) => p.constructor.name === "HtmlWebpackPlugin",
57
+ )?.constructor;
58
+
59
+ if (!HtmlWebpackPlugin) return;
60
+
61
+ const hooks = (HtmlWebpackPlugin as any).getHooks(compilation);
62
+ hooks.alterAssetTags.tap("AutoExternalPlugin", (data: any) => {
63
+ if (!this.config.autoExternal) return data;
64
+
65
+ // 重新计算 importmaps
66
+ const { importMap } = getExternalsAndImportMap(
67
+ this.args,
68
+ this.config.externals,
69
+ this.config.externalsType,
70
+ );
71
+
72
+ // 过滤未使用的依赖
73
+ const filteredImportMap = Object.fromEntries(
74
+ Object.entries(importMap).filter(([key]) =>
75
+ this.usedExternals.has(key),
76
+ ),
77
+ );
78
+
79
+ if (Object.keys(filteredImportMap).length === 0) return data;
80
+
81
+ let tags: any[] = [];
82
+ if (this.config.externalsType === "script") {
83
+ tags = Object.values(filteredImportMap).map((url) => ({
84
+ tagName: "script",
85
+ voidTag: false,
86
+ attributes: { src: url },
87
+ }));
88
+ } else {
89
+ tags = [
90
+ {
91
+ tagName: "script",
92
+ voidTag: false,
93
+ attributes: { type: "importmap" },
94
+ innerHTML: JSON.stringify(
95
+ { imports: filteredImportMap },
96
+ null,
97
+ 2,
98
+ ),
99
+ },
100
+ ];
101
+ }
102
+
103
+ // 插入到 head 的最前面
104
+ data.assetTags.scripts.unshift(...tags);
105
+ return data;
106
+ });
107
+ },
108
+ );
109
+ }
110
+ }