mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-01 18:26:10 +08:00
feat(webpack): support es module bundling (#10788)
This commit is contained in:
@ -23,8 +23,8 @@ export function test_global_registerModule() {
|
||||
TKUnit.assert(typeof global.registerModule === 'function', 'global.registerModule not a function');
|
||||
}
|
||||
|
||||
export function test_global_registerWebpackModules() {
|
||||
TKUnit.assert(typeof global.registerWebpackModules === 'function', 'global.registerWebpackModules not a function');
|
||||
export function test_global_registerBundlerModules() {
|
||||
TKUnit.assert(typeof global.registerBundlerModules === 'function', 'global.registerBundlerModules not a function');
|
||||
}
|
||||
|
||||
export function test_global_loadModule() {
|
||||
|
||||
12
packages/core/global-types.d.ts
vendored
12
packages/core/global-types.d.ts
vendored
@ -53,11 +53,11 @@ declare module globalThis {
|
||||
|
||||
function registerModule(name: string, loader: (name: string) => any): void;
|
||||
/**
|
||||
* Register all modules from a webpack context.
|
||||
* The context is one created using the following webpack utility:
|
||||
* Register all modules from a bundler context.
|
||||
* For example, the context could be one created using the following webpack utility:
|
||||
* https://webpack.js.org/guides/dependency-management/#requirecontext
|
||||
*
|
||||
* The extension map is optional, modules in the webpack context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
|
||||
* The extension map is optional, modules in the bundler context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
|
||||
* while the built-in module builders in {N} will look for ".js", ".css" or ".xml" files. Adding a map such as:
|
||||
* ```
|
||||
* { ".ts": ".js" }
|
||||
@ -65,7 +65,7 @@ declare module globalThis {
|
||||
* Will resolve lookups for .js to the .ts file.
|
||||
* By default scss and ts files are mapped.
|
||||
*/
|
||||
function registerWebpackModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
|
||||
function registerBundlerModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
|
||||
|
||||
/**
|
||||
* The NativeScript XML builder, style-scope, application modules use various resources such as:
|
||||
@ -91,7 +91,7 @@ declare module globalThis {
|
||||
function loadModule(name: string, loadForUI?: boolean): any;
|
||||
|
||||
/**
|
||||
* Checks if the module has been registered with `registerModule` or in `registerWebpackModules`
|
||||
* Checks if the module has been registered with `registerModule` or in `registerBundlerModules`
|
||||
* @param name Name of the module
|
||||
*/
|
||||
function moduleExists(name: string): boolean;
|
||||
@ -100,8 +100,6 @@ declare module globalThis {
|
||||
|
||||
function _unregisterModule(name: string): void;
|
||||
|
||||
function _isModuleLoadedForUI(moduleName: string): boolean;
|
||||
|
||||
var onGlobalLayoutListener: any;
|
||||
function zonedCallback<T = Function>(callback: T): T;
|
||||
var Reflect: any;
|
||||
|
||||
@ -170,15 +170,20 @@ export function initGlobal() {
|
||||
modules.delete(name);
|
||||
};
|
||||
|
||||
global._isModuleLoadedForUI = function _isModuleLoadedForUI(moduleName: string): boolean {
|
||||
return modulesLoadedForUI.has(moduleName);
|
||||
};
|
||||
global.registerBundlerModules = function registerBundlerModules(context: Context, extensionMap: ExtensionMap = {}) {
|
||||
const registerWithName = (nickName: string, moduleId: string) => {
|
||||
modules.set(nickName, {
|
||||
moduleId,
|
||||
loader: () => {
|
||||
return context(moduleId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
global.registerWebpackModules = function registerWebpackModules(context: Context, extensionMap: ExtensionMap = {}) {
|
||||
context.keys().forEach((moduleId) => {
|
||||
const registerModuleById = (moduleId: string) => {
|
||||
const extDotIndex = moduleId.lastIndexOf('.');
|
||||
const base = moduleId.substr(0, extDotIndex);
|
||||
const originalExt = moduleId.substr(extDotIndex);
|
||||
const base = moduleId.substring(0, extDotIndex);
|
||||
const originalExt = moduleId.substring(extDotIndex);
|
||||
const registerExt = extensionMap[originalExt] || defaultExtensionMap[originalExt] || originalExt;
|
||||
|
||||
// We prefer source files for webpack scenarios before compilation leftovers,
|
||||
@ -187,47 +192,40 @@ export function initGlobal() {
|
||||
const isSourceFile = originalExt !== registerExt;
|
||||
const registerName = base + registerExt;
|
||||
|
||||
const registerWithName = (nickName: string) => {
|
||||
modules.set(nickName, {
|
||||
moduleId,
|
||||
loader: () => {
|
||||
return context(moduleId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (registerName.startsWith('./') && registerName.endsWith('.js')) {
|
||||
const jsNickNames = [
|
||||
// This is extremely short version like "main-page" that was promoted to be used with global.registerModule("module-name", loaderFunc);
|
||||
registerName.substr(2, registerName.length - 5),
|
||||
registerName.substring(2, registerName.length - 3),
|
||||
// This is for supporting module names like "./main/main-page"
|
||||
registerName.substr(0, registerName.length - 3),
|
||||
registerName.substring(0, registerName.length - 3),
|
||||
// This is for supporting module names like "main/main-page.js"
|
||||
registerName.substr(2),
|
||||
registerName.substring(2),
|
||||
];
|
||||
|
||||
jsNickNames.forEach((jsNickName) => {
|
||||
if (isSourceFile || !global.moduleExists(jsNickName)) {
|
||||
registerWithName(jsNickName);
|
||||
registerWithName(jsNickName, moduleId);
|
||||
}
|
||||
});
|
||||
} else if (registerName.startsWith('./')) {
|
||||
const moduleNickNames = [
|
||||
// This is for supporting module names like "main/main-page.xml"
|
||||
registerName.substr(2),
|
||||
registerName.substring(2),
|
||||
];
|
||||
|
||||
moduleNickNames.forEach((moduleNickName) => {
|
||||
if (!global.moduleExists(moduleNickName)) {
|
||||
registerWithName(moduleNickName);
|
||||
registerWithName(moduleNickName, moduleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isSourceFile || !global.moduleExists(registerName)) {
|
||||
registerWithName(registerName);
|
||||
registerWithName(registerName, moduleId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
context.keys().forEach(registerModuleById);
|
||||
};
|
||||
|
||||
global.moduleExists = function moduleExists(name: string): boolean {
|
||||
|
||||
515
packages/webpack5/package-lock.json
generated
515
packages/webpack5/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nativescript/webpack",
|
||||
"version": "5.0.24",
|
||||
"version": "5.0.25-alpha.1",
|
||||
"private": false,
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
"assets": [
|
||||
{
|
||||
"input": "{projectRoot}/src/stubs",
|
||||
"glob": "*.js",
|
||||
"glob": "*.{js,mjs}",
|
||||
"output": "stubs"
|
||||
},
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -12,7 +12,10 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
|
||||
const entryPath = getEntryPath();
|
||||
const virtualEntryPath = path.resolve(
|
||||
__dirname,
|
||||
'../stubs/virtual-entry-typescript.js'
|
||||
// Note: this is possible if needed
|
||||
// at moment it's not but just leaving as note for futre
|
||||
// `../stubs/virtual-entry-typescript.${env.commonjs ? 'js' : 'mjs'}`,
|
||||
`../stubs/virtual-entry-typescript.js`,
|
||||
);
|
||||
|
||||
// exclude files starting with _ from require.context
|
||||
@ -23,7 +26,7 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
|
||||
chainedSetAddAfter(
|
||||
config.entry('bundle'),
|
||||
'@nativescript/core/globals/index',
|
||||
virtualEntryPath
|
||||
virtualEntryPath,
|
||||
);
|
||||
|
||||
config.when(env.hmr, (config) => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
import { satisfies } from 'semver';
|
||||
import { getPackageJson, getProjectRootPath } from './project';
|
||||
|
||||
// todo: memoize
|
||||
@ -67,3 +68,77 @@ export function getDependencyVersion(dependencyName: string): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a usable version string for checks (eg. semver.satisfies).
|
||||
* Strategy:
|
||||
* - prefer installed package.json version (getDependencyVersion)
|
||||
* - fall back to declared version in project package.json (dependencies/devDependencies)
|
||||
* - if declared is a common dist-tag (alpha|beta|rc|next) return a 9.x prerelease
|
||||
*/
|
||||
export function getResolvedDependencyVersionForCheck(
|
||||
dependencyName: string,
|
||||
target: string,
|
||||
): string | null {
|
||||
// try installed
|
||||
const installed = getDependencyVersion(dependencyName);
|
||||
if (installed) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
// try declared in project package.json
|
||||
const pkg = getPackageJson();
|
||||
const declared =
|
||||
(pkg.dependencies && pkg.dependencies[dependencyName]) ||
|
||||
(pkg.devDependencies && pkg.devDependencies[dependencyName]);
|
||||
if (!declared) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if declared already satisfies semver check, use it
|
||||
// Note: declared may be a dist-tag like 'alpha' or a range. We only treat
|
||||
// common tags as prereleases of target. Avoid trying to interpret arbitrary
|
||||
// ranges here.
|
||||
|
||||
// common dist-tags -> treat as prerelease of 9.x for the purpose of >=9 checks
|
||||
if (/^(alpha|beta|rc|next)$/.test(String(declared))) {
|
||||
return `${target}-0`;
|
||||
}
|
||||
|
||||
return declared ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric comparison that treats prerelease versions as being at the same
|
||||
* numeric level as their base version. e.g. 9.0.0-alpha.2 >= 9.0.0
|
||||
*/
|
||||
export function isVersionGteConsideringPrerelease(
|
||||
version: string | null | undefined,
|
||||
target: string,
|
||||
): boolean {
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const v = require('semver').parse(String(version));
|
||||
const t = require('semver').parse(String(target));
|
||||
if (!v || !t) {
|
||||
// fallback to semver.satisfies with a prerelease-aware lower bound
|
||||
return require('semver').satisfies(String(version), `>=${target}-0`);
|
||||
}
|
||||
|
||||
if (v.major > t.major) return true;
|
||||
if (v.major < t.major) return false;
|
||||
if (v.minor > t.minor) return true;
|
||||
if (v.minor < t.minor) return false;
|
||||
if (v.patch >= t.patch) return true;
|
||||
return false;
|
||||
} catch (e) {
|
||||
try {
|
||||
return require('semver').satisfies(String(version), `>=${target}-0`);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,9 @@ export interface IWebpackEnv {
|
||||
// print webpack stats (default: true)
|
||||
stats?: boolean;
|
||||
|
||||
// enable commonjs modules (default: ES modules, esm)
|
||||
commonjs?: boolean;
|
||||
|
||||
// misc
|
||||
replace?: string[] | string;
|
||||
watchNodeModules?: boolean;
|
||||
@ -158,10 +161,9 @@ export function chainWebpack(
|
||||
* @param mergeFn An object or a function that optionally returns an object (can mutate the object directly and return nothing)
|
||||
*/
|
||||
export function mergeWebpack(
|
||||
mergeFn: ((
|
||||
config: Partial<webpack.Configuration>,
|
||||
env: IWebpackEnv,
|
||||
) => any) | Partial<webpack.Configuration>,
|
||||
mergeFn:
|
||||
| ((config: Partial<webpack.Configuration>, env: IWebpackEnv) => any)
|
||||
| Partial<webpack.Configuration>,
|
||||
) {
|
||||
webpackMerges.push(mergeFn);
|
||||
}
|
||||
|
||||
@ -24,15 +24,10 @@ export default function loader(content: string, map: any) {
|
||||
return this.callback(null, `${content}\n${hmrRuntime}`, map);
|
||||
}
|
||||
|
||||
const relativePath = relative(
|
||||
opts.appPath ?? this.rootContext,
|
||||
this.resourcePath
|
||||
).replace(/\\/g, '/');
|
||||
|
||||
const hmrCode = this.hot
|
||||
? dedent`
|
||||
/* NATIVESCRIPT-HOT-LOADER */
|
||||
if(module.hot && global._isModuleLoadedForUI && global._isModuleLoadedForUI("./${relativePath}")) {
|
||||
if(module.hot?.accept) {
|
||||
module.hot.accept()
|
||||
}
|
||||
`
|
||||
|
||||
14
packages/webpack5/src/plugins/CompatDefinePlugin.ts
Normal file
14
packages/webpack5/src/plugins/CompatDefinePlugin.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export class CompatDefinePlugin {
|
||||
private readonly definitions: Record<string, any>;
|
||||
|
||||
constructor(definitions: Record<string, any>) {
|
||||
this.definitions = definitions || {};
|
||||
}
|
||||
|
||||
apply(compiler: any) {
|
||||
// Use the same webpack instance as the compiler to avoid version mismatches
|
||||
const wp = compiler?.webpack || require('webpack');
|
||||
const DefinePlugin = wp.DefinePlugin || require('webpack').DefinePlugin;
|
||||
new DefinePlugin(this.definitions).apply(compiler);
|
||||
}
|
||||
}
|
||||
@ -37,22 +37,28 @@ export class PlatformSuffixPlugin {
|
||||
// require.context
|
||||
compiler.hooks.contextModuleFactory.tap(id, (cmf) => {
|
||||
// @ts-ignore
|
||||
cmf.hooks.alternativeRequests.tap(id, (modules, options) => {
|
||||
const additionalModules = [];
|
||||
// we are looking for modules that are platform specific (something.<platform>.ext)
|
||||
// and we are duplicating them without the platform suffix
|
||||
// this allows using require.context with non-existent platformless filenames
|
||||
// but mapped to the platform specific variant (done in the resolver hook below)
|
||||
for (const module of modules) {
|
||||
if (platformRE.test(module.request)) {
|
||||
additionalModules.push({
|
||||
...module,
|
||||
request: module.request.replace(platformRE, '.'),
|
||||
});
|
||||
const altHook = (cmf as any).hooks?.alternativeRequests;
|
||||
if (altHook && typeof altHook.tap === 'function') {
|
||||
altHook.tap(id, (modules: any[], options: any) => {
|
||||
const additionalModules = [];
|
||||
// we are looking for modules that are platform specific (something.<platform>.ext)
|
||||
// and we are duplicating them without the platform suffix
|
||||
// this allows using require.context with non-existent platformless filenames
|
||||
// but mapped to the platform specific variant (done in the resolver hook below)
|
||||
for (const module of modules) {
|
||||
if (platformRE.test(module.request)) {
|
||||
additionalModules.push({
|
||||
...module,
|
||||
request: module.request.replace(platformRE, '.'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
modules.push(...additionalModules);
|
||||
});
|
||||
modules.push(...additionalModules);
|
||||
});
|
||||
} else {
|
||||
// Hook may be absent on some webpack versions; skip gracefully
|
||||
// console.log(`[${id}] alternativeRequests hook not available; skipping.`)
|
||||
}
|
||||
});
|
||||
|
||||
compiler.resolverFactory.hooks.resolver
|
||||
@ -73,57 +79,68 @@ export class PlatformSuffixPlugin {
|
||||
// });
|
||||
// })
|
||||
|
||||
resolver.hooks.normalResolve.tapAsync(
|
||||
id,
|
||||
(request_, resolveContext, callback) => {
|
||||
for (const platform of this.extensions) {
|
||||
const { path, request } = request_;
|
||||
const ext = request && extname(request);
|
||||
const platformExt = ext ? `.${platform}${ext}` : '';
|
||||
const normalResolveHook = (resolver as any).hooks?.normalResolve;
|
||||
const ensureHook = (name: string) => {
|
||||
return typeof (resolver as any).ensureHook === 'function'
|
||||
? (resolver as any).ensureHook(name)
|
||||
: (resolver as any).hooks?.[name];
|
||||
};
|
||||
|
||||
if (path && request && ext && !request.includes(platformExt)) {
|
||||
const platformRequest = request.replace(ext, platformExt);
|
||||
const extPath = resolve(path, platformRequest);
|
||||
if (
|
||||
!normalResolveHook ||
|
||||
typeof normalResolveHook.tapAsync !== 'function'
|
||||
) {
|
||||
// Missing or incompatible hook; skip to avoid crashes
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log({
|
||||
// path,
|
||||
// request,
|
||||
// ext,
|
||||
// extPath
|
||||
// })
|
||||
normalResolveHook.tapAsync(id, (request_, resolveContext, callback) => {
|
||||
for (const platform of this.extensions) {
|
||||
const { path, request } = request_;
|
||||
const ext = request && extname(request);
|
||||
const platformExt = ext ? `.${platform}${ext}` : '';
|
||||
|
||||
// if a file with the same + a platform suffix exists
|
||||
// we want to resolve that file instead
|
||||
if (existsSync(extPath)) {
|
||||
const message = `resolving "${request}" to "${platformRequest}"`;
|
||||
const hook = resolver.ensureHook('normalResolve');
|
||||
console.log(message);
|
||||
if (path && request && ext && !request.includes(platformExt)) {
|
||||
const platformRequest = request.replace(ext, platformExt);
|
||||
const extPath = resolve(path, platformRequest);
|
||||
|
||||
// here we are creating a new resolve object and replacing the path
|
||||
// with the .<platform>.<ext> suffix
|
||||
const obj = {
|
||||
...request_,
|
||||
path: resolver.join(path, platformRequest),
|
||||
relativePath:
|
||||
request_.relativePath &&
|
||||
resolver.join(request_.relativePath, platformRequest),
|
||||
request: undefined,
|
||||
};
|
||||
// console.log({
|
||||
// path,
|
||||
// request,
|
||||
// ext,
|
||||
// extPath
|
||||
// })
|
||||
|
||||
// we call to the actual resolver to do the resolving of this new file
|
||||
return resolver.doResolve(
|
||||
hook,
|
||||
obj,
|
||||
message,
|
||||
resolveContext,
|
||||
callback
|
||||
);
|
||||
}
|
||||
// if a file with the same + a platform suffix exists
|
||||
// we want to resolve that file instead
|
||||
if (existsSync(extPath)) {
|
||||
const message = `resolving "${request}" to "${platformRequest}"`;
|
||||
const hook = ensureHook('normalResolve');
|
||||
|
||||
// here we are creating a new resolve object and replacing the path
|
||||
// with the .<platform>.<ext> suffix
|
||||
const obj = {
|
||||
...request_,
|
||||
path: resolver.join(path, platformRequest),
|
||||
relativePath:
|
||||
request_.relativePath &&
|
||||
resolver.join(request_.relativePath, platformRequest),
|
||||
request: undefined,
|
||||
};
|
||||
|
||||
// we call to the actual resolver to do the resolving of this new file
|
||||
return (resolver as any).doResolve(
|
||||
hook as any,
|
||||
obj,
|
||||
message,
|
||||
resolveContext,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
);
|
||||
callback();
|
||||
});
|
||||
// resolver.hooks.rawFile.tap(id, (request, resolveContext, callback) => {
|
||||
// if(request.path && !/\.ios\..+$/.test(request.path)) {
|
||||
// const { ext } = parse(request.path)
|
||||
|
||||
8
packages/webpack5/src/polyfills/mdn-data-at-rules.ts
Normal file
8
packages/webpack5/src/polyfills/mdn-data-at-rules.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Mock for mdn-data/css/at-rules.json
|
||||
* Returns empty object since css-tree has its own comprehensive data
|
||||
* This prevents css-tree from failing when trying to patch its data
|
||||
*/
|
||||
|
||||
// Return empty object - css-tree will use its built-in data instead
|
||||
export = {};
|
||||
8
packages/webpack5/src/polyfills/mdn-data-properties.ts
Normal file
8
packages/webpack5/src/polyfills/mdn-data-properties.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Mock for mdn-data/css/properties.json
|
||||
* Returns empty object since css-tree has its own comprehensive data
|
||||
* This prevents css-tree from failing when trying to patch its data
|
||||
*/
|
||||
|
||||
// Return empty object - css-tree will use its built-in data instead
|
||||
export = {};
|
||||
8
packages/webpack5/src/polyfills/mdn-data-syntaxes.ts
Normal file
8
packages/webpack5/src/polyfills/mdn-data-syntaxes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Mock for mdn-data/css/syntaxes.json
|
||||
* Returns empty object since css-tree has its own comprehensive data
|
||||
* This prevents css-tree from failing when trying to patch its data
|
||||
*/
|
||||
|
||||
// Return empty object - css-tree will use its built-in data instead
|
||||
export = {};
|
||||
47
packages/webpack5/src/polyfills/module.ts
Normal file
47
packages/webpack5/src/polyfills/module.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Polyfill for Node.js 'module' built-in
|
||||
* Provides minimal implementation for NativeScript environment
|
||||
*/
|
||||
|
||||
// Mock createRequire function that css-tree uses
|
||||
function createRequire(filename: string) {
|
||||
// Return a mock require function
|
||||
return function mockRequire(id: string) {
|
||||
// Handle css-tree's internal patch.json file
|
||||
if (id.includes('../data/patch.json') || id.includes('patch.json')) {
|
||||
// Return css-tree's patch structure
|
||||
return {
|
||||
atrules: {},
|
||||
properties: {},
|
||||
types: {},
|
||||
};
|
||||
}
|
||||
|
||||
// For mdn-data files, return empty objects
|
||||
if (id.includes('mdn-data')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// For any other requires, return empty object
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
// CommonJS export
|
||||
module.exports = {
|
||||
createRequire: createRequire,
|
||||
};
|
||||
|
||||
// Provide a named export for ESM consumers: `import { createRequire } from 'module'`
|
||||
try {
|
||||
Object.defineProperty(module.exports, 'createRequire', {
|
||||
enumerable: true,
|
||||
value: createRequire,
|
||||
});
|
||||
// Also export under an __esModule flag for certain bundlers
|
||||
Object.defineProperty(module.exports, '__esModule', {
|
||||
value: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore in environments where defineProperty is unavailable
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
// VIRTUAL ENTRY START
|
||||
require('@nativescript/core/bundle-entry-points')
|
||||
const context = require.context("~/", /* deep: */ true, /* filter: */ /.(xml|js|s?css)$/);
|
||||
global.registerWebpackModules(context);
|
||||
global.registerBundlerModules(context);
|
||||
// VIRTUAL ENTRY END
|
||||
@ -1,5 +1,5 @@
|
||||
// VIRTUAL ENTRY START
|
||||
require('@nativescript/core/bundle-entry-points')
|
||||
const context = require.context("~/", /* deep: */ true, /* filter: */ /\.(xml|js|(?<!\.d\.)ts|s?css)$/);
|
||||
global.registerWebpackModules(context);
|
||||
global.registerBundlerModules(context);
|
||||
// VIRTUAL ENTRY END
|
||||
68
packages/webpack5/src/stubs/virtual-entry-typescript.mjs
Normal file
68
packages/webpack5/src/stubs/virtual-entry-typescript.mjs
Normal file
@ -0,0 +1,68 @@
|
||||
// VIRTUAL ENTRY START
|
||||
require('@nativescript/core/bundle-entry-points')
|
||||
|
||||
import { readdirSync } from 'fs';
|
||||
import { join, extname, relative } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
if (typeof import.meta.glob !== 'undefined') {
|
||||
// Vite environment
|
||||
const modules = import.meta.glob(
|
||||
// adjust the pattern to your layout:
|
||||
'~/**/*.@(xml|js|ts|scss|css)',
|
||||
{ eager: true }, // uncomment to import immediately
|
||||
);
|
||||
global.registerBundlerModules(modules);
|
||||
} else {
|
||||
const require = createRequire(import.meta.url);
|
||||
const root = fileURLToPath(new URL('./src', import.meta.url));
|
||||
|
||||
/**
|
||||
* Recursively walk `dir`, collecting all files whose extension is in `exts`,
|
||||
* ignoring any `*.d.ts` files.
|
||||
*/
|
||||
function collectFilesSync(
|
||||
dir,
|
||||
exts = ['.xml', '.js', '.ts', '.css', '.scss'],
|
||||
) {
|
||||
let results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results = results.concat(collectFilesSync(full, exts));
|
||||
} else {
|
||||
// skip declaration files
|
||||
if (entry.name.endsWith('.d.ts')) continue;
|
||||
// filter by extension
|
||||
if (exts.includes(extname(entry.name))) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously load every matching module under `./src`,
|
||||
* keyed by its path relative to `root`.
|
||||
*/
|
||||
function loadContextSync() {
|
||||
const files = collectFilesSync(root);
|
||||
const context = {};
|
||||
|
||||
for (const fullPath of files) {
|
||||
// make the key look like fast‑glob’s relative paths
|
||||
const relPath = relative(root, fullPath).replace(/\\/g, '/');
|
||||
// require() each module
|
||||
context[relPath] = require(fullPath);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
const context = loadContextSync();
|
||||
|
||||
global.registerBundlerModules(context);
|
||||
}
|
||||
// VIRTUAL ENTRY END
|
||||
Reference in New Issue
Block a user