@meteorjs/rspack 3.0.0-rc.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/clean.js ADDED
@@ -0,0 +1,47 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function isOutside(relative) {
5
+ return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
6
+ }
7
+
8
+ // output.path can be an app's public/ or private/ source directory. Only the
9
+ // configured generated contexts belong to this compiler's cleanup.
10
+ function keepOutsideContexts(outputPath, contexts) {
11
+ const outputRoot = path.resolve(outputPath);
12
+ const owned = contexts
13
+ .filter(context => typeof context === 'string' && context.length > 0)
14
+ .map(context => path.relative(outputRoot, path.resolve(outputRoot, context)))
15
+ .filter(context => context && !isOutside(context));
16
+
17
+ return filename => {
18
+ if (typeof filename !== 'string' || filename.includes('\0')) return true;
19
+ const relative = path.relative(outputRoot, path.resolve(outputRoot, filename));
20
+ if (isOutside(relative)) return true;
21
+
22
+ const canClean = owned.some(context =>
23
+ relative === context || relative.startsWith(`${context}${path.sep}`) ||
24
+ // Rspack calls keep on directories before walking their children. Let
25
+ // it traverse the output root and parents of nested owned contexts.
26
+ relative === '' || context.startsWith(`${relative}${path.sep}`)
27
+ );
28
+ if (!canClean) return true;
29
+
30
+ // Rspack follows directory symlinks while cleaning. Check every component,
31
+ // including the output root, even when it calls keep directly on a file.
32
+ let current = outputRoot;
33
+ const segments = relative ? relative.split(path.sep) : [];
34
+ try {
35
+ for (const segment of ['', ...segments]) {
36
+ current = path.join(current, segment);
37
+ if (fs.lstatSync(current).isSymbolicLink()) return true;
38
+ }
39
+ } catch {
40
+ // If ownership cannot be checked, preserve the path.
41
+ return true;
42
+ }
43
+ return false;
44
+ };
45
+ }
46
+
47
+ module.exports = { keepOutsideContexts };
@@ -0,0 +1,125 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+ const { keepOutsideContexts } = require('./clean');
5
+
6
+ describe('Rspack output cleanup ownership', () => {
7
+ let root;
8
+ let outputPath;
9
+
10
+ function writeFile(filename, content = 'generated') {
11
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
12
+ fs.writeFileSync(filename, content);
13
+ }
14
+
15
+ function linkDirectory(target, link) {
16
+ fs.mkdirSync(path.dirname(link), { recursive: true });
17
+ fs.symlinkSync(target, link, process.platform === 'win32' ? 'junction' : 'dir');
18
+ }
19
+
20
+ beforeEach(() => {
21
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'meteor-clean-'));
22
+ outputPath = path.join(root, 'build', 'build-agent', 'app', 'private');
23
+ fs.mkdirSync(outputPath, { recursive: true });
24
+ });
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(root, { recursive: true, force: true });
28
+ });
29
+
30
+ test.each([
31
+ ['build-assets', 'build-chunks'],
32
+ ['build-assets-test', 'build-chunks-test'],
33
+ ['build-assets-local-secondary-app-test', 'build-chunks-local-secondary-app-test'],
34
+ ['generated/assets', 'generated/chunks'],
35
+ ])('cleans only %s and %s, with absolute or relative callbacks', (assets, chunks) => {
36
+ writeFile(path.join(outputPath, assets, 'stale.svg'));
37
+ writeFile(path.join(outputPath, chunks, 'stale.js'));
38
+ const keep = keepOutsideContexts(outputPath, [assets, chunks]);
39
+ const cases = [
40
+ ['', false],
41
+ [assets, false],
42
+ [`${assets}/stale.svg`, false],
43
+ [`${chunks}/stale.js`, false],
44
+ ['ordinary.txt', true],
45
+ ['build', true],
46
+ ['build-notes/guide.txt', true],
47
+ ['docs/build/guide.txt', true],
48
+ [`${assets}-another-mode/keep.svg`, true],
49
+ ['build-assets-local-unrelated/keep.svg', true],
50
+ ['unrelated/build-chunks/stale.js', true],
51
+ ['../private-neighbor/build-assets/keep.txt', true],
52
+ ];
53
+ for (const [file, expected] of cases) {
54
+ for (const input of [file, path.resolve(outputPath, file)]) {
55
+ expect({ input, keep: keep(input) }).toEqual({ input, keep: expected });
56
+ }
57
+ }
58
+ });
59
+
60
+ test('traverses a shared parent without cleaning a sibling context', () => {
61
+ writeFile(path.join(outputPath, 'build-chunks/web.browser/stale.js'));
62
+ const keep = keepOutsideContexts(outputPath, [
63
+ 'build-chunks/web.browser/assets', 'build-chunks/web.browser',
64
+ ]);
65
+ expect(keep(outputPath)).toBe(false);
66
+ expect(keep(path.join(outputPath, 'build-chunks'))).toBe(false);
67
+ expect(keep('build-chunks/web.browser/stale.js')).toBe(false);
68
+ expect(keep('build-chunks/web.browser.legacy/keep.js')).toBe(true);
69
+ expect(keep('build-chunks/web.cordova/keep.js')).toBe(true);
70
+ });
71
+
72
+ test('keeps files when no safe child context is configured', () => {
73
+ const keep = keepOutsideContexts(outputPath, ['', '.', '..', '../public', null]);
74
+ for (const input of [outputPath, 'ordinary.txt', '../public/keep.txt', null, 'bad\0path']) {
75
+ expect({ input, keep: keep(input) }).toEqual({ input, keep: true });
76
+ }
77
+ });
78
+
79
+ test.each([
80
+ ['context root', 'build-assets', 'build-assets', false, 'keep.txt'],
81
+ ['nested link', 'build-assets/linked', 'build-assets', true, 'keep.txt'],
82
+ ['custom context parent', 'generated', 'generated/assets', false, 'assets/keep.txt'],
83
+ ])('keeps a symlink at the %s and paths below it', (_name, linkPath, context, targetInsideOutput, child) => {
84
+ const target = path.join(targetInsideOutput ? outputPath : root, 'source-assets');
85
+ writeFile(path.join(target, child), 'source asset');
86
+ linkDirectory(target, path.join(outputPath, linkPath));
87
+ writeFile(path.join(outputPath, 'build-chunks/stale.js'));
88
+ const keep = keepOutsideContexts(outputPath, [context, 'build-chunks']);
89
+
90
+ for (const file of [linkPath, `${linkPath}/${child}`]) {
91
+ expect(keep(file)).toBe(true);
92
+ expect(keep(path.join(outputPath, file))).toBe(true);
93
+ }
94
+ expect(keep('build-chunks/stale.js')).toBe(false);
95
+ });
96
+
97
+ test('keeps a symlinked output root and its generated descendants', () => {
98
+ const target = path.join(root, 'shared-public');
99
+ writeFile(path.join(target, 'build-assets/keep.txt'), 'shared asset');
100
+ fs.rmdirSync(outputPath);
101
+ linkDirectory(target, outputPath);
102
+ const keep = keepOutsideContexts(outputPath, ['build-assets']);
103
+
104
+ expect(keep(outputPath)).toBe(true);
105
+ expect(keep('build-assets/keep.txt')).toBe(true);
106
+ });
107
+
108
+ test('preserves an uninspectable path without disabling cleanup elsewhere', () => {
109
+ const denied = path.join(outputPath, 'build-assets');
110
+ writeFile(path.join(denied, 'keep.txt'));
111
+ writeFile(path.join(outputPath, 'build-chunks/stale.js'));
112
+ const lstat = fs.lstatSync;
113
+ const spy = jest.spyOn(fs, 'lstatSync').mockImplementation((filename, ...args) => {
114
+ if (filename === denied) throw Object.assign(new Error('permission denied'), { code: 'EACCES' });
115
+ return lstat(filename, ...args);
116
+ });
117
+ try {
118
+ const keep = keepOutsideContexts(outputPath, ['build-assets', 'build-chunks']);
119
+ expect(keep('build-assets/keep.txt')).toBe(true);
120
+ expect(keep('build-chunks/stale.js')).toBe(false);
121
+ } finally {
122
+ spy.mockRestore();
123
+ }
124
+ });
125
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "3.0.0-rc.1",
3
+ "version": "3.0.0",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
package/rspack.config.js CHANGED
@@ -31,6 +31,7 @@ const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory")
31
31
  const { extractLocalDependencies } = require('./lib/localDependenciesHelpers.js');
