Skip to content

Commit 68832a7

Browse files
committed
feat(webpack): support es module bundling
Defaults to es module output when runtime version v9+ is detected otherwise falls back to commonjs. Allows flag `--env.commonjs` to be used anytime for explicit commonjs bundling if needed for any reason with v9+.
1 parent c9368ef commit 68832a7

17 files changed

+773
-386
lines changed

packages/webpack5/package-lock.json

Lines changed: 258 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webpack5/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nativescript/webpack",
3-
"version": "5.0.24",
3+
"version": "5.0.25-alpha.1",
44
"private": false,
55
"main": "dist/index.js",
66
"files": [

packages/webpack5/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"assets": [
2424
{
2525
"input": "{projectRoot}/src/stubs",
26-
"glob": "*.js",
26+
"glob": "*.{js,mjs}",
2727
"output": "stubs"
2828
},
2929
{

packages/webpack5/src/configuration/base.ts

Lines changed: 191 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import { extname, relative, resolve } from 'path';
2-
import {
3-
ContextExclusionPlugin,
4-
DefinePlugin,
5-
HotModuleReplacementPlugin,
6-
} from 'webpack';
2+
import { ContextExclusionPlugin, HotModuleReplacementPlugin } from 'webpack';
73
import Config from 'webpack-chain';
84
import { satisfies } from 'semver';
5+
import { isVersionGteConsideringPrerelease } from '../helpers/dependencies';
96
import { existsSync } from 'fs';
107

118
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
129
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
1310
import TerserPlugin from 'terser-webpack-plugin';
1411

1512
import { getProjectFilePath, getProjectTSConfigPath } from '../helpers/project';
16-
import { getDependencyVersion, hasDependency } from '../helpers/dependencies';
13+
import {
14+
getDependencyVersion,
15+
hasDependency,
16+
getResolvedDependencyVersionForCheck,
17+
} from '../helpers/dependencies';
1718
import { PlatformSuffixPlugin } from '../plugins/PlatformSuffixPlugin';
1819
import { applyFileReplacements } from '../helpers/fileReplacements';
1920
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
2021
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
22+
import { CompatDefinePlugin } from '../plugins/CompatDefinePlugin';
2123
import { applyDotEnvPlugin } from '../helpers/dotEnv';
2224
import { env as _env, IWebpackEnv } from '../index';
2325
import { getValue } from '../helpers/config';
@@ -39,6 +41,64 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
3941
// set mode
4042
config.mode(mode);
4143

44+
// use source map files by default with v9+
45+
function useSourceMapFiles() {
46+
if (mode === 'development') {
47+
// in development we always use source-map files with v9+ runtimes
48+
// they are parsed and mapped to display in-flight app error screens
49+
env.sourceMap = 'source-map';
50+
}
51+
}
52+
// determine target output by @nativescript/* runtime version
53+
// v9+ supports ESM output, anything below uses CommonJS
54+
if (
55+
hasDependency('@nativescript/ios') ||
56+
hasDependency('@nativescript/visionos') ||
57+
hasDependency('@nativescript/android')
58+
) {
59+
const iosVersion = getDependencyVersion('@nativescript/ios');
60+
const visionosVersion = getDependencyVersion('@nativescript/visionos');
61+
const androidVersion = getDependencyVersion('@nativescript/android');
62+
63+
if (platform === 'ios') {
64+
const iosResolved =
65+
getResolvedDependencyVersionForCheck('@nativescript/ios', '9.0.0') ??
66+
iosVersion ??
67+
undefined;
68+
if (isVersionGteConsideringPrerelease(iosResolved, '9.0.0')) {
69+
useSourceMapFiles();
70+
} else {
71+
env.commonjs = true;
72+
}
73+
} else if (platform === 'visionos') {
74+
const visionosResolved =
75+
getResolvedDependencyVersionForCheck(
76+
'@nativescript/visionos',
77+
'9.0.0',
78+
) ??
79+
visionosVersion ??
80+
undefined;
81+
if (isVersionGteConsideringPrerelease(visionosResolved, '9.0.0')) {
82+
useSourceMapFiles();
83+
} else {
84+
env.commonjs = true;
85+
}
86+
} else if (platform === 'android') {
87+
const androidResolved =
88+
getResolvedDependencyVersionForCheck(
89+
'@nativescript/android',
90+
'9.0.0',
91+
) ??
92+
androidVersion ??
93+
undefined;
94+
if (isVersionGteConsideringPrerelease(androidResolved, '9.0.0')) {
95+
useSourceMapFiles();
96+
} else {
97+
env.commonjs = true;
98+
}
99+
}
100+
}
101+
42102
// config.stats({
43103
// logging: 'verbose'
44104
// })
@@ -57,6 +117,28 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
57117
node: false,
58118
});
59119

120+
// Mock Node.js built-ins that are not available in NativeScript runtime
121+
// but are required by some packages like css-tree
122+
config.resolve.merge({
123+
fallback: {
124+
module: require.resolve('../polyfills/module.js'),
125+
},
126+
alias: {
127+
// Mock mdn-data modules that css-tree tries to load
128+
'mdn-data/css/properties.json': require.resolve(
129+
'../polyfills/mdn-data-properties.js',
130+
),
131+
'mdn-data/css/syntaxes.json': require.resolve(
132+
'../polyfills/mdn-data-syntaxes.js',
133+
),
134+
'mdn-data/css/at-rules.json': require.resolve(
135+
'../polyfills/mdn-data-at-rules.js',
136+
),
137+
// Ensure imports of the Node 'module' builtin resolve to our polyfill
138+
module: require.resolve('../polyfills/module.js'),
139+
},
140+
});
141+
60142
const getSourceMapType = (map: string | boolean): Config.DevTool => {
61143
const defaultSourceMap = 'inline-source-map';
62144

@@ -98,6 +180,8 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
98180
// appears to be working - but we still have to deal with HMR
99181
config.target('node');
100182

183+
// config.entry('globals').add('@nativescript/core/globals/index').end();
184+
101185
config
102186
.entry('bundle')
103187
// ensure we load nativescript globals first
@@ -124,16 +208,38 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
124208
.add('@nativescript/core/inspector_modules');
125209
});
126210

