@meteorjs/rspack 3.0.0-beta.2 → 3.0.0-beta.3

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/index.d.ts CHANGED
@@ -18,6 +18,10 @@ type MeteorEnv = Record<string, any> & {
18
18
  isProduction: boolean;
19
19
  isClient: boolean;
20
20
  isServer: boolean;
21
+ /** Explicit client architecture, e.g. web.browser.legacy or web.cordova. */
22
+ arch?: string;
23
+ /** Whether this client compilation uses legacy (ES5) targets. */
24
+ isLegacy: boolean;
21
25
  isTest: boolean;
22
26
  isDebug: boolean;
23
27
  isRun: boolean;
package/lib/ignore.js CHANGED
@@ -2,8 +2,26 @@ var fs = require('fs');
2
2
  var path = require('path');
3
3
 
4
4
  /**
5
- * Reads the .meteorignore file from the given project directory and returns
6
- * the parsed entries. Empty lines and comment lines (starting with #) are filtered out.
5
+ * Splits a whitespace-delimited list of ignore patterns, as accepted by the
6
+ * METEOR_IGNORE environment variable.
7
+ *
8
+ * @param {string} value - Raw environment variable value
9
+ * @returns {string[]} - Array of ignore patterns
10
+ */
11
+ function parseIgnoreEnv(value) {
12
+ return (value || '').trim().split(/\s+/).filter(Boolean);
13
+ }
14
+
15
+ /**
16
+ * Reads the ignore patterns that apply to the given project directory: the
17
+ * entries of its .meteorignore file followed by the ones in the METEOR_IGNORE
18
+ * environment variable. Empty lines and comment lines (starting with #) are
19
+ * filtered out.
20
+ *
21
+ * The two sources are combined the way meteor-tool combines them (see
22
+ * optimisticReadMeteorIgnore in tools/fs/optimistic.ts), with METEOR_IGNORE
23
+ * last so that it can override the file under gitignore's last-match-wins
24
+ * rule.
7
25
  *
8
26
  * @param {string} projectDir - The project directory path
9
27
  * @returns {string[]} - Array of ignore patterns
@@ -11,20 +29,22 @@ var path = require('path');
11
29
  const getMeteorIgnoreEntries = function (projectDir) {
12
30
  const meteorIgnorePath = path.join(projectDir, '.meteorignore');
13
31
 
32
+ let fileEntries = [];
33
+
14
34
  // Check if .meteorignore file exists
15
35
  try {
16
36
  const fileContent = fs.readFileSync(meteorIgnorePath, 'utf8');
17
37
 
18
38
  // Process each line in the file
19
- const entries = fileContent.split(/\r?\n/)
39
+ fileEntries = fileContent.split(/\r?\n/)
20
40
  .map(line => line.trim())
21
41
  .filter(line => line !== '' && !line.startsWith('#'));
22
-
23
- return entries;
24
42
  } catch (e) {
25
- // If the file doesn't exist or can't be read, return empty array
26
- return [];
43
+ // If the file doesn't exist or can't be read, fall back to the
44
+ // environment variable alone.
27
45
  }
46
+
47
+ return [...fileEntries, ...parseIgnoreEnv(process.env.METEOR_IGNORE)];
28
48
  };
29
49
 
30
50
  /**
@@ -86,6 +106,11 @@ function createIgnoreGlobConfig(entries = []) {
86
106
  * Creates a regex pattern to match the specified glob patterns.
87
107
  * Converts glob patterns with * and ** into regex equivalents.
88
108
  *
109
+ * Negation patterns cannot be expressed in a single regex and are dropped, so
110
+ * this only suits pattern lists known to be positive-only — Rspack's `exclude`
111
+ * option, which accepts nothing but a RegExp. Patterns that come from the app
112
+ * author go through createIgnoreMatcherSource instead.
113
+ *
89
114
  * @param {string[]} globPatterns - Array of glob patterns from createIgnoreGlobConfig
90
115
  * @param {string} [rootPath] - Absolute root that matched paths must belong to
91
116
  * @returns {RegExp} - Regex pattern to match the specified patterns
@@ -95,14 +120,21 @@ function createIgnoreRegex(globPatterns, rootPath) {
95
120
  throw new Error('globPatterns must be a non-empty array');
96
121
  }
97
122
 
98
- // Rspack applies context exclusions to absolute module paths. Anchor rooted
99
- // patterns here so matching starts inside the app, not in a parent folder.
123
+ // Rspack applies context exclusions to absolute module paths joined with the
124
+ // platform separator, so on Windows they arrive with backslashes. Every
125
+ // separator in the regex accepts both forms (meteor/meteor#14513).
126
+ const SEPARATOR = '[\\\\/]';
127
+ const NOT_SEPARATOR = '[^\\\\/]';
128
+
129
+ // Anchor rooted patterns so matching starts inside the app, not in a parent
130
+ // folder.
100
131
  const pathPrefix = rootPath
101
132
  ? `^${rootPath
102
133
  .replace(/\\/g, '/')
103
134
  .replace(/\/+$/, '')
104
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=/|$)`
105
- : '(?:^|/)';
135
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
136
+ .replace(/\//g, SEPARATOR)}(?=${SEPARATOR}|$)`
137
+ : `(?:^|${SEPARATOR})`;
106
138
 
107
139
  // Process each glob pattern and convert to regex
108
140
  const regexPatterns = globPatterns.map(pattern => {
@@ -114,13 +146,16 @@ function createIgnoreRegex(globPatterns, rootPath) {
114
146
  // Escape special regex characters, but not * and /
115
147
  let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
116
148
 
149
+ // Accept either separator wherever the pattern has a /
150
+ regexPattern = regexPattern.replace(/\//g, SEPARATOR);
151
+
117
152
  // Use a temporary placeholder for ** that won't be affected by the * replacement
118
153
  // This is necessary because if we directly replace ** with .* and then replace * with [^/]*
119
154
  const DOUBLE_ASTERISK_PLACEHOLDER = '__DOUBLE_ASTERISK__';
120
155
  regexPattern = regexPattern.replace(/\*\*/g, DOUBLE_ASTERISK_PLACEHOLDER);
