Skip to content

Commit 2ea5e78

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

File tree

29 files changed

+541
-58
lines changed

29 files changed

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

0 commit comments

Comments
 (0)