Skip to content

Commit d5971e8

Browse files
committed
feat: add EntryChunkPlugin to handle shebang and shims
1 parent 95e1179 commit d5971e8

File tree

28 files changed

+549
-56
lines changed

28 files changed

+549
-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,
@@ -596,7 +598,10 @@ const composeFormatConfig = ({
596598
}
597599
};
598600

599-
const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
601+
const composeShimsConfig = (
602+
format: Format,
603+
shims?: Shims,
604+
): { rsbuildConfig: RsbuildConfig; resolvedShims: Shims } => {
600605
const resolvedShims = {
601606
cjs: {
602607
'import.meta.url': shims?.cjs?.['import.meta.url'] ?? true,
@@ -608,9 +613,10 @@ const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
608613
},
609614
};
610615

616+
let rsbuildConfig: RsbuildConfig = {};
611617
switch (format) {
612-
case 'esm':
613-
return {
618+
case 'esm': {
619+
rsbuildConfig = {
614620
tools: {
615621
rspack: {
616622
node: {
@@ -624,19 +630,23 @@ const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
624630
Boolean,
625631
),
626632
};
633+
break;
634+
}
627635
case 'cjs':
628-
return {
636+
rsbuildConfig = {
629637
plugins: [
630638
resolvedShims.cjs['import.meta.url'] && pluginCjsImportMetaUrlShim(),
631639
].filter(Boolean),
632640
};
641+
break;
633642
case 'umd':
634-
return {};
635643
case 'mf':
636-
return {};
644+
break;
637645
default:
638646
throw new Error(`Unsupported format: ${format}`);
639647
}
648+
649+
return { rsbuildConfig, resolvedShims };
640650
};
641651

642652
export const composeModuleImportWarn = (request: string): string => {
@@ -744,6 +754,16 @@ const composeSyntaxConfig = (
744754
};
745755
};
746756

757+
const appendEntryQuery = (
758+
entry: NonNullable<RsbuildConfig['source']>['entry'],
759+
): NonNullable<RsbuildConfig['source']>['entry'] => {
760+
const newEntry: Record<string, string> = {};
761+
for (const key in entry) {
762+
newEntry[key] = `${entry[key]}?${RSLIB_ENTRY_QUERY}`;
763+
}
764+
return newEntry;
765+
};
766+
747767
const composeEntryConfig = async (
748768
entries: NonNullable<RsbuildConfig['source']>['entry'],
749769
bundle: LibConfig['bundle'],
@@ -758,7 +778,7 @@ const composeEntryConfig = async (
758778
return {
759779
entryConfig: {
760780
source: {
761-
entry: entries,
781+
entry: appendEntryQuery(entries),
762782
},
763783
},
764784
lcp: null,
@@ -834,7 +854,7 @@ const composeEntryConfig = async (
834854
const lcp = await calcLongestCommonPath(Object.values(resolvedEntries));
835855
const entryConfig: RsbuildConfig = {
836856
source: {
837-
entry: resolvedEntries,
857+
entry: appendEntryQuery(resolvedEntries),
838858
},
839859
};
840860

@@ -1041,7 +1061,10 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
10411061
redirect = {},
10421062
umdName,
10431063
} = config;
1044-
const shimsConfig = composeShimsConfig(format!, shims);
1064+
const { rsbuildConfig: shimsConfig, resolvedShims } = composeShimsConfig(
1065+
format!,
1066+
shims,
1067+
);
10451068
const formatConfig = composeFormatConfig({
10461069
format: format!,
10471070
pkgJson: pkgJson!,
@@ -1084,6 +1107,9 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
10841107
cssModulesAuto,
10851108
);
10861109
const cssConfig = composeCssConfig(lcp, config.bundle);
1110+
const postEntryChunkConfig = composePostEntryChunkConfig({
1111+
importMetaUrlShim: !!resolvedShims?.cjs?.['import.meta.url'],
1112+
});
10871113
const dtsConfig = await composeDtsConfig(config, dtsExtension);
10881114
const externalsWarnConfig = composeExternalsWarnConfig(
10891115
format!,
@@ -1111,6 +1137,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11111137
targetConfig,
11121138
entryConfig,
11131139
cssConfig,
1140+
postEntryChunkConfig,
11141141
minifyConfig,
11151142
dtsConfig,
11161143
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)