121
156
 
122
- // Convert * to regex equivalent (any number of characters except /)
123
- regexPattern = regexPattern.replace(/\*/g, '[^/]*');
157
+ // Convert * to regex equivalent (any number of characters except a separator)
158
+ regexPattern = regexPattern.replace(/\*/g, `${NOT_SEPARATOR}*`);
124
159
 
125
160
  // Convert the ** placeholder to its regex equivalent (any number of characters including /)
126
161
  regexPattern = regexPattern.replace(new RegExp(DOUBLE_ASTERISK_PLACEHOLDER, 'g'), '.*');
@@ -140,8 +175,51 @@ function createIgnoreRegex(globPatterns, rootPath) {
140
175
  return new RegExp(combinedPattern);
141
176
  }
142
177
 
178
+ /**
179
+ * Builds the pieces a generated eager-test module needs to decide which test
180
+ * files .meteorignore keeps.
181
+ *
182
+ * .meteorignore uses gitignore syntax, where the last matching pattern wins and
183
+ * a `!pattern` re-includes a path an earlier pattern excluded. Reimplementing
184
+ * that on top of a RegExp lost every negation, which silently reduced a sharded
185
+ * test run to zero test files, so the generated module is handed the same
186
+ * `ignore` package meteor-tool applies to .meteorignore (see
187
+ * tools/fs/optimistic.ts). Both bundlers then agree on the semantics.
188
+ *
189
+ * Returned as source rather than as a predicate because it runs inside the
190
+ * bundle, not in this process: the paths it tests come from
191
+ * import.meta.webpackContext.
192
+ *
193
+ * @param {string[]} entries - .meteorignore-style patterns, in order
194
+ * @returns {{ modulePath: string, source: string }|null} - The `ignore` module
195
+ * to import, and a `(createIgnore) => (contextKey) => boolean` factory
196
+ * expression. Null when there is nothing to filter.
197
+ */
198
+ function createIgnoreMatcherSource(entries = []) {
199
+ if (!Array.isArray(entries)) {
200
+ throw new Error('Entries must be an array');
201
+ }
202
+
203
+ if (entries.length === 0) {
204
+ return null;
205
+ }
206
+
207
+ return {
208
+ // Resolved here rather than at module load so that requiring this file
209
+ // stays cheap and does not depend on `ignore` being installed.
210
+ modulePath: require.resolve('ignore'),
211
+ source: `(createIgnore => {
212
+ const ig = createIgnore().add(${JSON.stringify(entries)});
213
+ // import.meta.webpackContext keys look like './imports/a.tests.ts', but
214
+ // \`ignore\` wants a path relative to the project root and rejects the './'.
215
+ return key => ig.ignores(key.replace(/^\\.\\//, ''));
216
+ })`,
217
+ };
218
+ }
219
+
143
220
  module.exports = {
221
+ createIgnoreGlobConfig,
222
+ createIgnoreMatcherSource,
144
223
  createIgnoreRegex,
145
224
  getMeteorIgnoreEntries,
146
- createIgnoreGlobConfig,
147
225
  };
