@into-mini/sfc-split-plugin 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,48 @@
1
+ import { toJSONString } from '@into-mini/sfc-transformer/utils.mjs';
2
+
3
+ import { readConfig } from './read.mjs';
4
+
5
+ export function createEmitFile({ PLUGIN_NAME, compilation, RawSource }) {
6
+ return (name, content) => {
7
+ compilation.hooks.processAssets.tap(
8
+ {
9
+ name: PLUGIN_NAME,
10
+ stage: compilation.constructor.PROCESS_ASSETS_STAGE_ADDITIONAL,
11
+ },
12
+ () => {
13
+ compilation.emitAsset(
14
+ name,
15
+ new RawSource(
16
+ typeof content === 'string' ? content : toJSONString(content),
17
+ ),
18
+ );
19
+ },
20
+ );
21
+ };
22
+ }
23
+
24
+ export function readAndTrack(compiler, compilation) {
25
+ return (name) => {
26
+ const { filePath, config = {} } = readConfig(compiler.context, name);
27
+
28
+ if (filePath) {
29
+ compilation.fileDependencies.add(filePath);
30
+ }
31
+
32
+ return {
33
+ name: `${name}.json`,
34
+ content: config,
35
+ empty: Object.keys(config).length === 0,
36
+ };
37
+ };
38
+ }
39
+
40
+ export function createAddEntry(compiler) {
41
+ return (name, path) => {
42
+ new compiler.webpack.EntryPlugin(compiler.context, path, {
43
+ import: [path],
44
+ name,
45
+ // layer: 'base',
46
+ }).apply(compiler);
47
+ };
48
+ }
@@ -0,0 +1,62 @@
1
+ export const COMPONENT_ROOT = 'as-components';
2
+
3
+ function unique(...arr) {
4
+ return [...new Set(arr)];
5
+ }
6
+
7
+ export function getAllPages(config) {
8
+ const { entryPagePath, pages, subPackages, tabBar } = config ?? {};
9
+
10
+ const { custom = false, list = [] } = tabBar ?? {};
11
+
12
+ return unique(
13
+ entryPagePath,
14
+ ...(pages ?? []),
15
+ ...list.map(({ pagePath }) => pagePath),
16
+ ...(subPackages ?? []).flatMap(
17
+ (subPackage) =>
18
+ (subPackage.pages || []).map((page) => `${subPackage.root}/${page}`) ||
19
+ [],
20
+ ),
21
+ custom === true ? 'custom-tab-bar/index' : '',
22
+ ).filter(Boolean);
23
+ }
24
+
25
+ export function patchConfig(json) {
26
+ const object = structuredClone(json ?? {});
27
+
28
+ object.pages ??= [];
29
+
30
+ if (object.tabBar?.list?.length > 0) {
31
+ for (const tab of object.tabBar.list) {
32
+ if (tab.pagePath && !object.pages.includes(tab.pagePath)) {
33
+ object.pages.push(tab.pagePath);
34
+ }
35
+ }
36
+ }
37
+
38
+ object.subPackages ??= [];
39
+ object.preloadRule ??= {};
40
+
41
+ for (const page of object.pages) {
42
+ object.preloadRule[page] ??= {};
43
+
44
+ object.preloadRule[page].network = 'all';
45
+ object.preloadRule[page].packages ??= [];
46
+
47
+ if (!object.preloadRule[page].packages.includes(COMPONENT_ROOT)) {
48
+ object.preloadRule[page].packages.push(COMPONENT_ROOT);
49
+ }
50
+ }
51
+
52
+ if (
53
+ !object.subPackages.some((subPackage) => subPackage.root === COMPONENT_ROOT)
54
+ ) {
55
+ object.subPackages.push({
56
+ root: COMPONENT_ROOT,
57
+ pages: ['fake'],
58
+ });
59
+ }
60
+
61
+ return object;
62
+ }
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import { parse as yamlParse } from 'yaml';
5
+
6
+ function tryReadFileWithParsers(base, name, ext, parser) {
7
+ const filePath = resolve(base, `${name}${ext}`);
8
+
9
+ try {
10
+ const content = readFileSync(filePath, 'utf8');
11
+
12
+ return {
13
+ filePath,
14
+ config: (parser ? parser(content) : content) || {},
15
+ };
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ const candidates = [
22
+ { ext: '.yaml', parser: yamlParse },
23
+ { ext: '.yml', parser: yamlParse },
24
+ { ext: '.json', parser: JSON.parse },
25
+ ];
26
+
27
+ export function readConfig(base, name) {
28
+ for (const { ext, parser } of candidates) {
29
+ const result = tryReadFileWithParsers(base, name, ext, parser);
30
+
31
+ if (result !== false) {
32
+ return result;
33
+ }
34
+ }
35
+
36
+ return false;
37
+ }
@@ -0,0 +1,6 @@
1
+ export const configKeys = {
2
+ app: 'app-json',
3
+ projectPrivate: 'project.private.config',
4
+ project: 'project.config',
5
+ hack: 'hack.entry',
6
+ };
@@ -0,0 +1,140 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { toJSONString } from '@into-mini/sfc-transformer/utils.mjs';
6
+ import slash from 'slash';
7
+
8
+ function createShortHash(input) {
9
+ return createHash('sha256').update(input).digest('hex').slice(0, 8);
10
+ }
11
+
12
+ function reach(path) {
13
+ return fileURLToPath(import.meta.resolve(path));
14
+ }
15
+
16
+ function handleImport({
17
+ toThis,
18
+ addSmartEntry,
19
+ componentRoot,
20
+ context,
21
+ rootContext,
22
+ maps,
23
+ callback,
24
+ }) {
25
+ if (Object.keys(maps).length > 0) {
26
+ for (const [name, path] of Object.entries(maps)) {
27
+ if (path.endsWith('.vue') && !path.startsWith('plugin://')) {
28
+ try {
29
+ const absolutePath = slash(
30
+ path.startsWith('.') ? resolve(context, path) : reach(path),
31
+ );
32
+ const relativePath = slash(relative(rootContext, absolutePath));
33
+ const hack = relativePath.startsWith('..');
34
+ const entryName = hack
35
+ ? [
36
+ componentRoot,
37
+ absolutePath
38
+ .split('/')
39
+ .slice(-2)
40
+ .join('/')
41
+ .replace(/\.vue$/, ''),
42
+ createShortHash(slash(relativePath)),
43
+ ].join('/')
44
+ : relativePath.replace(/\.vue$/, '');
45
+ const placer = toThis(entryName);
46
+ callback({
47
+ name,
48
+ placer,
49
+ });
50
+
51
+ const entryPath = relativePath.startsWith('..')
52
+ ? absolutePath
53
+ : `./${relativePath}`;
54
+
55
+ this.addDependency(resolve(absolutePath));
56
+ this.addMissingDependency(resolve(absolutePath));
57
+ addSmartEntry({
58
+ name: entryName,
59
+ path: entryPath,
60
+ });
61
+ } catch (error) {
62
+ console.error(error);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ export default function loader(source, map, meta) {
70
+ this.cacheable();
71
+ const callback = this.async();
72
+ const { componentRoot } = this.getOptions();
73
+ const { entryName: thisEntryName } = this;
74
+ const resourcePath = slash(this.resourcePath);
75
+ const { paths, config, script } = this.processSfcFile({
76
+ source,
77
+ resourcePath,
78
+ });
79
+ const { rootContext, context } = this;
80
+
81
+ for (const path of paths) {
82
+ const filePath = join(rootContext, path);
83
+ this.addDependency(filePath);
84
+ this.addMissingDependency(filePath);
85
+ }
86
+
87
+ function toThis(entryName) {
88
+ return slash(relative(`/${thisEntryName}/..`, `/${entryName}`));
89
+ }
90
+
91
+ const addSmartEntry = (io) => {
92
+ this.addSmartEntry(io);
93
+ };
94
+
95
+ if (config?.usingComponents) {
96
+ handleImport.bind(this)({
97
+ toThis,
98
+ addSmartEntry,
99
+ componentRoot,
100
+ context,
101
+ rootContext,
102
+ maps: config.usingComponents,
103
+ callback({ name, placer }) {
104
+ config.usingComponents[name] = placer;
105
+
106
+ if (placer.includes(componentRoot)) {
107
+ config.componentPlaceholder ??= {};
108
+ config.componentPlaceholder[name] = 'view';
109
+ }
110
+ },
111
+ });
112
+ }
113
+
114
+ if (config?.componentGenerics) {
115
+ handleImport.bind(this)({
116
+ toThis,
117
+ addSmartEntry,
118
+ componentRoot,
119
+ context,
120
+ rootContext,
121
+ maps: Object.fromEntries(
122
+ Object.entries(config.componentGenerics)
123
+ .filter(([_, item]) => item?.default)
124
+ .map(([key, item]) => [key, item.default]),
125
+ ),
126
+ callback({ name, placer }) {
127
+ config.componentGenerics[name].default = placer;
128
+ },
129
+ });
130
+ }
131
+
132
+ const file = [
133
+ ...paths
134
+ .map((path) => relative(`${resourcePath}/..`, path))
135
+ .map((path) => `import "./${path}";`),
136
+ script,
137
+ ].join('\n');
138
+ this.emitFile(`${thisEntryName}.json`, toJSONString(config));
139
+ callback(null, file, map, meta);
140
+ }
@@ -0,0 +1,5 @@
1
+ export default function loader() {
2
+ console.log(this._compiler.__entries__);
3
+
4
+ return '';
5
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@into-mini/sfc-split-plugin",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "keywords": [
6
+ "loader",
7
+ "mini-program",
8
+ "miniprogram",
9
+ "sfc",
10
+ "vue",
11
+ "weapp",
12
+ "webpack",
13
+ "webpack-plugin",
14
+ "wxml"
15
+ ],
16
+ "homepage": "https://github.com/into-mini/into-mini/tree/master/packages/sfc-split-plugin",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/into-mini/into-mini.git",
20
+ "directory": "packages/sfc-split-plugin"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/into-mini/into-mini/issues"
24
+ },
25
+ "main": "plugin.mjs",
26
+ "files": [
27
+ "**/*.json",
28
+ "**/*.mjs"
29
+ ],
30
+ "type": "module",
31
+ "dependencies": {
32
+ "slash": "^5.1.0",
33
+ "webpack-virtual-modules": "^0.6.2",
34
+ "yaml": "^2.8.2",
35
+ "yaml-patch-loader": "^0.1.0",
36
+ "@into-mini/clsx": "^0.1.0",
37
+ "@into-mini/sfc-transformer": "^0.5.12",
38
+ "@into-mini/wxml-loader": "^0.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@babel/core": "^7.28.5",
42
+ "webpack": "^5.103.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.18.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public",
49
+ "registry": "https://registry.npmjs.org/"
50
+ }
51
+ }
@@ -0,0 +1,85 @@
1
+ export class AddEntryPlugin {
2
+ PLUGIN_NAME = 'AddEntryPlugin';
3
+
4
+ #addSmartEntry({ name, path, layer }) {
5
+ if (this.compiler.__entries__.get(name) !== path) {
6
+ this.compiler.__entries__.set(name, { path, layer });
7
+ }
8
+ }
9
+
10
+ #addEntries(compilation) {
11
+ const { compiler } = this;
12
+
13
+ const { createDependency } = compiler.webpack.EntryPlugin;
14
+
15
+ compilation.hooks.buildModule.tap(this.PLUGIN_NAME, () => {
16
+ for (const [name, { path, layer }] of compiler.__entries__.entries()) {
17
+ compilation.addEntry(
18
+ compiler.context,
19
+ createDependency(path, { name }),
20
+ {
21
+ name,
22
+ import: [path],
23
+ layer,
24
+ },
25
+ (err) => {
26
+ if (err) {
27
+ throw err;
28
+ } else {
29
+ compilation.fileDependencies.add(path);
30
+ }
31
+ },
32
+ );
33
+ }
34
+ });
35
+ }
36
+
37
+ #expose(compiler) {
38
+ this.compiler = compiler;
39
+
40
+ const { PLUGIN_NAME } = this;
41
+
42
+ const {
43
+ NormalModule: { getCompilationHooks },
44
+ } = compiler.webpack;
45
+
46
+ Object.defineProperty(compiler, 'addSmartEntry', {
47
+ enumerable: true,
48
+ configurable: false,
49
+ value: (options) => {
50
+ this.#addSmartEntry(options);
51
+ },
52
+ });
53
+
54
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
55
+ getCompilationHooks(compilation).loader.tap(
56
+ PLUGIN_NAME,
57
+ (loaderContext) => {
58
+ Object.defineProperty(loaderContext, 'addSmartEntry', {
59
+ enumerable: true,
60
+ configurable: false,
61
+ value: (options) => {
62
+ this.#addSmartEntry(options);
63
+ },
64
+ });
65
+ },
66
+ );
67
+ });
68
+ }
69
+
70
+ apply(compiler) {
71
+ this.#expose(compiler);
72
+
73
+ const { PLUGIN_NAME } = this;
74
+
75
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
76
+ this.#addEntries(compilation);
77
+ });
78
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
79
+ this.#addEntries(compilation);
80
+ });
81
+ compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
82
+ this.#addEntries(compilation);
83
+ });
84
+ }
85
+ }
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extname, join, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { CLSX_PLACEHOLDER } from '@into-mini/sfc-transformer/utils.mjs';
6
+ import slash from 'slash';
7
+
8
+ // WXS文件输出路径
9
+ const WXS_FILENAME = 'wxs/clsx.wxs';
10
+
11
+ /**
12
+ * 将clsx.wxs文件添加到编译结果中,并替换WXML文件中的占位符。
13
+ * 只在发现CLSX_PLACEHOLDER时添加wxs文件,且只添加一次。
14
+ */
15
+ export class AddWxsPlugin {
16
+ PLUGIN_NAME = 'AddWxsPlugin';
17
+
18
+ apply(compiler) {
19
+ const { RawSource } = compiler.webpack.sources;
20
+
21
+ compiler.hooks.compilation.tap(this.PLUGIN_NAME, (compilation) => {
22
+ compilation.hooks.processAssets.tap(
23
+ {
24
+ name: this.PLUGIN_NAME,
25
+ stage: compilation.constructor.PROCESS_ASSETS_STAGE_ADDITIONAL,
26
+ },
27
+ (assets) => this.#processAssets(assets, compilation, RawSource),
28
+ );
29
+ });
30
+ }
31
+
32
+ #processAssets(assets, compilation, RawSource) {
33
+ // 处理所有wxml文件
34
+ for (const [filename, source] of Object.entries(assets)) {
35
+ if (extname(filename) === '.wxml') {
36
+ const content = source.source().toString();
37
+
38
+ if (content.includes(CLSX_PLACEHOLDER)) {
39
+ this.#addWxsFile(compilation, RawSource);
40
+
41
+ this.#replaceSource(compilation, RawSource, {
42
+ filename,
43
+ content,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ #replaceSource(compilation, RawSource, { filename, content }) {
51
+ const relativePath = slash(relative(join(filename, '..'), WXS_FILENAME));
52
+ const newContent = content.replace(CLSX_PLACEHOLDER, relativePath);
53
+ compilation.updateAsset(filename, new RawSource(newContent));
54
+ }
55
+
56
+ #addWxsFile(compilation, RawSource) {
57
+ const wxsPath = import.meta.resolve('@into-mini/clsx/index.wxs');
58
+ const wxsContent = readFileSync(fileURLToPath(wxsPath), 'utf8');
59
+
60
+ compilation.emitAsset(WXS_FILENAME, new RawSource(wxsContent));
61
+ }
62
+ }
@@ -0,0 +1,89 @@
1
+ import { fileURLToPath } from 'node:url';
2
+
3
+ import { patchConfig } from '../helper/index.mjs';
4
+ import { configKeys } from '../helper/utils.mjs';
5
+
6
+ function reach(path) {
7
+ return fileURLToPath(import.meta.resolve(path));
8
+ }
9
+
10
+ const emptyJSON = reach('../helper/empty.json');
11
+ const yamlLoader = reach('yaml-patch-loader');
12
+
13
+ export class CopyConfigPlugin {
14
+ constructor({ type = false } = {}) {
15
+ this.type = type;
16
+ }
17
+
18
+ addConfigSmartEntry({
19
+ layer,
20
+ from = layer,
21
+ name = layer,
22
+ filename = from,
23
+ options,
24
+ }) {
25
+ const path = `./${from}.yaml`;
26
+
27
+ this.compiler.options.entry[name] = {
28
+ import: [path],
29
+ layer,
30
+ runtime: false,
31
+ filename,
32
+ };
33
+
34
+ this.compiler.options.resolve.fallback[path] = emptyJSON;
35
+
36
+ this.compiler.options.module.rules.push({
37
+ issuerLayer: layer,
38
+ loader: yamlLoader,
39
+ type: 'asset/resource',
40
+ generator: {
41
+ filename: `${filename}.json`,
42
+ },
43
+ options,
44
+ });
45
+ }
46
+
47
+ apply(compiler) {
48
+ const { type } = this;
49
+ this.compiler = compiler;
50
+
51
+ if (type) {
52
+ this.addConfigSmartEntry({
53
+ layer: configKeys.project,
54
+ options: {
55
+ modify: (json) => ({
56
+ srcMiniprogramRoot: '',
57
+ miniprogramRoot: '',
58
+ pluginRoot: '',
59
+ ...json,
60
+ compileType: type,
61
+ }),
62
+ },
63
+ });
64
+
65
+ this.addConfigSmartEntry({
66
+ layer: configKeys.projectPrivate,
67
+ });
68
+
69
+ if (this.type === 'miniprogram') {
70
+ this.addConfigSmartEntry({
71
+ layer: configKeys.app,
72
+ from: 'app',
73
+ options: {
74
+ modify: patchConfig,
75
+ },
76
+ });
77
+
78
+ this.compiler.options.entry.app = {
79
+ import: ['./app'],
80
+ };
81
+
82
+ // this.compiler.options.entry.__hack__ = {
83
+ // import: [reach('../helper/hack.hack')],
84
+ // layer: configKeys.hack,
85
+ // };
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,35 @@
1
+ import { COMPONENT_ROOT } from '../helper/index.mjs';
2
+
3
+ const files = {
4
+ '/fake.json': '{}',
5
+ '/fake.js': '/**用于创建分包的假页面**/',
6
+ '/fake.wxml': '<!--用于创建分包的假页面-->',
7
+ };
8
+
9
+ export class EmitFakePlugin {
10
+ PLUGIN_NAME = 'EmitFakePlugin';
11
+
12
+ apply(compiler) {
13
+ const {
14
+ sources: { RawSource },
15
+ Compilation: { PROCESS_ASSETS_STAGE_ADDITIONAL },
16
+ } = compiler.webpack;
17
+
18
+ compiler.hooks.make.tap(this.PLUGIN_NAME, (compilation) => {
19
+ compilation.hooks.processAssets.tap(
20
+ {
21
+ name: this.PLUGIN_NAME,
22
+ stage: PROCESS_ASSETS_STAGE_ADDITIONAL,
23
+ },
24
+ () => {
25
+ for (const [path, content] of Object.entries(files)) {
26
+ compilation.emitAsset(
27
+ COMPONENT_ROOT + path,
28
+ new RawSource(content),
29
+ );
30
+ }
31
+ },
32
+ );
33
+ });
34
+ }
35
+ }
@@ -0,0 +1,73 @@
1
+ import slash from 'slash';
2
+ const PLUGIN_NAME = 'ExposeEntryNamePlugin';
3
+ export class ExposeEntryNamePlugin {
4
+ getEntryNameFromEntries(compilation, module) {
5
+ const { moduleGraph, entries } = compilation;
6
+ for (const [name, io] of entries) {
7
+ for (const dep of io.dependencies) {
8
+ const entryModule = moduleGraph.getModule(dep);
9
+ if (entryModule) {
10
+ if (
11
+ // @ts-expect-error ------------
12
+ entryModule.request && // @ts-expect-error ------------
13
+ slash(entryModule.request) === slash(module.request)
14
+ ) {
15
+ return name;
16
+ }
17
+ if (
18
+ // @ts-expect-error ------------
19
+ entryModule?.resource && // @ts-expect-error ------------
20
+ slash(entryModule?.resource) === slash(module.resource)
21
+ ) {
22
+ return name;
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return '';
28
+ }
29
+ getEntryNameFromPathData(compilation, pathData) {
30
+ const mod = pathData.module;
31
+ const graph = pathData.chunkGraph;
32
+ if (mod && graph) {
33
+ const [entryModule] = graph
34
+ .getModuleChunks(mod)
35
+ .map((chunk) => [...graph.getChunkEntryModulesIterable(chunk)][0]);
36
+ if (entryModule) {
37
+ return this.getEntryNameFromEntries(compilation, entryModule);
38
+ }
39
+ }
40
+ return '';
41
+ }
42
+ apply(compiler) {
43
+ const {
44
+ NormalModule: { getCompilationHooks },
45
+ } = compiler.webpack;
46
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
47
+ compilation.hooks.assetPath.tap(PLUGIN_NAME, (path, pathData) => {
48
+ if (path.includes('[entry]')) {
49
+ const entryName = this.getEntryNameFromPathData(
50
+ compilation,
51
+ pathData,
52
+ );
53
+ return entryName
54
+ ? path.replaceAll('[entry]', entryName)
55
+ : path.replaceAll('[entry]', '[hash:8]');
56
+ }
57
+ return path;
58
+ });
59
+ getCompilationHooks(compilation).loader.tap(
60
+ PLUGIN_NAME,
61
+ (loaderContext, module) => {
62
+ Object.defineProperty(loaderContext, 'entryName', {
63
+ enumerable: true,
64
+ configurable: false,
65
+ get: () => {
66
+ return this.getEntryNameFromEntries(compilation, module);
67
+ },
68
+ });
69
+ },
70
+ );
71
+ });
72
+ }
73
+ }
@@ -0,0 +1,86 @@
1
+ import slash from 'slash';
2
+ const PLUGIN_NAME = 'ExposeEntryNamePlugin';
3
+ export class ExposeEntryNamePlugin {
4
+ getEntryNameFromChunk(chunk) {
5
+ if (!chunk?.groupsIterable) {
6
+ return '';
7
+ }
8
+ for (const group of chunk.groupsIterable) {
9
+ if (group.isInitial()) {
10
+ return group.name;
11
+ }
12
+ }
13
+ return '';
14
+ }
15
+ getEntryNameFromEntries(compilation, module) {
16
+ const { moduleGraph, entries } = compilation;
17
+ for (const [name, io] of entries) {
18
+ for (const dep of io.dependencies) {
19
+ const entryModule = moduleGraph.getModule(dep);
20
+ if (entryModule) {
21
+ if (
22
+ // @ts-expect-error ------------
23
+ entryModule.request && // @ts-expect-error ------------
24
+ slash(entryModule.request) === slash(module.request)
25
+ ) {
26
+ return name;
27
+ }
28
+ if (
29
+ // @ts-expect-error ------------
30
+ entryModule?.resource && // @ts-expect-error ------------
31
+ slash(entryModule?.resource) === slash(module.resource)
32
+ ) {
33
+ return name;
34
+ }
35
+ }
36
+ }
37
+ }
38
+ return '';
39
+ }
40
+ getEntryNameFromPathData(pathData) {
41
+ if (pathData?.chunk) {
42
+ const entryName = this.getEntryNameFromChunk(pathData.chunk);
43
+ if (entryName) {
44
+ return entryName;
45
+ }
46
+ }
47
+ if (pathData?.module && pathData?.chunkGraph) {
48
+ const chunks = pathData.chunkGraph.getModuleChunks(pathData.module);
49
+ for (const chunk of chunks) {
50
+ const entryName = this.getEntryNameFromChunk(chunk);
51
+ if (entryName) {
52
+ return entryName;
53
+ }
54
+ }
55
+ }
56
+ return '';
57
+ }
58
+ apply(compiler) {
59
+ const {
60
+ NormalModule: { getCompilationHooks },
61
+ } = compiler.webpack;
62
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
63
+ compilation.hooks.assetPath.tap(PLUGIN_NAME, (path, pathData) => {
64
+ if (path.includes('[entry]')) {
65
+ const entryName = this.getEntryNameFromPathData(pathData);
66
+ return entryName
67
+ ? path.replaceAll('[entry]', entryName)
68
+ : path.replaceAll('[entry]', '[hash:8]');
69
+ }
70
+ return path;
71
+ });
72
+ getCompilationHooks(compilation).loader.tap(
73
+ PLUGIN_NAME,
74
+ (loaderContext, module) => {
75
+ Object.defineProperty(loaderContext, 'entryName', {
76
+ enumerable: true,
77
+ configurable: false,
78
+ get: () => {
79
+ return this.getEntryNameFromEntries(compilation, module);
80
+ },
81
+ });
82
+ },
83
+ );
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,111 @@
1
+ import { extname } from 'node:path';
2
+
3
+ import {
4
+ createAddEntry,
5
+ createEmitFile,
6
+ readAndTrack,
7
+ } from '../helper/hooks.mjs';
8
+ import { getAllPages } from '../helper/index.mjs';
9
+
10
+ /**
11
+ * Webpack插件,用于处理小程序和插件的入口文件
12
+ */
13
+ export class FindEntryPlugin {
14
+ PLUGIN_NAME = 'FindEntryPlugin';
15
+
16
+ /**
17
+ * @param {Object} options - 插件配置选项
18
+ * @param {string} [options.type=false] - 项目类型,可选值:'miniprogram'或'plugin'
19
+ */
20
+ constructor({ type = false } = {}) {
21
+ this.type = type;
22
+ }
23
+
24
+ /**
25
+ * 处理Vue页面入口
26
+ * @private
27
+ */
28
+ #handleVuePages(params, pages, basePath = '.') {
29
+ const { addEntry, compilation } = params;
30
+
31
+ for (const page of pages) {
32
+ const source = `${basePath}/${page}.vue`;
33
+ addEntry(page, source);
34
+ compilation.fileDependencies.add(source);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 处理插件页面和组件
40
+ * @private
41
+ */
42
+ #handlePluginSection(params, config, section) {
43
+ const entries = config[section];
44
+
45
+ if (!entries) {
46
+ return;
47
+ }
48
+
49
+ Object.entries(entries).forEach(([key, path]) => {
50
+ if (typeof path === 'string' && extname(path) === '.vue') {
51
+ const source = `${section}/${key}/index`;
52
+ params.addEntry(source, path);
53
+ params.compilation.fileDependencies.add(path);
54
+
55
+ if (section === 'pages') {
56
+ config.pages = config.pages || {};
57
+ config.pages[key] = source;
58
+ }
59
+ }
60
+ });
61
+ }
62
+
63
+ /**
64
+ * 处理配置和入口
65
+ * @private
66
+ */
67
+ #processConfig(params, configType) {
68
+ const { readFrom, emitFile, addEntry, compilation } = params;
69
+ const { content: config, name } = readFrom(
70
+ configType === 'miniprogram' ? 'app' : configType,
71
+ );
72
+
73
+ if (configType === 'miniprogram') {
74
+ this.#handleVuePages(params, getAllPages(config));
75
+ } else if (configType === 'plugin') {
76
+ if (config.main) {
77
+ addEntry('main', config.main);
78
+ compilation.fileDependencies.add(config.main);
79
+ config.main = 'main.js';
80
+ }
81
+
82
+ this.#handlePluginSection(params, config, 'pages');
83
+ this.#handlePluginSection(params, config, 'publicComponents');
84
+ emitFile(name, config);
85
+ }
86
+ }
87
+
88
+ apply(compiler) {
89
+ const {
90
+ sources: { RawSource },
91
+ } = compiler.webpack;
92
+
93
+ const addEntry = createAddEntry(compiler);
94
+
95
+ // 处理入口文件和配置
96
+ compiler.hooks.compilation.tap(this.PLUGIN_NAME, (compilation) => {
97
+ const params = {
98
+ compilation,
99
+ addEntry,
100
+ emitFile: createEmitFile({
101
+ PLUGIN_NAME: this.PLUGIN_NAME,
102
+ compilation,
103
+ RawSource,
104
+ }),
105
+ readFrom: readAndTrack(compiler, compilation),
106
+ };
107
+
108
+ this.#processConfig(params, this.type);
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,58 @@
1
+ import { dirname, relative, sep } from 'node:path';
2
+
3
+ const PLUGIN_NAME = 'MinaRuntimeWebpackPlugin';
4
+
5
+ export class MinaRuntimeWebpackPlugin {
6
+ // 格式化依赖路径为require语句
7
+ #formatRequire = (from, to) =>
8
+ `require('./${relative(dirname(from), to).split(sep).join('/')}');\n`;
9
+
10
+ // 收集并生成所有依赖的require语句
11
+ #generateDependencies = (chunk) => {
12
+ if (!chunk?.name) {
13
+ return '';
14
+ }
15
+
16
+ let result = '';
17
+
18
+ for (const { chunks } of chunk.groupsIterable) {
19
+ for (const depChunk of chunks) {
20
+ if (depChunk !== chunk && depChunk.name) {
21
+ result += this.#formatRequire(chunk.name, depChunk.name);
22
+ }
23
+ }
24
+ }
25
+
26
+ return result;
27
+ };
28
+
29
+ apply(compiler) {
30
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
31
+ const {
32
+ javascript: { JavascriptModulesPlugin },
33
+ sources: { ConcatSource },
34
+ } = compiler.webpack;
35
+
36
+ JavascriptModulesPlugin.getCompilationHooks(compilation).renderChunk.tap(
37
+ PLUGIN_NAME,
38
+ (source, { chunk }) => {
39
+ if (
40
+ !chunk ||
41
+ !compilation.chunkGraph.getNumberOfEntryModules(chunk)
42
+ ) {
43
+ // 跳过非入口模块
44
+ return source;
45
+ }
46
+
47
+ const deps = this.#generateDependencies(chunk);
48
+
49
+ if (!deps) {
50
+ return source;
51
+ }
52
+
53
+ return new ConcatSource(deps, source);
54
+ },
55
+ );
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,102 @@
1
+ import path from 'node:path';
2
+
3
+ import { parse } from '@into-mini/sfc-transformer';
4
+ import slash from 'slash';
5
+ import VirtualModulesPlugin from 'webpack-virtual-modules';
6
+
7
+ export class SfcSplitPlugin extends VirtualModulesPlugin {
8
+ PLUGIN_NAME = 'SfcSplitPlugin';
9
+
10
+ constructor({ tagMatcher, preserveTap }) {
11
+ super();
12
+ this.tagMatcher = tagMatcher;
13
+ this.preserveTap = preserveTap;
14
+ }
15
+
16
+ apply(compiler) {
17
+ this.#expose(compiler);
18
+
19
+ super.apply(compiler);
20
+ }
21
+
22
+ #expose(compiler) {
23
+ const { PLUGIN_NAME } = this;
24
+
25
+ const {
26
+ NormalModule: { getCompilationHooks },
27
+ } = compiler.webpack;
28
+
29
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
30
+ getCompilationHooks(compilation).loader.tap(
31
+ PLUGIN_NAME,
32
+ (loaderContext) => {
33
+ Object.defineProperty(loaderContext, 'processSfcFile', {
34
+ enumerable: true,
35
+ configurable: false,
36
+ value: (options) => {
37
+ return this.#processSfcFile(options);
38
+ },
39
+ });
40
+ },
41
+ );
42
+ });
43
+ }
44
+
45
+ #inject(resourcePath, ext, content) {
46
+ const src = path.resolve(resourcePath.replace(/\.vue$/, ext));
47
+
48
+ super.writeModule(src, content);
49
+
50
+ return src;
51
+ }
52
+
53
+ #injectStyle(resourcePath, id, style) {
54
+ return this.#inject(
55
+ resourcePath,
56
+ `-${id}.${style.lang ?? 'css'}`,
57
+ style.content,
58
+ );
59
+ }
60
+
61
+ #injectStyles(resourcePath, styles) {
62
+ const io = [];
63
+
64
+ const css = styles?.length > 0 ? styles : [];
65
+
66
+ css.forEach((style, idx) => {
67
+ if (style?.content) {
68
+ const src = this.#injectStyle(resourcePath, idx, style);
69
+ io.push(src);
70
+ }
71
+ });
72
+
73
+ return io;
74
+ }
75
+
76
+ #injectTemplate(resourcePath, tpl) {
77
+ return this.#inject(resourcePath, '.wxml', tpl);
78
+ }
79
+
80
+ #processSfcFile({ source, resourcePath }) {
81
+ const { tagMatcher, preserveTap } = this;
82
+
83
+ const { tpl, styles, code, config } = parse(source, {
84
+ tagMatcher,
85
+ preserveTap,
86
+ });
87
+
88
+ const paths = [];
89
+
90
+ const wxml = this.#injectTemplate(resourcePath, tpl);
91
+ paths.push(wxml);
92
+
93
+ const css = this.#injectStyles(resourcePath, styles);
94
+ paths.push(...css);
95
+
96
+ return {
97
+ config,
98
+ paths: paths.map((src) => slash(src)),
99
+ script: code,
100
+ };
101
+ }
102
+ }
package/plugin.mjs ADDED
@@ -0,0 +1,96 @@
1
+ /* eslint-disable no-param-reassign */
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { COMPONENT_ROOT } from './helper/index.mjs';
5
+ import { configKeys } from './helper/utils.mjs';
6
+ import { AddEntryPlugin } from './plugin/add-entry.mjs';
7
+ import { AddWxsPlugin } from './plugin/add-wxs.mjs';
8
+ import { CopyConfigPlugin } from './plugin/copy-config.mjs';
9
+ import { EmitFakePlugin } from './plugin/emit-fake.mjs';
10
+ import { ExposeEntryNamePlugin } from './plugin/expose-entry.mjs';
11
+ import { FindEntryPlugin } from './plugin/find-entry.mjs';
12
+ import { SfcSplitPlugin } from './plugin/sfc-split.mjs';
13
+ import { MinaRuntimeWebpackPlugin } from './plugin/mina-runtime.mjs';
14
+
15
+ function reach(path) {
16
+ return fileURLToPath(import.meta.resolve(path));
17
+ }
18
+
19
+ export class AllInOnePlugin {
20
+ constructor({ type = false, tagMatcher, preserveTap } = {}) {
21
+ this.type = type;
22
+ this.tagMatcher = tagMatcher;
23
+ this.preserveTap = preserveTap;
24
+ }
25
+
26
+ #applyLoader(compiler) {
27
+ compiler.options.module.rules.push(
28
+ // {
29
+ // exclude: /\.(vue|wxml)$/,
30
+ // layer: 'other',
31
+ // },
32
+ {
33
+ test: /\.vue$/,
34
+ loader: reach('./loader/fake-vue-loader.mjs'),
35
+ options: {
36
+ componentRoot: COMPONENT_ROOT,
37
+ },
38
+ },
39
+ {
40
+ test: /\.wxml$/,
41
+ type: 'asset/resource',
42
+ loader: reach('@into-mini/wxml-loader'),
43
+ generator: {
44
+ filename: '[entry][ext]',
45
+ },
46
+ },
47
+ {
48
+ test: /\.hack$/,
49
+ type: 'javascript/esm',
50
+ layer: configKeys.hack,
51
+ loader: reach('./loader/hack-entry-loader.mjs'),
52
+ },
53
+ );
54
+ }
55
+
56
+ #prepare(compiler) {
57
+ compiler.options.resolve.extensionAlias ??= {};
58
+
59
+ compiler.options.resolve.extensionAlias['.yaml'] = [
60
+ '.yaml',
61
+ '.yml',
62
+ '.json',
63
+ ];
64
+
65
+ compiler.options.resolve.fallback ??= {};
66
+
67
+ if (compiler.options.entry?.main) {
68
+ delete compiler.options.entry.main;
69
+ }
70
+
71
+ Object.assign(compiler, {
72
+ __entries__: new Map(),
73
+ });
74
+ }
75
+
76
+ apply(compiler) {
77
+ this.#prepare(compiler);
78
+ this.#applyLoader(compiler);
79
+
80
+ const { type, tagMatcher, preserveTap } = this;
81
+
82
+ if (type) {
83
+ new MinaRuntimeWebpackPlugin().apply(compiler);
84
+ new AddEntryPlugin().apply(compiler);
85
+ new AddWxsPlugin().apply(compiler);
86
+ new SfcSplitPlugin({ tagMatcher, preserveTap }).apply(compiler);
87
+ new ExposeEntryNamePlugin().apply(compiler);
88
+ new FindEntryPlugin({ type }).apply(compiler);
89
+ new CopyConfigPlugin({ type }).apply(compiler);
90
+ }
91
+
92
+ if (type === 'miniprogram') {
93
+ new EmitFakePlugin().apply(compiler);
94
+ }
95
+ }
96
+ }