Files

638 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { extname, relative, resolve } from 'path';
import {
ContextExclusionPlugin,
DefinePlugin,
HotModuleReplacementPlugin,
BannerPlugin,
} from 'webpack';
import Config from 'webpack-chain';
import { satisfies } from 'semver';
import { existsSync } from 'fs';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import TerserPlugin from 'terser-webpack-plugin';
import { getProjectFilePath, getProjectTSConfigPath } from '../helpers/project';
import {
getAllDependencies,
getDependencyVersion,
hasDependency,
} 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 { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index';
import { getValue } from '../helpers/config';
import { getIPS } from '../helpers/host';
import {
getAvailablePlatforms,
getAbsoluteDistPath,
getPlatformName,
getEntryDirPath,
getEntryPath,
} from '../helpers/platform';
export default function (config: Config, env: IWebpackEnv = _env): Config {
const entryPath = getEntryPath();
const platform = getPlatformName();
const outputPath = getAbsoluteDistPath();
const mode = env.production ? 'production' : 'development';
// set mode
config.mode(mode);
// use source map files 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/core version
// v9+ supports ESM output, anything below uses CommonJS
if (hasDependency('@nativescript/core')) {
const coreVersion = getDependencyVersion('@nativescript/core');
// ensure alpha/beta/rc versions are considered as well
if (coreVersion && !coreVersion.includes('9.0.0')) {
if (!satisfies(coreVersion, '>=9.0.0')) {
// @nativescript/core < 9 uses CommonJS output
env.commonjs = true;
} else {
useSourceMapFiles();
}
} else {
useSourceMapFiles();
}
}
// config.stats({
// logging: 'verbose'
// })
// package.json is generated by the CLI with runtime options
// this ensures it's not included in the bundle, but rather
// resolved at runtime
config.externals(['package.json', '~/package.json']);
// disable marking built-in node modules as external
// since they are not available at runtime and
// should be bundled (requires polyfills)
// for example `npm i --save url` to
// polyfill the node url module.
config.set('externalsPresets', {
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',
),
},
});
const getSourceMapType = (map: string | boolean): Config.DevTool => {
const defaultSourceMap = 'inline-source-map';
if (typeof map === 'undefined') {
// source-maps disabled in production by default
// enabled with --env.sourceMap=<type>
if (mode === 'production') {
// todo: we may set up SourceMapDevToolPlugin to generate external maps in production
return false;
}
return defaultSourceMap;
}
// when --env.sourceMap=true is passed, use default
if (typeof map === 'boolean' && map) {
return defaultSourceMap;
}
// pass any type of sourceMap with --env.sourceMap=<type>
return map as Config.DevTool;
};
config.devtool(getSourceMapType(env.sourceMap));
// when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder
if (env.sourceMap === 'hidden-source-map') {
const sourceMapAbsolutePath = getProjectFilePath(
`./${
env.buildPath ?? 'platforms'
}/${platform}-sourceMaps/[file].map[query]`,
);
const sourceMapRelativePath = relative(outputPath, sourceMapAbsolutePath);
config.output.sourceMapFilename(sourceMapRelativePath);
}
// todo: figure out easiest way to make "node" target work in ns
// rather than the custom ns target implementation that's hard to maintain
// 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
.add('@nativescript/core/globals/index')
.add('@nativescript/core/bundle-entry-points')
.add(entryPath);
// Add android app components to the bundle to SBG can generate the java classes
if (platform === 'android') {
const appComponents = Array.isArray(env.appComponents)
? env.appComponents
: (env.appComponents && [env.appComponents]) || [];
appComponents.push('@nativescript/core/ui/frame');
appComponents.push('@nativescript/core/ui/frame/activity');
appComponents.map((component) => {
config.entry('bundle').add(component);
});
}
// inspector_modules
config.when(shouldIncludeInspectorModules(), (config) => {
config
.entry('tns_modules/inspector_modules')
.add('@nativescript/core/inspector_modules');
});
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,
},
});
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({
ignored: [
`${getProjectFilePath(env.buildPath ?? 'platforms')}/**`,
`${getProjectFilePath(env.appResourcesPath ?? 'App_Resources')}/**`,
],
});
// allow watching node_modules
config.when(env.watchNodeModules, (config) => {
config.set('snapshot', {
managedPaths: [],
});
});
// Set up Terser options
config.optimization.minimizer('TerserPlugin').use(TerserPlugin, [
{
terserOptions: {
// @ts-ignore - https://github.com/webpack-contrib/terser-webpack-plugin/pull/463 broke the types?
compress: {
collapse_vars: platform !== 'android',
sequences: platform !== 'android',
keep_infinity: true,
drop_console: mode === 'production',
global_defs: {
__UGLIFIED__: true,
},
},
keep_fnames: true,
keep_classnames: true,
format: {
keep_quoted_props: true,
},
},
},
]);
config.optimization.runtimeChunk('single');
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
// NOTE: this fixes all worker bundling issues
// however it causes issues with angular lazy loading.
// TODO: still need to investigate the right combination of webpack settings there
// TODO: test if standalone lazy loaded routes work, maybe it's just with loadChildren modules?
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
// - node_modules/@nativescript/webpack/node_modules
// - node_modules
// allows for cleaner rules, without having to specify full paths to loaders
config.resolveLoader.modules
.add(resolve(__dirname, '../loaders'))
.add(resolve(__dirname, '../../node_modules'))
.add(getProjectFilePath('node_modules'))
.add('node_modules');
config.resolve.extensions
.add(`.${platform}.ts`)
.add('.ts')
.add(`.${platform}.js`)
.add('.js')
.add(`.${platform}.mjs`)
.add('.mjs')
.add(`.${platform}.css`)
.add('.css')
.add(`.${platform}.scss`)
.add('.scss')
.add(`.${platform}.json`)
.add('.json');
if (platform === 'visionos') {
// visionOS allows for both .ios and .visionos extensions
const extensions = config.resolve.extensions.values();
const newExtensions = [];
extensions.forEach((ext) => {
newExtensions.push(ext);
if (ext.includes('visionos')) {
newExtensions.push(ext.replace('visionos', 'ios'));
}
});
config.resolve.extensions.clear().merge(newExtensions);
}
// base aliases
config.resolve.alias.set('~', getEntryDirPath()).set('@', getEntryDirPath());
// resolve symlinks
config.resolve.symlinks(true);
// resolve modules in project node_modules first
// then fall-back to default node resolution (up the parent folder chain)
config.resolve.modules
.add(getProjectFilePath('node_modules'))
.add('node_modules');
config.module
.rule('bundle')
.enforce('post')
.test(entryPath)
.use('app-css-loader')
.loader('app-css-loader')
.options({
// TODO: allow both visionos and ios to resolve for css
// only resolve .ios css on visionOS for now
// platform: platform === 'visionos' ? 'ios' : platform,
platform,
})
.end();
config.when(env.hmr, (config) => {
config.module
.rule('bundle')
.use('nativescript-hot-loader')
.loader('nativescript-hot-loader')
.options({
injectHMRRuntime: true,
});
});
// enable profiling with --env.profile
config.when(env.profile, (config) => {
config.profile(true);
});
// worker-loader should be declared before ts-loader
config.module
.rule('workers')
.test(/\.(mjs|js|ts)$/)
.use('nativescript-worker-loader')
.loader('nativescript-worker-loader');
const tsConfigPath = getProjectTSConfigPath();
const configFile = tsConfigPath
? {
configFile: tsConfigPath,
}
: undefined;
// set up ts support
config.module
.rule('ts')
.test([/\.ts$/])
.use('ts-loader')
.loader('ts-loader')
.options({
// todo: perhaps we can provide a default tsconfig
// and use that if the project doesn't have one?
...configFile,
transpileOnly: true,
allowTsInNodeModules: true,
compilerOptions: {
sourceMap: true,
declaration: false,
},
getCustomTransformers() {
return {
before: [require('../transformers/NativeClass').default],
};
},
});
// Use Fork TS Checker to do type checking in a separate non-blocking process
config.when(hasDependency('typescript'), (config) => {
config
.plugin('ForkTsCheckerWebpackPlugin')
.use(ForkTsCheckerWebpackPlugin, [
{
async: !!env.watch,
typescript: {
memoryLimit: 4096,
...configFile,
},
},
]);
});
// set up js
config.module
.rule('js')
.test(/\.js$/)
.exclude.add(/node_modules/)
.end();
// config.resolve.extensions.add('.xml');
// set up xml
config.module
.rule('xml')
.test(/\.xml$/)
.use('xml-namespace-loader')
.loader('xml-namespace-loader');
// default PostCSS options to use
// projects can change settings
// via postcss.config.js
const postCSSOptions = {
postcssOptions: {
plugins: [
// inlines @imported stylesheets
[
'postcss-import',
{
// custom resolver to resolve platform extensions in @import statements
// ie. @import "foo.css" would import "foo.ios.css" if the platform is ios and it exists
resolve(id, baseDir, importOptions) {
const extensions =
platform === 'visionos' ? [platform, 'ios'] : [platform];
for (const platformTarget of extensions) {
const ext = extname(id);
const platformExt = ext ? `.${platformTarget}${ext}` : '';
if (!id.includes(platformExt)) {
const platformRequest = id.replace(ext, platformExt);
const extPath = resolve(baseDir, platformRequest);
try {
return require.resolve(platformRequest, {
paths: [baseDir],
});
} catch {}
if (existsSync(extPath)) {
console.log(`resolving "${id}" to "${platformRequest}"`);
return extPath;
}
}
}
// fallback to postcss-import default resolution
return id;
},
},
],
],
},
};
// set up css
config.module
.rule('css')
.test(/\.css$/)
.use('apply-css-loader')
.loader('apply-css-loader')
.end()
.use('css2json-loader')
.loader('css2json-loader')
.end()
.use('postcss-loader')
.loader('postcss-loader')
.options(postCSSOptions);
// set up scss
config.module
.rule('scss')
.test(/\.scss$/)
.use('apply-css-loader')
.loader('apply-css-loader')
.end()
.use('css2json-loader')
.loader('css2json-loader')
.end()
.use('postcss-loader')
.loader('postcss-loader')
.options(postCSSOptions)
.end()
.use('sass-loader')
.loader('sass-loader');
// config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [
// /.*/,
// request => {
// if (new RegExp(`\.${platform}\..+$`).test(request.request)) {
// request.rawRequest = request.rawRequest.replace(`.${platform}.`, '.')
// console.log(request)
// }
// }
// ])
config.plugin('PlatformSuffixPlugin').use(PlatformSuffixPlugin, [
{
extensions: platform === 'visionos' ? [platform, 'ios'] : [platform],
},
]);
// Makes sure that require.context will never include
// App_Resources, regardless where they are located.
config
.plugin('ContextExclusionPlugin|App_Resources')
.use(ContextExclusionPlugin, [new RegExp(`(.*)App_Resources(.*)`)]);
// Makes sure that require.context will never include code from
// another platform (ie .android.ts when building for ios)
const otherPlatformsRE = getAvailablePlatforms()
.filter((platform) => platform !== getPlatformName())
.join('|');
config
.plugin('ContextExclusionPlugin|Other_Platforms')
.use(ContextExclusionPlugin, [
new RegExp(`\\.(${otherPlatformsRE})\\.(\\w+)$`),
]);
// Filter common undesirable warnings
config.set(
'ignoreWarnings',
(config.get('ignoreWarnings') ?? []).concat([
/**
* This rule hides
* +-----------------------------------------------------------------------------------------+
* | WARNING in ./node_modules/@angular/core/fesm2015/core.js 29714:15-102 |
* | System.import() is deprecated and will be removed soon. Use import() instead. |
* | For more info visit https://webpack.js.org/guides/code-splitting/ |
* +-----------------------------------------------------------------------------------------+
*/
/System.import\(\) is deprecated/,
]),
);
// 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,
__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',
// todo: ?!?!
// profile: '() => {}',
},
]);
// enable DotEnv
applyDotEnvPlugin(config);
// replacements
applyFileReplacements(config);
// set up default copy rules
addCopyRule('assets/**');
addCopyRule('fonts/**');
addCopyRule('**/*.+(jpg|png)');
applyCopyRules(config);
config.plugin('WatchStatePlugin').use(WatchStatePlugin);
config.when(env.hmr, (config) => {
config.plugin('HotModuleReplacementPlugin').use(HotModuleReplacementPlugin);
});
config.when(env.report, (config) => {
config.plugin('BundleAnalyzerPlugin').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static',
generateStatsFile: true,
openAnalyzer: false,
reportFilename: getProjectFilePath('report/report.html'),
statsFilename: getProjectFilePath('report/stats.json'),
},
]);
});
return config;
}
function shouldIncludeInspectorModules(): boolean {
const platform = getPlatformName();
const coreVersion = getDependencyVersion('@nativescript/core');
if (coreVersion && satisfies(coreVersion, '>=8.7.0')) {
return platform === 'ios' || platform === 'android';
}
return platform === 'ios';
}