mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(webpack): support es module bundling (#10788)
This commit is contained in:
@@ -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 re‑add 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);
|
||||
|
||||
Reference in New Issue
Block a user