@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
@@ -1,77 +1,66 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
1
  import { resolve } from "node:path";
3
2
  import picocolors from "picocolors";
4
- import { get_global_config } from "../config/index.ts";
5
- import { debug_log } from "../tools/debug-log.ts";
3
+ import { debug_log } from "../tools/log.ts";
6
4
  import { get_framework_name } from "../tools/get-framework-name.ts";
5
+ import { get_directories, find_entry_file, exists } from "../tools/file.ts";
7
6
  import type { ModuloArgs_Pack } from "../args/index.ts";
7
+ import type { GLOBAL_CONFIG } from "../config/type.ts";
8
8
 
9
+ /**
10
+ * 收集模块入口文件
11
+ * 扫描指定目录下的子目录,查找符合规则的入口文件(index/main/同名文件)
12
+ *
13
+ * @param args CLI 参数
14
+ * @param kind 模块类型(page 或 module)
15
+ * @returns 模块名到入口文件路径的映射对象,如果未找到任何模块则返回 undefined
16
+ */
9
17
  export function collect_modules(
10
- args: ModuloArgs_Pack,
11
- kind: "page" | "module"
12
- ) {
13
- const global_config = get_global_config(args);
14
- const framework_name = get_framework_name();
15
- const module_path = global_config.input[`${kind}s`];
16
- const exist = existsSync(module_path);
17
- debug_log(
18
- picocolors.blue("check module_path"),
19
- module_path,
20
- exist ? "exists" : "NOT exists"
21
- );
22
- // 解析目录下的所有模块
23
- const module_entries = !exist
24
- ? []
25
- : readdirSync(module_path, { withFileTypes: true })
26
- .filter((item) => {
27
- debug_log(
28
- "checking module is directory",
29
- item.name,
30
- item.isDirectory()
31
- );
32
- return item.isDirectory();
33
- })
34
- // 每个模块都要求以index或者main或者文件夹同名命名
35
- .map((dirent) => {
36
- const dir_path = resolve(module_path, dirent.name);
37
- const entry_file_path = [
38
- resolve(dir_path, "index.ts"),
39
- resolve(dir_path, "index.tsx"),
40
- resolve(dir_path, "index.js"),
41
- resolve(dir_path, "index.jsx"),
42
- resolve(dir_path, "main.ts"),
43
- resolve(dir_path, "main.tsx"),
44
- resolve(dir_path, "main.js"),
45
- resolve(dir_path, "main.jsx"),
46
- // vue组件常常用vue文件作为入口,因此优先级高于同名ts和tsx文件
47
- ...(framework_name === "vue"
48
- ? [
49
- resolve(dir_path, "index.vue"),
50
- resolve(dir_path, "main.vue"),
51
- resolve(dir_path, `${dirent.name}.vue`),
52
- ]
53
- : []),
54
- resolve(dir_path, `${dirent.name}.ts`),
55
- resolve(dir_path, `${dirent.name}.tsx`),
56
- resolve(dir_path, `${dirent.name}.js`),
57
- resolve(dir_path, `${dirent.name}.jsx`),
58
- ].find((path) => {
59
- const exists = existsSync(path);
60
- debug_log(
61
- "checking entry candidate",
62
- `${path} ${picocolors[exists ? "green" : "red"](
63
- `exists - ${exists}`
64
- )}`
65
- );
66
- return exists;
67
- });
68
- debug_log("found entry", entry_file_path);
69
- return [dirent.name, entry_file_path];
70
- })
71
- // 没有找到入口的就不管
72
- .filter((entry): entry is [string, string] => !!entry[1]);
18
+ args: ModuloArgs_Pack,
19
+ kind: "page" | "module",
20
+ global_config: GLOBAL_CONFIG,
21
+ ): Record<string, string> | undefined {
22
+ const framework_name = get_framework_name();
23
+ const module_path = global_config.input[`${kind}s`];
24
+ const isExist = exists(module_path);
73
25
 
74
- return module_entries.length > 0
75
- ? Object.fromEntries(module_entries)
76
- : undefined;
26
+ debug_log(
27
+ picocolors.blue("check module_path"),
28
+ module_path,
29
+ isExist ? "exists" : "NOT exists",
30
+ );
31
+
32
+ if (!isExist) {
33
+ return undefined;
34
+ }
35
+
36
+ // 基础候选文件名
37
+ const baseCandidates = ["index", "main"];
38
+ // 扩展名列表
39
+ const extensions = [".ts", ".tsx", ".js", ".jsx"];
40
+ // Vue 特有扩展名
41
+ if (framework_name === "vue") {
42
+ extensions.unshift(".vue");
43
+ }
44
+
45
+ // 解析目录下的所有模块
46
+ const module_entries = get_directories(module_path)
47
+ .map((dirName) => {
48
+ const dir_path = resolve(module_path, dirName);
49
+
50
+ // 构建候选文件名列表,包含目录名
51
+ const candidates = [...baseCandidates, dirName];
52
+
53
+ // 查找入口文件
54
+ const entry_file_path = find_entry_file(dir_path, candidates, extensions);
55
+
56
+ debug_log("found entry", dirName, entry_file_path || "NOT FOUND");
57
+
58
+ return [dirName, entry_file_path];
59
+ })
60
+ // 没有找到入口的就不管
61
+ .filter((entry): entry is [string, string] => !!entry[1]);
62
+
63
+ return module_entries.length > 0
64
+ ? Object.fromEntries(module_entries)
65
+ : undefined;
77
66
  }