@@ -0,0 +1,259 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const {
6
+ createIgnoreGlobConfig,
7
+ createIgnoreMatcherSource,
8
+ createIgnoreRegex,
9
+ getMeteorIgnoreEntries,
10
+ } = require('./ignore.js');
11
+
12
+ // Compiles the factory emitted into the generated eager-test module and wires
13
+ // it to the same `ignore` module that module imports, so the assertions below
14
+ // exercise the exact code that runs inside the bundle.
15
+ const compileMatcher = entries => {
16
+ const { modulePath, source } = createIgnoreMatcherSource(entries);
17
+
18
+ // eslint-disable-next-line no-new-func -- the input is this package's own
19
+ // generated source, not user data.
20
+ return new Function('createIgnore', `return (${source})(createIgnore);`)(
21
+ require(modulePath),
22
+ );
23
+ };
24
+
25
+ const withMeteorIgnoreEnv = (value, fn) => {
26
+ const previous = process.env.METEOR_IGNORE;
27
+ if (value === undefined) {
28
+ delete process.env.METEOR_IGNORE;
29
+ } else {
30
+ process.env.METEOR_IGNORE = value;
31
+ }
32
+ try {
33
+ return fn();
34
+ } finally {
35
+ if (previous === undefined) {
36
+ delete process.env.METEOR_IGNORE;
37
+ } else {
38
+ process.env.METEOR_IGNORE = previous;
39
+ }
40
+ }
41
+ };
42
+
43
+ describe('createIgnoreMatcherSource', () => {
44
+ test('re-includes a file a later negation pattern rescues', () => {
45
+ const isIgnored = compileMatcher([
46
+ '*.tests.ts',
47
+ '!/imports/localization/LanguageSections.tests.ts',
48
+ ]);
49
+
50
+ expect(isIgnored('./imports/localization/LanguageSections.tests.ts')).toBe(
51
+ false,
52
+ );
53
+ });
54
+
55
+ test('still ignores files no negation pattern rescues', () => {
56
+ const isIgnored = compileMatcher([
57
+ '*.tests.ts',
58
+ '!/imports/localization/LanguageSections.tests.ts',
59
+ ]);
60
+
61
+ expect(isIgnored('./imports/localization/Other.tests.ts')).toBe(true);
62
+ });
63
+
64
+ test('lets the last matching pattern win when a positive follows a negation', () => {
65
+ const isIgnored = compileMatcher([
66
+ '*.tests.ts',
67
+ '!/imports/a/B.tests.ts',
68
+ '/imports/a/',
69
+ ]);
70
+
71
+ expect(isIgnored('./imports/a/B.tests.ts')).toBe(true);
72
+ });
73
+
74
+ test('ignores nothing when every pattern is a negation', () => {
75
+ const isIgnored = compileMatcher(['!/imports/a/B.tests.ts']);
76
+
77
+ expect(isIgnored('./imports/a/B.tests.ts')).toBe(false);
78
+ });
79
+
80
+ test('returns null when there are no patterns', () => {
81
+ expect(createIgnoreMatcherSource([])).toBeNull();
82
+ });
83
+
84
+ test('anchors a rooted pattern to the project root', () => {
85
+ const isIgnored = compileMatcher(['/imports/a.tests.ts']);
86
+
87
+ expect(isIgnored('./imports/a.tests.ts')).toBe(true);
88
+ expect(isIgnored('./packages/imports/a.tests.ts')).toBe(false);
89
+ });
90
+
91
+ test('supports character classes', () => {
92
+ const isIgnored = compileMatcher(['shard-[a-c].tests.ts']);
93
+
94
+ expect(isIgnored('./imports/shard-b.tests.ts')).toBe(true);
95
+ expect(isIgnored('./imports/shard-d.tests.ts')).toBe(false);
96
+ });
97
+
98
+ test('supports the single-character wildcard', () => {
99
+ const isIgnored = compileMatcher(['shard-?.tests.ts']);
100
+
101
+ expect(isIgnored('./imports/shard-1.tests.ts')).toBe(true);
102
+ expect(isIgnored('./imports/shard-12.tests.ts')).toBe(false);
103
+ });
104
+
105
+ test('matches a **/ pattern at the project root too', () => {
106
+ const isIgnored = compileMatcher(['**/legacy.tests.ts']);
107
+
108
+ expect(isIgnored('./legacy.tests.ts')).toBe(true);
109
+ expect(isIgnored('./imports/legacy.tests.ts')).toBe(true);
110
+ });
111
+
112
+ test('does not let a negation rescue a file under an excluded directory', () => {
113
+ const isIgnored = compileMatcher(['/imports/', '!/imports/a.tests.ts']);
114
+
115
+ expect(isIgnored('./imports/a.tests.ts')).toBe(true);
116
+ });
117
+ });
118
+
119
+ describe('createIgnoreRegex', () => {
120
+ test('matches paths covered by a positive pattern', () => {
121
+ const regex = createIgnoreRegex(createIgnoreGlobConfig(['node_modules/']));
122
+
123
+ expect(regex.test('./node_modules/foo/index.js')).toBe(true);
124
+ expect(regex.test('./imports/a.js')).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('getMeteorIgnoreEntries', () => {
129
+ let projectDir;
130
+
131
+ beforeEach(() => {
132
+ projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'meteor-ignore-'));
133
+ });
134
+
135
+ afterEach(() => {
136
+ fs.rmSync(projectDir, { recursive: true, force: true });
137
+ });
138
+
139
+ test('appends METEOR_IGNORE patterns after the .meteorignore ones', () => {
140
+ fs.writeFileSync(
141
+ path.join(projectDir, '.meteorignore'),
142
+ '# comment\n*.tests.ts\n\n',
143
+ );
144
+
145
+ const entries = withMeteorIgnoreEnv(
146
+ '!/imports/a/B.tests.ts /imports/c',
147
+ () => getMeteorIgnoreEntries(projectDir),
148
+ );
149
+
150
+ expect(entries).toEqual([
151
+ '*.tests.ts',
152
+ '!/imports/a/B.tests.ts',
153
+ '/imports/c',
154
+ ]);
155
+ });
156
+
157
+ test('returns METEOR_IGNORE patterns when there is no .meteorignore file', () => {
158
+ const entries = withMeteorIgnoreEnv('*.tests.ts', () =>
159
+ getMeteorIgnoreEntries(projectDir),
160
+ );
161
+
162
+ expect(entries).toEqual(['*.tests.ts']);
163
+ });
164
+
165
+ test('returns an empty array when neither source has patterns', () => {
166
+ const entries = withMeteorIgnoreEnv(undefined, () =>
167
+ getMeteorIgnoreEntries(projectDir),
168
+ );
169
+
170
+ expect(entries).toEqual([]);
171
+ });
172
+ });
173
+
174
+ // The folder ignores generateEagerTestFile hands to Rspack as the context
175
+ // `exclude`. Rspack tests that regex against absolute module paths built with
176
+ // the platform separator, so on Windows they arrive with backslashes.
177
+ const EAGER_TEST_IGNORES = [
178
+ '**/node_modules/**',
179
+ '**/.meteor/**',
180
+ '**/public/**',
181
+ '**/private/**',
182
+ '**/_build/**',
183
+ '**/client/**',
184
+ ];
185
+
186
+ function excludeFor(rootPath) {
187
+ return createIgnoreRegex(createIgnoreGlobConfig(EAGER_TEST_IGNORES), rootPath);
188
+ }
189
+
190
+ describe('createIgnoreRegex', () => {
191
+ describe('POSIX absolute paths', () => {
192
+ const exclude = excludeFor('/Users/dev/app');
193
+
194
+ test('excludes ignored folders below the project root', () => {
195
+ expect(exclude.test('/Users/dev/app/node_modules/pkg/index.test.js')).toBe(true);
196
+ expect(exclude.test('/Users/dev/app/client/thing.test.js')).toBe(true);
197
+ });
198
+
199
+ test('keeps app test files', () => {
200
+ expect(exclude.test('/Users/dev/app/imports/api/thing.test.js')).toBe(false);
201
+ });
202
+
203
+ test('keeps app test files when a parent folder is named private (#14688)', () => {
204
+ const privateExclude = excludeFor('/private/tmp/app');
205
+ expect(privateExclude.test('/private/tmp/app/imports/thing.test.js')).toBe(false);
206
+ expect(privateExclude.test('/private/tmp/app/private/thing.test.js')).toBe(true);
207
+ });
208
+ });
209
+
210
+ describe('Windows absolute paths', () => {
211
+ const exclude = excludeFor('C:\\Users\\dev\\app');
212
+
213
+ test('excludes ignored folders joined with backslashes (#14513)', () => {
214
+ expect(exclude.test('C:\\Users\\dev\\app\\node_modules\\pkg\\index.test.js')).toBe(true);
215
+ expect(exclude.test('C:\\Users\\dev\\app\\client\\thing.test.js')).toBe(true);
216
+ });
217
+
218
+ test('keeps app test files', () => {
219
+ expect(exclude.test('C:\\Users\\dev\\app\\imports\\api\\thing.test.js')).toBe(false);
220
+ });
221
+
222
+ test('accepts a forward-slash root joined with backslashes', () => {
223
+ const mixedExclude = excludeFor('C:/Users/dev/app');
224
+ expect(mixedExclude.test('C:/Users/dev/app\\node_modules\\pkg\\index.test.js')).toBe(true);
225
+ expect(mixedExclude.test('C:/Users/dev/app\\imports\\thing.test.js')).toBe(false);
226
+ });
227
+
228
+ test('keeps app test files when a parent folder is named private', () => {
229
+ const privateExclude = excludeFor('C:\\Users\\dev\\private\\app');
230
+ expect(privateExclude.test('C:\\Users\\dev\\private\\app\\imports\\thing.test.js')).toBe(false);
231
+ });
232
+ });
233
+
234
+ describe('without a root path', () => {
235
+ test('matches relative context keys', () => {
236
+ const exclude = createIgnoreRegex(createIgnoreGlobConfig(['tests/fixtures/**']));
237
+ expect(exclude.test('./tests/fixtures/thing.test.js')).toBe(true);
238
+ expect(exclude.test('./imports/thing.test.js')).toBe(false);
239
+ });
240
+
241
+ test('keeps a single star within one path segment', () => {
242
+ const exclude = createIgnoreRegex(createIgnoreGlobConfig(['imports/*.test.js']));
243
+ expect(exclude.test('./imports/thing.test.js')).toBe(true);
244
+ expect(exclude.test('./imports/nested/thing.test.js')).toBe(false);
245
+ });
246
+
247
+ test('never matches when every pattern is a negation', () => {
248
+ const exclude = createIgnoreRegex(createIgnoreGlobConfig(['!imports/**']));
249
+ expect(exclude.test('./imports/thing.test.js')).toBe(false);
250
+ });
251
+ });
252
+
253
+ test('survives being embedded as a regex literal in generated code', () => {
254
+ const exclude = excludeFor('C:\\Users\\dev\\app');
255
+ const embedded = new Function(`return ${exclude.toString()};`)();
256
+ expect(embedded.test('C:\\Users\\dev\\app\\node_modules\\pkg\\index.test.js')).toBe(true);
257
+ expect(embedded.test('C:\\Users\\dev\\app\\imports\\thing.test.js')).toBe(false);
258
+ });
259
+ });
package/lib/test.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
3
+ const {
4
+ createIgnoreGlobConfig,
5
+ createIgnoreMatcherSource,
6
+ createIgnoreRegex,
7
+ } = require("./ignore.js");
4
8
 
5
9
  // Normalize a path to always use forward slashes (POSIX style).
6
10
  // Module identifiers in bundled JS must use '/' regardless of OS.
@@ -47,10 +51,11 @@ const generateEagerTestFile = ({
47
51
  createIgnoreGlobConfig(ignoreEntries),
48
52
  projectDir,
49
53
  );
50
- // Create regex from meteor ignore entries
51
- const excludeMeteorIgnoreRegex = inMeteorIgnoreEntries.length > 0
52
- ? createIgnoreRegex(createIgnoreGlobConfig(inMeteorIgnoreEntries))
53
- : null;
54
+ // Build the .meteorignore filter. It is `ignore`-backed rather than a regex
55
+ // because gitignore's last-match-wins rule cannot be expressed as one, and
56
+ // collapsing it into one used to drop every `!` pattern — silently loading
57
+ // zero test files for a sharded run.
58
+ const meteorIgnoreMatcher = createIgnoreMatcherSource(inMeteorIgnoreEntries);
54
59
 
55
60
  const prefix = (inPrefix && `${inPrefix}-`) || "";
56
61
  const filename = isAppTest
@@ -64,8 +69,9 @@ const generateEagerTestFile = ({
64
69
  const content = `${
65
70
  globalImportPath ? `import '${toPosix(globalImportPath)}';\n\n` : ""
66
71
  }${
67
- excludeMeteorIgnoreRegex
68
- ? `const MeteorIgnoreRegex = ${excludeMeteorIgnoreRegex.toString()};`
72
+ meteorIgnoreMatcher
73
+ ? `import MeteorIgnore from '${toPosix(meteorIgnoreMatcher.modulePath)}';\n\n` +
74
+ `const MeteorIgnoreMatcher = ${meteorIgnoreMatcher.source}(MeteorIgnore);`
69
75
  : ""
70
76
  }
71
77
  {
@@ -77,9 +83,9 @@ const generateEagerTestFile = ({
77
83
  });
78
84
  await Promise.all(ctx.keys().filter((k) => {
79
85
  ${
80
- excludeMeteorIgnoreRegex
86
+ meteorIgnoreMatcher
81
87
  ? `// Only exclude based on *relative* path segments.
82
- return !MeteorIgnoreRegex.test(k);`
88
+ return !MeteorIgnoreMatcher(k);`
83
89
  : "return true;"
84
90
  }
85
91
  }).map(ctx));
@@ -0,0 +1,49 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const { createIgnoreMatcherSource } = require('./ignore.js');
6
+ const { generateEagerTestFile } = require('./test.js');
7
+
8
+ describe('generateEagerTestFile', () => {
9
+ let projectDir;
10
+
11
+ beforeEach(() => {
12
+ projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'meteor-eager-test-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(projectDir, { recursive: true, force: true });
17
+ });
18
+
19
+ const generate = meteorIgnoreEntries =>
20
+ fs.readFileSync(
21
+ generateEagerTestFile({
22
+ isAppTest: false,
23
+ projectDir,
24
+ buildContext: '_build',
25
+ meteorIgnoreEntries,
26
+ }),
27
+ 'utf8',
28
+ );
29
+
30
+ test('filters test files through a matcher built from the ignore package', () => {
31
+ const entries = ['*.tests.ts', '!/imports/a/B.tests.ts'];
32
+ const { modulePath, source } = createIgnoreMatcherSource(entries);
33
+
34
+ const content = generate(entries);
35
+
36
+ expect(content).toContain(
37
+ `import MeteorIgnore from '${modulePath.replace(/\\/g, '/')}';`,
38
+ );
39
+ expect(content).toContain(source);
40
+ expect(content).toContain('!MeteorIgnoreMatcher(k)');
41
+ });
42
+
43
+ test('keeps every test file when there are no meteor ignore entries', () => {
44
+ const content = generate([]);
45
+
46
+ expect(content).not.toContain('MeteorIgnore');
47
+ expect(content).toContain('return true;');
48
+ });
49
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meteorjs/rspack",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.3",
4
4
  "description": "Configuration logic for using Rspack in Meteor projects",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "fast-deep-equal": "^3.1.3",
15
+ "ignore": "^7.0.5",
15
16
  "ignore-loader": "^0.1.2",
16
17
  "isomorphic-timers-promises": "^1.0.1",
17
18
  "rspack-merge": "^1.0.1"
package/rspack.config.js CHANGED
@@ -123,6 +123,7 @@ function createSwcConfig({
123
123
  externalHelpers,
124
124
  isDevEnvironment,
125
125
  isClient,
126
+ isLegacy,
126
127
  isAngularEnabled,
127
128
  }) {
128
129
  const defaultConfig = {
@@ -133,7 +134,7 @@ function createSwcConfig({
133
134
  ...(isJsxEnabled && { jsx: true }),
134
135
  ...(isAngularEnabled && { decorators: true }),
135
136
  },
136
- target: isClient ? 'es2015' : 'es2022',
137
+ target: isLegacy ? 'es5' : isClient ? 'es2015' : 'es2022',
137
138
  ...(isReactEnabled && {
138
139
  transform: {
139
140
  react: {
@@ -257,6 +258,10 @@ module.exports = async function (inMeteor = {}, argv = {}) {
257
258
  Meteor.chunksContext ||
258
259
  process.env.RSPACK_CHUNKS_CONTEXT ||
259
260
  `build-chunks${(meteorLocalDirName && `-${meteorLocalDirName}`) || ""}`;
261
+ const clientOutputContext = Meteor.clientOutputContext || '';
262
+ const clientOutputDir = path.resolve(projectDir, 'public', clientOutputContext);
263
+ const clientChunkPrefix = clientOutputContext ? '' : `${chunksContext}/`;
264
+ const clientAssetContext = path.posix.relative(clientOutputContext, assetsContext);
260
265
 
261
266
  // Compute build paths before loading user config (needed by Meteor helpers below)
262
267
  const outputPath = Meteor.outputPath;
@@ -267,6 +272,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
267
272
  const isTest = !!Meteor.isTest;
268
273
  const isClient = !!Meteor.isClient;
269
274
  const isServer = !!Meteor.isServer;
275
+ const isLegacy = !!Meteor.isLegacy;
276
+ Meteor.isLegacy = isLegacy;
277
+ const arch = Meteor.arch;
270
278
  const isRun = !!Meteor.isRun;
271
279
  const isBuild = !!Meteor.isBuild;
272
280
  const isReactEnabled = !!Meteor.isReactEnabled;
@@ -312,7 +320,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
312
320
  // reassigned after load once mode is fully resolved.
313
321
  let cacheStrategy = createCacheStrategy(
314
322
  initialMode,
315
- (Meteor.isClient && "client") || "server",
323
+ arch || (Meteor.isClient && "client") || "server",
316
324
  { projectConfigPath, configPath, buildContext }
317
325
  );
318
326
  let swcConfigRule = createSwcConfig({
@@ -321,8 +329,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
321
329
  isJsxEnabled,
322
330
  isTsxEnabled,
323
331
  externalHelpers: enableSwcExternalHelpers,
324
- isDevEnvironment: isRun && initialIsDev && !isTest && !isNative,
332
+ isDevEnvironment: isRun && initialIsDev && !isTest && !isNative && !arch,
325
333
  isClient,
334
+ isLegacy,
326
335
  isAngularEnabled,
327
336
  });
328
337
  Meteor.swcConfigOptions = swcConfigRule.options;
@@ -355,7 +364,8 @@ module.exports = async function (inMeteor = {}, argv = {}) {
355
364
  return new HtmlRspackPlugin({
356
365
  inject: false,
357
366
  cache: true,
358
- filename: `../${buildContext}/${outputDir}/index.html`,
367
+ filename: path.relative(clientOutputDir,
368
+ path.resolve(projectDir, buildContext, outputDir, 'index.html')).replaceAll(path.sep, '/'),
359
369
  templateContent: `
360
370
  <head>
361
371
  <% for tag in htmlRspackPlugin.tags.headTags { %>
@@ -401,7 +411,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
401
411
 
402
412
  cacheStrategy = createCacheStrategy(
403
413
  mode,
404
- (Meteor.isClient && "client") || "server",
414
+ arch || (Meteor.isClient && "client") || "server",
405
415
  // buildContext must be passed here too: this reassignment is the
406
416
  // effective cache strategy, and omitting it made the cache directory
407
417
  // collide across build contexts (e.g. custom METEOR_LOCAL_DIR setups).
@@ -418,7 +428,6 @@ module.exports = async function (inMeteor = {}, argv = {}) {
418
428
  );
419
429
 
420
430
  // Determine output directories
421
- const clientOutputDir = path.resolve(projectDir, "public");
422
431
  const serverOutputDir = path.resolve(projectDir, "private");
423
432
 
424
433
 
@@ -446,7 +455,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
446
455
  console.log("[i] Meteor flags:", Meteor);
447
456
  }
448
457
 
449
- const isDevEnvironment = isRun && isDev && !isTest && !isNative;
458
+ const isDevEnvironment = isRun && isDev && !isTest && !isNative && !arch;
450
459
  swcConfigRule = createSwcConfig({
451
460
  isTypescriptEnabled,
452
461
  isReactEnabled,
@@ -455,6 +464,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
455
464
  externalHelpers: enableSwcExternalHelpers,
456
465
  isDevEnvironment,
457
466
  isClient,
467
+ isLegacy,
458
468
  isAngularEnabled,
459
469
  });
460
470
  Meteor.swcConfigOptions = swcConfigRule.options;
@@ -492,7 +502,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
492
502
  ...(Meteor.isBlazeEnabled && {
493
503
  externals: /\.html$/,
494
504
  isEagerImport: (module) => module.endsWith(".html"),
495
- ...((isProd || (isTest && isClient)) && {
505
+ ...((isProd || arch || (isTest && isClient)) && {
496
506
  lastImports: [`./${outputFilename}`],
497
507
  }),
498
508
  }),
@@ -505,7 +515,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
505
515
  const filename = _fileInfo.filename;
506
516
  const isPublic = filename.startsWith("/") || filename.startsWith("public");
507
517
  if (isPublic) return `[name][ext][query]`;
508
- return `${assetsContext}/[hash][ext][query]`;
518
+ return `${isClient ? clientAssetContext : assetsContext}/[hash][ext][query]`;
509
519
  };
510
520
 
511
521
  const rsdoctorModule = isBundleVisualizerEnabled
@@ -537,6 +547,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
537
547
  : { stats: "errors-warnings", infrastructureLogging: { level: "warn" } };
538
548
 
539
549
  const clientEntry =
550
+ arch && !(isTest && isTestEager) ? path.resolve(projectDir, buildContext, entryPath) :
540
551
  isClient && isTest && isTestEager && isTestFullApp
541
552
  ? generateEagerTestFile({
542
553
  isAppTest: true,
@@ -544,7 +555,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
544
555
  buildContext,
545
556
  ignoreEntries: ["**/server/**"],
546
557
  meteorIgnoreEntries,
547
- prefix: "client",
558
+ prefix: arch || "client",
548
559
  extraEntry: path.resolve(process.cwd(), Meteor.mainClientEntry),
549
560
  globalImportPath: path.resolve(projectDir, buildContext, entryPath),
550
561
  })
@@ -556,13 +567,13 @@ module.exports = async function (inMeteor = {}, argv = {}) {
556
567
  buildContext,
557
568
  ignoreEntries: ["**/server/**"],
558
569
  meteorIgnoreEntries,
559
- prefix: "client",
570
+ prefix: arch || "client",
560
571
  globalImportPath: path.resolve(projectDir, buildContext, entryPath),
561
572
  })
562
573
  : isClient && isTest && testEntry
563
574
  ? path.resolve(process.cwd(), testEntry)
564
575
  : path.resolve(process.cwd(), buildContext, entryPath);
565
- const clientNameConfig = `[${(isTest && "test-") || ""}client-rspack]`;
576
+ const clientNameConfig = `[${(isTest && "test-") || ""}${arch || 'client'}-rspack]`;
566
577
 
567
578
  // Default onListening provided by meteor-rspack. Kept as a named
568
579
  // reference so we can detect a user-supplied override after merge
@@ -603,7 +614,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
603
614
  // Base client config
604
615
  let clientConfig = {
605
616
  name: clientNameConfig,
606
- target: "web",
617
+ target: isLegacy ? ["web", "es5"] : "web",
607
618
  mode,
608
619
  entry: clientEntry,
609
620
  output: {
@@ -611,27 +622,28 @@ module.exports = async function (inMeteor = {}, argv = {}) {
611
622
  filename: (_module) => {
612
623
  const chunkName = _module.chunk?.name;
613
624
  const isMainChunk = !chunkName || chunkName === "main";
614
- const chunkSuffix = `${chunksContext}/[id]${
625
+ const chunkSuffix = `${clientChunkPrefix}[id]${
615
626
  isProd ? ".[chunkhash]" : ""
616
627
  }.js`;
617
628
  if (isDevEnvironment) {
618
629
  if (isMainChunk) return outputFilename;
619
630
  return chunkSuffix;
620
631
  }
621
- if (isMainChunk) return `../${buildContext}/${outputPath}`;
632
+ if (isMainChunk) return path.relative(clientOutputDir,
633
+ path.resolve(projectDir, buildContext, outputPath)).replaceAll(path.sep, '/');
622
634
  return chunkSuffix;
623
635
  },
624
636
  library: { type: "commonjs2" },
625
- publicPath: "/",
626
- chunkFilename: `${chunksContext}/[id]${isProd ? ".[chunkhash]" : ""}.js`,
637
+ publicPath: clientOutputContext ? `/${clientOutputContext}/` : "/",
638
+ chunkFilename: `${clientChunkPrefix}[id]${isProd ? ".[chunkhash]" : ""}.js`,
627
639
  assetModuleFilename,
628
- cssFilename: `${chunksContext}/[name]${
640
+ cssFilename: `${clientChunkPrefix}[name]${
629
641
  isProd ? ".[contenthash]" : ""
630
642
  }.css`,
631
- cssChunkFilename: `${chunksContext}/[id]${
643
+ cssChunkFilename: `${clientChunkPrefix}[id]${
632
644
  isProd ? ".[contenthash]" : ""
633
645
  }.css`,
634
- ...(isProd && { clean: { keep: keepOutsideBuild() } }),
646
+ ...(isProd && { clean: clientOutputContext ? true : { keep: keepOutsideBuild() } }),
635
647
  },
636
648
  optimization: {
637
649
  usedExports: true,