Skip to content

Commit f149a29

Browse files
committed
fix: handle add and unlink file in bundleless mode
1 parent 53db9a0 commit f149a29

File tree

8 files changed

+219
-94
lines changed

8 files changed

+219
-94
lines changed

packages/core/src/config.ts

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -918,76 +918,86 @@ const composeEntryConfig = async (
918918
};
919919
}
920920

921-
// In bundleless mode, resolve glob patterns and convert them to entry object.
922-
const resolvedEntries: Record<string, string> = {};
923-
for (const key of Object.keys(entries)) {
924-
const entry = entries[key];
925-
926-
// Entries in bundleless mode could be:
927-
// 1. A string of glob pattern: { entry: { index: 'src/*.ts' } }
928-
// 2. An array of glob patterns: { entry: { index: ['src/*.ts', 'src/*.tsx'] } }
929-
// Not supported for now: entry description object
930-
const entryFiles = Array.isArray(entry)
931-
? entry
932-
: typeof entry === 'string'
933-
? [entry]
934-
: null;
935-
936-
if (!entryFiles) {
937-
throw new Error(
938-
'Entry can only be a string or an array of strings for now',
939-
);
940-
}
921+
const globScanEntries = async (calcLcp: boolean) => {
922+
// In bundleless mode, resolve glob patterns and convert them to entry object.
923+
const resolvedEntries: Record<string, string> = {};
924+
for (const key of Object.keys(entries)) {
925+
const entry = entries[key];
941926

942-
// Turn entries in array into each separate entry.
943-
const globEntryFiles = await glob(entryFiles, {
944-
cwd: root,
945-
absolute: true,
946-
});
927+
const entryFiles = Array.isArray(entry)
928+
? entry
929+
: typeof entry === 'string'
930+
? [entry]
931+
: null;
947932

948-
// Filter the glob resolved entry files based on the allowed extensions
949-
const resolvedEntryFiles = globEntryFiles.filter((file) =>
950-
ENTRY_EXTENSIONS_PATTERN.test(file),
951-
);
933+
if (!entryFiles) {
934+
throw new Error(
935+
'Entry can only be a string or an array of strings for now',
936+
);
937+
}
952938

953-
if (resolvedEntryFiles.length === 0) {
954-
throw new Error(`Cannot find ${resolvedEntryFiles}`);
955-
}
939+
// Turn entries in array into each separate entry.
940+
const globEntryFiles = await glob(entryFiles, {
941+
cwd: root,
942+
absolute: true,
943+
});
956944

957-
// Similar to `rootDir` in tsconfig and `outbase` in esbuild.
958-
const lcp = await calcLongestCommonPath(resolvedEntryFiles);
959-
// Using the longest common path of all non-declaration input files by default.
960-
const outBase = lcp === null ? root : lcp;
945+
// Filter the glob resolved entry files based on the allowed extensions
946+
const resolvedEntryFiles = globEntryFiles.filter((file) =>
947+
ENTRY_EXTENSIONS_PATTERN.test(file),
948+
);
949+
950+
if (resolvedEntryFiles.length === 0) {
951+
throw new Error(`Cannot find ${resolvedEntryFiles}`);
952+
}
961953

962-
function getEntryName(file: string) {
963-
const { dir, name } = path.parse(path.relative(outBase, file));
964-
// Entry filename contains nested path to preserve source directory structure.
965-
const entryFileName = path.join(dir, name);
954+
// Similar to `rootDir` in tsconfig and `outbase` in esbuild.
955+
const lcp = await calcLongestCommonPath(resolvedEntryFiles);
956+
// Using the longest common path of all non-declaration input files by default.
957+
const outBase = lcp === null ? root : lcp;
966958

967-
// 1. we mark the global css files (which will generate empty js chunk in cssExtract), and deleteAsset in RemoveCssExtractAssetPlugin
968-
// 2. avoid the same name e.g: `index.ts` and `index.css`
969-
if (isCssGlobalFile(file, cssModulesAuto)) {
970-
return `${RSLIB_CSS_ENTRY_FLAG}/${entryFileName}`;
959+
function getEntryName(file: string) {
960+
const { dir, name } = path.parse(path.relative(outBase, file));
961+
// Entry filename contains nested path to preserve source directory structure.
962+
const entryFileName = path.join(dir, name);
963+
964+
// 1. we mark the global css files (which will generate empty js chunk in cssExtract), and deleteAsset in RemoveCssExtractAssetPlugin
965+
// 2. avoid the same name e.g: `index.ts` and `index.css`
966+
if (isCssGlobalFile(file, cssModulesAuto)) {
967+
return `${RSLIB_CSS_ENTRY_FLAG}/${entryFileName}`;
968+
}
969+
970+
return entryFileName;
971971
}
972972

973-
return entryFileName;
973+
for (const file of resolvedEntryFiles) {
974+
const entryName = getEntryName(file);
975+
if (resolvedEntries[entryName]) {
976+
logger.warn(
977+
`duplicate entry: ${entryName}, this may lead to the incorrect output, please rename the file`,
978+
);
979+
}
980+
resolvedEntries[entryName] = file;
981+
}
974982
}
975983

976-
for (const file of resolvedEntryFiles) {
977-
const entryName = getEntryName(file);
978-
if (resolvedEntries[entryName]) {
979-
logger.warn(
980-
`duplicate entry: ${entryName}, this may lead to the incorrect output, please rename the file`,
981-
);
982-
}
983-
resolvedEntries[entryName] = file;
984+
if (calcLcp) {
985+
const lcp = await calcLongestCommonPath(Object.values(resolvedEntries));
986+
return { resolvedEntries, lcp };
984987
}
985-
}
988+
return { resolvedEntries, lcp: null };
989+
};
986990

987-
const lcp = await calcLongestCommonPath(Object.values(resolvedEntries));
991+
// LCP could only be determined at the first time of glob scan.
992+
const { lcp } = await globScanEntries(true);
988993
const entryConfig: EnvironmentConfig = {
989-
source: {
990-
entry: appendEntryQuery(resolvedEntries),
994+
tools: {
995+
rspack: {
996+
entry: async () => {
997+
const { resolvedEntries } = await globScanEntries(false);
998+
return appendEntryQuery(resolvedEntries);
999+
},
1000+
},
9911001
},
9921002
};
9931003

@@ -1342,6 +1352,7 @@ async function composeLibRsbuildConfig(
13421352

13431353
const entryChunkConfig = composeEntryChunkConfig({
13441354
enabledImportMetaUrlShim: enabledShims.cjs['import.meta.url'],
1355+
contextToWatch: lcp,
13451356
});
13461357
const dtsConfig = await composeDtsConfig(config, dtsExtension);
13471358
const externalsWarnConfig = composeExternalsWarnConfig(

packages/core/src/plugins/EntryChunkPlugin.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,48 +37,55 @@ class EntryChunkPlugin {
3737
private shebangInjectedAssets: Set<string> = new Set();
3838

3939
private enabledImportMetaUrlShim: boolean;
40+
private contextToWatch: string | null = null;
41+
private contextWatched = false;
4042

4143
constructor({
4244
enabledImportMetaUrlShim = true,
45+
contextToWatch,
4346
}: {
4447
enabledImportMetaUrlShim: boolean;
48+
contextToWatch: string | null;
4549
}) {
4650
this.enabledImportMetaUrlShim = enabledImportMetaUrlShim;
51+
this.contextToWatch = contextToWatch;
4752
}
4853

4954
apply(compiler: Rspack.Compiler) {
50-
compiler.hooks.entryOption.tap(PLUGIN_NAME, (_context, entries) => {
51-
for (const name in entries) {
52-
const entry = (entries as Rspack.EntryStaticNormalized)[name];
53-
if (!entry) continue;
54-
55-
let first: string | undefined;
56-
if (Array.isArray(entry)) {
57-
first = entry[0];
58-
} else if (Array.isArray(entry.import)) {
59-
first = entry.import[0];
60-
} else if (typeof entry === 'string') {
61-
first = entry;
62-
}
55+
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
56+
if (this.contextWatched || this.contextToWatch === null) return;
57+
58+
const contextDep = compilation.contextDependencies;
59+
contextDep.add(this.contextToWatch);
60+
this.contextWatched = true;
61+
});
6362

64-
if (typeof first !== 'string') continue;
63+
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
64+
const entries: Record<string, string> = {};
65+
const entry = compilation.entries;
66+
const values = entry;
67+
for (const [key, value] of values) {
68+
const firstDep = value.dependencies[0];
69+
if (firstDep?.request) {
70+
entries[key] = firstDep.request;
71+
}
72+
}
6573

74+
for (const name in entries) {
75+
const first = entries[name]!;
6676
const filename = first.split('?')[0]!;
6777
const isJs = JS_EXTENSIONS_PATTERN.test(filename);
6878
if (!isJs) continue;
69-
7079
const content = compiler.inputFileSystem!.readFileSync!(filename, {
7180
encoding: 'utf-8',
7281
});
73-
7482
// Shebang
7583
if (content.startsWith(SHEBANG_PREFIX)) {
7684
const shebangMatch = matchFirstLine(content, SHEBANG_REGEX);
7785
if (shebangMatch) {
7886
this.shebangEntries[name] = shebangMatch;
7987
}
8088
}
81-
8289
// React directive
8390
const reactDirective = matchFirstLine(content, REACT_DIRECTIVE_REGEX);
8491
if (reactDirective) {
@@ -87,7 +94,25 @@ class EntryChunkPlugin {
8794
}
8895
});
8996

90-
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
97+
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
98+
compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (chunk, filename) => {
99+
const isJs = JS_EXTENSIONS_PATTERN.test(filename);
100+
if (!isJs) return;
101+
102+
const name = chunk.name;
103+
if (!name) return;
104+
105+
const shebangEntry = this.shebangEntries[name];
106+
if (shebangEntry) {
107+
this.shebangEntries[filename] = shebangEntry;
108+
}
109+
110+
const reactDirective = this.reactDirectives[name];
111+
if (reactDirective) {
112+
this.reactDirectives[filename] = reactDirective;
113+
}
114+
});
115+
91116
compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (chunk, filename) => {
92117
const isJs = JS_EXTENSIONS_PATTERN.test(filename);
93118
if (!isJs) return;
@@ -192,8 +217,10 @@ const entryModuleLoaderRsbuildPlugin = (): RsbuildPlugin => ({
192217

193218
export const composeEntryChunkConfig = ({
194219
enabledImportMetaUrlShim,
220+
contextToWatch = null,
195221
}: {
196222
enabledImportMetaUrlShim: boolean;
223+
contextToWatch: string | null;
197224
}): EnvironmentConfig => {
198225
return {
199226
plugins: [entryModuleLoaderRsbuildPlugin()],
@@ -202,6 +229,7 @@ export const composeEntryChunkConfig = ({
202229
plugins: [
203230
new EntryChunkPlugin({
204231
enabledImportMetaUrlShim,
232+
contextToWatch,
205233
}),
206234
],
207235
},

tests/integration/cli/build-watch/build.test.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import { exec } from 'node:child_process';
22
import path from 'node:path';
33
import fse from 'fs-extra';
4-
import { awaitFileExists } from 'test-helper';
5-
import { describe, test } from 'vitest';
4+
import { awaitFileChanges, awaitFileExists } from 'test-helper';
5+
import { describe, expect, test } from 'vitest';
66

77
describe('build --watch command', async () => {
88
test('basic', async () => {
99
const distPath = path.join(__dirname, 'dist');
1010
const dist1Path = path.join(__dirname, 'dist-1');
1111
fse.removeSync(distPath);
12-
1312
fse.removeSync(dist1Path);
14-
1513
const distEsmIndexFile = path.join(__dirname, 'dist/esm/index.js');
1614
const dist1EsmIndexFile = path.join(__dirname, 'dist-1/esm/index.js');
1715

1816
const tempConfigFile = path.join(__dirname, 'test-temp-rslib.config.mjs');
19-
2017
fse.outputFileSync(
2118
tempConfigFile,
2219
`import { defineConfig } from '@rslib/core';
@@ -56,3 +53,68 @@ export default defineConfig({
5653
process.kill();
5754
});
5855
});
56+
57+
describe('build --watch should handle add / change / unlink', async () => {
58+
test('basic', async () => {
59+
const tempSrcPath = path.join(__dirname, 'test-temp-src');
60+
await fse.remove(tempSrcPath);
61+
await fse.remove(path.join(__dirname, 'dist'));
62+
await fse.copy(path.join(__dirname, 'src'), './test-temp-src');
63+
const tempConfigFile = path.join(__dirname, 'test-temp-rslib.config.mjs');
64+
await fse.remove(tempConfigFile);
65+
fse.outputFileSync(
66+
tempConfigFile,
67+
`import { defineConfig } from '@rslib/core';
68+
import { generateBundleEsmConfig } from 'test-helper';
69+
70+
export default defineConfig({
71+
lib: [
72+
generateBundleEsmConfig({
73+
source: {
74+
entry: {
75+
index: 'test-temp-src',
76+
},
77+
},
78+
bundle: false,
79+
}),
80+
],
81+
});
82+
`,
83+
);
84+
85+
const srcIndexFile = path.join(tempSrcPath, 'index.js');
86+
const srcFooFile = path.join(tempSrcPath, 'foo.js');
87+
const distFooFile = path.join(__dirname, 'dist/esm/foo.js');
88+
89+
const process = exec(`npx rslib build --watch -c ${tempConfigFile}`, {
90+
cwd: __dirname,
91+
});
92+
93+
// add
94+
fse.outputFileSync(srcFooFile, `export const foo = 'foo';`);
95+
await awaitFileExists(distFooFile);
96+
const content1 = await fse.readFile(distFooFile, 'utf-8');
97+
expect(content1!).toMatchInlineSnapshot(`
98+
"const foo = 'foo';
99+
export { foo };
100+
"
101+
`);
102+
103+
// unlink
104+
// Following "change" cases won't succeed if error is thrown in this step.
105+
fse.removeSync(srcIndexFile);
106+
107+
// change
108+
const wait = await awaitFileChanges(distFooFile);
109+
fse.outputFileSync(srcFooFile, `export const foo = 'foo1';`);
110+
await wait();
111+
const content2 = await fse.readFile(distFooFile, 'utf-8');
112+
expect(content2!).toMatchInlineSnapshot(`
113+
"const foo = 'foo1';
114+
export { foo };
115+
"
116+
`);
117+
118+
process.kill();
119+
});
120+
});
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { defineConfig } from '@rslib/core';
2-
import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper';
2+
import { generateBundleEsmConfig } from 'test-helper';
33

44
export default defineConfig({
55
lib: [
66
generateBundleEsmConfig({
7-
dts: true,
8-
}),
9-
generateBundleCjsConfig({
10-
dts: true,
7+
source: {
8+
entry: {
9+
index: 'src',
10+
},
11+
},
12+
bundle: false,
1113
}),
1214
],
1315
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const foo = 'foo';
1+
export const index = 'index';

0 commit comments

Comments
 (0)