Skip to content

Commit 593df1a

Browse files
committed
feat: add EntryChunkPlugin to handle shebang and shims
1 parent 2d9b75e commit 593df1a

File tree

28 files changed

+551
-56
lines changed

28 files changed

+551
-56
lines changed

packages/core/rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default defineConfig({
1515
entry: {
1616
index: './src/index.ts',
1717
libCssExtractLoader: './src/css/libCssExtractLoader.ts',
18+
entryModuleLoader: './src/plugins/entryModuleLoader.ts',
1819
},
1920
define: {
2021
RSLIB_VERSION: JSON.stringify(require('./package.json').version),

packages/core/src/config.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DEFAULT_CONFIG_NAME,
1515
ENTRY_EXTENSIONS_PATTERN,
1616
JS_EXTENSIONS_PATTERN,
17+
RSLIB_ENTRY_QUERY,
1718
SWC_HELPERS,
1819
} from './constant';
1920
import {
@@ -23,6 +24,7 @@ import {
2324
cssExternalHandler,
2425
isCssGlobalFile,
2526
} from './css/cssConfig';
27+
import { composePostEntryChunkConfig } from './plugins/PostEntryChunkPlugin';
2628
import {
2729
pluginCjsImportMetaUrlShim,
2830
pluginEsmRequireShim,
@@ -598,7 +600,10 @@ const composeFormatConfig = ({
598600
}
599601
};
600602

601-
const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
603+
const composeShimsConfig = (
604+
format: Format,
605+
shims?: Shims,
606+
): { rsbuildConfig: RsbuildConfig; resolvedShims: Shims } => {
602607
const resolvedShims = {
603608
cjs: {
604609
'import.meta.url': shims?.cjs?.['import.meta.url'] ?? true,
@@ -610,9 +615,10 @@ const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
610615
},
611616
};
612617

618+
let rsbuildConfig: RsbuildConfig = {};
613619
switch (format) {
614-
case 'esm':
615-
return {
620+
case 'esm': {
621+
rsbuildConfig = {
616622
tools: {
617623
rspack: {
618624
node: {
@@ -626,19 +632,23 @@ const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
626632
Boolean,
627633
),
628634
};
635+
break;
636+
}
629637
case 'cjs':
630-
return {
638+
rsbuildConfig = {
631639
plugins: [
632640
resolvedShims.cjs['import.meta.url'] && pluginCjsImportMetaUrlShim(),
633641
].filter(Boolean),
634642
};
643+
break;
635644
case 'umd':
636-
return {};
637645
case 'mf':
638-
return {};
646+
break;
639647
default:
640648
throw new Error(`Unsupported format: ${format}`);
641649
}
650+
651+
return { rsbuildConfig, resolvedShims };
642652
};
643653

644654
export const composeModuleImportWarn = (request: string): string => {
@@ -746,6 +756,16 @@ const composeSyntaxConfig = (
746756
};
747757
};
748758

759+
const appendEntryQuery = (
760+
entry: NonNullable<RsbuildConfig['source']>['entry'],
761+
): NonNullable<RsbuildConfig['source']>['entry'] => {
762+
const newEntry: Record<string, string> = {};
763+
for (const key in entry) {
764+
newEntry[key] = `${entry[key]}?${RSLIB_ENTRY_QUERY}`;
765+
}
766+
return newEntry;
767+
};
768+
749769
const composeEntryConfig = async (
750770
entries: NonNullable<RsbuildConfig['source']>['entry'],
751771
bundle: LibConfig['bundle'],
@@ -760,7 +780,7 @@ const composeEntryConfig = async (
760780
return {
761781
entryConfig: {
762782
source: {
763-
entry: entries,
783+
entry: appendEntryQuery(entries),
764784
},
765785
},
766786
lcp: null,
@@ -836,7 +856,7 @@ const composeEntryConfig = async (
836856
const lcp = await calcLongestCommonPath(Object.values(resolvedEntries));
837857
const entryConfig: RsbuildConfig = {
838858
source: {
839-
entry: resolvedEntries,
859+
entry: appendEntryQuery(resolvedEntries),
840860
},
841861
};
842862

@@ -1057,7 +1077,10 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
10571077
redirect = {},
10581078
umdName,
10591079
} = config;
1060-
const shimsConfig = composeShimsConfig(format!, shims);
1080+
const { rsbuildConfig: shimsConfig, resolvedShims } = composeShimsConfig(
1081+
format!,
1082+
shims,
1083+
);
10611084
const formatConfig = composeFormatConfig({
10621085
format: format!,
10631086
pkgJson: pkgJson!,
@@ -1100,6 +1123,9 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11001123
cssModulesAuto,
11011124
);
11021125
const cssConfig = composeCssConfig(lcp, config.bundle);
1126+
const postEntryChunkConfig = composePostEntryChunkConfig({
1127+
importMetaUrlShim: !!resolvedShims?.cjs?.['import.meta.url'],
1128+
});
11031129
const dtsConfig = await composeDtsConfig(config, dtsExtension);
11041130
const externalsWarnConfig = composeExternalsWarnConfig(
11051131
format!,
@@ -1127,6 +1153,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11271153
targetConfig,
11281154
entryConfig,
11291155
cssConfig,
1156+
postEntryChunkConfig,
11301157
minifyConfig,
11311158
dtsConfig,
11321159
bannerFooterConfig,

packages/core/src/constant.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export const DEFAULT_CONFIG_EXTENSIONS = [
1010
] as const;
1111

1212
export const SWC_HELPERS = '@swc/helpers';
13+
export const RSLIB_ENTRY_QUERY = '__rslib_entry__';
14+
export const SHEBANG_PREFIX = '#!';
15+
export const SHEBANG_REGEX: RegExp = /#!.*[\s\n\r]*/;
16+
export const REACT_DIRECTIVE_REGEX: RegExp =
17+
/^['"]use (client|server)['"](;?)$/;
1318

1419
export const JS_EXTENSIONS: string[] = [
1520
'js',

packages/core/src/css/cssConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ export function cssExternalHandler(
110110
return false;
111111
}
112112

113-
const pluginName = 'rsbuild:lib-css';
113+
const PLUGIN_NAME = 'rsbuild:lib-css';
114114

115115
const pluginLibCss = (rootDir: string): RsbuildPlugin => ({
116-
name: pluginName,
116+
name: PLUGIN_NAME,
117117
setup(api) {
118118
api.modifyBundlerChain((config, { CHAIN_ID }) => {
119119
let isUsingCssExtract = false;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { createRequire } from 'node:module';
2+
import {
3+
type RsbuildConfig,
4+
type RsbuildPlugin,
5+
type Rspack,
6+
rspack,
7+
} from '@rsbuild/core';
8+
import {
9+
JS_EXTENSIONS_PATTERN,
10+
REACT_DIRECTIVE_REGEX,
11+
SHEBANG_PREFIX,
12+
SHEBANG_REGEX,
13+
} from '../constant';
14+
import { importMetaUrlShim } from './shims';
15+
const require = createRequire(import.meta.url);
16+
17+
const PLUGIN_NAME = 'rsbuild:entry';
18+
19+
const matchFirstLine = (source: string, regex: RegExp) => {
20+
const [firstLine] = source.split('\n');
21+
if (!firstLine) {
22+
return false;
23+
}
24+
const matched = regex.exec(firstLine);
25+
if (!matched) {
26+
return false;
27+
}
28+
29+
return matched[0];
30+
};
31+
32+
class PostEntryPlugin {
33+
private enabledImportMetaUrlShim: boolean;
34+
private shebangEntries: Record<string, string> = {};
35+
private reactDirectives: Record<string, string> = {};
36+
private importMetaUrlShims: Record<string, { startsWithUseStrict: boolean }> =
37+
{};
38+
39+
constructor({
40+
importMetaUrlShim = true,
41+
}: {
42+
importMetaUrlShim: boolean;
43+
}) {
44+
this.enabledImportMetaUrlShim = importMetaUrlShim;
45+
}
46+
47+
apply(compiler: Rspack.Compiler) {
48+
compiler.hooks.entryOption.tap(PLUGIN_NAME, (_context, entries) => {
49+
for (const name in entries) {
50+
const entry = (entries as Rspack.EntryStaticNormalized)[name];
51+
if (!entry) continue;
52+
53+
let first: string | undefined;
54+
if (Array.isArray(entry)) {
55+
first = entry[0];
56+
} else if (Array.isArray(entry.import)) {
57+
first = entry.import[0];
58+
} else if (typeof entry === 'string') {
59+
first = entry;
60+
}
61+
62+
if (typeof first !== 'string') continue;
63+
64+
const filename = first.split('?')[0]!;
65+
const isJs = JS_EXTENSIONS_PATTERN.test(filename);
66+
if (!isJs) continue;
67+
68+
const content = compiler.inputFileSystem!.readFileSync!(filename, {
69+
encoding: 'utf-8',
70+
});
71+
72+
// Shebang
73+
if (content.startsWith(SHEBANG_PREFIX)) {
74+
const shebangMatch = matchFirstLine(content, SHEBANG_REGEX);
75+
if (shebangMatch) {
76+
this.shebangEntries[name] = shebangMatch;
77+
}
78+
}
79+
80+
// React directive
81+
const reactDirective = matchFirstLine(content, REACT_DIRECTIVE_REGEX);
82+
if (reactDirective) {
83+
this.reactDirectives[name] = reactDirective;
84+
}
85+
86+
// import.meta.url shim
87+
if (this.enabledImportMetaUrlShim) {
88+
this.importMetaUrlShims[name] = {
89+
startsWithUseStrict:
90+
// This is a hypothesis that no comments will occur before "use strict;".
91+
// But it should cover most cases.
92+
content.startsWith('use strict;') ||
93+
content.startsWith('"use strict";'),
94+
};
95+
}
96+
}
97+
});
98+
99+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
100+
compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (chunk, filename) => {
101+
const isJs = JS_EXTENSIONS_PATTERN.test(filename);
102+
if (!isJs) return;
103+
104+
const name = chunk.name;
105+
if (!name) return;
106+
107+
const shebangEntry = this.shebangEntries[name];
108+
if (shebangEntry) {
109+
this.shebangEntries[filename] = shebangEntry;
110+
}
111+
112+
const reactDirective = this.reactDirectives[name];
113+
if (reactDirective) {
114+
this.reactDirectives[filename] = reactDirective;
115+
}
116+
117+
const importMetaUrlShimInfo = this.importMetaUrlShims[name];
118+
if (importMetaUrlShimInfo) {
119+
this.importMetaUrlShims[filename] = importMetaUrlShimInfo;
120+
}
121+
});
122+
});
123+
124+
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
125+
compilation.hooks.processAssets.tap(PLUGIN_NAME, (assets) => {
126+
const chunkAsset = Object.keys(assets);
127+
for (const name of chunkAsset) {
128+
if (this.enabledImportMetaUrlShim) {
129+
compilation.updateAsset(name, (old) => {
130+
const importMetaUrlShimInfo = this.importMetaUrlShims[name];
131+
if (importMetaUrlShimInfo) {
132+
const replaceSource = new rspack.sources.ReplaceSource(old);
133+
134+
if (importMetaUrlShimInfo.startsWithUseStrict) {
135+
replaceSource.replace(
136+
0,
137+
11, // 'use strict;'.length,
138+
`"use strict";\n${importMetaUrlShim}`,
139+
);
140+
} else {
141+
replaceSource.insert(0, importMetaUrlShim);
142+
}
143+
144+
return replaceSource;
145+
}
146+
147+
return old;
148+
});
149+
}
150+
}
151+
});
152+
153+
compilation.hooks.processAssets.tap(
154+
{
155+
name: PLUGIN_NAME,
156+
// Just after minify stage, to avoid from being minified.
157+
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE + 1,
158+
},
159+
(assets) => {
160+
const chunkAsset = Object.keys(assets);
161+
for (const name of chunkAsset) {
162+
const shebangValue = this.shebangEntries[name];
163+
const reactDirectiveValue = this.reactDirectives[name];
164+
165+
if (shebangValue || reactDirectiveValue) {
166+
compilation.updateAsset(name, (old) => {
167+
const replaceSource = new rspack.sources.ReplaceSource(old);
168+
// Shebang
169+
if (shebangValue) {
170+
replaceSource.insert(0, `${shebangValue}\n`);
171+
}
172+
173+
// React directives
174+
if (reactDirectiveValue) {
175+
replaceSource.insert(0, `${reactDirectiveValue}\n`);
176+
}
177+
178+
return replaceSource;
179+
});
180+
}
181+
}
182+
},
183+
);
184+
});
185+
}
186+
}
187+
188+
const entryModuleLoaderPlugin = (): RsbuildPlugin => ({
189+
name: PLUGIN_NAME,
190+
setup(api) {
191+
api.modifyBundlerChain((config, { CHAIN_ID }) => {
192+
const rule = config.module.rule(CHAIN_ID.RULE.JS);
193+
rule
194+
.use('shebang')
195+
.loader(require.resolve('./entryModuleLoader.js'))
196+
.options({});
197+
});
198+
},
199+
});
200+
201+
export const composePostEntryChunkConfig = ({
202+
importMetaUrlShim,
203+
}: {
204+
importMetaUrlShim: boolean;
205+
}): RsbuildConfig => {
206+
return {
207+
plugins: [entryModuleLoaderPlugin()],
208+
tools: {
209+
rspack: {
210+
plugins: [
211+
new PostEntryPlugin({
212+
importMetaUrlShim: importMetaUrlShim,
213+
}),
214+
],
215+
},
216+
},
217+
};
218+
};

0 commit comments

Comments
 (0)