@@ -1,65 +1,88 @@
1
1
  import type { ModuloArgs_Pack } from "../args/index.ts";
2
2
  import {
3
- is_env_external,
4
- type ExternalLibs,
5
- type ImportExternal,
6
- is_module_typed_external_url,
7
- type ModuleTypedExternalUrl,
8
- type ConfigExternalUrl,
9
- } from "../config/externals.ts";
10
- import type { Tag } from "../config/preset/html.ts";
11
- import { is_string } from "../type/guard.ts";
3
+ type ExternalLibs,
4
+ type ConfigExternalUrl,
5
+ type ImportExternal,
6
+ } from "../config/type.ts";
7
+ import {
8
+ is_env_external,
9
+ is_string,
10
+ is_import_external,
11
+ } from "../type/guard.ts";
12
12
 
13
- function get_external_url(args: ModuloArgs_Pack, url: ConfigExternalUrl) {
14
- let _url = url;
15
- while (!is_string(_url)) {
16
- if (is_env_external(_url)) {
17
- _url = _url[args.pack.env];
18
- } else {
19
- const mode = args.pack.esm ? "esm" : "umd";
20
- _url = mode in _url ? (_url as any)[mode] : undefined;
21
- }
22
- }
23
- return _url;
13
+ function getExternalUrl(args: ModuloArgs_Pack, url: ConfigExternalUrl) {
14
+ let resolvedUrl = url;
15
+ while (!is_string(resolvedUrl)) {
16
+ if (is_env_external(resolvedUrl)) {
17
+ resolvedUrl = resolvedUrl[args.pack.env];
18
+ } else {
19
+ // 理论上不会走到这里,因为现在只有 string EnvExternalUrl
20
+ // 但为了类型安全,还是保留一个 else 分支或者抛出错误
21
+ return undefined;
22
+ }
23
+ }
24
+ return resolvedUrl;
24
25
  }
25
26
 
26
- export function get_externals_importmaps(
27
- args: ModuloArgs_Pack,
28
- external_list: ExternalLibs
27
+ /**
28
+ * 解析 External 配置,生成 externals 对象和 importMap
29
+ * @param args CLI 参数
30
+ * @param externalLibs 外部依赖配置
31
+ * @param externalsType External 类型(importmap 或 script)
32
+ * @returns externals 和 importMap
33
+ */
34
+ export function getExternalsAndImportMap(
35
+ args: ModuloArgs_Pack,
36
+ externalLibs: ExternalLibs,
37
+ externalsType: "importmap" | "script" = "importmap",
29
38
  ) {
30
- return Object.entries(external_list).reduce(
31
- ({ externals, importmaps }, [lib_name, data]) => {
32
- // 归一化为GlobalExternal或者ImportExternal
33
- // 此处类型推导有问题,因此手动断言
34
- const _data = is_env_external(data) ? data[args.pack.env] : data;
35
- const external_lib =
36
- typeof _data === "string"
37
- ? { url: { esm: _data, umd: _data } }
38
- : is_module_typed_external_url(_data)
39
- ? { url: _data }
40
- : _data;
39
+ return Object.entries(externalLibs).reduce(
40
+ ({ externals, importMap }, [libName, data]) => {
41
+ // 归一化为GlobalExternal或者ImportExternal
42
+ const externalData = is_env_external(data) ? data[args.pack.env] : data;
43
+
44
+ let externalLib: ImportExternal;
45
+
46
+ if (typeof externalData === "string") {
47
+ externalLib = { url: externalData };
48
+ } else {
49
+ // 此时 externalData 已经是 ImportExternal 类型
50
+ externalLib = externalData as ImportExternal;
51
+ }
52
+
53
+ const url = getExternalUrl(args, externalLib.url);
54
+
55
+ if (externalsType === "script") {
56
+ // Script 模式:external 映射为全局变量
57
+ const globalVar = externalLib.global || libName;
58
+ const importName = externalLib.importName || libName;
59
+ (Array.isArray(importName) ? importName : [importName]).forEach(
60
+ (name) => (externals[name] = globalVar),
61
+ );
41
62
 
42
- // 合并externals,以剔除打包内容
43
- const _importName = external_lib.importName || lib_name;
44
- (Array.isArray(_importName) ? _importName : [_importName]).forEach(
45
- (name) => (externals[name] = lib_name)
46
- );
63
+ if (url) {
64
+ importMap[libName] = url; // 复用 importMap 存储 URL,但在 prepare 中会生成 script 标签
65
+ }
66
+ } else {
67
+ // Importmap 模式:external 映射为包名(由 importmap 解析)
68
+ const importName = externalLib.importName || libName;
69
+ (Array.isArray(importName) ? importName : [importName]).forEach(
70
+ (name) => (externals[name] = libName),
71
+ );
47
72
 
48
- const url = get_external_url(args, external_lib.url);
49
- // 如果有external的url
50
- if (url) {
51
- // 如果是import的,加入到importmaps
52
- importmaps[lib_name] = url;
53
- }
73
+ if (url) {
74
+ importMap[libName] = url;
75
+ }
76
+ }
54
77
 
55
- return {
56
- externals,
57
- importmaps,
58
- };
59
- },
60
- {
61
- externals: {} as Record<string, string>,
62
- importmaps: {} as Record<string, string>,
63
- }
64
- );
78
+ return {
79
+ externals,
80
+ importMap,
81
+ };
82
+ },
83
+ {
84
+ externals: {} as Record<string, string>,
85
+ importMap: {} as Record<string, string>,
86
+ },
87
+ );
65
88
  }
package/src/packer/lib.ts CHANGED
@@ -6,79 +6,86 @@ import { get_global_config, get_packagejson } from "../config/index.ts";
6
6
  import { framework_plugin } from "../tools/get-ui-plugin.ts";
7
7
  import { prepare_config } from "./prepare.ts";
8
8
 
9
+ /**
10
+ * 执行库(module)打包
11
+ *
12
+ * 使用 Rslib 将模块目录下的代码打包为 ESM 和 UMD 格式。
13
+ *
14
+ * @param args CLI 参数
15
+ */
9
16
  export async function lib_pack(args: ModuloArgs_Pack) {
10
- const config = get_global_config(args);
11
- const packagejson = get_packagejson();
17
+ const config = await get_global_config(args);
18
+ const packagejson = get_packagejson();
12
19
 
13
- const { entries, externals } = prepare_config(args, "module", config);
20
+ const { entries, externals } = prepare_config(args, "module", config);
14
21
 
15
- if (!entries) {
16
- return;
17
- }
22
+ if (!entries) {
23
+ return;
24
+ }
18
25
 
19
- const rslibConfig = defineConfig({
20
- source: {
21
- define: config.define,
22
- entry: entries,
23
- },
24
- plugins: [framework_plugin(args), pluginLess()],
25
- resolve: {
26
- alias: config.alias,
27
- },
28
- lib: [
29
- {
30
- format: "esm",
31
- syntax: "esnext",
32
- dts: false,
33
- output: {
34
- assetPrefix: `${config.url.base}/modules`,
35
- externals,
36
- distPath: {
37
- root: config.output.modules,
38
- js: "esm",
39
- jsAsync: "esm",
40
- css: "esm",
41
- },
42
- minify: config.minify,
43
- },
44
- },
45
- {
46
- format: "umd",
47
- output: {
48
- assetPrefix: `${config.url.base}/modules`,
49
- externals,
50
- distPath: {
51
- root: config.output.modules,
52
- js: "umd",
53
- jsAsync: "umd",
54
- css: "umd",
55
- },
56
- minify: config.minify,
57
- },
58
- syntax: "es6",
59
- umdName: `${packagejson.name}-modules-[name]`,
60
- },
61
- ],
62
- output: {
63
- legalComments: "none",
64
- target: "web",
65
- },
66
- performance: {
67
- bundleAnalyze: config.analyze
68
- ? {
69
- analyzerMode: "disabled",
70
- generateStatsFile: true,
71
- }
72
- : undefined,
73
- chunkSplit: {
74
- strategy: "all-in-one",
75
- },
76
- },
77
- });
26
+ const rslibConfig = defineConfig({
27
+ source: {
28
+ define: config.define,
29
+ entry: entries,
30
+ },
31
+ plugins: [framework_plugin(args, config), pluginLess()],
32
+ resolve: {
33
+ alias: config.alias,
34
+ },
35
+ lib: [
36
+ {
37
+ format: "esm",
38
+ syntax: "esnext",
39
+ dts: false,
40
+ output: {
41
+ assetPrefix: `${config.url.base}/modules`,
42
+ externals,
43
+ distPath: {
44
+ root: config.output.modules,
45
+ js: "esm",
46
+ jsAsync: "esm",
47
+ css: "esm",
48
+ },
49
+ minify: config.minify,
50
+ },
51
+ },
52
+ {
53
+ format: "umd",
54
+ output: {
55
+ assetPrefix: `${config.url.base}/modules`,
56
+ externals,
57
+ distPath: {
58
+ root: config.output.modules,
59
+ js: "umd",
60
+ jsAsync: "umd",
61
+ css: "umd",
62
+ },
63
+ minify: config.minify,
64
+ },
65
+ syntax: "es6",
66
+ umdName: `${packagejson.name}-modules-[name]`,
67
+ },
68
+ ],
69
+ output: {
70
+ legalComments: "none",
71
+ target: "web",
72
+ },
73
+ performance: {
74
+ bundleAnalyze: config.analyze
75
+ ? {
76
+ analyzerMode: "disabled",
77
+ generateStatsFile: true,
78
+ }
79
+ : undefined,
80
+ chunkSplit: {
81
+ strategy: "all-in-one",
82
+ },
83
+ },
84
+ });
78
85
 
79
- await build(rslibConfig, { watch: args.cmd === "build" && args.pack.watch });
86
+ await build(rslibConfig, { watch: args.cmd === "build" && args.pack.watch });
80
87
 
81
- if (args.cmd === "build") {
82
- console.log(picocolors.green("\n**** 构建【module】完成 ****\n"));
83
- }
88
+ if (args.cmd === "build") {
89
+ console.log(picocolors.green("\n**** 构建【module】完成 ****\n"));
90
+ }
84
91
  }
@@ -4,95 +4,119 @@ import { pluginLess } from "@rsbuild/plugin-less";
4
4
  import picocolors from "picocolors";
5
5
  import type { ModuloArgs_Pack } from "../args/index.ts";
6
6
  import { get_global_config } from "../config/index.ts";
7
- import { get_package_root } from "../tools/find-path-root.ts";
7
+ import {
8
+ get_package_root,
9
+ find_workspace_root,
10
+ } from "../tools/find-path-root.ts";
8
11
  import { framework_plugin } from "../tools/get-ui-plugin.ts";
9
- import { pluginUmd } from "@rsbuild/plugin-umd";
10
12
  import { prepare_config } from "./prepare.ts";
13
+ import { AutoExternalPlugin } from "./auto-external-plugin.ts";
11
14
 
15
+ /**
16
+ * 执行页面(page)打包
17
+ *
18
+ * 使用 Rsbuild 构建单页应用或多页应用。
19
+ *
20
+ * @param args CLI 参数
21
+ */
12
22
  export async function page_pack(args: ModuloArgs_Pack) {
13
- const config = get_global_config(args);
23
+ const config = await get_global_config(args);
14
24
 
15
- const { entries, externals, importmaps_tag } = prepare_config(
16
- args,
17
- "page",
18
- config
19
- );
25
+ const { entries, externals } = prepare_config(args, "page", config);
20
26
 
21
- if (!entries) {
22
- return;
23
- }
27
+ if (!entries) {
28
+ return;
29
+ }
24
30
 
25
- const rsbuildConfig = defineConfig({
26
- source: {
27
- define: config.define,
28
- entry: entries,
29
- },
30
- plugins: [
31
- framework_plugin(args),
32
- pluginLess(),
33
- pluginUmd({
34
- name: "modulo-page",
35
- }),
36
- ],
37
- tools: {
38
- rspack: {
39
- experiments: {
40
- outputModule: args.pack.esm,
41
- },
42
- },
43
- htmlPlugin: true,
44
- },
45
- output: {
46
- assetPrefix: config.url.cdn || config.url.base,
47
- distPath: {
48
- root: config.output.dist,
49
- },
50
- externals,
51
- filenameHash: config.output.filenameHash,
52
- legalComments: "none",
53
- minify: config.minify,
54
- },
55
- html: {
56
- meta: config.html.meta,
57
- mountId: config.html.root,
58
- scriptLoading: args.pack.esm ? "module" : "defer",
59
- tags: [importmaps_tag, ...config.html.tags],
60
- template:
61
- config.html.template ||
62
- resolve(get_package_root(), "template/index.html"),
63
- templateParameters: {
64
- base_prefix: config.url.base,
65
- },
66
- title: config.html.title,
67
- },
68
- resolve: {
69
- alias: config.alias,
70
- },
71
- server: {
72
- base: config.url.base,
73
- open: config.dev_server.open
74
- ? config.dev_server.open.map(
75
- (name: string) =>
76
- config.url.base +
77
- (name.endsWith("html") ? `/${name}` : `/${name}.html`)
78
- )
79
- : false,
80
- port: config.dev_server.port,
81
- proxy: config.dev_server.proxy,
82
- },
83
- performance: {
84
- chunkSplit: {
85
- strategy: "split-by-experience",
86
- },
87
- },
88
- });
31
+ const workspaceRoot = find_workspace_root(process.cwd());
89
32
 
90
- const rsbuild = await createRsbuild({ rsbuildConfig });
91
- await rsbuild[args.cmd === "dev" ? "startDevServer" : "build"]({
92
- watch: args.cmd === "build" && args.pack.watch,
93
- });
33
+ const rsbuildConfig = defineConfig({
34
+ source: {
35
+ define: config.define,
36
+ entry: entries,
37
+ },
38
+ plugins: [framework_plugin(config), pluginLess()],
39
+ tools: {
40
+ rspack: {
41
+ experiments: {
42
+ outputModule: true,
43
+ },
44
+ plugins: [
45
+ // @ts-ignore Rspack 插件类型兼容问题
46
+ new AutoExternalPlugin(args, config),
47
+ ],
48
+ },
49
+ htmlPlugin: true,
50
+ },
51
+ output: {
52
+ assetPrefix: config.url.cdn || config.url.base,
53
+ distPath: {
54
+ root: config.output.dist,
55
+ },
56
+ externals,
57
+ filenameHash: config.output.filenameHash,
58
+ legalComments: "none",
59
+ minify: config.minify,
60
+ },
61
+ html: {
62
+ meta: config.html.meta,
63
+ mountId: config.html.root,
64
+ scriptLoading: "module",
65
+ tags: config.html.tags,
66
+ template:
67
+ config.html.template ||
68
+ resolve(get_package_root(), "template/index.html"),
69
+ templateParameters: {
70
+ base_prefix: config.url.base,
71
+ },
72
+ title: config.html.title,
73
+ },
74
+ resolve: {
75
+ alias: config.alias,
76
+ },
77
+ server: {
78
+ publicDir: workspaceRoot
79
+ ? {
80
+ name: workspaceRoot,
81
+ copyOnBuild: false,
82
+ watch: false,
83
+ }
84
+ : undefined,
85
+ open: config.dev_server.open
86
+ ? config.dev_server.open.map(
87
+ (name: string) =>
88
+ config.url.base +
89
+ (name.endsWith("html") ? `/${name}` : `/${name}.html`),
90
+ )
91
+ : false,
92
+ port: config.dev_server.port,
93
+ proxy: config.dev_server.proxy,
94
+ },
95
+ performance: {
96
+ chunkSplit: {
97
+ strategy: "split-by-experience",
98
+ },
99
+ },
100
+ });
94
101
 
95
- if (args.cmd === "build") {
96
- console.log(picocolors.green("\n**** 构建【page】完成 ****"));
97
- }
102
+ console.log(
103
+ "Dev Server Config:",
104
+ JSON.stringify(rsbuildConfig.server, null, 2),
105
+ );
106
+
107
+ const rsbuild = await createRsbuild({ rsbuildConfig });
108
+
109
+ if (args.cmd === "dev") {
110
+ await rsbuild.startDevServer();
111
+ } else if (args.cmd === "preview") {
112
+ await rsbuild.preview();
113
+ } else {
114
+ await rsbuild.build({
115
+ watch: args.pack.watch,
116
+ });
117
+ }
118
+
119
+ if (args.cmd === "build") {
120
+ console.log(picocolors.green("\n**** 构建【page】完成 ****"));
121
+ }
98
122
  }