Merge branch 'feat/webpack5' into pr/9133

This commit is contained in:
rigor789
2021-08-03 14:54:04 +02:00
63 changed files with 4367 additions and 714 deletions

View File

@@ -0,0 +1,56 @@
// import { createServer } from 'http'
//
// export interface IHMRStatusData {
// seq: number
// uuid: string,
// hash: string
// status: string
// }
//
// export function run() {
// createServer((req, res) => {
// if (req.url === '/ping') {
// console.log('PING -> PONG!')
// return res.end("Pong.");
// }
//
// if (req.method !== 'POST') {
// res.statusCode = 400;
// return res.end("Unsupported method.");
// }
//
// let data = "";
// req.on("data", chunk => {
// data += chunk;
// });
//
// req.on("end", () => {
// try {
// const signal = JSON.parse(data) as IHMRStatusData;
// // if (!statuses[signal.hash] || statuses[signal.hash].seq < signal.seq) {
// // statuses[signal.hash] = signal
// // }
// if (process.send) {
// process.send({
// type: 'hmr-status',
// version: 1,
// hash: signal.hash,
// data: signal
// }, (error) => {
// if (error) {
// console.error(`Process Send Error: `, error);
// }
//
// return null;
// });
// }
//
// res.end('ok.');
// } catch (e) {
// res.statusCode = 400;
// res.end("Invalid JSON.");
// }
// });
// }).listen(8238)
// }
//

View File

@@ -1,11 +1,14 @@
#!/user/bin/env node
#!/usr/bin/env node
import { redBright, green, greenBright } from 'chalk';
import { program } from 'commander';
import dedent from 'ts-dedent';
import webpack from 'webpack';
import path from 'path';
import fs from 'fs';
import { parseEnvFlags } from '../cli/parseEnvFlags';
const defaultConfig = path.resolve(
__dirname,
'../stubs/default.config.stub.js'
@@ -20,6 +23,8 @@ function info(message: string) {
console.info(`${tag} ${greenBright(dedent(message))}`);
}
program.enablePositionalOptions();
program
.command('init')
.description('Initialize a new webpack.config.js in the current directory.')
@@ -35,4 +40,83 @@ program
info('Initialized config.');
});
program
.command('build')
.description('Build...')
.option('--env [name]', 'environment name')
.option('--config [path]', 'config path')
.option('--watch', 'watch for changes')
.allowUnknownOption()
.action((options, command) => {
const env = parseEnvFlags(command.args);
// add --env <val> into the env object
// for example if we use --env prod
// we'd have env.env = 'prod'
if (options.env) {
env['env'] = options.env;
}
const configPath = (() => {
if (options.config) {
return path.resolve(options.config);
}
return path.resolve(process.cwd(), 'webpack.config.js');
})();
// todo: validate config exists
// todo: guard against invalid config
let configuration: webpack.Configuration;
try {
configuration = require(configPath)(env);
} catch (err) {
console.log(err);
}
if (!configuration) {
console.log('No configuration!');
return;
}
const compiler = webpack(configuration);
const webpackCompilationCallback = (
err: webpack.WebpackError,
stats: webpack.Stats
) => {
if (err) {
// Do not keep cache anymore
compiler.purgeInputFileSystem();
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
process.exitCode = 1;
return;
}
if (stats) {
console.log(
stats.toString({
chunks: false,
colors: true,
errorDetails: env.verbose,
})
);
}
};
if (options.watch) {
console.log('webpack is watching the files...');
compiler.watch(
configuration.watchOptions ?? {},
webpackCompilationCallback
);
} else {
compiler.run(webpackCompilationCallback);
}
});
program.parse(process.argv);

View File

@@ -0,0 +1,39 @@
import type { IWebpackEnv } from '@nativescript/webpack';
const ENV_FLAG_RE = /--env\.(\w+)(?:=(.+))?/;
export function parseEnvFlags(flags: string[]): IWebpackEnv {
const envFlags = flags.filter((flag) => flag.includes('--env.'));
const env = {};
envFlags.map((flag) => {
let [_, name, v] = ENV_FLAG_RE.exec(flag);
let value: any = v;
// convert --env.foo to --env.foo=true
if (value === undefined) {
value = true;
}
// convert true/false to boolean
if (value === 'true' || value === 'false') {
value = value === 'true';
}
// convert numbers
if (!isNaN(value) && !isNaN(parseFloat(value))) {
value = +value;
}
// duplicate key/name - convert to array
if (name in env && value) {
const orig = Array.isArray(env[name]) ? env[name] : [env[name]];
env[name] = [...orig, value];
} else {
env[name] = value;
}
});
return env;
}

View File

