@openmrs/webpack-config 10.0.1-pre.5322 → 10.0.1-pre.5364

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.
Files changed (3) hide show
  1. package/dist/index.js +133 -8
  2. package/package.json +3 -1
  3. package/src/index.ts +168 -2
package/dist/index.js CHANGED
@@ -44,16 +44,21 @@ exports.optimizationConfig = exports.watchConfig = exports.assetRuleConfig = exp
44
44
  */
45
45
  const fs_1 = require("fs");
46
46
  const path_1 = require("path");
47
+ const browserslist_1 = __importDefault(require("browserslist"));
48
+ const node_1 = require("browserslist/node");
49
+ const browserslist_config_openmrs_1 = __importDefault(require("browserslist-config-openmrs"));
47
50
  const clean_webpack_plugin_1 = require("clean-webpack-plugin");
48
51
  const copy_webpack_plugin_1 = __importDefault(require("copy-webpack-plugin"));
49
52
  const fork_ts_checker_webpack_plugin_1 = __importDefault(require("fork-ts-checker-webpack-plugin"));
50
53
  // eslint-disable-next-line no-restricted-imports
51
54
  const lodash_1 = require("lodash");
52
55
  const semver_1 = require("semver");
53
- const webpack_1 = require("@module-federation/enhanced/webpack");
54
- const webpack_2 = require("webpack");
56
+ const webpack_1 = require("webpack");
57
+ // webpack's own browsers-to-features mapping
58
+ const browserslistTargetHandler_1 = __importDefault(require("webpack/lib/config/browserslistTargetHandler"));
55
59
  const webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer");
56
60
  const webpack_stats_plugin_1 = require("webpack-stats-plugin");
61
+ const webpack_2 = require("@module-federation/enhanced/webpack");
57
62
  const production = 'production';
58
63
  // Read from our own pin rather than `@module-federation/enhanced/package.json`, which its `exports` map
59
64
  // makes unreadable. `parse` rather than `coerce`, so that loosening the pin to a range disables the skew
@@ -61,6 +66,25 @@ const production = 'production';
61
66
  // eslint-disable-next-line @typescript-eslint/no-require-imports
62
67
  const moduleFederationPin = require('../package.json').dependencies['@module-federation/enhanced'];
63
68
  const moduleFederationVersion = (0, semver_1.parse)(moduleFederationPin);