32
32
  const { createTestClientNodePolyfillConfig } = require("./lib/testClientNodePolyfills.js");
33
33
  const { createOutputTracker } = require('./lib/compilationOutput.js');
34
+ const { keepOutsideContexts } = require('./lib/clean.js');
34
35
 
35
36
  // Safe require that doesn't throw if the module isn't found
36
37
  function safeRequire(moduleName) {
@@ -207,16 +208,6 @@ function createRemoteDevServerConfig() {
207
208
  return {};
208
209
  }
209
210
 
210
- // Keep files outside of build folders
211
- function keepOutsideBuild() {
212
- return (p) => {
213
- const normalized = '/' + path.normalize(p).replaceAll(path.sep, '/').replace(/^\/+/, '');
214
- const isInBuildRoot = /\/build(\/|$)/.test(normalized);
215
- const isInBuildStar = /\/build-[^/]+(\/|$)/.test(normalized);
216
- return !(isInBuildRoot || isInBuildStar);
217
- };
218
- }
219
-
220
211
  /**
221
212
  * @param {{ isClient: boolean; isServer: boolean; isDevelopment?: boolean; isProduction?: boolean; isTest?: boolean }} Meteor
222
213
  * @param {{ mode?: string; clientEntry?: string; serverEntry?: string; clientOutputFolder?: string; serverOutputFolder?: string; chunksContext?: string; assetsContext?: string; serverAssetsContext?: string }} argv
@@ -644,7 +635,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
644
635
  cssChunkFilename: `${clientChunkPrefix}[id]${
645
636
  isProd ? ".[contenthash]" : ""
646
637
  }.css`,
647
- ...(isProd && { clean: clientOutputContext ? true : { keep: keepOutsideBuild() } }),
638
+ ...(isProd && { clean: clientOutputContext ? true : {
639
+ keep: keepOutsideContexts(clientOutputDir, [assetsContext, chunksContext]),
640
+ } }),
648
641
  },
649
642
  optimization: {
650
643
  usedExports: true,
@@ -767,7 +760,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
767
760
  library: { type: "commonjs2" },
768
761
  chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
769
762
  assetModuleFilename,
770
- ...(isProd && { clean: { keep: keepOutsideBuild() } }),
763
+ ...(isProd && { clean: {
764
+ keep: keepOutsideContexts(serverOutputDir, [assetsContext, chunksContext]),
765
+ } }),
771
766
  },
772
767
  optimization: {
773
768
  usedExports: true,