Skip to content

Commit 33befdc

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

File tree

29 files changed

+521
-69
lines changed

29 files changed

+521
-69
lines changed

packages/core/rslib.config.ts

Lines changed: 2 additions & 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),
@@ -23,6 +24,7 @@ export default defineConfig({
2324
output: {
2425
target: 'node',
2526
externals: {
27+
'./entryModuleLoader': './entryModuleLoader.js',
2628
picocolors: '../compiled/picocolors/index.js',
2729
commander: '../compiled/commander/index.js',
2830
rslog: '../compiled/rslog/index.js',

packages/core/src/config.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
cssExternalHandler,
2424
isCssGlobalFile,
2525
} from './css/cssConfig';
26+
import { composePostEntryChunkConfig } from './plugins/PostEntryChunkPlugin';
2627
import {
2728
pluginCjsImportMetaUrlShim,
2829
pluginEsmRequireShim,
@@ -596,7 +597,10 @@ const composeFormatConfig = ({
596597
}
597598
};
598599

599-
const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
600+
const composeShimsConfig = (
601+
format: Format,
602+
shims?: Shims,
603+
): { rsbuildConfig: RsbuildConfig; resolvedShims: Shims } => {
600604
const resolvedShims = {
601605
cjs: {
602606
'import.meta.url': shims?.cjs?.['import.meta.url'] ?? true,
@@ -611,29 +615,44 @@ const composeShimsConfig = (format: Format, shims?: Shims): RsbuildConfig => {
611615
switch (format) {
612616
case 'esm':
613617
return {
614-
tools: {
615-
rspack: {
616-
node: {
617-
// "__dirname" and "__filename" shims will automatically be enabled when `output.module` is `true`
618-
__dirname: resolvedShims.esm.__dirname ? 'node-module' : false,
619-
__filename: resolvedShims.esm.__filename ? 'node-module' : false,
618+
resolvedShims,
619+
rsbuildConfig: {
620+
tools: {
621+
rspack: {
622+
node: {
623+
// "__dirname" and "__filename" shims will automatically be enabled when `output.module` is `true`
624+
__dirname: resolvedShims.esm.__dirname ? 'node-module' : false,
625+
__filename: resolvedShims.esm.__filename
626+
? 'node-module'
627+
: false,
628+
},
620629
},
621630
},
631+
plugins: [resolvedShims.esm.require && pluginEsmRequireShim()].filter(
632+
Boolean,
633+
),
622634
},
623-
plugins: [resolvedShims.esm.require && pluginEsmRequireShim()].filter(
624-
Boolean,
625-
),
626635
};
627636
case 'cjs':
628637
return {
629-
plugins: [
630-
resolvedShims.cjs['import.meta.url'] && pluginCjsImportMetaUrlShim(),
631-
].filter(Boolean),
638+
resolvedShims,
639+
rsbuildConfig: {
640+
plugins: [
641+
resolvedShims.cjs['import.meta.url'] &&
642+
pluginCjsImportMetaUrlShim(),
643+
].filter(Boolean),
644+
},
632645
};
633646
case 'umd':
634-
return {};
647+
return {
648+
resolvedShims,
649+
rsbuildConfig: {},
650+
};
635651
case 'mf':
636-
return {};
652+
return {
653+
resolvedShims,
654+
rsbuildConfig: {},
655+
};
637656
default:
638657
throw new Error(`Unsupported format: ${format}`);
639658
}
@@ -744,6 +763,16 @@ const composeSyntaxConfig = (
744763
};
745764
};
746765

766+
const appendEntryQuery = (
767+
entry: NonNullable<RsbuildConfig['source']>['entry'],
768+
): NonNullable<RsbuildConfig['source']>['entry'] => {
769+
const newEntry: Record<string, string> = {};
770+
for (const key in entry) {
771+
newEntry[key] = `${entry[key]}?__rslib_entry__`;
772+
}
773+
return newEntry;
774+
};
775+
747776
const composeEntryConfig = async (
748777
entries: NonNullable<RsbuildConfig['source']>['entry'],
749778
bundle: LibConfig['bundle'],
@@ -758,7 +787,7 @@ const composeEntryConfig = async (
758787
return {
759788
entryConfig: {
760789
source: {
761-
entry: entries,
790+
entry: appendEntryQuery(entries),
762791
},
763792
},
764793
lcp: null,
@@ -834,7 +863,7 @@ const composeEntryConfig = async (
834863
const lcp = await calcLongestCommonPath(Object.values(resolvedEntries));
835864
const entryConfig: RsbuildConfig = {
836865
source: {
837-
entry: resolvedEntries,
866+
entry: appendEntryQuery(resolvedEntries),
838867
},
839868
};
840869

@@ -1041,7 +1070,10 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
10411070
redirect = {},
10421071
umdName,
10431072
} = config;
1044-
const shimsConfig = composeShimsConfig(format!, shims);
1073+
const { rsbuildConfig: shimsConfig, resolvedShims } = composeShimsConfig(
1074+
format!,
1075+
shims,
1076+
);
10451077
const formatConfig = composeFormatConfig({
10461078
format: format!,
10471079
pkgJson: pkgJson!,
@@ -1084,6 +1116,9 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
10841116
cssModulesAuto,
10851117
);
10861118
const cssConfig = composeCssConfig(lcp, config.bundle);
1119+
const postEntryChunkConfig = composePostEntryChunkConfig({
1120+
importMetaUrlShim: !!resolvedShims?.cjs?.['import.meta.url'],
1121+
});
10871122
const dtsConfig = await composeDtsConfig(config, dtsExtension);
10881123
const externalsWarnConfig = composeExternalsWarnConfig(
10891124
format!,
@@ -1111,6 +1146,7 @@ async function composeLibRsbuildConfig(config: LibConfig, configPath: string) {
11111146
targetConfig,
11121147
entryConfig,
11131148
cssConfig,
1149+
postEntryChunkConfig,
11141150
minifyConfig,
11151151
dtsConfig,
11161152
bannerFooterConfig,

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: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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 { importMetaUrlShim } from './shims';
9+
const require = createRequire(import.meta.url);
10+
11+
const PLUGIN_NAME = 'rsbuild:entry';
12+
13+
class PostEntryPlugin {
14+
// TODO: reserve shebang as-is
15+
private shebangEntries: Record<string, boolean> = {};
16+
private reactDirectives: Record<string, 'client' | 'server'> = {};
17+
private importMetaUrlShim: boolean;
18+
constructor({
19+
importMetaUrlShim = true,
20+
}: {
21+
importMetaUrlShim: boolean;
22+
}) {
23+
this.importMetaUrlShim = importMetaUrlShim;
24+
}
25+
26+
apply(compiler: Rspack.Compiler) {
27+
compiler.hooks.entryOption.tap(PLUGIN_NAME, (_context, entries) => {
28+
this.shebangEntries = {};
29+
for (const name in entries) {
30+
// @ts-ignore
31+
const entry = entries[name];
32+
let first = '';
33+
if (Array.isArray(entry)) {
34+
first = entry[0];
35+
} else if (Array.isArray(entry.import)) {
36+
first = entry.import[0];
37+
} else if (typeof entry === 'string') {
38+
first = entry;
39+
}
40+
41+
const content = compiler.inputFileSystem!.readFileSync!(
42+
// @ts-ignore
43+
first.split('?')[0],
44+
{
45+
encoding: 'utf-8',
46+
},
47+
);
48+
49+
// shebang
50+
if (content.startsWith('#!')) {
51+
this.shebangEntries[name] = true;
52+
}
53+
54+
// React directive
55+
if (
56+
content.startsWith(`'use client'`) ||
57+
content.startsWith(`"use client"`)
58+
) {
59+
this.reactDirectives[name] = 'client';
60+
}
61+
62+
if (
63+
content.startsWith(`'use server'`) ||
64+
content.startsWith(`"use server"`)
65+
) {
66+
this.reactDirectives[name] = 'server';
67+
}
68+
}
69+
});
70+
71+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
72+
compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (mod, filename) => {
73+
const name = mod.name!;
74+
75+
if (name in this.shebangEntries) {
76+
this.shebangEntries[filename] = this.shebangEntries[name]!;
77+
}
78+
79+
if (name in this.reactDirectives) {
80+
this.reactDirectives[filename] = this.reactDirectives[name]!;
81+
}
82+
});
83+
});
84+
85+
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
86+
compilation.hooks.processAssets.tap(PLUGIN_NAME, (assets) => {
87+
const chunkAsset = Object.keys(assets);
88+
for (const name of chunkAsset) {
89+
const hasShebang = name in this.shebangEntries;
90+
const hasReactDirective = name in this.reactDirectives;
91+
92+
if (hasShebang || this.importMetaUrlShim) {
93+
compilation.updateAsset(name, (old) => {
94+
let oldSource = old.source();
95+
const replaceSource = new rspack.sources.ReplaceSource(old);
96+
// Shebang will always present at the beginning of the file.
97+
if (hasShebang) {
98+
replaceSource.insert(0, '#!/usr/bin/env node\n');
99+
}
100+
101+
// React
102+
if (hasReactDirective) {
103+
const reactDirective = this.reactDirectives[name];
104+
if (reactDirective === 'client') {
105+
replaceSource.insert(0, `'use client';\n`);
106+
}
107+
if (reactDirective === 'server') {
108+
replaceSource.insert(0, `'use server';\n`);
109+
}
110+
}
111+
112+
// import.meta.url shim
113+
if (this.importMetaUrlShim) {
114+
if (typeof oldSource !== 'string') {
115+
oldSource = oldSource.toString();
116+
}
117+
118+
// TODO: This is a hypothesis that no comments will occur before "use strict;".
119+
// But it should cover most cases.
120+
const useStrictMatch =
121+
oldSource.startsWith('use strict;') ||
122+
oldSource.startsWith('"use strict";');
123+
124+
if (useStrictMatch) {
125+
replaceSource.replace(
126+
0,
127+
11, // 'use strict;'.length,
128+
`"use strict";\n${importMetaUrlShim}`,
129+
);
130+
} else {
131+
replaceSource.insert(0, importMetaUrlShim);
132+
}
133+
}
134+
135+
return replaceSource;
136+
});
137+
}
138+
}
139+
});
140+
});
141+
}
142+
}
143+
144+
const entryModuleLoaderPlugin = (): RsbuildPlugin => ({
145+
name: PLUGIN_NAME,
146+
setup(api) {
147+
api.modifyBundlerChain((config, { CHAIN_ID }) => {
148+
const rule = config.module.rule(CHAIN_ID.RULE.JS);
149+
rule
150+
.use('shebang')
151+
.loader(require.resolve('./entryModuleLoader.js'))
152+
.options({});
153+
});
154+
},
155+
});
156+
157+
export const composePostEntryChunkConfig = ({
158+
importMetaUrlShim,
159+
}: {
160+
importMetaUrlShim: boolean;
161+
}): RsbuildConfig => {
162+
return {
163+
plugins: [entryModuleLoaderPlugin()],
164+
tools: {
165+
rspack: {
166+
plugins: [
167+
new PostEntryPlugin({
168+
importMetaUrlShim: importMetaUrlShim,
169+
}),
170+
],
171+
},
172+
},
173+
};
174+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { LoaderDefinition } from '@rspack/core';
2+
3+
const REACT_DIRECTIVE_REGEX = /^['"]use (client|server)['"](;?)$/;
4+
5+
const loader: LoaderDefinition = function loader(source) {
6+
let result = source;
7+
8+
if (this.resourceQuery === '?__rslib_entry__') {
9+
if (source.startsWith('#!')) {
10+
result = source.replace(/^.*\n/, '');
11+
}
12+
13+
const [firstLine, ...rest] = source.split('\n');
14+
if (REACT_DIRECTIVE_REGEX.test(firstLine!)) {
15+
result = rest.join('\n');
16+
}
17+
}
18+
19+
return result;
20+
};
21+
22+
export default loader;

packages/core/src/plugins/shims.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { type RsbuildPlugin, rspack } from '@rsbuild/core';
22

3-
const importMetaUrlShim = `/*#__PURE__*/ (function () {
3+
// The shim will be injected in PostEntryPlugin.
4+
export const importMetaUrlShim = `const __rslib_import_meta_url__ = /*#__PURE__*/ (function () {
45
return typeof document === 'undefined'
5-
? new (module.require('url'.replace('', '')).URL)('file:' + __filename).href
6+
? new (require('url'.replace('', '')).URL)('file:' + __filename).href
67
: (document.currentScript && document.currentScript.src) ||
78
new URL('main.js', document.baseURI).href;
8-
})()`;
9+
})();
10+
`;
911

1012
// This Rsbuild plugin will shim `import.meta.url` for CommonJS modules.
1113
// - Replace `import.meta.url` with `importMetaUrl`.
@@ -17,7 +19,7 @@ export const pluginCjsImportMetaUrlShim = (): RsbuildPlugin => ({
1719
api.modifyEnvironmentConfig((config) => {
1820
config.source.define = {
1921
...config.source.define,
20-
'import.meta.url': importMetaUrlShim,
22+
'import.meta.url': '__rslib_import_meta_url__',
2123
};
2224
});
2325
},

0 commit comments

Comments
 (0)