69
+ // The subset of what `browserslistTargetHandler.resolve` reports that `output.environment` accepts;
70
+ // the rest describe the platform and available APIs, and webpack's schema rejects them here.
71
+ const environmentFlags = [
72
+ 'arrowFunction',
73
+ 'asyncFunction',
74
+ 'bigIntLiteral',
75
+ 'const',
76
+ 'destructuring',
77
+ 'document',
78
+ 'dynamicImport',
79
+ 'dynamicImportInWorker',
80
+ 'forOf',
81
+ 'globalThis',
82
+ 'importMetaDirnameAndFilename',
83
+ 'methodShorthand',
84
+ 'module',
85
+ 'optionalChaining',
86
+ 'templateLiteral',
87
+ ];
64
88
  /**
65
89
  * Prepended to this app's entry chunks. Without it, an app running under an app shell too old to
66
90
  * publish the runtime globals fails with a `TypeError` from inside minified runtime code. All three
@@ -98,6 +122,95 @@ function getFrameworkVersion() {
98
122
  return '5.x';
99
123
  }
100
124
  }
125
+ /**
126
+ * The browserslist queries swc compiles this module against, falling back to OpenMRS's shared config
127
+ * when the module declares none, so that frontend RFC 0003 stays the single source of truth. Given no
128
+ * target at all swc down-levels to ES5, and every supported browser pays for transform helpers it
129
+ * doesn't need.
130
+ *
131
+ * `@openmrs/rspack-config` has a copy of this; keep them in step. `browser-targets.test.ts` compares
132
+ * what the two hand their loaders and fails if they drift.
133
+ *
134
+ * @param root The directory of the module being built
135
+ */
136
+ function browserslistQueries(root) {
137
+ var _a, _b;
138
+ const warnings = [];
139
+ // deliberately unguarded
140
+ const loaded = browserslist_1.default.loadConfig({ path: root });
141
+ const configured = loaded === undefined ? [] : Array.isArray(loaded) ? loaded : [loaded];
142
+ if (loaded !== undefined && configured.length === 0) {
143
+ // warn when a browserlist config is empty
144
+ warnings.push(`This module declares a browserslist config, but it resolves to no queries for the ` +
145
+ `${(_b = (_a = process.env.BROWSERSLIST_ENV) !== null && _a !== void 0 ? _a : process.env.NODE_ENV) !== null && _b !== void 0 ? _b : production} environment. ` +
146
+ `Targeting ${browserslist_config_openmrs_1.default.join(', ')} instead.`);
147
+ }
148
+ const expanded = expandBrowserslistExtends(configured.length > 0 ? configured : browserslist_config_openmrs_1.default, root, warnings);
149
+ return { queries: expanded.length > 0 ? expanded : browserslist_config_openmrs_1.default, warnings };
150
+ }
151
+ function expandBrowserslistExtends(queries, root, warnings, seen = new Set()) {
152
+ // Split on commas because a browserslist config may be a single string of them
153
+ return queries.flatMap((query) => query
154
+ .split(',')
155
+ .flatMap((part) => {
156
+ const extended = /^extends\s+(.+)$/i.exec(part.trim());
157
+ if (!extended) {
158
+ return [part.trim()];
159
+ }
160
+ const name = extended[1].trim();
161
+ if (seen.has(name)) {
162
+ return [];
163
+ }
164
+ seen.add(name);
165
+ try {
166
+ // browserslist's own loader, rather than a bare `require` so we handle the same syntax
167
+ const resolved = (0, node_1.loadQueries)({ path: root }, name);
168
+ return expandBrowserslistExtends(Array.isArray(resolved) ? resolved : [resolved], root, warnings, seen);
169
+ }
170
+ catch (err) {
171
+ // Only browserslist's own verdict on a named config is survivable: not installed, or refused
172
+ // for its name.
173
+ const notInstalled = (err === null || err === void 0 ? void 0 : err.code) === 'MODULE_NOT_FOUND';
174
+ const refused = (err === null || err === void 0 ? void 0 : err.browserslist) === true;
175
+ if (!notInstalled && !refused) {
176
+ throw err;
177
+ }
178
+ warnings.push(`Could not load the browserslist config "${name}" (${err.message}). ` +
179
+ `Targeting ${browserslist_config_openmrs_1.default.join(', ')} instead.`);
180
+ return browserslist_config_openmrs_1.default;
181
+ }
182
+ })
183
+ .filter((part) => part.length > 0));
184
+ }
185
+ /** Reports how a module's browsers were worked out, where a developer will actually see it. */
186
+ class BrowserslistWarningsPlugin {
187
+ constructor(messages) {
188
+ this.messages = messages;
189
+ }
190
+ apply(compiler) {
191
+ compiler.hooks.thisCompilation.tap('OpenmrsBrowserslistWarnings', (compilation) => {
192
+ for (const message of this.messages) {
193
+ compilation.warnings.push(new webpack_1.WebpackError(message));
194
+ }
195
+ });
196
+ }
197
+ }
198
+ /**
199
+ * The `output.environment` flags for a set of browserslist queries: which JavaScript features webpack
200
+ * may use in the runtime it generates.
201
+ *
202
+ * Derived with webpack's own browserslist target handler, so the browsers-to-features mapping is the
203
+ * caniuse-backed one webpack uses for `target: 'browserslist'` rather than a table maintained here.
204
+ * `resolve` also reports platform and API properties that `output.environment` rejects, hence the
205
+ * filter to the flags webpack's schema accepts.
206
+ *
207
+ * @param queries Browserslist queries, already `extends`-expanded
208
+ * @param root The directory of the module being built, for resolving relative queries
209
+ */
210
+ function browserEnvironment(queries, root) {
211
+ const supported = browserslistTargetHandler_1.default.resolve((0, browserslist_1.default)(queries, { path: root }));
212
+ return Object.fromEntries(environmentFlags.filter((flag) => typeof supported[flag] === 'boolean').map((flag) => [flag, supported[flag]]));
213
+ }
101
214
  function makeIdent(name) {
102
215
  if (name.includes('/')) {
103
216
  name = name.slice(name.indexOf('/'));
@@ -176,6 +289,7 @@ exports.default = (env, argv = {}) => {
176
289
  const outDir = (0, path_1.dirname)(browser || main);
177
290
  const srcFile = (0, path_1.resolve)(root, browser ? main : types);
178
291
  const ident = makeIdent(name);
292
+ const { queries: browserTargets, warnings: browserslistWarnings } = browserslistQueries(root);
179
293
  const frameworkVersion = getFrameworkVersion();
180
294
  const routes = (0, path_1.resolve)(root, 'src', 'routes.json');
181
295
  const hasRoutesDefined = fileExistsSync(routes);
@@ -202,12 +316,22 @@ exports.default = (env, argv = {}) => {
202
316
  publicPath: 'auto',
203
317
  path: (0, path_1.resolve)(root, outDir),
204
318
  hashFunction: 'xxhash64',
319
+ environment: browserEnvironment(browserTargets, root),
205
320
  }, module: {
206
321
  rules: [
207
322
  (0, lodash_1.merge)({
208
323
  test: /\.m?(js|ts|tsx)$/,
209
324
  exclude: /node_modules/,
210
- use: require.resolve('swc-loader'),
325
+ use: {
326
+ loader: require.resolve('swc-loader'),
327
+ options: {
328
+ env: {
329
+ targets: browserTargets,
330
+ },
331
+ // ignore a project .swcrc to match rspack behavior
332
+ swcrc: false,
333
+ },
334
+ },
211
335
  }, exports.scriptRuleConfig),
212
336
  (0, lodash_1.merge)({
213
337
  test: /\.css$/,
@@ -237,7 +361,7 @@ exports.default = (env, argv = {}) => {
237
361
  type: 'asset/source',
238
362
  }),
239
363
  ],
240
- }, mode, devtool: mode === production ? 'hidden-nosources-source-map' : 'source-map', devServer: {
364
+ }, mode, target: 'web', devtool: mode === production ? 'hidden-nosources-source-map' : 'source-map', devServer: {
241
365
  headers: {
242
366
  'Access-Control-Allow-Origin': '*',
243
367
  },
@@ -257,6 +381,7 @@ exports.default = (env, argv = {}) => {
257
381
  maxInitialRequests: 1,
258
382
  },
259
383
  }, exports.optimizationConfig), plugins: [
384
+ browserslistWarnings.length > 0 && new BrowserslistWarningsPlugin(browserslistWarnings),
260
385
  new fork_ts_checker_webpack_plugin_1.default({
261
386
  issue: {
262
387
  exclude: [
@@ -271,10 +396,10 @@ exports.default = (env, argv = {}) => {
271
396
  new webpack_bundle_analyzer_1.BundleAnalyzerPlugin({
272
397
  analyzerMode: env && env.analyze ? 'server' : 'disabled',
273
398
  }),
274
- new webpack_2.DefinePlugin({
399
+ new webpack_1.DefinePlugin({
275
400
  'process.env.FRAMEWORK_VERSION': JSON.stringify(frameworkVersion),
276
401
  }),
277
- new webpack_1.ModuleFederationPlugin({
402
+ new webpack_2.ModuleFederationPlugin({
278
403
  // Look in the `esm-dynamic-loading` framework package for an explanation of how modules
279
404
  // get loaded into the application.
280
405
  name,
@@ -336,11 +461,11 @@ exports.default = (env, argv = {}) => {
336
461
  // The two runtime packages a remote can safely borrow from the app shell; see the
337
462
  // `ExternalsPlugin` block in `@openmrs/rspack-config` for why only these two are shareable,
338
463
  // what still ships per remote, and why this is a plugin rather than an `externals` entry.
339
- new webpack_2.ExternalsPlugin('global', {
464
+ new webpack_1.ExternalsPlugin('global', {
340
465
  '@module-federation/sdk': '_OPENMRS_FEDERATION_SDK',
341
466
  '@module-federation/error-codes': '_OPENMRS_FEDERATION_ERROR_CODES',
342
467
  }),
343
- new webpack_2.BannerPlugin({
468
+ new webpack_1.BannerPlugin({
344
469
  raw: true,
345
470
  entryOnly: true,
346
471
  test: /\.[cm]?js$/,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/webpack-config",
3
- "version": "10.0.1-pre.5322",
3
+ "version": "10.0.1-pre.5364",
4
4
  "license": "MPL-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,6 +33,8 @@
33
33
  "dependencies": {
34
34
  "@module-federation/enhanced": "2.8.1",
35
35
  "@swc/core": "1.15.21",
36
+ "browserslist": "4.28.9",
37
+ "browserslist-config-openmrs": "^1.0.1",
36
38
  "clean-webpack-plugin": "4.0.0",
37
39
  "copy-webpack-plugin": "11.0.0",
38
40
  "css-loader": "5.2.7",
package/src/index.ts CHANGED
@@ -38,23 +38,30 @@
38
38
  */
39
39
  import { existsSync, statSync } from 'fs';
40
40
  import { basename, dirname, resolve } from 'path';
41
+ import browserslist from 'browserslist';
42
+ import { loadQueries } from 'browserslist/node';
43
+ import defaultBrowserslistQueries from 'browserslist-config-openmrs';
41
44
  import { CleanWebpackPlugin } from 'clean-webpack-plugin';
42
45
  import CopyWebpackPlugin from 'copy-webpack-plugin';
43
46
  import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
44
47
  // eslint-disable-next-line no-restricted-imports
45
48
  import { isArray, merge, mergeWith } from 'lodash';
46
49
  import { inc, parse } from 'semver';
47
- import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
48
50
  import {
49
51
  BannerPlugin,
52
+ type Compiler,
50
53
  DefinePlugin,
51
54
  ExternalsPlugin,
52
55
  type ModuleOptions,
53
56
  type RuleSetRule,
57
+ WebpackError,
54
58
  type WebpackOptionsNormalized as WebpackConfiguration,
55
59
  } from 'webpack';
60
+ // webpack's own browsers-to-features mapping
61
+ import browserslistTargetHandler from 'webpack/lib/config/browserslistTargetHandler';
56
62
  import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
57
63
  import { StatsWriterPlugin } from 'webpack-stats-plugin';
64
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
58
65
 
59
66
  type OpenmrsWebpackConfig = Omit<Partial<WebpackConfiguration>, 'module' | 'output'> & {
60
67
  module: ModuleOptions;
@@ -70,6 +77,26 @@ const production = 'production';
70
77
  const moduleFederationPin: string = require('../package.json').dependencies['@module-federation/enhanced'];
71
78
  const moduleFederationVersion = parse(moduleFederationPin);
72
79
 
80
+ // The subset of what `browserslistTargetHandler.resolve` reports that `output.environment` accepts;
81
+ // the rest describe the platform and available APIs, and webpack's schema rejects them here.
82
+ const environmentFlags = [
83
+ 'arrowFunction',
84
+ 'asyncFunction',
85
+ 'bigIntLiteral',
86
+ 'const',
87
+ 'destructuring',
88
+ 'document',
89
+ 'dynamicImport',
90
+ 'dynamicImportInWorker',
91
+ 'forOf',
92
+ 'globalThis',
93
+ 'importMetaDirnameAndFilename',
94
+ 'methodShorthand',
95
+ 'module',
96
+ 'optionalChaining',
97
+ 'templateLiteral',
98
+ ] as const;
99
+
73
100
  /**
74
101
  * Prepended to this app's entry chunks. Without it, an app running under an app shell too old to
75
102
  * publish the runtime globals fails with a `TypeError` from inside minified runtime code. All three
@@ -111,6 +138,132 @@ function getFrameworkVersion() {
111
138
  }
112
139
  }
113
140
 
141
+ /** What a module's browsers resolved to, plus anything worth telling the developer about getting there. */
142
+ type BrowserPolicy = { queries: Array<string>; warnings: Array<string> };
143
+
144
+ /**
145
+ * The browserslist queries swc compiles this module against, falling back to OpenMRS's shared config
146
+ * when the module declares none, so that frontend RFC 0003 stays the single source of truth. Given no
147
+ * target at all swc down-levels to ES5, and every supported browser pays for transform helpers it
148
+ * doesn't need.
149
+ *
150
+ * `@openmrs/rspack-config` has a copy of this; keep them in step. `browser-targets.test.ts` compares
151
+ * what the two hand their loaders and fails if they drift.
152
+ *
153
+ * @param root The directory of the module being built
154
+ */
155
+ function browserslistQueries(root: string): BrowserPolicy {
156
+ const warnings: Array<string> = [];
157
+ // deliberately unguarded
158
+ const loaded = browserslist.loadConfig({ path: root });
159
+ const configured = loaded === undefined ? [] : Array.isArray(loaded) ? loaded : [loaded];
160
+
161
+ if (loaded !== undefined && configured.length === 0) {
162
+ // warn when a browserlist config is empty
163
+ warnings.push(
164
+ `This module declares a browserslist config, but it resolves to no queries for the ` +
165
+ `${process.env.BROWSERSLIST_ENV ?? process.env.NODE_ENV ?? production} environment. ` +
166
+ `Targeting ${defaultBrowserslistQueries.join(', ')} instead.`,
167
+ );
168
+ }
169
+
170
+ const expanded = expandBrowserslistExtends(
171
+ configured.length > 0 ? configured : defaultBrowserslistQueries,
172
+ root,
173
+ warnings,
174
+ );
175
+
176
+ return { queries: expanded.length > 0 ? expanded : defaultBrowserslistQueries, warnings };
177
+ }
178
+
179
+ function expandBrowserslistExtends(
180
+ queries: Array<string>,
181
+ root: string,
182
+ warnings: Array<string>,
183
+ seen = new Set<string>(),
184
+ ): Array<string> {
185
+ // Split on commas because a browserslist config may be a single string of them
186
+ return queries.flatMap((query) =>
187
+ query
188
+ .split(',')
189
+ .flatMap((part) => {
190
+ const extended = /^extends\s+(.+)$/i.exec(part.trim());
191
+
192
+ if (!extended) {
193
+ return [part.trim()];
194
+ }
195
+
196
+ const name = extended[1].trim();
197
+
198
+ if (seen.has(name)) {
199
+ return [];
200
+ }
201
+
202
+ seen.add(name);
203
+
204
+ try {
205
+ // browserslist's own loader, rather than a bare `require` so we handle the same syntax
206
+ const resolved = loadQueries({ path: root }, name);
207
+
208
+ return expandBrowserslistExtends(Array.isArray(resolved) ? resolved : [resolved], root, warnings, seen);
209
+ } catch (err) {
210
+ // Only browserslist's own verdict on a named config is survivable: not installed, or refused
211
+ // for its name.
212
+ const notInstalled = (err as { code?: string })?.code === 'MODULE_NOT_FOUND';
213
+ const refused = (err as { browserslist?: boolean })?.browserslist === true;
214
+
215
+ if (!notInstalled && !refused) {
216
+ throw err;
217
+ }
218
+
219
+ warnings.push(
220
+ `Could not load the browserslist config "${name}" (${(err as Error).message}). ` +
221
+ `Targeting ${defaultBrowserslistQueries.join(', ')} instead.`,
222
+ );
223
+
224
+ return defaultBrowserslistQueries;
225
+ }
226
+ })
227
+ .filter((part) => part.length > 0),
228
+ );
229
+ }
230
+
231
+ /** Reports how a module's browsers were worked out, where a developer will actually see it. */
232
+ class BrowserslistWarningsPlugin {
233
+ constructor(private readonly messages: Array<string>) {}
234
+
235
+ apply(compiler: Compiler) {
236
+ compiler.hooks.thisCompilation.tap('OpenmrsBrowserslistWarnings', (compilation) => {
237
+ for (const message of this.messages) {
238
+ compilation.warnings.push(new WebpackError(message));
239
+ }
240
+ });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * The `output.environment` flags for a set of browserslist queries: which JavaScript features webpack
246
+ * may use in the runtime it generates.
247
+ *
248
+ * Derived with webpack's own browserslist target handler, so the browsers-to-features mapping is the
249
+ * caniuse-backed one webpack uses for `target: 'browserslist'` rather than a table maintained here.
250
+ * `resolve` also reports platform and API properties that `output.environment` rejects, hence the
251
+ * filter to the flags webpack's schema accepts.
252
+ *
253
+ * @param queries Browserslist queries, already `extends`-expanded
254
+ * @param root The directory of the module being built, for resolving relative queries
255
+ */
256
+ function browserEnvironment(queries: Array<string>, root: string): WebpackConfiguration['output']['environment'] {
257
+ const supported = browserslistTargetHandler.resolve(browserslist(queries, { path: root })) as Record<
258
+ string,
259
+ boolean | null | undefined
260
+ >;
261
+
262
+ return Object.fromEntries(
263
+ environmentFlags.filter((flag) => typeof supported[flag] === 'boolean').map((flag) => [flag, supported[flag]]),
264
+ );
265
+ }
266
+
114
267
  function makeIdent(name: string): string {
115
268
  if (name.includes('/')) {
116
269
  name = name.slice(name.indexOf('/'));
@@ -201,6 +354,7 @@ export default (env: Record<string, string>, argv: Record<string, string> = {})
201
354
  const outDir = dirname(browser || main);
202
355
  const srcFile = resolve(root, browser ? main : types);
203
356
  const ident = makeIdent(name);
357
+ const { queries: browserTargets, warnings: browserslistWarnings } = browserslistQueries(root);
204
358
  const frameworkVersion = getFrameworkVersion();
205
359
  const routes = resolve(root, 'src', 'routes.json');
206
360
  const hasRoutesDefined = fileExistsSync(routes);
@@ -232,6 +386,7 @@ export default (env: Record<string, string>, argv: Record<string, string> = {})
232
386
  publicPath: 'auto',
233
387
  path: resolve(root, outDir),
234
388
  hashFunction: 'xxhash64',
389
+ environment: browserEnvironment(browserTargets, root),
235
390
  },
236
391
  module: {
237
392
  rules: [
@@ -239,7 +394,16 @@ export default (env: Record<string, string>, argv: Record<string, string> = {})
239
394
  {
240
395
  test: /\.m?(js|ts|tsx)$/,
241
396
  exclude: /node_modules/,
242
- use: require.resolve('swc-loader'),
397
+ use: {
398
+ loader: require.resolve('swc-loader'),
399
+ options: {
400
+ env: {
401
+ targets: browserTargets,
402
+ },
403
+ // ignore a project .swcrc to match rspack behavior
404
+ swcrc: false,
405
+ },
406
+ },
243
407
  },
244
408
  scriptRuleConfig,
245
409
  ),
@@ -282,6 +446,7 @@ export default (env: Record<string, string>, argv: Record<string, string> = {})
282
446
  ],
283
447
  },
284
448
  mode,
449
+ target: 'web',
285
450
  devtool: mode === production ? 'hidden-nosources-source-map' : 'source-map',
286
451
  devServer: {
287
452
  headers: {
@@ -313,6 +478,7 @@ export default (env: Record<string, string>, argv: Record<string, string> = {})
313
478
  optimizationConfig,
314
479
  ),
315
480
  plugins: [
481
+ browserslistWarnings.length > 0 && new BrowserslistWarningsPlugin(browserslistWarnings),
316
482
  new ForkTsCheckerWebpackPlugin({
317
483
  issue: {
318
484
  exclude: [