feat(webpack): support es module bundling (#10788)

This commit is contained in:
Nathan Walker
2025-09-18 13:03:17 -07:00
committed by GitHub
parent d6d3800884
commit 1e54baf198
20 changed files with 806 additions and 414 deletions

View File

@@ -1,11 +1,8 @@
import { extname, relative, resolve } from 'path';
import {
ContextExclusionPlugin,
DefinePlugin,
HotModuleReplacementPlugin,
} from 'webpack';
import { ContextExclusionPlugin, HotModuleReplacementPlugin } from 'webpack';
import Config from 'webpack-chain';
import { satisfies } from 'semver';
import { isVersionGteConsideringPrerelease } from '../helpers/dependencies';
import { existsSync } from 'fs';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
@@ -13,11 +10,16 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import TerserPlugin from 'terser-webpack-plugin';
import { getProjectFilePath, getProjectTSConfigPath } from '../helpers/project';
import { getDependencyVersion, hasDependency } from '../helpers/dependencies';
import {
getDependencyVersion,
hasDependency,
getResolvedDependencyVersionForCheck,
} from '../helpers/dependencies';
import { PlatformSuffixPlugin } from '../plugins/PlatformSuffixPlugin';
import { applyFileReplacements } from '../helpers/fileReplacements';
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
import { CompatDefinePlugin } from '../plugins/CompatDefinePlugin';
import { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index';
import { getValue } from '../helpers/config';
@@ -39,6 +41,64 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// set mode
config.mode(mode);
// use source map files by default with v9+
function useSourceMapFiles() {
if (mode === 'development') {
// in development we always use source-map files with v9+ runtimes
// they are parsed and mapped to display in-flight app error screens
env.sourceMap = 'source-map';
}
}
// determine target output by @nativescript/* runtime version
// v9+ supports ESM output, anything below uses CommonJS
if (
hasDependency('@nativescript/ios') ||
hasDependency('@nativescript/visionos') ||
hasDependency('@nativescript/android')
) {
const iosVersion = getDependencyVersion('@nativescript/ios');
const visionosVersion = getDependencyVersion('@nativescript/visionos');
const androidVersion = getDependencyVersion('@nativescript/android');
if (platform === 'ios') {
const iosResolved =
getResolvedDependencyVersionForCheck('@nativescript/ios', '9.0.0') ??
iosVersion ??
undefined;
if (isVersionGteConsideringPrerelease(iosResolved, '9.0.0')) {
useSourceMapFiles();
} else {
env.commonjs = true;
}
} else if (platform === 'visionos') {
const visionosResolved =
getResolvedDependencyVersionForCheck(
'@nativescript/visionos',
'9.0.0',
) ??
visionosVersion ??
undefined;
if (isVersionGteConsideringPrerelease(visionosResolved, '9.0.0')) {
useSourceMapFiles();
} else {
env.commonjs = true;
}
} else if (platform === 'android') {
const androidResolved =
getResolvedDependencyVersionForCheck(
'@nativescript/android',
'9.0.0',
) ??
androidVersion ??
undefined;
if (isVersionGteConsideringPrerelease(androidResolved, '9.0.0')) {
useSourceMapFiles();
} else {
env.commonjs = true;
}
}
}
// config.stats({
// logging: 'verbose'
// })
@@ -57,6 +117,37 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
node: false,
});
// Mock Node.js built-ins that are not available in NativeScript runtime
// but are required by some packages like css-tree
config.resolve.merge({
fallback: {
module: require.resolve('../polyfills/module.js'),
},
alias: {
// Mock mdn-data modules that css-tree tries to load
'mdn-data/css/properties.json': require.resolve(
'../polyfills/mdn-data-properties.js',
),
'mdn-data/css/syntaxes.json': require.resolve(
'../polyfills/mdn-data-syntaxes.js',
),
'mdn-data/css/at-rules.json': require.resolve(
'../polyfills/mdn-data-at-rules.js',
),
// Ensure imports of the Node 'module' builtin resolve to our polyfill
module: require.resolve('../polyfills/module.js'),
},
// Allow extension-less ESM imports (fixes "fully specified" errors)
// Example: '../timer' -> resolves to index.<platform>.js without requiring explicit extension
fullySpecified: false,
});
// As an extra guard, ensure rule-level resolve also allows extension-less imports
config.module
.rule('esm-extensionless')
.test(/\.(mjs|js|ts|tsx)$/)
.resolve.set('fullySpecified', false);
const getSourceMapType = (map: string | boolean): Config.DevTool => {
const defaultSourceMap = 'inline-source-map';
@@ -98,6 +189,8 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// appears to be working - but we still have to deal with HMR
config.target('node');
// config.entry('globals').add('@nativescript/core/globals/index').end();
config
.entry('bundle')
// ensure we load nativescript globals first
@@ -124,16 +217,38 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
.add('@nativescript/core/inspector_modules');
});
config.output
.path(outputPath)
.pathinfo(false)
.publicPath('')
.libraryTarget('commonjs')
.globalObject('global')
.set('clean', true);
if (env.commonjs) {
// CommonJS output
config.output
.path(outputPath)
.pathinfo(false)
.publicPath('')
.libraryTarget('commonjs')
.globalObject('global')
.set('clean', true);
if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
config.output.filename(`[name].${env.uniqueBundle}.js`);
}
} else {
// ESM output
config.merge({
experiments: {
// enable ES module syntax (import/exports)
outputModule: true,
},
});
if (env?.uniqueBundle) {
config.output.filename(`[name].${env.uniqueBundle}.js`);
config.output
.path(outputPath)
.pathinfo(false)
.publicPath('file:///app/')
.set('module', true)
.libraryTarget('module')
.globalObject('global')
.set('clean', true);
if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
config.output.filename(`[name].${env.uniqueBundle}.mjs`);
}
}
config.watchOptions({
@@ -175,16 +290,43 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
config.optimization.runtimeChunk('single');
config.optimization.splitChunks({
cacheGroups: {
defaultVendor: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendor',
chunks: 'all',
if (env.commonjs) {
// Set up CommonJS output
config.optimization.splitChunks({
cacheGroups: {
defaultVendor: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendor',
chunks: 'all',
},
},
},
});
});
} else {
// Set up ESM output
config.output.chunkFilename('[name].mjs');
// now readd exactly what you want:
config.optimization.splitChunks({
// only split out vendor from the main bundle…
chunks: 'initial',
cacheGroups: {
// no “default” group
default: false,
// only pull node_modules into vendor.js from the *initial* chunk
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10,
reuseExistingChunk: true,
},
},
});
config.optimization.set('moduleIds', 'named').set('chunkIds', 'named');
}
// look for loaders in
// - node_modules/@nativescript/webpack/dist/loaders
@@ -407,7 +549,14 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
.options(postCSSOptions)
.end()
.use('sass-loader')
.loader('sass-loader');
.loader('sass-loader')
.options({
// helps ensure proper project compatibility
// particularly in cases of workspaces
// which may have different nested Sass implementations
// via transient dependencies
implementation: require('sass'),
});
// config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [
// /.*/,
@@ -440,7 +589,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
config
.plugin('ContextExclusionPlugin|Other_Platforms')
.use(ContextExclusionPlugin, [
new RegExp(`\\.(${otherPlatformsRE})\\.(\\w+)$`),
new RegExp(`\.(${otherPlatformsRE})\.(\w+)$`),
]);
// Filter common undesirable warnings
@@ -460,30 +609,31 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
);
// todo: refine defaults
config.plugin('DefinePlugin').use(DefinePlugin, [
{
__DEV__: mode === 'development',
__NS_WEBPACK__: true,
__NS_ENV_VERBOSE__: !!env.verbose,
__NS_DEV_HOST_IPS__:
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
__UI_USE_XML_PARSER__: true,
__UI_USE_EXTERNAL_RENDERER__: false,
__ANDROID__: platform === 'android',
__IOS__: platform === 'ios',
__VISIONOS__: platform === 'visionos',
__APPLE__: platform === 'ios' || platform === 'visionos',
/* for compat only */ 'global.isAndroid': platform === 'android',
/* for compat only */ 'global.isIOS':
platform === 'ios' || platform === 'visionos',
/* for compat only */ 'global.isVisionOS': platform === 'visionos',
process: 'global.process',
// todo: ?!?!
// profile: '() => {}',
},
]);
config.plugin('DefinePlugin').use(
CompatDefinePlugin as any,
[
{
__DEV__: mode === 'development',
__NS_WEBPACK__: true,
__NS_ENV_VERBOSE__: !!env.verbose,
__NS_DEV_HOST_IPS__:
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
__UI_USE_XML_PARSER__: true,
__UI_USE_EXTERNAL_RENDERER__: false,
__COMMONJS__: !!env.commonjs,
__ANDROID__: platform === 'android',
__IOS__: platform === 'ios',
__VISIONOS__: platform === 'visionos',
__APPLE__: platform === 'ios' || platform === 'visionos',
/* for compat only */ 'global.isAndroid': platform === 'android',
/* for compat only */ 'global.isIOS':
platform === 'ios' || platform === 'visionos',
/* for compat only */ 'global.isVisionOS': platform === 'visionos',
process: 'global.process',
},
] as any,
);
// enable DotEnv
applyDotEnvPlugin(config);