@@ -1,19 +1,36 @@
import { extname, resolve } from 'path';
import Config from 'webpack-chain';
import path from 'path';
import { existsSync } from 'fs';
import { getProjectRootPath } from '../helpers/project';
import { getProjectFilePath } from '../helpers/project';
import { env as _env, IWebpackEnv } from '../index';
import { getEntryPath } from '../helpers/platform';
import {
getEntryDirPath,
getEntryPath,
getPlatformName,
} from '../helpers/platform';
import base from './base';
export default function (config: Config, env: IWebpackEnv = _env): Config {
base(config, env);
const tsConfigPath = path.join(getProjectRootPath(), 'tsconfig.json');
const platform = getPlatformName();
const tsConfigPath = [
getProjectFilePath('tsconfig.app.json'),
getProjectFilePath('tsconfig.json'),
].find((path) => existsSync(path));
// remove default ts rule
config.module.rules.delete('ts');
// remove fork ts checked as not needed
config.plugins.delete('ForkTsCheckerWebpackPlugin');
// explicitly define mainFields to make sure ngcc compiles as es2015 (module field)
// instead of umd (main field).
config.resolve.mainFields.add('module').add('main');
config.module
.rule('angular')
.test(/(?:\.ngfactory.js|\.ngstyle\.js|\.ts)$/)
@@ -32,18 +49,178 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
.use('raw-loader')
.loader('raw-loader');
config.plugin('AngularCompilerPlugin').use(getAngularCompilerPlugin(), [
{
tsConfigPath,
mainPath: getEntryPath(),
platformTransformers: [require('../transformers/NativeClass').default],
},
]);
// exclude component css files from the normal css rule
config.module
.rule('css')
.exclude // exclude *.component.{platform}.css
.add(/\.component(\.\w+)?\.css$/);
// and instead use raw-loader, since that's what angular expects
config.module
.rule('css|component')
.test(/\.component(\.\w+)?\.css$/)
.use('raw-loader')
.loader('raw-loader');
// get base postCSS options
const postCSSOptions = config.module
.rule('scss')
.uses.get('postcss-loader')
.get('options');
// exclude component css files from the normal css rule
config.module
.rule('scss')
.exclude // exclude *.component.{platform}.scss
.add(/\.component(\.\w+)?\.scss$/);
// and instead use raw-loader, since that's what angular expects
config.module
.rule('scss|component')
.test(/\.component(\.\w+)?\.scss$/)
.use('raw-loader')
.loader('raw-loader')
.end()
.use('postcss-loader')
.loader('postcss-loader')
.options(postCSSOptions)
.end()
.use('sass-loader')
.loader('sass-loader');
const angularCompilerPlugin = getAngularCompilerPlugin();
if (angularCompilerPlugin) {
config.plugin('AngularCompilerPlugin').use(angularCompilerPlugin, [
{
tsConfigPath,
mainPath: getEntryPath(),
// disable type checking in a forked process - it ignores
// the hostReplacementPaths and prints errors about
// platform suffixed files, even though they are
// working as expected.
forkTypeChecker: false,
hostReplacementPaths(path: string) {
const ext = extname(path);
const platformExt = `.${platform}${ext}`;
// already includes a platform specific extension - ignore
if (path.includes(platformExt)) {
return path;
}
const platformPath = path.replace(ext, platformExt);
// check if the same file exists with a platform suffix and return if it does.
if (existsSync(platformPath)) {
// console.log(`[hostReplacementPaths] resolving "${path}" to "${platformPath}"`);
return platformPath;
}
// just return the original path otherwise
return path;
},
platformTransformers: [require('../transformers/NativeClass').default],
},
]);
}
const angularWebpackPlugin = getAngularWebpackPlugin();
if (angularWebpackPlugin) {
// angular no longer supports transformers.
// so we patch their method until they do
// https://github.com/angular/angular-cli/pull/21046
const originalCreateFileEmitter =
angularWebpackPlugin.prototype.createFileEmitter;
angularWebpackPlugin.prototype.createFileEmitter = function (
...args: any[]
) {
let transformers = args[1] || {};
if (!transformers.before) {
transformers.before = [];
}
transformers.before.push(require('../transformers/NativeClass').default);
args[1] = transformers;
return originalCreateFileEmitter.apply(this, args);
};
config.plugin('AngularWebpackPlugin').use(angularWebpackPlugin, [
{
tsconfig: tsConfigPath,
directTemplateLoading: false,
},
]);
config.when(env.hmr, (config) => {
config.module
.rule('angular-hmr')
.enforce('post')
.test(getEntryPath())
.use('angular-hot-loader')
.loader('angular-hot-loader');
});
}
// look for platform specific polyfills first
// falling back to independent polyfills
const polyfillsPath = [
resolve(getEntryDirPath(), `polyfills.${platform}.ts`),
resolve(getEntryDirPath(), `polyfills.ts`),
].find((path) => existsSync(path));
if (polyfillsPath) {
const paths = config.entry('bundle').values();
// replace globals with the polyfills file which
// should handle loading the correct globals
// and any additional polyfills required.
if (paths.includes('@nativescript/core/globals/index.js')) {
paths[
paths.indexOf('@nativescript/core/globals/index.js')
] = polyfillsPath;
// replace paths with the update paths
config.entry('bundle').clear().merge(paths);
}
}
// Filter common undesirable warnings
config.set(
'ignoreWarnings',
(config.get('ignoreWarnings') ?? []).concat([
/**
* This rule hides
* +-----------------------------------------------------------------------------------------+
* | WARNING in Zone.js does not support native async/await in ES2017+. |
* | These blocks are not intercepted by zone.js and will not triggering change detection. |
* | See: https://github.com/angular/zone.js/pull/1140 for more information. |
* +-----------------------------------------------------------------------------------------+
*/
/Zone\.js does not support native async\/await/,
/**
* This rule hides
* +-----------------------------------------------------------------------------------------+
* | WARNING in environment.*.ts is part of the TypeScript compilation but it's unused. |
* | Add only entry points to the 'files' or 'include' properties in your tsconfig. |
* +-----------------------------------------------------------------------------------------+
*/
/environment(\.(\w+))?\.ts is part of the TypeScript compilation but it's unused/,
// Additional rules to suppress warnings that are safe to ignore
{
module: /@angular\/core\/(__ivy_ngcc__\/)?fesm2015\/core.js/,
message: /Critical dependency: the request of a dependency is an expression/,
},
/core\/profiling/,
/core\/ui\/styling/,
])
);
return config;
}
function getAngularCompilerPlugin() {
function getAngularCompilerPlugin(): any {
const { AngularCompilerPlugin } = require('@ngtools/webpack');
return AngularCompilerPlugin;
}
function getAngularWebpackPlugin(): any {
const { AngularWebpackPlugin } = require('@ngtools/webpack');
return AngularWebpackPlugin;
}

View File

@@ -1,21 +1,26 @@
import { DefinePlugin, HotModuleReplacementPlugin } from 'webpack';
import {
ContextExclusionPlugin,
DefinePlugin,
HotModuleReplacementPlugin,
} from 'webpack';
import Config from 'webpack-chain';
import { resolve } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
// import { WatchStateLoggerPlugin } from '../plugins/WatchStateLoggerPlugin';
import { PlatformSuffixPlugin } from '../plugins/PlatformSuffixPlugin';
import { applyFileReplacements } from '../helpers/fileReplacements';
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
import { getValue } from '../helpers/config';
import { projectUsesCustomFlavor } from '../helpers/flavor';
import { getProjectRootPath } from '../helpers/project';
import { getProjectFilePath } from '../helpers/project';
import { hasDependency } from '../helpers/dependencies';
import { IWebpackEnv } from '../index';
import { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index';
import { getValue } from '../helpers/config';
import { getIPS } from '../helpers/host';
import {
getPlatformName,
getAbsoluteDistPath,
@@ -23,7 +28,7 @@ import {
getEntryPath,
} from '../helpers/platform';
export default function (config: Config, env: IWebpackEnv): Config {
export default function (config: Config, env: IWebpackEnv = _env): Config {
const entryPath = getEntryPath();
const platform = getPlatformName();
const mode = env.production ? 'production' : 'development';
@@ -40,8 +45,39 @@ export default function (config: Config, env: IWebpackEnv): Config {
// resolved at runtime
config.externals(['package.json', '~/package.json']);
// todo: devtool
config.devtool('inline-source-map');
// 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,
});
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));
// todo: figure out easiest way to make "node" target work in ns
// rather than the custom ns target implementation that's hard to maintain
@@ -54,10 +90,20 @@ export default function (config: Config, env: IWebpackEnv): Config {
.add('@nativescript/core/globals/index.js')
.add(entryPath);
// Add android app components to the bundle to SBG can generate the java classes
if (platform === 'android') {
const 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/@nativescript/core/inspector_modules')
.entry('tns_modules/inspector_modules')
.add('@nativescript/core/inspector_modules');
});
@@ -66,7 +112,15 @@ export default function (config: Config, env: IWebpackEnv): Config {
.pathinfo(false)
.publicPath('')
.libraryTarget('commonjs')
.globalObject('global');
.globalObject('global')
.set('clean', true);
config.watchOptions({
ignored: [
`${getProjectFilePath('platforms')}/**`,
`${getProjectFilePath(env.appResourcesPath ?? 'App_Resources')}/**`,
],
});
// Set up Terser options
config.optimization.minimizer('TerserPlugin').use(TerserPlugin, [
@@ -75,13 +129,20 @@ export default function (config: Config, env: IWebpackEnv): Config {
compress: {
collapse_vars: platform !== 'android',
sequences: platform !== 'android',
keep_infinity: true,
drop_console: mode === 'production',
global_defs: {
__UGLIFIED__: true,
},
},
// todo: move into vue.ts if not required in other flavors?
keep_fnames: true,
keep_classnames: true,
},
},
]);
config.optimization.runtimeChunk('single');
config.optimization.splitChunks({
cacheGroups: {
defaultVendor: {
@@ -95,10 +156,13 @@ export default function (config: Config, env: IWebpackEnv): Config {
// 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('node_modules/@nativescript/webpack/dist/loaders')
.add(resolve(__dirname, '../loaders'))
.add(resolve(__dirname, '../../node_modules'))
.add(getProjectFilePath('node_modules'))
.add('node_modules');
config.resolve.extensions
@@ -119,6 +183,28 @@ export default function (config: Config, env: IWebpackEnv): Config {
// 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({
platform,
})
.end()
.use('nativescript-hot-loader')
.loader('nativescript-hot-loader')
.options({
injectHMRRuntime: true,
});
// set up ts support
config.module
.rule('ts')
@@ -156,19 +242,25 @@ export default function (config: Config, env: IWebpackEnv): Config {
});
// set up js
// todo: do we need babel-loader? It's useful to support it
config.module
.rule('js')
.test(/\.js$/)
.exclude.add(/node_modules/)
.end()
.use('babel-loader')
.loader('babel-loader')
.options({
generatorOpts: {
compact: false,
},
});
.end();
config.module
.rule('workers')
.test(/\.(js|ts)$/)
.use('nativescript-worker-loader')
.loader('nativescript-worker-loader');
// 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
@@ -213,14 +305,6 @@ export default function (config: Config, env: IWebpackEnv): Config {
.use('sass-loader')
.loader('sass-loader');
// items to clean
config.plugin('CleanWebpackPlugin').use(CleanWebpackPlugin, [
{
cleanOnceBeforeBuildPatterns: [`${getAbsoluteDistPath()}/**/*`],
verbose: !!env.verbose,
},
]);
// config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [
// /.*/,
// request => {
@@ -237,6 +321,28 @@ export default function (config: Config, env: IWebpackEnv): Config {
},
]);
// 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(.*)`)]);
// 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, [
{
@@ -244,7 +350,10 @@ export default function (config: Config, env: IWebpackEnv): Config {
__NS_WEBPACK__: true,
__UI_USE_XML_PARSER__: true,
__UI_USE_EXTERNAL_RENDERER__: projectUsesCustomFlavor(),
__CSS_PARSER__: JSON.stringify(getValue('cssParser')), // todo: replace from config value
__NS_ENV_VERBOSE__: !!env.verbose,
__NS_DEV_HOST_IPS__:
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
__ANDROID__: platform === 'android',
__IOS__: platform === 'ios',
/* for compat only */ 'global.isAndroid': platform === 'android',
@@ -256,6 +365,12 @@ export default function (config: Config, env: IWebpackEnv): Config {
},
]);
// enable DotEnv
applyDotEnvPlugin(config);
// replacements
applyFileReplacements(config);
// set up default copy rules
addCopyRule('assets/**');
addCopyRule('fonts/**');
@@ -263,8 +378,6 @@ export default function (config: Config, env: IWebpackEnv): Config {
applyCopyRules(config);
// add the WatchStateLogger plugin used to notify the CLI of build state
// config.plugin('WatchStateLoggerPlugin').use(WatchStateLoggerPlugin);
config.plugin('WatchStatePlugin').use(WatchStatePlugin);
config.when(env.hmr, (config) => {
@@ -272,14 +385,13 @@ export default function (config: Config, env: IWebpackEnv): Config {
});
config.when(env.report, (config) => {
const projectRoot = getProjectRootPath();
config.plugin('BundleAnalyzerPlugin').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static',
generateStatsFile: true,
openAnalyzer: false,
reportFilename: resolve(projectRoot, 'report', 'report.html'),
statsFilename: resolve(projectRoot, 'report', 'stats.json'),
reportFilename: getProjectFilePath('report/report.html'),
statsFilename: getProjectFilePath('report/stats.json'),
},
]);
});

View File

@@ -1,45 +1,42 @@
import VirtualModulesPlugin from 'webpack-virtual-modules';
import { ContextExclusionPlugin } from 'webpack';
import Config from 'webpack-chain';
import dedent from 'ts-dedent';
import { join } from 'path';
import { getEntryDirPath } from '../helpers/platform';
import { getEntryPath, getEntryDirPath } from '../helpers/platform';
import { addVirtualEntry } from '../helpers/virtualModules';
import { env as _env, IWebpackEnv } from '../index';
import base from './base';
export default function (config: Config, env: IWebpackEnv = _env): Config {
base(config, env);
const virtualEntryPath = join(getEntryDirPath(), '__virtual_entry__.js');
const entryPath = getEntryPath();
const filterRE = '/.(xml|js|s?css)$/';
const virtualEntryPath = addVirtualEntry(
config,
'javascript',
`
// VIRTUAL ENTRY START
require('@nativescript/core/bundle-entry-points')
const context = require.context("~/", /* deep: */ true, /* filter: */ ${filterRE});
global.registerWebpackModules(context);
// VIRTUAL ENTRY END
`
);
config.entry('bundle').add(virtualEntryPath);
config
.plugin('ContextExclusionPluginPlugin')
.use(ContextExclusionPlugin, [/__virtual_entry__\.js$/]);
// config.resolve.extensions.add('.xml');
// Add a virtual entry module that will register all modules into
// the nativescript module loader/handler
config.plugin('VirtualModulesPlugin').use(VirtualModulesPlugin, [
{
[virtualEntryPath]: dedent`
require('@nativescript/core/bundle-entry-points')
const context = require.context("~/", /* deep: */ true, /* filter: */ ${filterRE});
global.registerWebpackModules(context);
`,
},
]);
config.resolve.extensions.add('.xml');
// set up xml
// set up core HMR
config.module
.rule('xml')
.test(/\.xml$/)
.use('xml-namespace-loader')
.loader('xml-namespace-loader');
.rule('hmr-core')
.test(/\.js$/)
.exclude.add(/node_modules/)
.add(entryPath)
.end()
.use('nativescript-hot-loader')
.loader('nativescript-hot-loader')
.options({
appPath: getEntryDirPath(),
});
return config;
}

View File

@@ -1,8 +1,8 @@
import { merge } from 'webpack-merge';
import Config from 'webpack-chain';
import { env as _env, IWebpackEnv } from '../index';
import { getPlatformName } from '../helpers/platform';
import { env as _env, IWebpackEnv } from '../index';
import base from './base';
export default function (config: Config, env: IWebpackEnv = _env): Config {

View File

@@ -1,6 +1,6 @@
import Config from 'webpack-chain';
import { getProjectRootPath } from '../helpers/project';
import { getProjectFilePath, getProjectRootPath } from '../helpers/project';
import { getPlatformName } from '../helpers/platform';
import { env as _env, IWebpackEnv } from '../index';
import { error } from '../helpers/log';
@@ -52,15 +52,14 @@ function getSvelteConfigPreprocessor(): any {
return config?.preprocess;
}
function getSvelteConfig(): { preprocess: any } | undefined {
interface ISvelteConfig {
preprocess: any;
}
function getSvelteConfig(): ISvelteConfig | undefined {
try {
const resolvedPath = require.resolve(`./svelte.config.js`, {
paths: [getProjectRootPath()],
});
return require(resolvedPath);
return require(getProjectFilePath('svelte.config.js')) as ISvelteConfig;
} catch (err) {
// todo: remove when jest supports mocking require.resolve
if (__TEST__) return;
error('Could not find svelte.config.js.', err);
}
}

View File

@@ -1,11 +1,42 @@
import Config from 'webpack-chain';
import { IWebpackEnv } from '../index';
import { getEntryDirPath, getEntryPath } from '../helpers/platform';
import { addVirtualEntry } from '../helpers/virtualModules';
import { env as _env, IWebpackEnv } from '../index';
import base from './base';
// todo: add base configuration for core
export default function (config: Config, env: IWebpackEnv): Config {
export default function (config: Config, env: IWebpackEnv = _env): Config {
base(config, env);
const entryPath = getEntryPath();
const filterRE = '/\\.(xml|js|(?<!\\.d\\.)ts|s?css)$/';
const virtualEntryPath = addVirtualEntry(
config,
'typescript',
`
// VIRTUAL ENTRY START
require('@nativescript/core/bundle-entry-points')
const context = require.context("~/", /* deep: */ true, /* filter: */ ${filterRE});
global.registerWebpackModules(context);
// VIRTUAL ENTRY END
`
);
config.entry('bundle').add(virtualEntryPath);
// config.resolve.extensions.add('.xml');
// set up core HMR
config.module
.rule('hmr-core')
.test(/\.(js|ts)$/)
.exclude.add(/node_modules/)
.add(entryPath)
.end()
.use('nativescript-hot-loader')
.loader('nativescript-hot-loader')
.options({
appPath: getEntryDirPath(),
});
return config;
}

View File

@@ -1,9 +1,12 @@
import { VueLoaderPlugin } from 'vue-loader';
import { merge } from 'webpack-merge';
import Config from 'webpack-chain';
import fs from 'fs';
import { env as _env, IWebpackEnv } from '../index';
import { hasDependency } from '../helpers/dependencies';
import { getPlatformName } from '../helpers/platform';
import { env as _env, IWebpackEnv } from '../index';
import { error } from '../helpers/log';
import base from './base';
export default function (config: Config, env: IWebpackEnv = _env): Config {
@@ -11,6 +14,11 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
const platform = getPlatformName();
// we need to patch VueLoader if we want to enable hmr
if (env.hmr) {
patchVueLoaderForHMR();
}
// resolve .vue files
// the order is reversed because we are using prepend!
config.resolve.extensions.prepend('.vue').prepend(`.${platform}.vue`);
@@ -28,6 +36,21 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
};
});
// apply vue stylePostLoader to inject component scope into the css
// this would usually be automatic, however in NS we don't use the
// css-loader, so VueLoader doesn't inject the rule at all.
config.module
.rule('css')
.use('vue-css-loader')
.after('css2json-loader')
.loader('vue-loader/lib/loaders/stylePostLoader.js');
config.module
.rule('scss')
.use('vue-css-loader')
.after('css2json-loader')
.loader('vue-loader/lib/loaders/stylePostLoader.js');
// set up ts support in vue files
config.module
.rule('ts')
@@ -39,18 +62,20 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
});
});
config.plugin('ForkTsCheckerWebpackPlugin').tap((args) => {
args[0] = merge(args[0], {
typescript: {
extensions: {
vue: {
enabled: true,
compiler: 'nativescript-vue-template-compiler',
config.when(hasDependency('typescript'), (config) => {
config.plugin('ForkTsCheckerWebpackPlugin').tap((args) => {
args[0] = merge(args[0], {
typescript: {
extensions: {
vue: {
enabled: true,
compiler: 'nativescript-vue-template-compiler',
},
},
},
},
});
return args;
});
return args;
});
// add VueLoaderPlugin as the first plugin
@@ -65,3 +90,22 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
return config;
}
/**
* Patches source of vue-loader to set the isServer flag to false
* so hmr gets enabled.
*/
function patchVueLoaderForHMR() {
try {
const vueLoaderPath = require.resolve('vue-loader/lib/index.js');
const source = fs.readFileSync(vueLoaderPath).toString();
const patchedSource = source.replace(
/(isServer\s=\s)(target\s===\s'node')/g,
'$1false;'
);
fs.writeFileSync(vueLoaderPath, patchedSource);
delete require.cache[vueLoaderPath];
} catch (err) {
error('Failed to patch VueLoader - HMR may not work properly!');
}
}

View File

@@ -1 +1 @@
declare var __TEST__: boolean;
// define globals here

View File

@@ -1,11 +1,15 @@
import { env } from '../index';
import { error } from './log';
import { error, warnOnce } from './log';
function getCLILib() {
if (!env.nativescriptLibPath) {
throw error(`
warnOnce(
'getCLILib',
`
Cannot find NativeScript CLI path. Make sure --env.nativescriptLibPath is passed
`);
`
);
return false;
}
return require(env.nativescriptLibPath);
@@ -15,11 +19,16 @@ function getCLILib() {
* Utility to get a value from the nativescript.config.ts file.
*
* @param {string} key The key to get from the config. Supports dot-notation.
* @param defaultValue The fallback value if the key is not set in the config.
*/
export function getValue<T = any>(key: string): T {
export function getValue<T = any>(key: string, defaultValue?: any): T {
const lib = getCLILib();
return (lib.projectConfigService as { getValue(key: string): T }).getValue(
key
);
if (!lib) {
return defaultValue;
}
return (lib.projectConfigService as {
getValue(key: string, defaultValue?: any): T;
}).getValue(key, defaultValue);
}

View File

@@ -1,5 +1,5 @@
import CopyWebpackPlugin from 'copy-webpack-plugin';
import { relative, resolve } from 'path';
import { basename, relative, resolve } from 'path';
import Config from 'webpack-chain';
import { getProjectRootPath } from './project';
@@ -12,17 +12,29 @@ import { env } from '..';
export let copyRules = new Set([]);
/**
* Utility to add new copy rules. Accepts a glob. For example
* @internal
*/
export let additionalCopyRules = [];
/**
* Utility to add new copy rules. Accepts a glob or an object. For example
* - **\/*.html - copy all .html files found in any sub dir.
* - myFolder/* - copy all files from myFolder
*
* When passing an object - no additional processing is done, and it's
* applied as-is. Make sure to set every required property.
*
* The path is relative to the folder of the entry file
* (specified in the main field of the package.json)
*
* @param {string} glob
* @param {string|object} globOrObject
*/
export function addCopyRule(glob: string) {
copyRules.add(glob);
export function addCopyRule(globOrObject: string | object) {
if (typeof globOrObject === 'string') {
return copyRules.add(globOrObject);
}
additionalCopyRules.push(globOrObject);
}
/**
@@ -41,29 +53,30 @@ export function removeCopyRule(glob: string) {
*/
export function applyCopyRules(config: Config) {
const entryDir = getEntryDirPath();
// todo: handle empty appResourcesPath?
// (the CLI should always pass the path - maybe not required)
const appResourcesFullPath = resolve(
getProjectRootPath(),
env.appResourcesPath
);
const globOptions = {
dot: false,
ignore: [
// ignore everything in App_Resources (regardless where they are located)
`${relative(entryDir, appResourcesFullPath)}/**`,
],
ignore: [],
};
// todo: do we need to handle empty appResourcesPath?
// (the CLI should always pass the path - maybe not required)
if (env.appResourcesPath) {
const appResourcesFolderName = basename(env.appResourcesPath);
// ignore everything in App_Resources (regardless where they are located)
globOptions.ignore.push(`**/${appResourcesFolderName}/**`);
}
config.plugin('CopyWebpackPlugin').use(CopyWebpackPlugin, [
{
patterns: Array.from(copyRules).map((glob) => ({
from: glob,
context: entryDir,
noErrorOnMissing: true,
globOptions,
})),
patterns: Array.from(copyRules)
.map((glob) => ({
from: glob,
context: entryDir,
noErrorOnMissing: true,
globOptions,
}))
.concat(additionalCopyRules),
},
]);
}

View File

@@ -1,6 +1,7 @@
import { getPackageJson, getProjectRootPath } from './project';
import path from 'path';
import { getPackageJson, getProjectRootPath } from './project';
// todo: memoize
/**
* Utility to get all dependencies from the project package.json.

View File

@@ -0,0 +1,49 @@
import DotEnvPlugin from 'dotenv-webpack';
import Config from 'webpack-chain';
import { existsSync } from 'fs';
import { resolve } from 'path';
import { getProjectRootPath } from './project';
import { env } from '..';
/**
* @internal
*/
export function applyDotEnvPlugin(config: Config) {
const path = getDotEnvPath();
config.when(path !== null, (config) => {
config.plugin('DotEnvPlugin').use(DotEnvPlugin, [
{
path,
silent: true, // hide any errors
},
]);
});
}
function getDotEnvFileName(): string {
if (env.env) {
return `.env.${env.env}`;
}
return '.env';
}
function getDotEnvPath(): string {
const dotEnvPath = resolve(getProjectRootPath(), '.env');
const dotEnvWithEnvPath = resolve(getProjectRootPath(), getDotEnvFileName());
// look for .env.<env>
if (existsSync(dotEnvWithEnvPath)) {
return dotEnvWithEnvPath;
}
// fall back to .env
if (existsSync(dotEnvPath)) {
return dotEnvPath;
}
// don't use .env
return null;
}

View File

@@ -2,9 +2,9 @@ import path from 'path';
import fs from 'fs';
import { getAllDependencies, getDependencyPath } from './dependencies';
import { clearCurrentPlugin, setCurrentPlugin } from '../index';
import { info, warn } from './log';
import * as lib from '../index';
import { clearCurrentPlugin, setCurrentPlugin } from '../index';
/**
* @internal

View File

@@ -0,0 +1,68 @@
import { resolve } from 'path';
import { env as _env, IWebpackEnv } from '../index';
import { addCopyRule } from './copyRules';
import { getProjectRootPath } from './project';
interface IReplacementMap {
[_replace: string]: /* _with */ string;
}
/**
* @internal
*/
export function getFileReplacementsFromEnv(
env: IWebpackEnv = _env
): IReplacementMap {
const fileReplacements: IReplacementMap = {};
const entries: string[] = (() => {
if (Array.isArray(env.replace)) {
return env.replace;
}
if (typeof env.replace === 'string') {
return [env.replace];
}
return [];
})();
entries.forEach((replaceEntry) => {
replaceEntry.split(/,\s*/).forEach((r: string) => {
let [_replace, _with] = r.split(':');
if (!_replace || !_with) {
return;
}
// make sure to resolve replacements to a full path
// relative to the project root
_replace = resolve(getProjectRootPath(), _replace);
_with = resolve(getProjectRootPath(), _with);
fileReplacements[_replace] = _with;
});
});
return fileReplacements;
}
export function applyFileReplacements(
config,
fileReplacements: IReplacementMap = getFileReplacementsFromEnv()
) {
Object.entries(fileReplacements).forEach(([_replace, _with]) => {
// in case we are replacing source files - we'll use aliases
if (_replace.match(/\.(ts|js)$/)) {
return config.resolve.alias.set(_replace, _with);
}
// otherwise we will override the replaced file with the replacement
addCopyRule({
from: _with, // copy the replacement file
to: _replace, // to the original "to-be-replaced" file
force: true,
});
});
}

View File

@@ -0,0 +1,13 @@
import os from 'os';
export function getIPS() {
const interfaces = os.networkInterfaces();
return Object.keys(interfaces)
.map((name) => {
return interfaces[name].filter(
(binding: any) => binding.family === 'IPv4'
)[0];
})
.filter(Boolean)
.map((binding) => binding.address);
}

View File

@@ -1,15 +1,22 @@
import { merge } from 'webpack-merge';
import {
getPackageJson,
getProjectRootPath,
getProjectFilePath,
} from './project';
import { addVirtualEntry, addVirtualModule } from './virtualModules';
import { applyFileReplacements } from './fileReplacements';
import { addCopyRule, removeCopyRule } from './copyRules';
import { error, info, warn, warnOnce } from './log';
import { determineProjectFlavor, projectUsesCustomFlavor } from './flavor';
import { error, info, warn } from './log';
import { getValue } from './config';
import { getIPS } from './host';
import {
getAllDependencies,
hasDependency,
getDependencyPath,
} from './dependencies';
import { getPackageJson, getProjectRootPath } from './project';
import {
addPlatform,
getAbsoluteDistPath,
@@ -29,6 +36,7 @@ export default {
merge,
addCopyRule,
removeCopyRule,
applyFileReplacements,
config: {
getValue,
},
@@ -39,16 +47,16 @@ export default {
},
flavor: {
determineProjectFlavor,
projectUsesCustomFlavor
projectUsesCustomFlavor,
},
host: {
getIPS,
},
log: {
error,
info,
warn,
},
project: {
getProjectRootPath,
getPackageJson,
warnOnce,
},
platform: {
addPlatform,
@@ -59,4 +67,13 @@ export default {
getPlatform,
getPlatformName,
},
project: {
getProjectFilePath,
getProjectRootPath,
getPackageJson,
},
virtualModules: {
addVirtualEntry,
addVirtualModule,
},
};

View File

@@ -1,4 +1,5 @@
import dedent from 'ts-dedent';
import { env } from '@nativescript/webpack';
// de-indents strings so multi-line string literals can be used
function cleanup(data: any[]) {
@@ -27,8 +28,20 @@ export function warn(...data: any): void {
console.warn(`[@nativescript/webpack] Warn: \n`, ...cleanup(data));
}
const warnedMap: any = {};
export function warnOnce(key: string, ...data: any): void {
if (warnedMap[key]) {
return;
}
warnedMap[key] = true;
warn(...data);
}
export function info(...data: any): void {
console.log(`[@nativescript/webpack] Info: \n`, ...cleanup(data));
if (env.verbose) {
console.log(`[@nativescript/webpack] Info: \n`, ...cleanup(data));
}
}
// todo: refine

View File

@@ -1,7 +1,7 @@
import { dirname, resolve } from 'path';
import { getPackageJson, getProjectRootPath } from './project';
import { error } from './log';
import { error, info, warnOnce } from './log';
import { env } from '../';
import AndroidPlatform from '../platforms/android';
@@ -29,7 +29,7 @@ const platforms: {
* @param platform A platform definition of the platform specifics
*/
export function addPlatform(name: string, platform: INativeScriptPlatform) {
console.log('adding platform', name, platform);
info(`Adding platform ${name}`, platform);
platforms[name] = platform;
}
@@ -65,13 +65,20 @@ export function getPlatformName(): Platform {
`);
}
throw error(`
warnOnce(
'getPlatformName',
`
You need to provide a target platform!
Available platforms: ${Object.keys(platforms).join(', ')}
Use --env=platform=<platform> or --env=android, --env=ios to specify the target platform.
`);
Use --env.platform=<platform> or --env.android, --env.ios to specify the target platform.
Defaulting to "ios".
`
);
return 'ios';
}
/**

View File

@@ -19,7 +19,32 @@ interface IPackageJson {
* Utility function to get the contents of the project package.json
*/
export function getPackageJson() {
const packageJsonPath = resolve(getProjectRootPath(), 'package.json');
return require(packageJsonPath) as IPackageJson;
return require(getProjectFilePath('package.json')) as IPackageJson;
}
/**
* Utility to get project files relative to the project root.
* @param filePath path to get
*/
export function getProjectFilePath(filePath: string): string {
return resolve(getProjectRootPath(), filePath);
}
// unused helper, but keeping it here as we may need it
// todo: remove if unused for next few releases
// function findFile(fileName, currentDir): string | null {
// // console.log(`findFile(${fileName}, ${currentDir})`)
// const path = resolve(currentDir, fileName);
//
// if (existsSync(path)) {
// return path;
// }
//
// // bail if we reached the root dir
// if (currentDir === resolve('/')) {
// return null;
// }
//
// // traverse to the parent folder
// return findFile(fileName, resolve(currentDir, '..'));
// }

View File

@@ -0,0 +1,65 @@
import { ContextExclusionPlugin } from 'webpack';
import Config from 'webpack-chain';
import { dirname, join } from 'path';
import { writeFileSync, mkdirSync } from 'fs';
import VirtualModulesPlugin from 'webpack-virtual-modules';
import { getEntryDirPath } from './platform';
import dedent from 'ts-dedent';
import { getProjectFilePath } from './project';
export function addVirtualEntry(
config: Config,
name: string,
contents: string
): string {
return addVirtualModule(
config,
`__@nativescript_webpack_virtual_entry_${name}__`,
contents
);
}
export function addVirtualModule(
config: Config,
name: string,
contents: string
): string {
const virtualEntryPath = join(getEntryDirPath(), `${name}`);
// add the virtual entry to the context exclusions
// makes sure that require.context will never
// include the virtual entry.
config
.plugin(`ContextExclusionPlugin|${name}`)
.use(ContextExclusionPlugin, [new RegExp(`${name}\.js$`)]);
const options = {
[virtualEntryPath]: dedent(contents),
};
// AngularCompilerPlugin does not support virtual modules
// https://github.com/sysgears/webpack-virtual-modules/issues/96
// This is only an issue on v11, which has experimental webpack 5 support
// AngularCompilerPlugin gets replaced by AngularWebpackPlugin on v12
// todo: we can remove this special handling once we no longer support v11
if (config.plugins.has('AngularCompilerPlugin')) {
const compatEntryPath = getProjectFilePath(
join('node_modules', '.nativescript', `${name}`)
);
mkdirSync(dirname(compatEntryPath), { recursive: true });
writeFileSync(compatEntryPath, options[virtualEntryPath]);
return compatEntryPath;
}
if (config.plugins.has('VirtualModulesPlugin')) {
config.plugin('VirtualModulesPlugin').tap((args) => {
Object.assign(args[0], options);
return args;
});
} else {
config.plugin('VirtualModulesPlugin').use(VirtualModulesPlugin, [options]);
}
return virtualEntryPath;
}

View File

@@ -1,3 +1,13 @@
// Make sure the Acorn Parser (used by Webpack) can parse ES-Stage3 code
// This must be at the top BEFORE webpack is loaded so that we can extend
// and replace the parser before webpack uses it
// Based on the issue: https://github.com/webpack/webpack/issues/10216
import stage3 from 'acorn-stage3';
// we use require to be able to override the exports
const acorn = require('acorn');
acorn.Parser = acorn.Parser.extend(stage3);
import { highlight } from 'cli-highlight';
import { merge } from 'webpack-merge';
import Config from 'webpack-chain';
@@ -12,8 +22,11 @@ import helpers from './helpers';
export interface IWebpackEnv {
[name: string]: any;
env?: string;
appPath?: string;
appResourcesPath?: string;
appComponents?: string[];
nativescriptLibPath?: string;
@@ -22,13 +35,16 @@ export interface IWebpackEnv {
// for custom platforms
platform?: string;
sourceMap?: string | boolean;
production?: boolean;
report?: boolean;
hmr?: boolean;
// enable verbose output
verbose?: boolean;
// todo: add others
// misc
replace?: string[] | string;
}
interface IChainEntry {

View File

@@ -0,0 +1,117 @@
import {
isDevMode,
ɵresetCompiledComponents,
// @ts-ignore
} from '@angular/core';
declare const __webpack_require__: any;
declare const ng: any;
export default function (mod: any): void {
if (!mod['hot']) {
return;
}
if (!isDevMode()) {
console.error(
`[NG HMR] Cannot use HMR when Angular is running in production mode. To prevent production mode, do not call 'enableProdMode()'.`
);
return;
}
mod['hot'].accept();
mod['hot'].dispose(() => {
if (typeof ng === 'undefined') {
console.warn(
`[NG HMR] Cannot find global 'ng'. Likely this is caused because scripts optimization is enabled.`
);
return;
}
if (!ng.getInjector) {
// View Engine
return;
}
// Reset JIT compiled components cache
ɵresetCompiledComponents();
try {
if (global['__cleanup_ng_hot__']) global['__cleanup_ng_hot__']();
} catch (e) {
console.error('[NG HMR] Error disposing previous module');
console.error(e, e?.stack);
// HMR breaks when rejecting the main module dispose, so we manually trigger an HMR restart
const hash = __webpack_require__.h();
console.log(`[HMR][${hash}] failure | Error disposing previous module`);
throw e;
}
});
}
// TODO: maybe restore form values!
// function restoreFormValues(oldInputs: any[], oldOptions: any[]): void {
// // Restore input that are not hidden
// const newInputs = document.querySelectorAll('input:not([type="hidden"]), textarea');
// if (newInputs.length && newInputs.length === oldInputs.length) {
// console.log('[NG HMR] Restoring input/textarea values.');
// for (let index = 0; index < newInputs.length; index++) {
// const newElement = newInputs[index];
// const oldElement = oldInputs[index];
// switch (oldElement.type) {
// case 'button':
// case 'image':
// case 'submit':
// case 'reset':
// // These types don't need any value change.
// continue;
// case 'radio':
// case 'checkbox':
// newElement.checked = oldElement.checked;
// break;
// case 'color':
// case 'date':
// case 'datetime-local':
// case 'email':
// case 'file':
// case 'hidden':
// case 'month':
// case 'number':
// case 'password':
// case 'range':
// case 'search':
// case 'tel':
// case 'text':
// case 'textarea':
// case 'time':
// case 'url':
// case 'week':
// newElement.value = oldElement.value;
// break;
// default:
// console.warn('[NG HMR] Unknown input type ' + oldElement.type + '.');
// continue;
// }
// dispatchEvents(newElement);
// }
// } else if (oldInputs.length) {
// console.warn('[NG HMR] Cannot restore input/textarea values.');
// }
// // Restore option
// const newOptions = document.querySelectorAll('option');
// if (newOptions.length && newOptions.length === oldOptions.length) {
// console.log('[NG HMR] Restoring selected options.');
// for (let index = 0; index < newOptions.length; index++) {
// const newElement = newOptions[index];
// newElement.selected = oldOptions[index].selected;
// dispatchEvents(newElement);
// }
// } else if (oldOptions.length) {
// console.warn('[NG HMR] Cannot restore selected options.');
// }
// }

View File

@@ -0,0 +1,30 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { join } from 'path';
export const HmrLoader = __filename;
const hmrAcceptPath = join(__dirname, './hmr-accept.js').replace(/\\/g, '/');
export default function (
this: any,
content: string,
// Source map types are broken in the webpack type definitions
map: any
): void {
const source = `${content}
// HMR Accept Code
import ngHmrAccept from '${hmrAcceptPath}';
ngHmrAccept(module);
`;
this.callback(null, source, map);
return;
}

View File

@@ -0,0 +1,28 @@
import { dedent } from 'ts-dedent';
import { basename } from 'path';
/**
* This loader tries to load an `app.scss` or and `app.css` relative to the main entry
*/
export default function loader(content: string, map: any) {
const { platform } = this.getOptions();
const callback = this.async();
const resolve = this.getResolve({
extensions: [`.${platform}.scss`, `.${platform}.css`, '.scss', '.css'],
});
resolve(this.context, './app', (err, res) => {
if (err || !res) {
// if we ran into an error or there's no css file found, we just return
// original content and not append any additional imports.
return callback(null, content, map);
}
const code = dedent`
// Added by app-css-loader
import "./${basename(res)}";
${content}
`;
callback(null, code, map);
});
}

View File

@@ -28,7 +28,7 @@ export default function loader(content, map) {
`
: ``;
if (hasLoader('apply-css-loader')) {
if (hasLoader('css2json-loader')) {
content = dedent`
${content}
const { addTaggedAdditionalCSS } = require("@nativescript/core/ui/styling/style-scope");
@@ -53,5 +53,5 @@ export default function loader(content, map) {
this.emitWarning(new Error(cssLoaderWarning));
}
this.callback(null, content, map);
this.callback(null, content, null);
}

View File

@@ -36,7 +36,7 @@ export default function loader(content: string, map: any) {
this.callback(
null,
code, //`${dependencies.join('\n')}module.exports = ${str};`,
null
map
);
}

View File

@@ -0,0 +1,121 @@
// @ts-nocheck
// This is a runtime module - included by nativescript-hot-loader
// this file should not include external dependencies
// ---
if (module.hot) {
let hash = __webpack_require__.h();
const logVerbose = (title: string, ...info: any) => {
if (__NS_ENV_VERBOSE__) {
console.log(`[HMR][Verbose] ${title}`);
if (info?.length) {
console.log(...info);
console.log('---');
}
}
};
const setStatus = (
hash: string,
status: 'success' | 'failure',
message?: string,
...info: any
): boolean => {
// format is important - CLI expects this exact format
console.log(`[HMR][${hash}] ${status} | ${message}`);
if (info?.length) {
logVerbose('Additional Info', info);
}
// return true if operation was successful
return status === 'success';
};
const applyOptions = {
ignoreUnaccepted: false,
ignoreDeclined: false,
ignoreErrored: false,
onDeclined(info) {
setStatus(hash, 'failure', 'A module has been declined.', info);
},
onUnaccepted(info) {
setStatus(hash, 'failure', 'A module has not been accepted.', info);
},
onAccepted(info) {
// console.log('accepted', info)
logVerbose('Module Accepted', info);
},
onDisposed(info) {
// console.log('disposed', info)
logVerbose('Module Disposed', info);
},
onErrored(info) {
setStatus(hash, 'failure', 'A module has errored.', info);
},
};
const checkAndApply = async () => {
hash = __webpack_require__.h();
const modules = await module.hot.check().catch((error) => {
return setStatus(
hash,
'failure',
'Failed to check.',
error.message || error.stack
);
});
if (!modules) {
logVerbose('No modules to apply.');
return false;
}
const appliedModules = await module.hot
.apply(applyOptions)
.catch((error) => {
return setStatus(
hash,
'failure',
'Failed to apply.',
error.message || error.stack
);
});
if (!appliedModules) {
logVerbose('No modules applied.');
return false;
}
return setStatus(hash, 'success', 'Successfully applied update.');
};
const requireExists = (path) => {
try {
__non_webpack_require__(path);
return true;
} catch (err) {
return false;
}
};
const hasUpdate = () => {
return [
`~/bundle.${__webpack_hash__}.hot-update.json`,
`~/runtime.${__webpack_hash__}.hot-update.json`,
].some((path) => requireExists(path));
};
const originalOnLiveSync = global.__onLiveSync;
global.__onLiveSync = async function () {
logVerbose('LiveSync');
if (!hasUpdate()) {
return;
}
await checkAndApply();
originalOnLiveSync();
};
}

View File

@@ -0,0 +1,44 @@
import { relative, resolve } from 'path';
import dedent from 'ts-dedent';
import fs from 'fs';
// note: this will bail even if module.hot appears in a comment
const MODULE_HOT_RE = /module\.hot/;
export default function loader(content: string, map: any) {
if (MODULE_HOT_RE.test(content)) {
// Code already handles HMR - we don't need to do anything
return this.callback(null, content, map);
}
const opts = this.getOptions();
// used to inject the HMR runtime into the entry file
if (opts.injectHMRRuntime) {
const hmrRuntimePath = resolve(__dirname, './hmr.runtime.js');
const hmrRuntime = fs
.readFileSync(hmrRuntimePath)
.toString()
.split('// ---')[1]
.replace('//# sourceMappingURL=hmr.runtime.js.map', '');
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}")) {
module.hot.accept()
}
`
: ``;
const source = `${content}\n${hmrCode}`;
this.callback(null, source, map);
}

View File

@@ -0,0 +1,63 @@
const WorkerDependency = require('webpack/lib/dependencies/WorkerDependency');
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals');
/**
* Patch WorkerDependency to change:
*
* new Worker(new URL(workerPath, baseUrl))
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* to.
*
* new Worker('~/' + workerPath)
*
* Note: we are changing source **outside** of the dependency range, and this may
* break when the dependency range changes, for example if this PR is merged:
* - https://github.com/webpack/webpack/pull/12750
*/
WorkerDependency.Template.prototype.apply = function apply(
dependency,
source,
templateContext
) {
const { chunkGraph, moduleGraph, runtimeRequirements } = templateContext;
const dep = /** @type {WorkerDependency} */ dependency;
const block = /** @type {AsyncDependenciesBlock} */ moduleGraph.getParentBlock(
dependency
);
const entrypoint = /** @type {Entrypoint} */ chunkGraph.getBlockChunkGroup(
block
);
const chunk = entrypoint.getEntrypointChunk();
// runtimeRequirements.add(RuntimeGlobals.publicPath);
// runtimeRequirements.add(RuntimeGlobals.baseURI);
runtimeRequirements.add(RuntimeGlobals.getChunkScriptFilename);
/**
* new URL(
* ^^^^^^^^ = 8 characters, we subtract it from the dep.range[0]
*/
source.replace(
dep.range[0] - 8,
dep.range[1],
`/* worker import */ /* patched by nativescript-worker-loader */ '~/' + ${
RuntimeGlobals.getChunkScriptFilename
}(${JSON.stringify(chunk.id)})`
);
};
const NEW_WORKER_WITH_STRING_RE = /new\s+Worker\((['"`].+['"`])\)/;
/**
* Replaces
* new Worker('./somePath')
* with
* new Worker(new URL('./somePath', import.meta.url))
*/
export default function loader(content: string, map: any) {
const source = content.replace(
NEW_WORKER_WITH_STRING_RE,
'new Worker(new URL($1, import.meta.url))'
);
this.callback(null, source, map);
}

View File

@@ -178,11 +178,21 @@ async function parseXML(content: string): Promise<ParseResult> {
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
const hmrCode = this.hot
? dedent`
if(module.hot) {
module.hot.accept()
// module.hot.dispose(() => {})
}
`
: ``;
const code = dedent`
${moduleRegisters.join('\n')}
/* XML-NAMESPACE-LOADER */
const ___XML_NAMESPACE_LOADER_EXPORT___ = ${xml}
export default ___XML_NAMESPACE_LOADER_EXPORT___
${hmrCode}
`;
if (errors.length) {

View File

@@ -3,8 +3,13 @@ import { basename } from "path";
import { INativeScriptPlatform } from "../helpers/platform";
import { getProjectRootPath } from "../helpers/project";
function sanitizeName(appName: string): string {
return appName.split("").filter((c) =>
/[a-zA-Z0-9]/.test(c)
).join("");
}
function getDistPath() {
const appName = basename(getProjectRootPath());
const appName = sanitizeName(basename(getProjectRootPath()));
return `platforms/ios/${appName}/app`;
}

View File

@@ -34,14 +34,11 @@ export class PlatformSuffixPlugin {
}
apply(compiler: any) {
console.log(
// this.extensions,
this.platform
);
const platformRE = new RegExp(`\.${this.platform}\.`);
// 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)

View File

@@ -1,83 +0,0 @@
import webpack from 'webpack';
const id = 'WatchStateLoggerPlugin';
export enum messages {
compilationComplete = 'Webpack compilation complete.',
startWatching = 'Webpack compilation complete. Watching for file changes.',
changeDetected = 'File change detected. Starting incremental webpack compilation...',
}
/**
* This little plugin will report the webpack state through the console.
* So the {N} CLI can get some idea when compilation completes.
* @deprecated todo: remove soon
*/
export class WatchStateLoggerPlugin {
isRunningWatching: boolean;
apply(compiler) {
const plugin = this;
compiler.hooks.watchRun.tapAsync(id, function (compiler, callback) {
plugin.isRunningWatching = true;
if (plugin.isRunningWatching) {
console.log(messages.changeDetected);
}
notify(messages.changeDetected);
callback();
});
compiler.hooks.afterEmit.tapAsync(id, function (compilation, callback) {
callback();
if (plugin.isRunningWatching) {
console.log(messages.startWatching);
} else {
console.log(messages.compilationComplete);
}
const emittedFiles = Array.from(compilation.emittedAssets);
const chunkFiles = getChunkFiles(compilation);
notify(messages.compilationComplete);
// Send emitted files so they can be LiveSynced if need be
notify({ emittedFiles, chunkFiles, hash: compilation.hash });
});
}
}
function getChunkFiles(compilation: webpack.Compilation) {
const chunkFiles = [];
try {
compilation.chunks.forEach((chunk) => {
chunk.files.forEach((file) => {
if (file.indexOf('hot-update') === -1) {
chunkFiles.push(file);
}
});
});
} catch (e) {
console.log('Warning: Unable to find chunk files.');
}
return chunkFiles;
}
function notify(message: any) {
if (!process.send) {
return;
}
process.send(message, (error) => {
if (error) {
console.error(`[${id}] Process Send Error: `, error);
}
return null;
});
}

View File

@@ -1,3 +1,5 @@
import { env } from '../';
const id = 'WatchStatePlugin';
const version = 1;
@@ -8,12 +10,10 @@ export enum messages {
}
/**
* This little plugin will report the webpack state through the console.
* So the {N} CLI can get some idea when compilation completes.
* This little plugin will report the webpack state through the console
* and send status updates through IPC to the {N} CLI.
*/
export class WatchStatePlugin {
isRunningWatching: boolean;
apply(compiler: any) {
let isWatchMode = false;
let prevAssets = [];
@@ -21,8 +21,24 @@ export class WatchStatePlugin {
compiler.hooks.watchRun.tapAsync(id, function (compiler, callback) {
callback();
if (isWatchMode) {
console.log(messages.changeDetected);
if (env.verbose) {
if (compiler.modifiedFiles) {
Array.from(compiler.modifiedFiles).forEach((file) => {
console.log(`[${id}][WatchTriggers] MODIFIED: ${file}`);
});
}
if (compiler.removedFiles) {
Array.from(compiler.removedFiles).forEach((file) => {
console.log(`[${id}][WatchTriggers] REMOVED: ${file}`);
});
}
}
}
isWatchMode = true;
console.log(messages.changeDetected);
});
compiler.hooks.afterEmit.tapAsync(id, function (compilation, callback) {
@@ -53,21 +69,23 @@ export class WatchStatePlugin {
notify({
type: 'compilation',
version,
emittedAssets,
staleAssets,
hash: compilation.hash,
data: {
emittedAssets,
staleAssets,
},
});
});
}
}
function notify(message: any) {
env.verbose && console.log(`[${id}] Notify: `, message);
if (!process.send) {
return;
}
console.log(`[${id}] Notify: `, message);
process.send(message, (error) => {
if (error) {
console.error(`[${id}] Process Send Error: `, error);

View File

@@ -3,7 +3,8 @@ const webpack = require("@nativescript/webpack");
module.exports = (env) => {
webpack.init(env);
// todo: comments for common usage
// Learn how to customize:
// https://docs.nativescript.org/webpack
return webpack.resolveConfig();
};