127-
config.output
128-
.path(outputPath)
129-
.pathinfo(false)
130-
.publicPath('')
131-
.libraryTarget('commonjs')
132-
.globalObject('global')
133-
.set('clean', true);
211+
if (env.commonjs) {
212+
// CommonJS output
213+
config.output
214+
.path(outputPath)
215+
.pathinfo(false)
216+
.publicPath('')
217+
.libraryTarget('commonjs')
218+
.globalObject('global')
219+
.set('clean', true);
220+
if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
221+
config.output.filename(`[name].${env.uniqueBundle}.js`);
222+
}
223+
} else {
224+
// ESM output
225+
config.merge({
226+
experiments: {
227+
// enable ES module syntax (import/exports)
228+
outputModule: true,
229+
},
230+
});
134231

135-
if (env?.uniqueBundle) {
136-
config.output.filename(`[name].${env.uniqueBundle}.js`);
232+
config.output
233+
.path(outputPath)
234+
.pathinfo(false)
235+
.publicPath('file:///app/')
236+
.set('module', true)
237+
.libraryTarget('module')
238+
.globalObject('global')
239+
.set('clean', true);
240+
if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
241+
config.output.filename(`[name].${env.uniqueBundle}.mjs`);
242+
}
137243
}
138244

139245
config.watchOptions({
@@ -175,16 +281,43 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
175281

176282
config.optimization.runtimeChunk('single');
177283

178-
config.optimization.splitChunks({
179-
cacheGroups: {
180-
defaultVendor: {
181-
test: /[\\/]node_modules[\\/]/,
182-
priority: -10,
183-
name: 'vendor',
184-
chunks: 'all',
284+
if (env.commonjs) {
285+
// Set up CommonJS output
286+
config.optimization.splitChunks({
287+
cacheGroups: {
288+
defaultVendor: {
289+
test: /[\\/]node_modules[\\/]/,
290+
priority: -10,
291+
name: 'vendor',
292+
chunks: 'all',
293+
},
185294
},
186-
},
187-
});
295+
});
296+
} else {
297+
// Set up ESM output
298+
config.output.chunkFilename('[name].mjs');
299+
300+
// now re‑add exactly what you want:
301+
config.optimization.splitChunks({
302+
// only split out vendor from the main bundle…
303+
chunks: 'initial',
304+
cacheGroups: {
305+
// no “default” group
306+
default: false,
307+
308+
// only pull node_modules into vendor.js from the *initial* chunk
309+
vendor: {
310+
test: /[\\/]node_modules[\\/]/,
311+
name: 'vendor',
312+
chunks: 'initial',
313+
priority: -10,
314+
reuseExistingChunk: true,
315+
},
316+
},
317+
});
318+
319+
config.optimization.set('moduleIds', 'named').set('chunkIds', 'named');
320+
}
188321

189322
// look for loaders in
190323
// - node_modules/@nativescript/webpack/dist/loaders
@@ -407,7 +540,14 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
407540
.options(postCSSOptions)
408541
.end()
409542
.use('sass-loader')
410-
.loader('sass-loader');
543+
.loader('sass-loader')
544+
.options({
545+
// helps ensure proper project compatibility
546+
// particularly in cases of workspaces
547+
// which may have different nested Sass implementations
548+
// via transient dependencies
549+
implementation: require('sass'),
550+
});
411551

412552
// config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [
413553
// /.*/,
@@ -440,7 +580,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
440580
config
441581
.plugin('ContextExclusionPlugin|Other_Platforms')
442582
.use(ContextExclusionPlugin, [
443-
new RegExp(`\\.(${otherPlatformsRE})\\.(\\w+)$`),
583+
new RegExp(`\.(${otherPlatformsRE})\.(\w+)$`),
444584
]);
445585

446586
// Filter common undesirable warnings
@@ -460,30 +600,31 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
460600
);
461601

462602
// todo: refine defaults
463-
config.plugin('DefinePlugin').use(DefinePlugin, [
464-
{
465-
__DEV__: mode === 'development',
466-
__NS_WEBPACK__: true,
467-
__NS_ENV_VERBOSE__: !!env.verbose,
468-
__NS_DEV_HOST_IPS__:
469-
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
470-
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
471-
__UI_USE_XML_PARSER__: true,
472-
__UI_USE_EXTERNAL_RENDERER__: false,
473-
__ANDROID__: platform === 'android',
474-
__IOS__: platform === 'ios',
475-
__VISIONOS__: platform === 'visionos',
476-
__APPLE__: platform === 'ios' || platform === 'visionos',
477-
/* for compat only */ 'global.isAndroid': platform === 'android',
478-
/* for compat only */ 'global.isIOS':
479-
platform === 'ios' || platform === 'visionos',
480-
/* for compat only */ 'global.isVisionOS': platform === 'visionos',
481-
process: 'global.process',
482-
483-
// todo: ?!?!
484-
// profile: '() => {}',
485-
},
486-
]);
603+
config.plugin('DefinePlugin').use(
604+
CompatDefinePlugin as any,
605+
[
606+
{
607+
__DEV__: mode === 'development',
608+
__NS_WEBPACK__: true,
609+
__NS_ENV_VERBOSE__: !!env.verbose,
610+
__NS_DEV_HOST_IPS__:
611+
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
612+
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
613+
__UI_USE_XML_PARSER__: true,
614+
__UI_USE_EXTERNAL_RENDERER__: false,
615+
__COMMONJS__: !!env.commonjs,
616+
__ANDROID__: platform === 'android',
617+
__IOS__: platform === 'ios',
618+
__VISIONOS__: platform === 'visionos',
619+
__APPLE__: platform === 'ios' || platform === 'visionos',
620+
/* for compat only */ 'global.isAndroid': platform === 'android',
621+
/* for compat only */ 'global.isIOS':
622+
platform === 'ios' || platform === 'visionos',
623+
/* for compat only */ 'global.isVisionOS': platform === 'visionos',
624+
process: 'global.process',
625+
},
626+
] as any,
627+
);
487628

488629
// enable DotEnv
489630
applyDotEnvPlugin(config);

packages/webpack5/src/configuration/typescript.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
1212
const entryPath = getEntryPath();
1313
const virtualEntryPath = path.resolve(
1414
__dirname,
15-
'../stubs/virtual-entry-typescript.js'
15+
// Note: this is possible if needed
16+
// at moment it's not but just leaving as note for futre
17+
// `../stubs/virtual-entry-typescript.${env.commonjs ? 'js' : 'mjs'}`,
18+
`../stubs/virtual-entry-typescript.js`,
1619
);
1720

1821
// exclude files starting with _ from require.context
@@ -23,7 +26,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
2326
chainedSetAddAfter(
2427
config.entry('bundle'),
2528
'@nativescript/core/globals/index',
26-
virtualEntryPath
29+
virtualEntryPath,
2730
);
2831

2932
config.when(env.hmr, (config) => {

0 commit comments

Comments
 (0)