diff --git a/.gitignore b/.gitignore index 5386492d7..9143c0795 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,10 @@ /out-tsc # dependencies -node_modules -package-lock.json -yarn.lock -pnpm-lock.yaml +**/node_modules +**/package-lock.json +**/yarn.lock +**/pnpm-lock.yaml # IDEs and editors .idea diff --git a/nx.json b/nx.json index e3adc53f7..20d0fcd77 100644 --- a/nx.json +++ b/nx.json @@ -42,6 +42,9 @@ }, "webpack": { "tags": [] + }, + "webpack5": { + "tags": [] } } } diff --git a/packages/webpack5/.gitignore b/packages/webpack5/.gitignore new file mode 100644 index 000000000..24729192e --- /dev/null +++ b/packages/webpack5/.gitignore @@ -0,0 +1,3 @@ +# +dist +coverage diff --git a/packages/webpack5/.prettierrc.json b/packages/webpack5/.prettierrc.json new file mode 100644 index 000000000..af7f59f38 --- /dev/null +++ b/packages/webpack5/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "useTabs": true, + "printWidth": 80, + "tabWidth": 2, + "singleQuote": true +} diff --git a/packages/webpack5/README.md b/packages/webpack5/README.md new file mode 100644 index 000000000..73e509ce2 --- /dev/null +++ b/packages/webpack5/README.md @@ -0,0 +1,30 @@ +@nativescript/webpack rewrite + +The rewrite allows us to simplify things, and introduce some breaking changes. +Listing them here, so we can keep track of them - will be in the merge commit, and the release notes once we are ready. + +BREAKING CHANGES: + - `package.json` main should now use a relative path to the package.json instead of the app directory + + For example (given we have a `src` directory where our app is): + + `"main": "app.js"` becomes `"main": "src/app.js"` **OR** `"main": "src/app.ts"` (whether using JS or TS) + + This simplifies things, and will allow ctrl/cmd + clicking on the filename in some editors. + + - `postinstall` scripts have been removed. + + The configuration will not need to change in the user projects between updates. + + For existing projects we will provide an easy upgrade path, through `ns migrate` and a binary in the package. + + For new projects `ns create` should create the config file by invoking a binary in the package. + + - removed resolutions for short imports - use full imports instead. + + For example: + ``` + import http from 'http' + // becomes + import { http } from '@nativescript/core' + ``` diff --git a/packages/webpack5/__tests__/cli/parseEnvFlags.spec.ts b/packages/webpack5/__tests__/cli/parseEnvFlags.spec.ts new file mode 100644 index 000000000..c04dbe57d --- /dev/null +++ b/packages/webpack5/__tests__/cli/parseEnvFlags.spec.ts @@ -0,0 +1,47 @@ +import { parseEnvFlags } from '../../src/cli/parseEnvFlags'; + +describe.only('parseEnvFlags', () => { + it('parses all possible flags', () => { + const res = parseEnvFlags([ + '--env', // invalid + '--env.foo', + '--env.externals=ext1', + '--env.externals=ext2', + '--env.externals=ext3', + '--env.externals=ext4', + '--env.externals=ext4', + '--env.externals=/path/to/a/very/long/path with spaces/foo.js', + '--env.externals=~/package.json', + '--env.externals=package.json', + '--env.ios=false', + '--env.android', + '--env.verbose', + '--env.sourceMap', + '--env.appPath=app', + '--env.appResourcesPath=App_Resources', + '--env.num=5', + '--env.float=5.4', + '--env.numArray=3', + '--env.numArray=4', + '--env.numArray=5', + '--no-hmr', + '--not-env-flag', + ]); + + expect(res).toBeDefined(); + expect(res.foo).toBe(true); + expect(res.externals).toBeInstanceOf(Array); + expect(res.externals.length).toBe(8); + expect(res.ios).toBe(false); + expect(res.android).toBe(true); + expect(res.verbose).toBe(true); + expect(res.sourceMap).toBe(true); + expect(res.sourceMap).toBe(true); + expect(res.appPath).toBe('app'); + expect(res.appResourcesPath).toBe('App_Resources'); + expect(res.num).toBe(5); + expect(res.float).toBe(5.4); + expect(res.numArray).toStrictEqual([3, 4, 5]); + expect(Object.keys(res).length).toBe(11); + }); +}); diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/angular.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/angular.spec.ts.snap new file mode 100644 index 000000000..c21a344ae --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/angular.spec.ts.snap @@ -0,0 +1,714 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`angular configuration for android 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/, + /Zone\\\\.js does not support native async\\\\/await/, + /environment.(\\\\w+).ts is part of the TypeScript compilation but it's unused/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + mainFields: [ + 'module', + 'main' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + exclude: [ + /\\\\.component\\\\.css$/ + ], + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + exclude: [ + /\\\\.component\\\\.scss$/ + ], + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('angular') */ + { + test: /(?:\\\\.ngfactory.js|\\\\.ngstyle\\\\.js|\\\\.ts)$/, + use: [ + /* config.module.rule('angular').use('@ngtools/webpack') */ + { + loader: '@ngtools/webpack' + } + ] + }, + /* config.module.rule('@angular/core') */ + { + test: /[\\\\/\\\\\\\\]@angular[\\\\/\\\\\\\\]core[\\\\/\\\\\\\\].+\\\\.js$/, + parser: { + system: true + } + }, + /* config.module.rule('html') */ + { + test: /\\\\.html$/, + use: [ + /* config.module.rule('html').use('raw-loader') */ + { + loader: 'raw-loader' + } + ] + }, + /* config.module.rule('css|component') */ + { + test: /\\\\.component\\\\.css$/, + use: [ + /* config.module.rule('css|component').use('raw-loader') */ + { + loader: 'raw-loader' + } + ] + }, + /* config.module.rule('scss|component') */ + { + test: /\\\\.component\\\\.scss$/, + use: [ + /* config.module.rule('scss|component').use('raw-loader') */ + { + loader: 'raw-loader' + }, + /* config.module.rule('scss|component').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss|component').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('AngularCompilerPlugin') */ + new AngularCompilerPlugin( + { + tsConfigPath: '__jest__/tsconfig.json', + mainPath: '__jest__/src/app.js', + platformTransformers: [ + function () { /* omitted long function */ } + ] + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity' + ] + } +}" +`; + +exports[`angular configuration for ios 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/, + /Zone\\\\.js does not support native async\\\\/await/, + /environment.(\\\\w+).ts is part of the TypeScript compilation but it's unused/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + mainFields: [ + 'module', + 'main' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + exclude: [ + /\\\\.component\\\\.css$/ + ], + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + exclude: [ + /\\\\.component\\\\.scss$/ + ], + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('angular') */ + { + test: /(?:\\\\.ngfactory.js|\\\\.ngstyle\\\\.js|\\\\.ts)$/, + use: [ + /* config.module.rule('angular').use('@ngtools/webpack') */ + { + loader: '@ngtools/webpack' + } + ] + }, + /* config.module.rule('@angular/core') */ + { + test: /[\\\\/\\\\\\\\]@angular[\\\\/\\\\\\\\]core[\\\\/\\\\\\\\].+\\\\.js$/, + parser: { + system: true + } + }, + /* config.module.rule('html') */ + { + test: /\\\\.html$/, + use: [ + /* config.module.rule('html').use('raw-loader') */ + { + loader: 'raw-loader' + } + ] + }, + /* config.module.rule('css|component') */ + { + test: /\\\\.component\\\\.css$/, + use: [ + /* config.module.rule('css|component').use('raw-loader') */ + { + loader: 'raw-loader' + } + ] + }, + /* config.module.rule('scss|component') */ + { + test: /\\\\.component\\\\.scss$/, + use: [ + /* config.module.rule('scss|component').use('raw-loader') */ + { + loader: 'raw-loader' + }, + /* config.module.rule('scss|component').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss|component').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('AngularCompilerPlugin') */ + new AngularCompilerPlugin( + { + tsConfigPath: '__jest__/tsconfig.json', + mainPath: '__jest__/src/app.js', + platformTransformers: [ + function () { /* omitted long function */ } + ] + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/base.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/base.spec.ts.snap new file mode 100644 index 000000000..781b27a7c --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/base.spec.ts.snap @@ -0,0 +1,604 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`base configuration for android 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity' + ] + } +}" +`; + +exports[`base configuration for ios 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/javascript.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/javascript.spec.ts.snap new file mode 100644 index 000000000..138492e83 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/javascript.spec.ts.snap @@ -0,0 +1,680 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`javascript configuration for android 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('xml') */ + { + test: /\\\\.xml$/, + use: [ + /* config.module.rule('xml').use('xml-namespace-loader') */ + { + loader: 'xml-namespace-loader' + } + ] + }, + /* config.module.rule('hmr-core') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/, + '__jest__/src/app.js' + ], + use: [ + /* config.module.rule('hmr-core').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + appPath: '__jest__/src' + } + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('ContextExclusionPlugin|__@nativescript_webpack_virtual_entry_javascript__') */ + new ContextExclusionPlugin( + /__@nativescript_webpack_virtual_entry_javascript__.js$/ + ), + /* config.plugin('VirtualModulesPlugin') */ + new VirtualModulesPlugin( + { + '__jest__/src/__@nativescript_webpack_virtual_entry_javascript__': '// VIRTUAL ENTRY START\\\\nrequire(\\\\'@nativescript/core/bundle-entry-points\\\\')\\\\nconst context = require.context(\\"~/\\", /* deep: */ true, /* filter: */ /.(xml|js|s?css)$/);\\\\nglobal.registerWebpackModules(context);\\\\n// VIRTUAL ENTRY END' + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity', + '__jest__/src/__@nativescript_webpack_virtual_entry_javascript__' + ] + } +}" +`; + +exports[`javascript configuration for ios 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('xml') */ + { + test: /\\\\.xml$/, + use: [ + /* config.module.rule('xml').use('xml-namespace-loader') */ + { + loader: 'xml-namespace-loader' + } + ] + }, + /* config.module.rule('hmr-core') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/, + '__jest__/src/app.js' + ], + use: [ + /* config.module.rule('hmr-core').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + appPath: '__jest__/src' + } + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('ContextExclusionPlugin|__@nativescript_webpack_virtual_entry_javascript__') */ + new ContextExclusionPlugin( + /__@nativescript_webpack_virtual_entry_javascript__.js$/ + ), + /* config.plugin('VirtualModulesPlugin') */ + new VirtualModulesPlugin( + { + '__jest__/src/__@nativescript_webpack_virtual_entry_javascript__': '// VIRTUAL ENTRY START\\\\nrequire(\\\\'@nativescript/core/bundle-entry-points\\\\')\\\\nconst context = require.context(\\"~/\\", /* deep: */ true, /* filter: */ /.(xml|js|s?css)$/);\\\\nglobal.registerWebpackModules(context);\\\\n// VIRTUAL ENTRY END' + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '__jest__/src/__@nativescript_webpack_virtual_entry_javascript__' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap new file mode 100644 index 000000000..55375a7e6 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap @@ -0,0 +1,1275 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR enabled 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src', + 'react-dom': 'react-nativescript' + }, + extensions: [ + '.android.tsx', + '.tsx', + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/, + /\\\\.tsx$/ + ], + use: [ + /* config.module.rule('ts').use('babel-loader|react-refresh') */ + { + loader: 'babel-loader', + options: { + sourceMaps: 'inline', + babelrc: false, + plugins: [ + 'react-refresh/babel' + ] + } + }, + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process', + __TEST__: false, + 'process.env.NODE_ENV': '\\"development\\"' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('HotModuleReplacementPlugin') */ + new HotModuleReplacementPlugin(), + /* config.plugin('ReactRefreshPlugin') */ + new ReactRefreshPlugin( + { + overlay: false, + forceEnable: false, + exclude: /node_modules/i, + include: /\\\\.([jt]sx?|flow)$/i + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity' + ] + } +}" +`; + +exports[`react configuration > android > base config 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src', + 'react-dom': 'react-nativescript' + }, + extensions: [ + '.android.tsx', + '.tsx', + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/, + /\\\\.tsx$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process', + __TEST__: false, + 'process.env.NODE_ENV': '\\"development\\"' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity' + ] + } +}" +`; + +exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR enabled 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src', + 'react-dom': 'react-nativescript' + }, + extensions: [ + '.ios.tsx', + '.tsx', + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/, + /\\\\.tsx$/ + ], + use: [ + /* config.module.rule('ts').use('babel-loader|react-refresh') */ + { + loader: 'babel-loader', + options: { + sourceMaps: 'inline', + babelrc: false, + plugins: [ + 'react-refresh/babel' + ] + } + }, + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process', + __TEST__: false, + 'process.env.NODE_ENV': '\\"development\\"' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('HotModuleReplacementPlugin') */ + new HotModuleReplacementPlugin(), + /* config.plugin('ReactRefreshPlugin') */ + new ReactRefreshPlugin( + { + overlay: false, + forceEnable: false, + exclude: /node_modules/i, + include: /\\\\.([jt]sx?|flow)$/i + } + ) + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; + +exports[`react configuration > ios > base config 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src', + 'react-dom': 'react-nativescript' + }, + extensions: [ + '.ios.tsx', + '.tsx', + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/, + /\\\\.tsx$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process', + __TEST__: false, + 'process.env.NODE_ENV': '\\"development\\"' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/svelte.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/svelte.spec.ts.snap new file mode 100644 index 000000000..2eda2b9bb --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/svelte.spec.ts.snap @@ -0,0 +1,654 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`svelte configuration for android 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.android.svelte', + '.svelte', + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('svelte') */ + { + test: /\\\\.svelte$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('svelte').use('svelte-loader-hot') */ + { + loader: 'svelte-loader-hot', + options: { + dev: true, + preprocess: undefined, + hotReload: true, + hotOptions: { + injectCss: false, + 'native': true + }, + onwarn: function () { /* omitted long function */ } + } + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js', + '@nativescript/core/ui/frame', + '@nativescript/core/ui/frame/activity' + ] + } +}" +`; + +exports[`svelte configuration for ios 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/ios/jest/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.ios.svelte', + '.svelte', + '.ios.ts', + '.ts', + '.ios.js', + '.js', + '.ios.css', + '.css', + '.ios.scss', + '.scss', + '.ios.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'ios' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('svelte') */ + { + test: /\\\\.svelte$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('svelte').use('svelte-loader-hot') */ + { + loader: 'svelte-loader-hot', + options: { + dev: true, + preprocess: undefined, + hotReload: true, + hotOptions: { + injectCss: false, + 'native': true + }, + onwarn: function () { /* omitted long function */ } + } + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: true, + sequences: true, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'ios' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: false, + __IOS__: true, + 'global.isAndroid': false, + 'global.isIOS': true, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin() + ], + entry: { + bundle: [ + '@nativescript/core/globals/index.js', + '__jest__/src/app.js' + ], + 'tns_modules/inspector_modules': [ + '@nativescript/core/inspector_modules' + ] + } +}" +`; diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/typescript.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/typescript.spec.ts.snap new file mode 100644 index 000000000..23d785fd1 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/__snapshots__/typescript.spec.ts.snap @@ -0,0 +1,680 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`typescript configuration for android 1`] = ` +"{ + mode: 'development', + externals: [ + 'package.json', + '~/package.json' + ], + devtool: 'inline-source-map', + target: 'node', + watchOptions: { + ignored: [ + '__jest__/platforms/**', + '__jest__/App_Resources/**' + ] + }, + ignoreWarnings: [ + /System.import\\\\(\\\\) is deprecated/ + ], + output: { + path: '__jest__/platforms/android/app/src/main/assets/app', + pathinfo: false, + publicPath: '', + libraryTarget: 'commonjs', + globalObject: 'global', + clean: true + }, + resolve: { + symlinks: true, + alias: { + '~': '__jest__/src', + '@': '__jest__/src' + }, + extensions: [ + '.android.ts', + '.ts', + '.android.js', + '.js', + '.android.css', + '.css', + '.android.scss', + '.scss', + '.android.json', + '.json' + ], + modules: [ + '__jest__/node_modules', + 'node_modules' + ] + }, + resolveLoader: { + modules: [ + '__jest__/node_modules/@nativescript/webpack/dist/loaders', + '__jest__/node_modules', + 'node_modules' + ] + }, + module: { + rules: [ + /* config.module.rule('bundle') */ + { + enforce: 'post', + test: '__jest__/src/app.js', + use: [ + /* config.module.rule('bundle').use('app-css-loader') */ + { + loader: 'app-css-loader', + options: { + platform: 'android' + } + }, + /* config.module.rule('bundle').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + injectHMRRuntime: true + } + } + ] + }, + /* config.module.rule('ts') */ + { + test: [ + /\\\\.ts$/ + ], + use: [ + /* config.module.rule('ts').use('ts-loader') */ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false + }, + getCustomTransformers: function () { /* omitted long function */ } + } + } + ] + }, + /* config.module.rule('js') */ + { + test: /\\\\.js$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('js').use('babel-loader') */ + { + loader: 'babel-loader', + options: { + generatorOpts: { + compact: false + } + } + } + ] + }, + /* config.module.rule('workers') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/ + ], + use: [ + /* config.module.rule('workers').use('nativescript-worker-loader') */ + { + loader: 'nativescript-worker-loader' + } + ] + }, + /* config.module.rule('css') */ + { + test: /\\\\.css$/, + use: [ + /* config.module.rule('css').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('css').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('css').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + } + ] + }, + /* config.module.rule('scss') */ + { + test: /\\\\.scss$/, + use: [ + /* config.module.rule('scss').use('apply-css-loader') */ + { + loader: 'apply-css-loader' + }, + /* config.module.rule('scss').use('css2json-loader') */ + { + loader: 'css2json-loader' + }, + /* config.module.rule('scss').use('postcss-loader') */ + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'postcss-import' + ] + } + } + }, + /* config.module.rule('scss').use('sass-loader') */ + { + loader: 'sass-loader' + } + ] + }, + /* config.module.rule('xml') */ + { + test: /\\\\.xml$/, + use: [ + /* config.module.rule('xml').use('xml-namespace-loader') */ + { + loader: 'xml-namespace-loader' + } + ] + }, + /* config.module.rule('hmr-core') */ + { + test: /\\\\.(js|ts)$/, + exclude: [ + /node_modules/, + '__jest__/src/app.js' + ], + use: [ + /* config.module.rule('hmr-core').use('nativescript-hot-loader') */ + { + loader: 'nativescript-hot-loader', + options: { + appPath: '__jest__/src' + } + } + ] + } + ] + }, + optimization: { + splitChunks: { + cacheGroups: { + defaultVendor: { + test: /[\\\\\\\\/]node_modules[\\\\\\\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all' + } + } + }, + minimizer: [ + /* config.optimization.minimizer('TerserPlugin') */ + new TerserPlugin( + { + terserOptions: { + compress: { + collapse_vars: false, + sequences: false, + keep_infinity: true, + drop_console: false, + global_defs: { + __UGLIFIED__: true + } + }, + keep_fnames: true, + keep_classnames: true + } + } + ) + ] + }, + plugins: [ + /* config.plugin('ForkTsCheckerWebpackPlugin') */ + new ForkTsCheckerWebpackPlugin( + { + typescript: { + memoryLimit: 4096 + } + } + ), + /* config.plugin('PlatformSuffixPlugin') */ + new PlatformSuffixPlugin( + { + platform: 'android' + } + ), + /* config.plugin('ContextExclusionPlugin|App_Resources') */ + new ContextExclusionPlugin( + /(.*)App_Resources(.*)/ + ), + /* config.plugin('DefinePlugin') */ + new DefinePlugin( + { + __DEV__: true, + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: false, + __NS_DEV_HOST_IPS__: '[\\"127.0.0.1\\",\\"192.168.0.10\\"]', + __CSS_PARSER__: '\\"css-tree\\"', + __ANDROID__: true, + __IOS__: false, + 'global.isAndroid': true, + 'global.isIOS': false, + process: 'global.process' + } + ), + /* config.plugin('CopyWebpackPlugin') */ + new CopyPlugin( + { + patterns: [ + { + from: 'assets/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: 'fonts/**', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + }, + { + from: '**/*.+(jpg|png)', + context: '__jest__/src', + noErrorOnMissing: true, + globOptions: { + dot: false, + ignore: [] + } + } + ] + } + ), + /* config.plugin('WatchStatePlugin') */ + new WatchStatePlugin(), + /* config.plugin('ContextExclusionPlugin|__@nativescript_webpack_virtual_entry_typescript__') */ + new ContextExclusionPlugin( + /__@nativescript_webpack_virtual_entry_typescript__.js$/ + ), + /* config.plugin('VirtualModulesPlugin') */ + new VirtualModulesPlugin( + { + '__jest__/src/__@nativescript_webpack_virtual_entry_typescript__': '// VIRTUAL ENTRY START\\\\nrequire(\\\\'@nativescript/core/bundle-entry-points\\\\')\\\\nconst context = require.context(\\"~/\\", /* deep: */ true, /* filter: */ /\\\\\\\\.(xml|js|(? { + class AngularCompilerPlugin {} + + return { + AngularCompilerPlugin, + }; + }, + { virtual: true } +); + +describe('angular configuration', () => { + const platforms = ['ios', 'android']; + let fsExistsSyncSpy: jest.SpiedFunction; + + beforeAll(() => { + const fs = require('fs'); + const original = fs.existsSync; + fsExistsSyncSpy = jest.spyOn(fs, 'existsSync'); + + fsExistsSyncSpy.mockImplementation((path) => { + if (path === '__jest__/tsconfig.json') { + return true; + } + return original.call(fs, path); + }); + }); + + afterAll(() => { + fsExistsSyncSpy.mockRestore(); + }); + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(angular(new Config()).toString()).toMatchSnapshot(); + }); + } +}); diff --git a/packages/webpack5/__tests__/configuration/base.spec.ts b/packages/webpack5/__tests__/configuration/base.spec.ts new file mode 100644 index 000000000..fd33bb7c2 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/base.spec.ts @@ -0,0 +1,103 @@ +import Config from 'webpack-chain'; +import fs from 'fs'; + +import base from '../../src/configuration/base'; +import { init } from '../../src'; +import { applyFileReplacements } from "../../src/helpers/fileReplacements"; +import { additionalCopyRules } from "../../src/helpers/copyRules"; + +describe('base configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(base(new Config()).toString()).toMatchSnapshot(); + }); + } + + it('supports dotenv', () => { + const fsSpy = jest.spyOn(fs, "existsSync") + fsSpy.mockReturnValue(true) + + init({ + ios: true, + }); + const config = base(new Config()); + + expect(config.plugin('DotEnvPlugin')).toBeDefined(); + config.plugin('DotEnvPlugin').tap((args) => { + expect(args[0].path).toEqual('__jest__/.env'); + return args; + }); + + fsSpy.mockRestore() + }); + + it('supports env specific dotenv', () => { + const fsSpy = jest.spyOn(fs, "existsSync") + fsSpy.mockReturnValue(true) + + init({ + ios: true, + env: 'prod', + }); + const config = base(new Config()); + + expect(fsSpy).toHaveBeenCalledWith('__jest__/.env.prod') + expect(fsSpy).toHaveBeenCalledTimes(1) + expect(config.plugin('DotEnvPlugin')).toBeDefined(); + config.plugin('DotEnvPlugin').tap((args) => { + expect(args[0].path).toEqual('__jest__/.env.prod'); + return args; + }); + fsSpy.mockRestore() + }); + + it('falls back to default .env', () => { + const fsSpy = jest.spyOn(fs, "existsSync") + fsSpy + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) + + + init({ + ios: true, + env: 'prod', + }); + const config = base(new Config()); + + expect(fsSpy).toHaveBeenCalledWith('__jest__/.env.prod') + expect(fsSpy).toHaveBeenCalledWith('__jest__/.env') + expect(fsSpy).toHaveBeenCalledTimes(2) + expect(config.plugin('DotEnvPlugin')).toBeDefined(); + config.plugin('DotEnvPlugin').tap((args) => { + expect(args[0].path).toEqual('__jest__/.env'); + return args; + }); + fsSpy.mockRestore() + }); + + it('applies file replacements', () => { + const config = new Config(); + applyFileReplacements(config, { + // should apply as an alias + 'foo.ts': 'foo.replaced.ts', + 'bar.js': 'bar.replaced.js', + + // should apply as a file replacement using the copy plugin + 'foo.json': 'foo.replaced.json' + }) + + expect(config.resolve.alias.get('foo.ts')).toBe('foo.replaced.ts') + expect(config.resolve.alias.get('bar.js')).toBe('bar.replaced.js') + expect(additionalCopyRules.length).toBe(1) + expect(additionalCopyRules[0]).toEqual({ + from: 'foo.replaced.json', + to: 'foo.json', + force: true, + }) + }) +}); diff --git a/packages/webpack5/__tests__/configuration/javascript.spec.ts b/packages/webpack5/__tests__/configuration/javascript.spec.ts new file mode 100644 index 000000000..7b026f82e --- /dev/null +++ b/packages/webpack5/__tests__/configuration/javascript.spec.ts @@ -0,0 +1,17 @@ +import Config from 'webpack-chain'; + +import javascript from '../../src/configuration/javascript'; +import { init } from '../../src'; + +describe('javascript configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(javascript(new Config()).toString()).toMatchSnapshot(); + }); + } +}); diff --git a/packages/webpack5/__tests__/configuration/react.spec.ts b/packages/webpack5/__tests__/configuration/react.spec.ts new file mode 100644 index 000000000..74a10d111 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/react.spec.ts @@ -0,0 +1,27 @@ +import Config from 'webpack-chain'; + +import react from '../../src/configuration/react'; +import { init } from '../../src'; + +describe('react configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + describe(`> ${platform} >`, () => { + it(`base config`, () => { + init({ + [platform]: true, + }); + expect(react(new Config()).toString()).toMatchSnapshot(); + }); + + it(`adds ReactRefreshWebpackPlugin when HMR enabled`, () => { + init({ + [platform]: true, + hmr: true, + }); + expect(react(new Config()).toString()).toMatchSnapshot(); + }); + }); + } +}); diff --git a/packages/webpack5/__tests__/configuration/svelte.spec.ts b/packages/webpack5/__tests__/configuration/svelte.spec.ts new file mode 100644 index 000000000..cf89ba9f1 --- /dev/null +++ b/packages/webpack5/__tests__/configuration/svelte.spec.ts @@ -0,0 +1,21 @@ +import Config from 'webpack-chain'; + +import svelte from '../../src/configuration/svelte'; +import { init } from '../../src'; + +jest.mock('__jest__/svelte.config.js', () => { + +}, { virtual: true }) + +describe('svelte configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(svelte(new Config()).toString()).toMatchSnapshot(); + }); + } +}); diff --git a/packages/webpack5/__tests__/configuration/typescript.spec.ts b/packages/webpack5/__tests__/configuration/typescript.spec.ts new file mode 100644 index 000000000..0f7e0950f --- /dev/null +++ b/packages/webpack5/__tests__/configuration/typescript.spec.ts @@ -0,0 +1,17 @@ +import Config from 'webpack-chain'; + +import typescript from '../../src/configuration/typescript'; +import { init } from '../../src'; + +describe('typescript configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(typescript(new Config()).toString()).toMatchSnapshot(); + }); + } +}); diff --git a/packages/webpack5/__tests__/configuration/vue.spec.ts b/packages/webpack5/__tests__/configuration/vue.spec.ts new file mode 100644 index 000000000..2234ab7bb --- /dev/null +++ b/packages/webpack5/__tests__/configuration/vue.spec.ts @@ -0,0 +1,17 @@ +import Config from 'webpack-chain'; + +import vue from '../../src/configuration/vue'; +import { init } from '../../src'; + +describe('vue configuration', () => { + const platforms = ['ios', 'android']; + + for (let platform of platforms) { + it(`for ${platform}`, () => { + init({ + [platform]: true, + }); + expect(vue(new Config()).toString()).toMatchSnapshot(); + }); + } +}); diff --git a/packages/webpack5/__tests__/helpers/fileReplacements.spec.ts b/packages/webpack5/__tests__/helpers/fileReplacements.spec.ts new file mode 100644 index 000000000..621348d9f --- /dev/null +++ b/packages/webpack5/__tests__/helpers/fileReplacements.spec.ts @@ -0,0 +1,86 @@ +import { getFileReplacementsFromEnv } from '../../src/helpers/fileReplacements'; + +describe('getFileReplacementsFromEnv', () => { + it('handles no replacements', () => { + const res = getFileReplacementsFromEnv({}); + expect(res).toEqual({}); + }); + + it('ignores invalid env', () => { + const res = getFileReplacementsFromEnv({ + // @ts-ignore + replace: {}, + }); + expect(res).toEqual({}); + }); + + it('resolves replacements relative to the project root', () => { + const res = getFileReplacementsFromEnv({ + replace: './src/foo.ts:./src/bar.ts', + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(1); + expect(entries[0]).toEqual(['__jest__/src/foo.ts', '__jest__/src/bar.ts']); + }); + + it('ignores invalid replacements', () => { + const res = getFileReplacementsFromEnv({ + replace: ['one', 'two:', 'three:four'], + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(1); + expect(entries[0]).toEqual(['__jest__/three', '__jest__/four']); + }); + + it('can parse replacements from a string', () => { + const res = getFileReplacementsFromEnv({ + replace: 'one:two', + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(1); + expect(entries[0]).toEqual(['__jest__/one', '__jest__/two']); + }); + + it('can parse multiple replacements from a string', () => { + const res = getFileReplacementsFromEnv({ + replace: 'one:two,three:four', + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(2); + expect(entries).toEqual([ + ['__jest__/one', '__jest__/two'], + ['__jest__/three', '__jest__/four'], + ]); + }); + + it('can parse replacements from an array', () => { + const res = getFileReplacementsFromEnv({ + replace: ['one:two', 'three:four'], + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(2); + expect(entries).toEqual([ + ['__jest__/one', '__jest__/two'], + ['__jest__/three', '__jest__/four'], + ]); + }); + + it('can parse multiple replacements from an array', () => { + const res = getFileReplacementsFromEnv({ + replace: ['one:two,three:four', 'five:six'], + }); + const entries = Object.entries(res); + expect(res).toBeDefined(); + expect(entries.length).toBe(3); + expect(entries).toEqual([ + ['__jest__/one', '__jest__/two'], + ['__jest__/three', '__jest__/four'], + ['__jest__/five', '__jest__/six'], + ]); + }); +}); diff --git a/packages/webpack5/__tests__/index.spec.ts b/packages/webpack5/__tests__/index.spec.ts new file mode 100644 index 000000000..40931b186 --- /dev/null +++ b/packages/webpack5/__tests__/index.spec.ts @@ -0,0 +1,141 @@ +describe('@nativescript/webpack', () => { + let webpack: typeof import('../src'); + + beforeEach(() => { + jest.resetModules(); + webpack = require('../src'); + }); + + it('exports the public api', () => { + expect(webpack.init).toBeInstanceOf(Function); + expect(webpack.useConfig).toBeInstanceOf(Function); + expect(webpack.chainWebpack).toBeInstanceOf(Function); + expect(webpack.mergeWebpack).toBeInstanceOf(Function); + expect(webpack.resolveChainableConfig).toBeInstanceOf(Function); + expect(webpack.resolveConfig).toBeInstanceOf(Function); + }); + + it('applies chain configs', () => { + webpack.useConfig(false); + + const chainFn = jest.fn(); + webpack.chainWebpack(chainFn); + + // chainFn should not be called yet + expect(chainFn).not.toHaveBeenCalled(); + + // chainFn should only be called when + // resolving a chainable config + const config = webpack.resolveChainableConfig(); + + expect(chainFn).toHaveBeenCalledTimes(1); + expect(chainFn).toHaveBeenCalledWith(config, {}); + }); + + it('applies chain configs in the right order', () => { + webpack.useConfig(false); + let lastCalled = false; + + // this is registered before chainFnNormal + // however, should be called after chainFnNormal + const chainFnLast = jest.fn((config) => { + lastCalled = true; + expect(config.normal).toBe(true); + }); + webpack.chainWebpack(chainFnLast, { order: 10 }); + + const chainFnNormal = jest.fn((config) => { + config.normal = true; + + // chainFnLast should not have been called yet + expect(lastCalled).toBe(false); + }); + webpack.chainWebpack(chainFnNormal); + + webpack.resolveChainableConfig(); + }); + + it('prints plugin name that has a chain function that throws an error', () => { + webpack.useConfig(false); + webpack.setCurrentPlugin('test-plugin'); + const chainFn = jest.fn(() => { + throw new Error('something wrong'); + }); + webpack.chainWebpack(chainFn); + + // should not throw + expect(() => webpack.resolveChainableConfig()).not.toThrow(); + + expect( + 'Unable to apply chain function from: test-plugin' + ).toHaveBeenWarned(); + }); + + it('applies merge configs', () => { + const dummyEnv = { foo: true }; + webpack.init(dummyEnv); + webpack.useConfig(false); + + const mergeFn = jest.fn(); + webpack.mergeWebpack(mergeFn); + + // mergeFn should not be called yet + expect(mergeFn).not.toHaveBeenCalled(); + + const config = webpack.resolveChainableConfig(); + + // mergeFn should not be called yet + expect(mergeFn).not.toHaveBeenCalled(); + + // mergeFn should only be called when + // resolving the final config + webpack.resolveConfig(); + + expect(mergeFn).toHaveBeenCalledTimes(1); + expect(mergeFn).toHaveBeenCalledWith(config.toConfig(), dummyEnv); + }); + + it('merges mutate config', () => { + const dummyEnv = { foo: true }; + webpack.init(dummyEnv); + webpack.useConfig(false); + + webpack.mergeWebpack((config) => { + (config as any).mutated = true; + }); + + expect(webpack.resolveConfig()).toMatchObject({ + mutated: true, + }); + }); + + it('merges returned config', () => { + const dummyEnv = { foo: true }; + webpack.init(dummyEnv); + webpack.useConfig(false); + + webpack.mergeWebpack(() => { + return { + returned: true, + }; + }); + + expect(webpack.resolveConfig()).toMatchObject({ + returned: true, + }); + }); + + it('merges objects', () => { + const dummyEnv = { foo: true }; + webpack.init(dummyEnv); + webpack.useConfig(false); + + webpack.mergeWebpack({ + object: true, + } as any); + + expect(webpack.resolveConfig()).toMatchObject({ + object: true, + }); + }); +}); diff --git a/packages/webpack5/__tests__/loaders/xml-namespace-loader.spec.ts b/packages/webpack5/__tests__/loaders/xml-namespace-loader.spec.ts new file mode 100644 index 000000000..adcde7dc9 --- /dev/null +++ b/packages/webpack5/__tests__/loaders/xml-namespace-loader.spec.ts @@ -0,0 +1,426 @@ +import dedent from 'ts-dedent'; + +import xmlNsLoader from '../../src/loaders/xml-namespace-loader'; + +const CODE_FILE = dedent` + + + + + + + + + + +`; + +interface TestSetup { + resolveMap: { [path: string]: string }; + expectedDeps: string[]; + expectedRegs: { name: string; path: string }[]; + ignore?: RegExp; + assureNoDeps?: boolean; + expectError?: boolean; + expectWarnings?: number; +} + +function getContext( + done, + { + resolveMap, + expectedDeps, + expectedRegs, + assureNoDeps, + ignore, + expectError, + expectWarnings, + }: TestSetup +) { + const actualDeps: string[] = []; + const actualWarnings: Error[] = []; + let callbackCalled = false; + + return { + rootContext: 'app', + context: 'app/component', + async: () => (error, source: string) => { + if (callbackCalled) { + done.fail('Callback called more than once!'); + } + callbackCalled = true; + + expectedDeps.forEach((expectedDep) => { + expect(actualDeps).toContain(expectedDep); + }); + + expectedRegs.forEach(({ name, path }) => { + expect(source).toContain(dedent` + global.registerModule( + '${name}', + () => require("${path}") + ) + `); + }); + + if (assureNoDeps) { + expect(actualDeps.length).toBe(0); + expect(source).not.toContain('global.registerModule'); + } + + if (expectWarnings) { + expect(actualWarnings.length).toEqual(expectWarnings); + } + + if (error && !expectError) { + done.fail(error); + } else if (!error && expectError) { + done.fail('Error expected here'); + } else { + done(); + } + }, + resolve: ( + context: string, + request: string, + callback: (err: Error, result: string) => void + ) => { + request = request.replace(/\\/g, '/'); + if (resolveMap[request]) { + callback(undefined, resolveMap[request]); + } else { + callback(new Error(`Module ${request} not found`), undefined); + } + }, + addDependency: (dep: string) => { + actualDeps.push(dep); + }, + emitWarning: (err: Error) => { + actualWarnings.push(err); + }, + emitError: (err: Error) => { + //actualWarnings.push(err); + }, + query: { ignore }, + }; +} + +describe('xml-namespace-loader', () => { + it('with namespace pointing to files', (done) => { + const resolveMap = { + 'app/nativescript-ui-chart': 'app/nativescript-ui-chart.js', + 'app/nativescript-ui-chart.xml': 'app/nativescript-ui-chart.xml', + 'app/nativescript-ui-chart.css': 'app/nativescript-ui-chart.css', + }; + + const expectedDeps = [ + 'app/nativescript-ui-chart.js', + 'app/nativescript-ui-chart.xml', + 'app/nativescript-ui-chart.css', + ]; + + const expectedRegs = [ + { name: 'nativescript-ui-chart', path: 'app/nativescript-ui-chart.js' }, + { + name: 'nativescript-ui-chart/RadCartesianChart', + path: 'app/nativescript-ui-chart.js', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.xml', + path: 'app/nativescript-ui-chart.xml', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.css', + path: 'app/nativescript-ui-chart.css', + }, + ]; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + }); + + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with namespace/elementName pointing to files (with package.json)', (done) => { + const resolveMap = { + 'app/nativescript-ui-chart': + 'app/nativescript-ui-chart/RadCartesianChart.js', //simulate package.json + 'app/nativescript-ui-chart/RadCartesianChart': + 'app/nativescript-ui-chart/RadCartesianChart.js', + 'app/nativescript-ui-chart/RadCartesianChart.xml': + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css': + 'app/nativescript-ui-chart/RadCartesianChart.css', + }; + + const expectedDeps = [ + 'app/nativescript-ui-chart/RadCartesianChart.js', + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css', + ]; + + const expectedRegs = [ + { + name: 'nativescript-ui-chart', + path: 'app/nativescript-ui-chart/RadCartesianChart.js', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart', + path: 'app/nativescript-ui-chart/RadCartesianChart.js', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.xml', + path: 'app/nativescript-ui-chart/RadCartesianChart.xml', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.css', + path: 'app/nativescript-ui-chart/RadCartesianChart.css', + }, + ]; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with namespace/elementName pointing to files', (done) => { + const resolveMap = { + 'app/nativescript-ui-chart/RadCartesianChart': + 'app/nativescript-ui-chart/RadCartesianChart.js', + 'app/nativescript-ui-chart/RadCartesianChart.xml': + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css': + 'app/nativescript-ui-chart/RadCartesianChart.css', + }; + + const expectedDeps = [ + 'app/nativescript-ui-chart/RadCartesianChart.js', + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css', + ]; + + const expectedRegs = [ + { + name: 'nativescript-ui-chart', + path: 'app/nativescript-ui-chart/RadCartesianChart.js', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart', + path: 'app/nativescript-ui-chart/RadCartesianChart.js', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.xml', + path: 'app/nativescript-ui-chart/RadCartesianChart.xml', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.css', + path: 'app/nativescript-ui-chart/RadCartesianChart.css', + }, + ]; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with namespace/elementName pointing to files - only XML and CSS', (done) => { + const resolveMap = { + 'app/nativescript-ui-chart/RadCartesianChart.xml': + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css': + 'app/nativescript-ui-chart/RadCartesianChart.css', + }; + + const expectedDeps = [ + 'app/nativescript-ui-chart/RadCartesianChart.xml', + 'app/nativescript-ui-chart/RadCartesianChart.css', + ]; + + const expectedRegs = [ + { + name: 'nativescript-ui-chart/RadCartesianChart.xml', + path: 'app/nativescript-ui-chart/RadCartesianChart.xml', + }, + { + name: 'nativescript-ui-chart/RadCartesianChart.css', + path: 'app/nativescript-ui-chart/RadCartesianChart.css', + }, + ]; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with plugin path', (done) => { + const resolveMap = { + 'nativescript-ui-chart': 'node_modules/nativescript-ui-chart/ui-chart.js', + }; + + const expectedDeps = []; + + const expectedRegs = [ + { name: 'nativescript-ui-chart', path: 'nativescript-ui-chart' }, + { + name: 'nativescript-ui-chart/RadCartesianChart', + path: 'nativescript-ui-chart', + }, + ]; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with ignored namespace should not add deps or register calls', (done) => { + const resolveMap = { + 'app/nativescript-ui-chart': 'app/nativescript-ui-chart.js', + 'app/nativescript-ui-chart.xml': 'app/nativescript-ui-chart.xml', + 'app/nativescript-ui-chart.css': 'app/nativescript-ui-chart.css', + }; + const expectedDeps = []; + const expectedRegs = []; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + ignore: /nativescript-ui-chart/, + assureNoDeps: true, + }); + + xmlNsLoader.call(loaderContext, CODE_FILE); + }); + + it('with XML declaration and Doctype does not fail', (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = dedent` + + + + + `; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + assureNoDeps: true, + }); + + xmlNsLoader.call(loaderContext, testXml); + }); + + it('with invalid XML fails', (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ``; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + expectError: true, + }); + + xmlNsLoader.call(loaderContext, testXml); + }); + + it("doesn't throw with ios and android platform namespaces", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = dedent` + + + + + + + `; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + assureNoDeps: true, + }); + + xmlNsLoader.call(loaderContext, testXml); + }); + + it('throws with unbound namespace namespaces', (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ` + + + + `; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + expectError: true, + }); + + xmlNsLoader.call(loaderContext, testXml); + }); + + it("with '&&', '||', '<=' and '>=' in binding expression, emits warnings, but does not fail", (done) => { + const resolveMap = { + 'nativescript-ui-chart': 'node_modules/nativescript-ui-chart/ui-chart.js', + }; + + const expectedDeps = []; + + const expectedRegs = [ + { name: 'nativescript-ui-chart', path: 'nativescript-ui-chart' }, + { + name: 'nativescript-ui-chart/RadCartesianChart', + path: 'nativescript-ui-chart', + }, + ]; + + const testXml = ` + + + + + + `; + + const loaderContext = getContext(done, { + resolveMap, + expectedDeps, + expectedRegs, + expectWarnings: 1, + }); + + xmlNsLoader.call(loaderContext, testXml); + }); +}); diff --git a/packages/webpack5/jest.config.js b/packages/webpack5/jest.config.js new file mode 100644 index 000000000..ace328d00 --- /dev/null +++ b/packages/webpack5/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^@nativescript/webpack$': '/src' + }, + setupFiles: [ + '/scripts/jest.setup.ts' + ], + setupFilesAfterEnv: [ + '/scripts/jest.mockWarn.ts', + '/scripts/jest.copyRules.ts' + ], + globals: { + __TEST__: true, + } +}; diff --git a/packages/webpack5/package.json b/packages/webpack5/package.json new file mode 100644 index 000000000..b6b562104 --- /dev/null +++ b/packages/webpack5/package.json @@ -0,0 +1,77 @@ +{ + "name": "@nativescript/webpack", + "version": "5.0.0-dev", + "private": true, + "main": "dist/index.js", + "files": [ + "dist" + ], + "bin": { + "nativescript-webpack": "dist/bin/index.js" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc --project tsconfig.build.json", + "test": "jest", + "copy-stubs": "mkdirp dist/stubs && cp -R src/stubs/* dist/stubs", + "prepack": "npm test && npm run build && npm run copy-stubs && chmod +x dist/bin/index.js" + }, + "dependencies": { + "@babel/core": "7.13.14", + "@pmmmwh/react-refresh-webpack-plugin": "0.4.3", + "babel-loader": "8.2.2", + "chalk": "4.1.0", + "cli-highlight": "2.1.11", + "commander": "7.2.0", + "copy-webpack-plugin": "8.1.0", + "css": "3.0.0", + "css-loader": "5.2.0", + "dotenv-webpack": "7.0.2", + "fork-ts-checker-webpack-plugin": "6.2.0", + "loader-utils": "2.0.0", + "lodash.get": "4.4.2", + "micromatch": "4.0.2", + "postcss": "8.2.8", + "postcss-import": "14.0.0", + "postcss-loader": "5.2.0", + "raw-loader": "4.0.2", + "react-refresh": "0.10.0", + "sass": "1.32.8", + "sass-loader": "11.0.1", + "sax": "1.2.4", + "source-map": "0.7.3", + "terser-webpack-plugin": "5.1.1", + "ts-dedent": "2.1.0", + "ts-loader": "8.1.0", + "vue-loader": "15.9.6", + "webpack": "5.28.0", + "webpack-bundle-analyzer": "4.4.0", + "webpack-chain": "6.5.1", + "webpack-cli": "4.6.0", + "webpack-merge": "5.7.3", + "webpack-virtual-modules": "0.4.2" + }, + "devDependencies": { + "@types/lodash.get": "4.4.6", + "@types/sax": "1.2.1", + "@types/css": "0.0.31", + "@types/jest": "26.0.22", + "@types/loader-utils": "2.0.2", + "@types/micromatch": "4.0.1", + "@types/terser-webpack-plugin": "5.0.3", + "@types/webpack-virtual-modules": "0.1.1", + "jest": "26.6.3", + "jest-matcher-utils": "26.6.2", + "nativescript-vue-template-compiler": "2.8.4", + "ts-jest": "26.5.4", + "typescript": "4.2.3" + }, + "peerDependencies": { + "nativescript-vue-template-compiler": "^2.8.1" + }, + "peerDependenciesMeta": { + "nativescript-vue-template-compiler": { + "optional": true + } + } +} diff --git a/packages/webpack5/scripts/jest.copyRules.ts b/packages/webpack5/scripts/jest.copyRules.ts new file mode 100644 index 000000000..672d195b8 --- /dev/null +++ b/packages/webpack5/scripts/jest.copyRules.ts @@ -0,0 +1,7 @@ +import { copyRules, additionalCopyRules } from '../src/helpers/copyRules'; + +afterEach(() => { + // Clear copy rules + copyRules.clear(); + additionalCopyRules.length = 0 +}); diff --git a/packages/webpack5/scripts/jest.globals.d.ts b/packages/webpack5/scripts/jest.globals.d.ts new file mode 100644 index 000000000..ede612063 --- /dev/null +++ b/packages/webpack5/scripts/jest.globals.d.ts @@ -0,0 +1,8 @@ +// define test-specific globals here + +declare namespace jest { + interface Matchers { + toHaveBeenWarned(): R; + toHaveBeenPrinted(): R; + } +} diff --git a/packages/webpack5/scripts/jest.mockWarn.ts b/packages/webpack5/scripts/jest.mockWarn.ts new file mode 100644 index 000000000..161ad31ce --- /dev/null +++ b/packages/webpack5/scripts/jest.mockWarn.ts @@ -0,0 +1,64 @@ +import { printExpected, printReceived } from 'jest-matcher-utils'; +import dedent from 'ts-dedent'; + +expect.extend({ + toHaveBeenWarned(received: string) { + asserted.add(received); + const passed = warnSpy.mock.calls + .map((args) => args[1]) + .some((arg) => arg.indexOf(received) > -1); + if (passed) { + return { + pass: true, + message() { + return `expected ${printReceived(received)} not to have been warned`; + }, + }; + } + + const warnings = warnSpy.mock.calls.map((args) => args[1]).join('\n\n'); + return { + pass: false, + message() { + return dedent` + expected ${printExpected(received)} to have been warned. + + Actual warnings: + + ${warnings} + `; + }, + }; + }, +}); + +let warnSpy: any; +let asserted = new Set([]); +beforeEach(() => { + asserted.clear(); + + warnSpy = jest.spyOn(console, 'warn'); + + warnSpy.mockImplementation(() => {}); +}); + +afterEach(() => { + const assertedArray = Array.from(asserted); + const nonAssertedWarns = warnSpy.mock.calls + .map((args) => args[1]) + .filter((received) => { + return !assertedArray.some((assertedMessage) => { + return received.indexOf(assertedMessage) > -1; + }); + }); + + warnSpy.mockRestore(); + + if (nonAssertedWarns.length) { + throw new Error(dedent` + Test case printed unexpected warnings: + + ${printReceived(nonAssertedWarns.join('\n\n'))} + `); + } +}); diff --git a/packages/webpack5/scripts/jest.setup.ts b/packages/webpack5/scripts/jest.setup.ts new file mode 100644 index 000000000..65a90cfae --- /dev/null +++ b/packages/webpack5/scripts/jest.setup.ts @@ -0,0 +1,100 @@ +// we are mocking the cwd for the tests, since webpack needs absolute paths +// and we don't want them in tests +process.cwd = () => '__jest__'; + +jest.mock('cosmiconfig', () => ({ + cosmiconfigSync(moduleName) { + return { + search() { + // no-op in tests + return null; + }, + }; + }, +})); + +jest.mock('../src/helpers/config.ts', () => ({ + getValue(key, defaultValue) { + return defaultValue; + }, +})); + +jest.mock('os', () => { + const os = jest.requireActual('os'); + + return { + ...os, + networkInterfaces() { + return { + in0: [ + { + address: '127.0.0.1', + family: 'IPv4', + }, + { + address: 'in0-ipv6-should-not-use', + family: 'IPv6', + }, + ], + in1: [ + { + address: '192.168.0.10', + family: 'IPv4', + }, + { + address: 'in1-ipv6-should-not-use', + family: 'IPv6', + }, + ], + }; + }, + }; +}); + +jest.mock('path', () => { + const path = jest.requireActual('path'); + return { + ...path, + resolve(...args) { + if (args[0] === '__jest__') { + return path.join(...args.filter(Boolean)); + } + + const resolved = path.resolve(...args); + if (resolved.includes('__jest__')) { + const li = resolved.lastIndexOf('__jest__'); + return resolved.substr(li); + } + + // handle resolutions with __dirname + // used in base config's resolveLoader + const root = path.resolve(__dirname, '..'); + if (resolved.startsWith(root)) { + const newPath = resolved.replace(root, '__jest__'); + + if (newPath.startsWith('__jest__/src')) { + return newPath.replace( + '__jest__/src', + '__jest__/node_modules/@nativescript/webpack/dist' + ); + } + + return newPath; + } + + return resolved; + }, + }; +}); + +// a virtual mock for package.json +jest.mock( + '__jest__/package.json', + () => ({ + main: 'src/app.js', + devDependencies: { + typescript: '*', + }, + }), + { virtual: true } +); diff --git a/packages/webpack5/src/bin/devServer.ts b/packages/webpack5/src/bin/devServer.ts new file mode 100644 index 000000000..22dd01e44 --- /dev/null +++ b/packages/webpack5/src/bin/devServer.ts @@ -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) +// } +// diff --git a/packages/webpack5/src/bin/index.ts b/packages/webpack5/src/bin/index.ts new file mode 100644 index 000000000..fa530ffe9 --- /dev/null +++ b/packages/webpack5/src/bin/index.ts @@ -0,0 +1,122 @@ +#!/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' +); +const tag = `[${green('@nativescript/webpack')}]`; + +function error(message: string) { + console.error(`${tag} ${redBright(dedent(message))}`); +} + +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.') + .action(() => { + const targetPath = path.resolve(process.cwd(), 'webpack.config.js'); + + if (fs.existsSync(targetPath)) { + return error(`File Already Exists: ${targetPath}`); + } + + fs.copyFileSync(defaultConfig, targetPath); + + 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 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); diff --git a/packages/webpack5/src/cli/parseEnvFlags.ts b/packages/webpack5/src/cli/parseEnvFlags.ts new file mode 100644 index 000000000..e11bb26d3 --- /dev/null +++ b/packages/webpack5/src/cli/parseEnvFlags.ts @@ -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; +} diff --git a/packages/webpack5/src/configuration/angular.ts b/packages/webpack5/src/configuration/angular.ts new file mode 100644 index 000000000..ebb434c43 --- /dev/null +++ b/packages/webpack5/src/configuration/angular.ts @@ -0,0 +1,116 @@ +import Config from 'webpack-chain'; +import { existsSync } from 'fs'; + +import { getProjectFilePath } from '../helpers/project'; +import { env as _env, IWebpackEnv } from '../index'; +import { getEntryPath } from '../helpers/platform'; +import base from './base'; + +export default function (config: Config, env: IWebpackEnv = _env): Config { + base(config, env); + + 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)$/) + .use('@ngtools/webpack') + .loader('@ngtools/webpack'); + + config.module + .rule('@angular/core') + .test(/[\/\\]@angular[\/\\]core[\/\\].+\.js$/) + .parser({ system: true }); + + // set up html + config.module + .rule('html') + .test(/\.html$/) + .use('raw-loader') + .loader('raw-loader'); + + // exclude component css files from the normal css rule + config.module.rule('css').exclude.add(/\.component\.css$/); + + // and instead use raw-loader, since that's what angular expects + config.module + .rule('css|component') + .test(/\.component\.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.add(/\.component\.scss$/); + + // and instead use raw-loader, since that's what angular expects + config.module + .rule('scss|component') + .test(/\.component\.scss$/) + .use('raw-loader') + .loader('raw-loader') + .end() + .use('postcss-loader') + .loader('postcss-loader') + .options(postCSSOptions) + .end() + .use('sass-loader') + .loader('sass-loader'); + + config.plugin('AngularCompilerPlugin').use(getAngularCompilerPlugin(), [ + { + tsConfigPath, + mainPath: getEntryPath(), + platformTransformers: [require('../transformers/NativeClass').default], + }, + ]); + + // 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/, + ]) + ); + + return config; +} + +function getAngularCompilerPlugin() { + const { AngularCompilerPlugin } = require('@ngtools/webpack'); + return AngularCompilerPlugin; +} diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts new file mode 100644 index 000000000..ec420ee89 --- /dev/null +++ b/packages/webpack5/src/configuration/base.ts @@ -0,0 +1,374 @@ +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 TerserPlugin from 'terser-webpack-plugin'; + +import { getProjectFilePath, getProjectRootPath } from '../helpers/project'; +import { PlatformSuffixPlugin } from '../plugins/PlatformSuffixPlugin'; +import { applyFileReplacements } from '../helpers/fileReplacements'; +import { addCopyRule, applyCopyRules } from '../helpers/copyRules'; +import { WatchStatePlugin } from '../plugins/WatchStatePlugin'; +import { hasDependency } from '../helpers/dependencies'; +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, + getEntryDirPath, + getEntryPath, +} from '../helpers/platform'; + +export default function (config: Config, env: IWebpackEnv = _env): Config { + const entryPath = getEntryPath(); + const platform = getPlatformName(); + const mode = env.production ? 'production' : 'development'; + + // set mode + config.mode(mode); + + // config.stats({ + // logging: 'verbose' + // }) + + // package.json is generated by the CLI with runtime options + // this ensures it's not included in the bundle, but rather + // resolved at runtime + config.externals(['package.json', '~/package.json']); + + // todo: devtool + config.devtool('inline-source-map'); + + // todo: figure out easiest way to make "node" target work in ns + // rather than the custom ns target implementation that's hard to maintain + // appears to be working - but we still have to deal with HMR + config.target('node'); + + config + .entry('bundle') + // ensure we load nativescript globals first + .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/inspector_modules') + .add('@nativescript/core/inspector_modules'); + }); + + config.output + .path(getAbsoluteDistPath()) + .pathinfo(false) + .publicPath('') + .libraryTarget('commonjs') + .globalObject('global') + .set('clean', true); + + config.watchOptions({ + ignored: [ + `${getProjectFilePath('platforms')}/**`, + `${env.appResourcesPath ?? getProjectFilePath('App_Resources')}/**`, + ], + }); + + // Set up Terser options + config.optimization.minimizer('TerserPlugin').use(TerserPlugin, [ + { + terserOptions: { + compress: { + collapse_vars: platform !== 'android', + sequences: platform !== 'android', + keep_infinity: true, + drop_console: mode === 'production', + global_defs: { + __UGLIFIED__: true, + }, + }, + keep_fnames: true, + keep_classnames: true, + }, + }, + ]); + + config.optimization.splitChunks({ + cacheGroups: { + defaultVendor: { + test: /[\\/]node_modules[\\/]/, + priority: -10, + name: 'vendor', + chunks: 'all', + }, + }, + }); + + // look for loaders in + // - node_modules/@nativescript/webpack/dist/loaders + // - node_modules/@nativescript/webpack/node_modules + // - node_modules + // allows for cleaner rules, without having to specify full paths to loaders + config.resolveLoader.modules + .add(resolve(__dirname, '../loaders')) + .add(resolve(__dirname, '../../node_modules')) + .add(getProjectFilePath('node_modules')) + .add('node_modules'); + + config.resolve.extensions + .add(`.${platform}.ts`) + .add('.ts') + .add(`.${platform}.js`) + .add('.js') + .add(`.${platform}.css`) + .add('.css') + .add(`.${platform}.scss`) + .add('.scss') + .add(`.${platform}.json`) + .add('.json'); + + // base aliases + config.resolve.alias.set('~', getEntryDirPath()).set('@', getEntryDirPath()); + + // resolve symlinks + config.resolve.symlinks(true); + + // resolve modules in project node_modules first + // then fall-back to default node resolution (up the parent folder chain) + config.resolve.modules + .add(getProjectFilePath('node_modules')) + .add('node_modules'); + + config.module + .rule('bundle') + .enforce('post') + .test(entryPath) + .use('app-css-loader') + .loader('app-css-loader') + .options({ + platform, + }) + .end() + .use('nativescript-hot-loader') + .loader('nativescript-hot-loader') + .options({ + injectHMRRuntime: true, + }); + + // set up ts support + config.module + .rule('ts') + .test([/\.ts$/]) + .use('ts-loader') + .loader('ts-loader') + .options({ + // todo: perhaps we can provide a default tsconfig + // and use that if the project doesn't have one? + // configFile: '', + transpileOnly: true, + allowTsInNodeModules: true, + compilerOptions: { + sourceMap: true, + declaration: false, + }, + getCustomTransformers() { + return { + before: [require('../transformers/NativeClass').default], + }; + }, + }); + + // Use Fork TS Checker to do type checking in a separate non-blocking process + config.when(hasDependency('typescript'), (config) => { + config + .plugin('ForkTsCheckerWebpackPlugin') + .use(ForkTsCheckerWebpackPlugin, [ + { + typescript: { + memoryLimit: 4096, + }, + }, + ]); + }); + + // 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, + }, + }); + + config.module + .rule('workers') + .test(/\.(js|ts)$/) + .exclude.add(/node_modules/) + .end() + .use('nativescript-worker-loader') + .loader('nativescript-worker-loader'); + + // default PostCSS options to use + // projects can change settings + // via postcss.config.js + const postCSSOptions = { + postcssOptions: { + plugins: [ + // inlines @imported stylesheets + 'postcss-import', + ], + }, + }; + + // set up css + config.module + .rule('css') + .test(/\.css$/) + .use('apply-css-loader') + .loader('apply-css-loader') + .end() + .use('css2json-loader') + .loader('css2json-loader') + .end() + .use('postcss-loader') + .loader('postcss-loader') + .options(postCSSOptions); + + // set up scss + config.module + .rule('scss') + .test(/\.scss$/) + .use('apply-css-loader') + .loader('apply-css-loader') + .end() + .use('css2json-loader') + .loader('css2json-loader') + .end() + .use('postcss-loader') + .loader('postcss-loader') + .options(postCSSOptions) + .end() + .use('sass-loader') + .loader('sass-loader'); + + // config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [ + // /.*/, + // request => { + // if (new RegExp(`\.${platform}\..+$`).test(request.request)) { + // request.rawRequest = request.rawRequest.replace(`.${platform}.`, '.') + // console.log(request) + // } + // } + // ]) + + config.plugin('PlatformSuffixPlugin').use(PlatformSuffixPlugin, [ + { + platform, + }, + ]); + + // Makes sure that require.context will never include + // App_Resources, regardless where they are located. + config + .plugin('ContextExclusionPlugin|App_Resources') + .use(ContextExclusionPlugin, [new RegExp(`(.*)App_Resources(.*)`)]); + + // Filter common undesirable warnings + config.set( + 'ignoreWarnings', + (config.get('ignoreWarnings') ?? []).concat([ + /** + * This rule hides + * +-----------------------------------------------------------------------------------------+ + * | WARNING in ./node_modules/@angular/core/fesm2015/core.js 29714:15-102 | + * | System.import() is deprecated and will be removed soon. Use import() instead. | + * | For more info visit https://webpack.js.org/guides/code-splitting/ | + * +-----------------------------------------------------------------------------------------+ + */ + /System.import\(\) is deprecated/, + ]) + ); + + // todo: refine defaults + config.plugin('DefinePlugin').use(DefinePlugin, [ + { + __DEV__: mode === 'development', + __NS_WEBPACK__: true, + __NS_ENV_VERBOSE__: !!env.verbose, + __NS_DEV_HOST_IPS__: + mode === 'development' ? JSON.stringify(getIPS()) : `[]`, + __CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')), + __ANDROID__: platform === 'android', + __IOS__: platform === 'ios', + /* for compat only */ 'global.isAndroid': platform === 'android', + /* for compat only */ 'global.isIOS': platform === 'ios', + process: 'global.process', + + // todo: ?!?! + // profile: '() => {}', + }, + ]); + + // enable DotEnv + applyDotEnvPlugin(config); + + // replacements + applyFileReplacements(config); + + // set up default copy rules + addCopyRule('assets/**'); + addCopyRule('fonts/**'); + addCopyRule('**/*.+(jpg|png)'); + + applyCopyRules(config); + + config.plugin('WatchStatePlugin').use(WatchStatePlugin); + + config.when(env.hmr, (config) => { + config.plugin('HotModuleReplacementPlugin').use(HotModuleReplacementPlugin); + }); + + config.when(env.report, (config) => { + 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'), + }, + ]); + }); + + return config; +} + +function shouldIncludeInspectorModules(): boolean { + const platform = getPlatformName(); + // todo: check if core modules are external + // todo: check if we are testing + return platform === 'ios'; +} diff --git a/packages/webpack5/src/configuration/index.ts b/packages/webpack5/src/configuration/index.ts new file mode 100644 index 000000000..e34691086 --- /dev/null +++ b/packages/webpack5/src/configuration/index.ts @@ -0,0 +1,18 @@ +import base from './base'; + +import angular from './angular'; +import javascript from './javascript'; +import react from './react'; +import svelte from './svelte'; +import typescript from './typescript'; +import vue from './vue'; + +export const configs = { + base, + angular, + javascript, + react, + svelte, + typescript, + vue, +}; diff --git a/packages/webpack5/src/configuration/javascript.ts b/packages/webpack5/src/configuration/javascript.ts new file mode 100644 index 000000000..ccc821c63 --- /dev/null +++ b/packages/webpack5/src/configuration/javascript.ts @@ -0,0 +1,49 @@ +import Config from 'webpack-chain'; + +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 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.resolve.extensions.add('.xml'); + + // set up xml + config.module + .rule('xml') + .test(/\.xml$/) + .use('xml-namespace-loader') + .loader('xml-namespace-loader'); + + // set up core HMR + config.module + .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; +} diff --git a/packages/webpack5/src/configuration/react.ts b/packages/webpack5/src/configuration/react.ts new file mode 100644 index 000000000..be0a552dd --- /dev/null +++ b/packages/webpack5/src/configuration/react.ts @@ -0,0 +1,72 @@ +import { merge } from 'webpack-merge'; +import Config from 'webpack-chain'; + +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 { + base(config, env); + + const platform = getPlatformName(); + const mode = env.production ? 'production' : 'development'; + const production = mode === 'production'; + + // todo: use env + let isAnySourceMapEnabled = true; + + config.resolve.extensions.prepend('.tsx').prepend(`.${platform}.tsx`); + config.resolve.alias.set('react-dom', 'react-nativescript'); + + config.module + .rule('ts') + .test([...config.module.rule('ts').get('test'), /\.tsx$/]); + + config.plugin('DefinePlugin').tap((args) => { + args[0] = merge(args[0], { + /** For various libraries in the React ecosystem. */ + __TEST__: false, + /** + * Primarily for React Fast Refresh plugin, but technically the allowHmrInProduction option could be used instead. + * Worth including anyway, as there are plenty of Node libraries that use this flag. + */ + 'process.env.NODE_ENV': JSON.stringify(mode), + }); + + return args; + }); + + // todo: env flag to forceEnable? + config.when(env.hmr && !production, (config) => { + config.module + .rule('ts') + .use('babel-loader|react-refresh') + .loader('babel-loader') + .before('ts-loader') + .options({ + sourceMaps: isAnySourceMapEnabled ? 'inline' : false, + babelrc: false, + plugins: ['react-refresh/babel'], + }); + + config + .plugin('ReactRefreshPlugin') + .use(require('@pmmmwh/react-refresh-webpack-plugin'), [ + { + /** + * Maybe one day we'll implement an Error Overlay, but the work involved is too daunting for now. + * @see https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/79#issuecomment-644324557 + */ + overlay: false, + /** + * If you (temporarily) want to enable HMR on a production build: + * 1) Set `forceEnable` to `true` + * 2) Remove the `!production` condition on `tsxRule` to ensure that babel-loader gets used. + */ + forceEnable: false, + }, + ]); + }); + + return config; +} diff --git a/packages/webpack5/src/configuration/svelte.ts b/packages/webpack5/src/configuration/svelte.ts new file mode 100644 index 000000000..1f1dbba2a --- /dev/null +++ b/packages/webpack5/src/configuration/svelte.ts @@ -0,0 +1,65 @@ +import Config from 'webpack-chain'; + +import { getProjectFilePath, getProjectRootPath } from '../helpers/project'; +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 { + base(config, env); + + const platform = getPlatformName(); + const mode = env.production ? 'production' : 'development'; + const production = mode === 'production'; + + // resolve .svelte files + // the order is reversed because we are using prepend! + config.resolve.extensions.prepend('.svelte').prepend(`.${platform}.svelte`); + // add a rule for .svelte files + config.module + .rule('svelte') + .test(/\.svelte$/) + .exclude.add(/node_modules/) + .end() + .use('svelte-loader-hot') + .loader('svelte-loader-hot') + .tap((options) => { + return { + ...options, + dev: !production, + preprocess: getSvelteConfigPreprocessor(), + hotReload: !production, + hotOptions: { + injectCss: false, + native: true, + }, + // Suppress A11y warnings + onwarn(warning, warn) { + if (!/A11y:/.test(warning.message)) { + warn(warning); + } + }, + }; + }); + + return config; +} + +function getSvelteConfigPreprocessor(): any { + const config = getSvelteConfig(); + + return config?.preprocess; +} + +interface ISvelteConfig { + preprocess: any; +} + +function getSvelteConfig(): ISvelteConfig | undefined { + try { + return require(getProjectFilePath('svelte.config.js')) as ISvelteConfig; + } catch (err) { + error('Could not find svelte.config.js.', err); + } +} diff --git a/packages/webpack5/src/configuration/typescript.ts b/packages/webpack5/src/configuration/typescript.ts new file mode 100644 index 000000000..cfc4709f5 --- /dev/null +++ b/packages/webpack5/src/configuration/typescript.ts @@ -0,0 +1,49 @@ +import Config from 'webpack-chain'; + +import { getEntryDirPath, getEntryPath } 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 entryPath = getEntryPath(); + const filterRE = '/\\.(xml|js|(? { + return { + ...options, + compiler: require('nativescript-vue-template-compiler'), + }; + }); + + // set up ts support in vue files + config.module + .rule('ts') + .use('ts-loader') + .loader('ts-loader') + .tap((options = {}) => { + return merge(options, { + appendTsSuffixTo: ['\\.vue$'], + }); + }); + + 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; + }); + }); + + // add VueLoaderPlugin as the first plugin + config + .plugin('VueLoaderPlugin') + // @ts-ignore + .before(config.plugins.values()[0].name) + .use(VueLoaderPlugin); + + // add an alias for vue, since some plugins may try to import it + config.resolve.alias.set('vue', 'nativescript-vue'); + + 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!'); + } +} diff --git a/packages/webpack5/src/globals.d.ts b/packages/webpack5/src/globals.d.ts new file mode 100644 index 000000000..d2209bc77 --- /dev/null +++ b/packages/webpack5/src/globals.d.ts @@ -0,0 +1 @@ +// define globals here diff --git a/packages/webpack5/src/helpers/config.ts b/packages/webpack5/src/helpers/config.ts new file mode 100644 index 000000000..ae649ead9 --- /dev/null +++ b/packages/webpack5/src/helpers/config.ts @@ -0,0 +1,34 @@ +import { env } from '../index'; +import { error, warnOnce } from './log'; + +function getCLILib() { + if (!env.nativescriptLibPath) { + warnOnce( + 'getCLILib', + ` + Cannot find NativeScript CLI path. Make sure --env.nativescriptLibPath is passed + ` + ); + return false; + } + + return require(env.nativescriptLibPath); +} + +/** + * 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(key: string, defaultValue?: any): T { + const lib = getCLILib(); + + if (!lib) { + return defaultValue; + } + + return (lib.projectConfigService as { + getValue(key: string, defaultValue?: any): T; + }).getValue(key, defaultValue); +} diff --git a/packages/webpack5/src/helpers/copyRules.ts b/packages/webpack5/src/helpers/copyRules.ts new file mode 100644 index 000000000..0f19d724e --- /dev/null +++ b/packages/webpack5/src/helpers/copyRules.ts @@ -0,0 +1,85 @@ +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import { relative, resolve } from 'path'; +import Config from 'webpack-chain'; + +import { getProjectRootPath } from './project'; +import { getEntryDirPath } from './platform'; +import { env } from '..'; + +/** + * @internal + */ +export let copyRules = new Set([]); + +/** + * @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|object} globOrObject + */ +export function addCopyRule(globOrObject: string | object) { + if (typeof globOrObject === 'string') { + return copyRules.add(globOrObject); + } + + additionalCopyRules.push(globOrObject); +} + +/** + * Utility to remove a copy rule. The glob should be the exact glob + * to remove. For example + * - fonts/** - to remove the default copy rule for fonts + * + * @param {string} glob + */ +export function removeCopyRule(glob: string) { + copyRules.delete(glob); +} + +/** + * @internal + */ +export function applyCopyRules(config: Config) { + const entryDir = getEntryDirPath(); + const globOptions = { + dot: false, + ignore: [], + }; + + // todo: do we need to handle empty appResourcesPath? + // (the CLI should always pass the path - maybe not required) + if (env.appResourcesPath) { + const appResourcesFullPath = resolve( + getProjectRootPath(), + env.appResourcesPath + ); + + // ignore everything in App_Resources (regardless where they are located) + globOptions.ignore.push(`${relative(entryDir, appResourcesFullPath)}/**`); + } + + config.plugin('CopyWebpackPlugin').use(CopyWebpackPlugin, [ + { + patterns: Array.from(copyRules) + .map((glob) => ({ + from: glob, + context: entryDir, + noErrorOnMissing: true, + globOptions, + })) + .concat(additionalCopyRules), + }, + ]); +} diff --git a/packages/webpack5/src/helpers/dependencies.ts b/packages/webpack5/src/helpers/dependencies.ts new file mode 100644 index 000000000..99828ec27 --- /dev/null +++ b/packages/webpack5/src/helpers/dependencies.ts @@ -0,0 +1,49 @@ +import path from 'path'; + +import { getPackageJson, getProjectRootPath } from './project'; + +// todo: memoize +/** + * Utility to get all dependencies from the project package.json. + * The result combines dependencies and devDependencies + * + * @returns string[] dependencies + */ +export function getAllDependencies(): string[] { + const packageJSON = getPackageJson(); + + return [ + ...Object.keys(packageJSON.dependencies ?? {}), + ...Object.keys(packageJSON.devDependencies ?? {}), + ]; +} + +// todo: memoize +/** + * Utility to check if the project has a specific dependency + * in either dependencies or devDependencies. + * + * @param {string} dependencyName + * @returns boolean + */ +export function hasDependency(dependencyName: string) { + return getAllDependencies().includes(dependencyName); +} + +// todo: memoize +/** + * Utility to get the path (usually nested in node_modules) of a dependency. + * + * @param dependencyName + */ +export function getDependencyPath(dependencyName: string): string | null { + try { + const resolvedPath = require.resolve(`${dependencyName}/package.json`, { + paths: [getProjectRootPath()], + }); + + return path.dirname(resolvedPath); + } catch (err) { + return null; + } +} diff --git a/packages/webpack5/src/helpers/dotEnv.ts b/packages/webpack5/src/helpers/dotEnv.ts new file mode 100644 index 000000000..2403008b1 --- /dev/null +++ b/packages/webpack5/src/helpers/dotEnv.ts @@ -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. + if (existsSync(dotEnvWithEnvPath)) { + return dotEnvWithEnvPath; + } + + // fall back to .env + if (existsSync(dotEnvPath)) { + return dotEnvPath; + } + + // don't use .env + return null; +} diff --git a/packages/webpack5/src/helpers/externalConfigs.ts b/packages/webpack5/src/helpers/externalConfigs.ts new file mode 100644 index 000000000..3dc2a5d83 --- /dev/null +++ b/packages/webpack5/src/helpers/externalConfigs.ts @@ -0,0 +1,49 @@ +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'; + +/** + * @internal + */ +export function applyExternalConfigs() { + getAllDependencies().forEach((dependency) => { + const packagePath = getDependencyPath(dependency); + + if (!packagePath) { + return; + } + + const configPath = path.join(packagePath, 'nativescript.webpack.js'); + + if (fs.existsSync(configPath)) { + info(`Discovered config: ${configPath}`); + setCurrentPlugin(dependency); + try { + const externalConfig = require(configPath); + + if (typeof externalConfig === 'function') { + info('Applying external config...'); + externalConfig(lib); + } else if (externalConfig) { + info('Merging external config...'); + lib.mergeWebpack(externalConfig); + } else { + warn( + 'Unsupported external config. The config must export a function or an object.' + ); + } + } catch (err) { + warn(` + Unable to apply config: ${configPath}. + Error is: ${err} + `); + } + } + }); + + clearCurrentPlugin(); +} diff --git a/packages/webpack5/src/helpers/fileReplacements.ts b/packages/webpack5/src/helpers/fileReplacements.ts new file mode 100644 index 000000000..f36f1b8a4 --- /dev/null +++ b/packages/webpack5/src/helpers/fileReplacements.ts @@ -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, + }); + }); +} diff --git a/packages/webpack5/src/helpers/flavor.ts b/packages/webpack5/src/helpers/flavor.ts new file mode 100644 index 000000000..87b2bd8d5 --- /dev/null +++ b/packages/webpack5/src/helpers/flavor.ts @@ -0,0 +1,47 @@ +import { defaultConfigs } from '@nativescript/webpack'; +import { getAllDependencies } from './dependencies'; +import { error } from './log'; + +/** + * Utility to determine the project flavor based on installed dependencies + * (vue, angular, react, svelete, typescript, javascript...) + */ +export function determineProjectFlavor(): keyof typeof defaultConfigs | false { + const dependencies = getAllDependencies(); + + if (dependencies.includes('nativescript-vue')) { + return 'vue'; + } + + if (dependencies.includes('@nativescript/angular')) { + return 'angular'; + } + + if (dependencies.includes('react-nativescript')) { + return 'react'; + } + + if (dependencies.includes('svelte-native')) { + return 'svelte'; + } + + // the order is important - angular, react, and svelte also include these deps + // but should return prior to this condition! + if ( + dependencies.includes('@nativescript/core') && + dependencies.includes('typescript') + ) { + return 'typescript'; + } + + if (dependencies.includes('@nativescript/core')) { + return 'javascript'; + } + + error(` + Could not determine project flavor. + Please use webpack.useConfig('') to explicitly set the base config. + `); + + return false; +} diff --git a/packages/webpack5/src/helpers/host.ts b/packages/webpack5/src/helpers/host.ts new file mode 100644 index 000000000..5a909a196 --- /dev/null +++ b/packages/webpack5/src/helpers/host.ts @@ -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); +} diff --git a/packages/webpack5/src/helpers/index.ts b/packages/webpack5/src/helpers/index.ts new file mode 100644 index 000000000..56637d566 --- /dev/null +++ b/packages/webpack5/src/helpers/index.ts @@ -0,0 +1,78 @@ +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 } from './flavor'; +import { getValue } from './config'; +import { getIPS } from './host'; +import { + getAllDependencies, + hasDependency, + getDependencyPath, +} from './dependencies'; +import { + addPlatform, + getAbsoluteDistPath, + getDistPath, + getEntryDirPath, + getEntryPath, + getPlatform, + getPlatformName, +} from './platform'; + +// intentionally populated manually +// as this generates nicer typings +// that show all the utils inline +// rather than imports to types +// todo: maybe use api-extractor instead +export default { + merge, + addCopyRule, + removeCopyRule, + applyFileReplacements, + config: { + getValue, + }, + dependencies: { + getAllDependencies, + hasDependency, + getDependencyPath, + }, + flavor: { + determineProjectFlavor, + }, + host: { + getIPS, + }, + log: { + error, + info, + warn, + warnOnce, + }, + platform: { + addPlatform, + getAbsoluteDistPath, + getDistPath, + getEntryDirPath, + getEntryPath, + getPlatform, + getPlatformName, + }, + project: { + getProjectFilePath, + getProjectRootPath, + getPackageJson, + }, + virtualModules: { + addVirtualEntry, + addVirtualModule, + }, +}; diff --git a/packages/webpack5/src/helpers/log.ts b/packages/webpack5/src/helpers/log.ts new file mode 100644 index 000000000..ac70c163a --- /dev/null +++ b/packages/webpack5/src/helpers/log.ts @@ -0,0 +1,57 @@ +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[]) { + return data.map((d) => { + if (typeof d === 'string') { + return dedent(d); + } + return d; + }); +} + +export function error(...data: any): Error { + console.warn(`[@nativescript/webpack] Error: \n`, ...cleanup(data)); + + // we return the error - the caller can throw or ignore + if (typeof data[0] === 'string') { + return new Error( + '\n\n[@nativescript/webpack]\n---\n\n' + dedent(data[0]) + '\n\n---\n' + ); + } + + return new Error('@nativescript/webpack ran into a problem...'); +} + +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 { + if (env.verbose) { + console.log(`[@nativescript/webpack] Info: \n`, ...cleanup(data)); + } +} + +// todo: refine +// export function error(message: string, info?: { possibleCauses?: string[] }) { +// console.error(` +// NativeScript Webpack encountered an error and cannot proceed with the build: +// +// ${message} +// +// Possible causes: +// ${info?.possibleCauses?.map((cause) => `- ${cause}`).join('\n')} +// `); +// } diff --git a/packages/webpack5/src/helpers/platform.ts b/packages/webpack5/src/helpers/platform.ts new file mode 100644 index 000000000..f59389af1 --- /dev/null +++ b/packages/webpack5/src/helpers/platform.ts @@ -0,0 +1,128 @@ +import { dirname, resolve } from 'path'; + +import { getPackageJson, getProjectRootPath } from './project'; +import { error, info, warnOnce } from './log'; +import { env } from '../'; + +import AndroidPlatform from '../platforms/android'; +import iOSPlatform from '../platforms/ios'; + +export interface INativeScriptPlatform { + getEntryPath?(): string; + + getDistPath?(): string; +} + +export type Platform = Extract; + +const platforms: { + [name: string]: INativeScriptPlatform; +} = { + android: AndroidPlatform, + ios: iOSPlatform, +}; + +/** + * Utility to register a new supported platform. + * + * @param {string} name The name of the platform (eg. web, desktop) + * @param platform A platform definition of the platform specifics + */ +export function addPlatform(name: string, platform: INativeScriptPlatform) { + info(`Adding platform ${name}`, platform); + platforms[name] = platform; +} + +/** + * Utility to get the currently targeted platform definition + */ +export function getPlatform(): INativeScriptPlatform { + return platforms[getPlatformName()]; +} + +/** + * Utility to get the currently targeted platform name + */ +export function getPlatformName(): Platform { + if (env?.android) { + return 'android'; + } + + if (env?.ios) { + return 'ios'; + } + + // support custom platforms + if (env?.platform) { + if (platforms[env.platform]) { + return env.platform; + } + + throw error(` + Invalid platform: ${env.platform} + + Valid platforms: ${Object.keys(platforms).join(', ')} + `); + } + + warnOnce( + 'getPlatformName', + ` + You need to provide a target platform! + + Available platforms: ${Object.keys(platforms).join(', ')} + + Use --env.platform= or --env.android, --env.ios to specify the target platform. + + Defaulting to "ios". + ` + ); + + return 'ios'; +} + +/** + * Utility to get the entry file path for the currently targeted platform + */ +export function getEntryPath() { + const platform = getPlatform(); + + // use platform specific entry path + if (platform.getEntryPath) { + return platform.getEntryPath(); + } + + // fallback to main field in package.json + const packageJson = getPackageJson(); + + return resolve(getProjectRootPath(), packageJson.main); +} + +/** + * Utility to get the entry file directory path for the currently targeted platform + */ +export function getEntryDirPath() { + return dirname(getEntryPath()); +} + +/** + * Utility to get the dist file path for the currently targeted platform + */ +export function getDistPath() { + const platform = getPlatform(); + + // use platform specific entry path + if (platform.getDistPath) { + return platform.getDistPath(); + } + + // fallback to a generic platforms//dist folder + return `platforms/${getPlatformName()}/dist`; +} + +/** + * Utility to get the absolute dist file path for the currently targeted platform + */ +export function getAbsoluteDistPath() { + return resolve(getProjectRootPath(), getDistPath()); +} diff --git a/packages/webpack5/src/helpers/project.ts b/packages/webpack5/src/helpers/project.ts new file mode 100644 index 000000000..494735090 --- /dev/null +++ b/packages/webpack5/src/helpers/project.ts @@ -0,0 +1,50 @@ +import { resolve } from 'path'; + +export function getProjectRootPath(): string { + return process.cwd(); +} + +interface IPackageJson { + main?: string; + dependencies?: { + [name: string]: string; + }; + devDependencies?: { + [name: string]: string; + }; + // todo: add additional fields as we require them +} + +/** + * Utility function to get the contents of the project package.json + */ +export function getPackageJson() { + 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, '..')); +// } diff --git a/packages/webpack5/src/helpers/virtualModules.ts b/packages/webpack5/src/helpers/virtualModules.ts new file mode 100644 index 000000000..8fc671ec2 --- /dev/null +++ b/packages/webpack5/src/helpers/virtualModules.ts @@ -0,0 +1,49 @@ +import { ContextExclusionPlugin } from 'webpack'; +import Config from 'webpack-chain'; +import { join } from 'path'; + +import VirtualModulesPlugin from 'webpack-virtual-modules'; +import { getEntryDirPath } from './platform'; +import dedent from 'ts-dedent'; + +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), + }; + + 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; +} diff --git a/packages/webpack5/src/index.ts b/packages/webpack5/src/index.ts new file mode 100644 index 000000000..bee44df52 --- /dev/null +++ b/packages/webpack5/src/index.ts @@ -0,0 +1,216 @@ +import { highlight } from 'cli-highlight'; +import { merge } from 'webpack-merge'; +import Config from 'webpack-chain'; +import webpack from 'webpack'; + +import { applyExternalConfigs } from './helpers/externalConfigs'; +import { determineProjectFlavor } from './helpers/flavor'; +import { error, info } from './helpers/log'; +import { configs } from './configuration'; +import helpers from './helpers'; + +export interface IWebpackEnv { + [name: string]: any; + + env?: string; + + appPath?: string; + appResourcesPath?: string; + appComponents?: string[]; + + nativescriptLibPath?: string; + + android?: boolean; + ios?: boolean; + // for custom platforms + platform?: string; + + production?: boolean; + report?: boolean; + hmr?: boolean; + + // enable verbose output + verbose?: boolean; + + // misc + replace?: string[] | string; +} + +interface IChainEntry { + chainFn: any; + order?: number; + plugin?: string; +} + +let webpackChains: IChainEntry[] = []; +let webpackMerges: any[] = []; +let explicitUseConfig = false; +let hasInitialized = false; +let currentPlugin: string | undefined; +/** + * @internal + */ +export let env: IWebpackEnv = {}; + +/** + * @internal + */ +export function setCurrentPlugin(plugin: string) { + currentPlugin = plugin; +} + +/** + * @internal + */ +export function clearCurrentPlugin() { + currentPlugin = undefined; +} + +////// PUBLIC API +/** + * The default flavor specific configs + */ +export const defaultConfigs = configs; + +/** + * Utilities to simplify various tasks + */ +export const Utils = helpers; + +/** + * Initialize @nativescript/webpack with the webpack env. + * Must be called first. + * + * @param _env The webpack env + */ +export function init(_env: IWebpackEnv) { + hasInitialized = true; + if (_env) { + env = _env; + } +} + +/** + * Explicitly specify the base config to use. + * Calling this will opt-out from automatic flavor detection. + * + * Useful when the flavor cannot be detected due to the project structure + * for example in a custom monorepo. + * + * @param config Name of the base config to use. + */ +export function useConfig(config: keyof typeof defaultConfigs | false) { + explicitUseConfig = true; + if (config) { + webpackChains.push({ + order: -1, + chainFn: configs[config], + }); + } +} + +/** + * Add a new function to be called when building the internal config using webpack-chain. + * + * @param chainFn A function that accepts the internal chain config, and the current environment + * @param options Optional options to control the order in which the chain function should be applied. + */ +export function chainWebpack( + chainFn: (config: Config, env: IWebpackEnv) => any, + options?: { order?: number } +) { + webpackChains.push({ + order: options?.order || 0, + chainFn, + plugin: currentPlugin, + }); +} + +/** + * Merge an object into the resolved chain config. + * + * @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, + env: IWebpackEnv + ) => any | Partial +) { + webpackMerges.push(mergeFn); +} + +/** + * Resolve a new instance of the internal chain config with all chain functions applied. + */ +export function resolveChainableConfig(): Config { + const config = new Config(); + + if (!explicitUseConfig) { + useConfig(determineProjectFlavor()); + } + + // apply configs from dependencies + // todo: allow opt-out + applyExternalConfigs(); + + webpackChains + .splice(0) + .sort((a, b) => { + return a.order - b.order; + }) + .forEach(({ chainFn, plugin }) => { + try { + chainFn(config, env); + } catch (err) { + if (plugin) { + // catch and print errors from plugins + return error(` + Unable to apply chain function from: ${plugin}. + Error is: ${err} + `); + } + + // otherwise throw - as the error is likely from the user config + // or missing env flags (eg. missing platform) + throw err; + } + }); + + if (env.verbose) { + info('Resolved chainable config (before merges):'); + info(highlight(config.toString(), { language: 'js' })); + } + + return config; +} + +/** + * Resolve a "final" configuration that has all chain functions and merges applied. + * + * @param chainableConfig Optional chain config to use. + */ +export function resolveConfig( + chainableConfig = resolveChainableConfig() +): webpack.Configuration { + if (!hasInitialized) { + throw error('resolveConfig() must be called after init()'); + } + + let config = chainableConfig.toConfig(); + + // this applies webpack merges + webpackMerges.forEach((mergeFn) => { + if (typeof mergeFn === 'function') { + // mergeFn is a function with optional return value + const res = mergeFn(config, env); + if (res) config = merge(config, res); + } else if (mergeFn) { + // mergeFn is a literal value (object) + config = merge(config, mergeFn); + } + }); + + // return a config usable by webpack + return config; +} diff --git a/packages/webpack5/src/loaders/app-css-loader/index.ts b/packages/webpack5/src/loaders/app-css-loader/index.ts new file mode 100644 index 000000000..b754f7def --- /dev/null +++ b/packages/webpack5/src/loaders/app-css-loader/index.ts @@ -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); + }); +} diff --git a/packages/webpack5/src/loaders/apply-css-loader/index.ts b/packages/webpack5/src/loaders/apply-css-loader/index.ts new file mode 100644 index 000000000..78bb7c8ba --- /dev/null +++ b/packages/webpack5/src/loaders/apply-css-loader/index.ts @@ -0,0 +1,57 @@ +import { dedent } from 'ts-dedent'; + +const cssLoaderWarning = dedent` + The apply-css-loader requires the file to be pre-processed by either css-loader or css2json-loader. + Make sure the appropriate loader is applied before apply-css-loader. +`; + +export default function loader(content, map) { + const hasLoader = (loader: string) => { + return this.loaders + ?.slice(this.loaderIndex) + .some(({ path }) => path.includes(loader)); + }; + // add a tag to the applied css + const tag = JSON.stringify(this.resourcePath); + const tagCode = + this.mode === 'development' ? `, ${JSON.stringify(this.resourcePath)}` : ''; + + const hmrCode = this.hot + ? dedent` + if(module.hot) { + module.hot.accept() + module.hot.dispose(() => { + const { removeTaggedAdditionalCSS } = require("@nativescript/core/ui/styling/style-scope"); + removeTaggedAdditionalCSS(${tag}) + }) + } + ` + : ``; + + if (hasLoader('css2json-loader')) { + content = dedent` + ${content} + const { addTaggedAdditionalCSS } = require("@nativescript/core/ui/styling/style-scope"); + addTaggedAdditionalCSS(___CSS2JSON_LOADER_EXPORT___${tagCode}) + ${hmrCode} + `; + } else if (hasLoader('css-loader')) { + content = dedent` + ${content} + const { addTaggedAdditionalCSS } = require("@nativescript/core/ui/styling/style-scope"); + if (___CSS_LOADER_EXPORT___ && typeof ___CSS_LOADER_EXPORT___.forEach === "function") { + ___CSS_LOADER_EXPORT___.forEach(cssExport => { + if (cssExport.length > 1 && cssExport[1]) { + // applying the second item of the export as it contains the css contents + addTaggedAdditionalCSS(cssExport[1]${tagCode}); + } + }); + } + ${hmrCode} + `; + } else { + this.emitWarning(new Error(cssLoaderWarning)); + } + + this.callback(null, content, null); +} diff --git a/packages/webpack5/src/loaders/css2json-loader/index.ts b/packages/webpack5/src/loaders/css2json-loader/index.ts new file mode 100644 index 000000000..7e5fe1b22 --- /dev/null +++ b/packages/webpack5/src/loaders/css2json-loader/index.ts @@ -0,0 +1,81 @@ +import { parse, Import, Stylesheet } from 'css'; +import { urlToRequest } from 'loader-utils'; +import { dedent } from 'ts-dedent'; + +const betweenQuotesPattern = /('|")(.*?)\1/; +const unpackUrlPattern = /url\(([^\)]+)\)/; +const inlineLoader = '!css2json-loader?useForImports!'; + +export default function loader(content: string, map: any) { + const options = this.getOptions() || {}; + const inline = !!options.useForImports; + const requirePrefix = inline ? inlineLoader : ''; + + const ast = parse(content); + + // todo: revise if this is necessary + // todo: perhaps use postCSS and just build imports into a single file? + let dependencies = []; + getAndRemoveImportRules(ast) + .map(extractUrlFromRule) + .map(createRequireUri) + .forEach(({ uri, requireURI }) => { + dependencies.push(`require("${requirePrefix}${requireURI}")`); + }); + + const str = JSON.stringify(ast, (k, v) => (k === 'position' ? undefined : v)); + + // map.mappings = map.mappings.replace(/;{2,}/, '') + + const code = dedent` + /* CSS2JSON */ + ${dependencies.join('\n')} + const ___CSS2JSON_LOADER_EXPORT___ = ${str} + export default ___CSS2JSON_LOADER_EXPORT___ + `; + this.callback( + null, + code, //`${dependencies.join('\n')}module.exports = ${str};`, + map + ); +} + +function getImportRules(ast: Stylesheet): Import[] { + if (!ast || (ast).type !== 'stylesheet' || !ast.stylesheet) { + return []; + } + return ( + ast.stylesheet.rules.filter( + (rule) => rule.type === 'import' && (rule).import + ) + ); +} + +function getAndRemoveImportRules(ast: Stylesheet): Import[] { + const imports = getImportRules(ast); + ast.stylesheet.rules = ast.stylesheet.rules.filter( + (rule) => rule.type !== 'import' + ); + + return imports; +} + +/** + * Extracts the url from import rule (ex. `url("./platform.css")`) + */ +function extractUrlFromRule(importRule: Import): string { + const urlValue = importRule.import; + + const unpackedUrlMatch = urlValue.match(unpackUrlPattern); + const unpackedValue = unpackedUrlMatch ? unpackedUrlMatch[1] : urlValue; + + const quotesMatch = unpackedValue.match(betweenQuotesPattern); + return quotesMatch ? quotesMatch[2] : unpackedValue; +} + +function createRequireUri(uri): { uri: string; requireURI: string } { + return { + uri: uri, + requireURI: urlToRequest(uri), + }; +} diff --git a/packages/webpack5/src/loaders/nativescript-hot-loader/hmr.runtime.ts b/packages/webpack5/src/loaders/nativescript-hot-loader/hmr.runtime.ts new file mode 100644 index 000000000..662ce8b47 --- /dev/null +++ b/packages/webpack5/src/loaders/nativescript-hot-loader/hmr.runtime.ts @@ -0,0 +1,114 @@ +// @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 hasUpdate = () => { + try { + __non_webpack_require__(`~/bundle.${__webpack_hash__}.hot-update.json`); + return true; + } catch (err) { + return false; + } + }; + + const originalOnLiveSync = global.__onLiveSync; + global.__onLiveSync = async function () { + logVerbose('LiveSync'); + + if (!hasUpdate()) { + return; + } + + await checkAndApply(); + originalOnLiveSync(); + }; +} diff --git a/packages/webpack5/src/loaders/nativescript-hot-loader/index.ts b/packages/webpack5/src/loaders/nativescript-hot-loader/index.ts new file mode 100644 index 000000000..6a91088f8 --- /dev/null +++ b/packages/webpack5/src/loaders/nativescript-hot-loader/index.ts @@ -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); +} diff --git a/packages/webpack5/src/loaders/nativescript-worker-loader/index.ts b/packages/webpack5/src/loaders/nativescript-worker-loader/index.ts new file mode 100644 index 000000000..454d166bb --- /dev/null +++ b/packages/webpack5/src/loaders/nativescript-worker-loader/index.ts @@ -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); +} diff --git a/packages/webpack5/src/loaders/xml-namespace-loader/index.ts b/packages/webpack5/src/loaders/xml-namespace-loader/index.ts new file mode 100644 index 000000000..f3bff1a83 --- /dev/null +++ b/packages/webpack5/src/loaders/xml-namespace-loader/index.ts @@ -0,0 +1,238 @@ +import { parse, join } from 'path'; +import { promisify } from 'util'; +import dedent from 'ts-dedent'; +import { parser } from 'sax'; + +const noop = () => {}; + +const DEBUG = false; + +interface NamespaceEntry { + name: string; + path: string; +} + +interface ParseResult { + code: string; +} + +export default function loader(content: string, map: any) { + const callback = this.async(); + + // parse content and dependencies async + parseXML + .bind(this)(content) + .then((res) => { + DEBUG && console.log({ res }); + callback(null, res.code, map); + }) + .catch((err) => { + DEBUG && console.log({ err }); + callback(err); + }); +} + +async function parseXML(content: string): Promise { + // wrap this.resolve into a promise + const resolveAsync = promisify(this.resolve); + const promises: Promise[] = []; + const namespaces: NamespaceEntry[] = []; + const distinctNamespaces = new Map(); + const moduleRegisters: string[] = []; + const { ignore } = this.query; + const errors = []; + + const saxParser = parser(true, { xmlns: true }); + + // // Register ios and android prefixes as namespaces to avoid "unbound xml namespace" errors + (saxParser as any).ns['ios'] = 'http://schemas.nativescript.org/tns.xsd'; + (saxParser as any).ns['android'] = 'http://schemas.nativescript.org/tns.xsd'; + (saxParser as any).ns['desktop'] = 'http://schemas.nativescript.org/tns.xsd'; + (saxParser as any).ns['web'] = 'http://schemas.nativescript.org/tns.xsd'; + + const handleOpenTag = async (namespace: string, elementName: string) => { + if (!namespace) { + return; + } + + if (namespace.startsWith('http')) { + return; + } + + const moduleName = `${namespace}/${elementName}`; + + if (namespaces.some((n) => n.name === moduleName)) { + return; + } + + if (ignore && moduleName.match(ignore)) { + return; + } + + const localNamespacePath = join(this.rootContext, namespace); + const localModulePath = join(localNamespacePath, elementName); + + const resolvePaths = [ + localNamespacePath, + localModulePath, + `${localModulePath}.xml`, + moduleName, + namespace, + ]; + DEBUG && console.log({ resolvePaths }); + let resolvedPath; + + for (const p of resolvePaths) { + resolvedPath = await resolveAsync(this.context, p).catch(noop); + + // break on first match + if (resolvedPath) { + break; + } + } + + DEBUG && console.log({ resolvedPath }); + + // bail if we haven't resolved a path + if (!resolvedPath) { + return; + } + + const { dir, name } = parse(resolvedPath); + + // register resolved path + short name + namespaces.push({ name: namespace, path: resolvedPath }); + namespaces.push({ name: moduleName, path: resolvedPath }); + this.addDependency(resolvedPath); + + const noExtFilename = join(dir, name); + + DEBUG && + console.log({ + noExtFilename, + }); + + // finally try resolving an XML file + await resolveAsync(this.context, `${noExtFilename}.xml`) + .then((xml) => { + this.addDependency(xml); + namespaces.push({ name: `${moduleName}.xml`, path: xml }); + }) + .catch(() => { + // if there is no XML file, fall back to namespace as the path + // will become require() + namespaces.push({ name: namespace, path: namespace }); + namespaces.push({ name: moduleName, path: namespace }); + }); + + // look for css files with the same name + await resolveAsync(this.context, `${noExtFilename}.css`) + .then((css) => { + this.addDependency(css); + namespaces.push({ name: `${moduleName}.css`, path: css }); + }) + .catch(noop); + }; + + saxParser.onopentag = (node) => { + if ('uri' in node) { + promises.push(handleOpenTag(node.uri, node.local)); + } + }; + saxParser.onerror = (error) => { + saxParser.error = null; + + // Do only warning about invalid character "&"" for back-compatibility + // as it is common to use it in a binding expression + if ( + error.message.includes('Invalid character') && + error.message.includes('Char: &') + ) { + return this.emitWarning(error); + } + errors.push(error); + }; + + saxParser.write(content).close(); + + await Promise.all(promises); + + DEBUG && console.log({ namespaces }); + + namespaces.forEach(({ name, path }) => { + distinctNamespaces.set(name, path.replace(/\\/g, '/')); + }); + + distinctNamespaces.forEach((path, name) => { + moduleRegisters.push(dedent` + global.registerModule( + '${name}', + () => require("${path}") + ) + `); + }); + + // escape special whitespace characters + // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Issue_with_plain_JSON.stringify_for_use_as_JavaScript + const xml = JSON.stringify(content) + .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) { + errors.map(this.emitError); + + // finally throw the first one + throw errors[0]; + } + + return { + code, + }; +} + +// +// +// +// function parseXML(xml: string) { +// const saxParser = parser(true, { xmlns: true }); +// +// saxParser.onopentag = (node) => { +// if('ns' in node) { +// const uri = node.uri +// const tag = node.local +// +// DEBUG && console.log({ +// uri, +// tag +// }) +// } +// } +// +// saxParser.onerror = (err) => { +// DEBUG && console.log(err) +// } +// +// // Register ios and android prefixes as namespaces to avoid "unbound xml namespace" errors +// // saxParser.ns['ios'] = 'http://schemas.nativescript.org/tns.xsd'; +// // saxParser.ns['android'] = 'http://schemas.nativescript.org/tns.xsd'; +// // saxParser.ns['desktop'] = 'http://schemas.nativescript.org/tns.xsd'; +// // saxParser.ns['web'] = 'http://schemas.nativescript.org/tns.xsd'; +// saxParser.write(xml).close() +// } diff --git a/packages/webpack5/src/platforms/android.ts b/packages/webpack5/src/platforms/android.ts new file mode 100644 index 000000000..c83f9db8f --- /dev/null +++ b/packages/webpack5/src/platforms/android.ts @@ -0,0 +1,11 @@ +import { INativeScriptPlatform } from "../helpers/platform"; + +function getDistPath() { + return `platforms/android/app/src/main/assets/app`; +} + +const AndroidPlatform: INativeScriptPlatform = { + getDistPath, +} + +export default AndroidPlatform; diff --git a/packages/webpack5/src/platforms/ios.ts b/packages/webpack5/src/platforms/ios.ts new file mode 100644 index 000000000..dd2d3defb --- /dev/null +++ b/packages/webpack5/src/platforms/ios.ts @@ -0,0 +1,20 @@ +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 = sanitizeName(basename(getProjectRootPath())); + return `platforms/ios/${appName}/app`; +} + +const iOSPlatform: INativeScriptPlatform = { + getDistPath, +} + +export default iOSPlatform; diff --git a/packages/webpack5/src/plugins/PlatformSuffixPlugin.ts b/packages/webpack5/src/plugins/PlatformSuffixPlugin.ts new file mode 100644 index 000000000..51c9ae5c6 --- /dev/null +++ b/packages/webpack5/src/plugins/PlatformSuffixPlugin.ts @@ -0,0 +1,161 @@ +import { extname, resolve } from 'path'; +import { existsSync } from 'fs'; + +const id = 'PlatformSuffixPlugin'; + +interface PlatformSuffixPluginOptions { + platform: string; + // extensions: string[] | (() => string[]) +} + +/** + * The platform suffix plugin will try to resolve files with a platform specifier (suffix) + * falling back to the non-platform-specific version. + * + * For example: + * import something from './something.js' + * + * will first look for './something..js' + * and if not found look for './something.js' + * + */ +export class PlatformSuffixPlugin { + private readonly platform: string; + // private readonly extensions: string[] + + constructor(options: PlatformSuffixPluginOptions) { + this.platform = options.platform; + + // if (typeof options.extensions === "function") { + // this.extensions = options.extensions() + // } else { + // this.extensions = options.extensions + // } + } + + apply(compiler: any) { + 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..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); + }); + }); + + compiler.resolverFactory.hooks.resolver + .for('normal') + .tap(id, (resolver) => { + // Object.keys(resolver.hooks).forEach(hook => { + // resolver.hooks[hook].tap(id, (request, resolveContext) => { + // if( + // request?.path?.includes('foo.xml') || + // request?.request?.includes('foo.xml') + // ) { + // console.log( + // `>>> ${hook}: ${request.path}`, + // // request + // ) + // } + // // callback(); + // }); + // }) + + resolver.hooks.normalResolve.tapAsync( + id, + (request_, resolveContext, callback) => { + const { path, request } = request_; + const ext = request && extname(request); + const platformExt = ext ? `.${this.platform}${ext}` : ''; + + if (path && request && ext && !request.includes(platformExt)) { + const platformRequest = request.replace(ext, platformExt); + const extPath = resolve(path, platformRequest); + + // console.log({ + // path, + // request, + // ext, + // extPath + // }) + + // 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); + + // here we are creating a new resolve object and replacing the path + // with the .. 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.doResolve( + hook, + obj, + message, + resolveContext, + callback + ); + } + } + callback(); + } + ); + // resolver.hooks.rawFile.tap(id, (request, resolveContext, callback) => { + // if(request.path && !/\.ios\..+$/.test(request.path)) { + // const { ext } = parse(request.path) + // const platformExtPath = request.path.replace(ext, `.${this.platform}${ext}`) + // // console.log({ + // // p1: request.path, + // // p2: platformExtPath + // // }) + // if(existsSync(platformExtPath)) { + // // request.path = platformExtPath + // // console.log('-'.repeat(100)) + // // console.log(request) + // const obj = { + // ...request, + // path: platformExtPath, + // fullySpecified: false + // } + // return resolver.doResolve( + // 'raw-file', + // obj, + // `resolved ${request.path} to platform specific file: ${platformExtPath}`, + // resolveContext, + // (err, result) => { + // if(err) return callback(err); + // if(result) return callback(null, result); + // return callback(); + // } + // ) + // // return request + // } + // } + // }); + }); + } +} diff --git a/packages/webpack5/src/plugins/WatchStatePlugin.ts b/packages/webpack5/src/plugins/WatchStatePlugin.ts new file mode 100644 index 000000000..c4a4671fa --- /dev/null +++ b/packages/webpack5/src/plugins/WatchStatePlugin.ts @@ -0,0 +1,96 @@ +import { env } from '../'; + +const id = 'WatchStatePlugin'; +const version = 1; + +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 + * and send status updates through IPC to the {N} CLI. + */ +export class WatchStatePlugin { + apply(compiler: any) { + let isWatchMode = false; + let prevAssets = []; + + 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; + }); + + compiler.hooks.afterEmit.tapAsync(id, function (compilation, callback) { + callback(); + + console.log( + isWatchMode ? messages.startWatching : messages.compilationComplete + ); + + // logic taken from CleanWebpackPlugin + const assets = + compilation.getStats().toJson( + { + assets: true, + }, + true + ).assets || []; + const assetList = assets.map((asset) => asset.name); + + const emittedAssets = Array.from(compilation.emittedAssets); + const staleAssets = prevAssets.filter((asset) => { + return assetList.includes(asset) === false; + }); + + // store assets for next compilation + prevAssets = assetList.sort(); + + notify({ + type: 'compilation', + version, + hash: compilation.hash, + + data: { + emittedAssets, + staleAssets, + }, + }); + }); + } +} + +function notify(message: any) { + env.verbose && console.log(`[${id}] Notify: `, message); + if (!process.send) { + return; + } + + process.send(message, (error) => { + if (error) { + console.error(`[${id}] Process Send Error: `, error); + } + + return null; + }); +} diff --git a/packages/webpack5/src/stubs/default.config.stub.js b/packages/webpack5/src/stubs/default.config.stub.js new file mode 100644 index 000000000..a561e0e2b --- /dev/null +++ b/packages/webpack5/src/stubs/default.config.stub.js @@ -0,0 +1,12 @@ +const webpack = require("@nativescript/webpack"); + +module.exports = (env) => { + webpack.init(env); + + // Learn how to customize: + // https://docs.nativescript.org/webpack + + return webpack.resolveConfig(); +}; + + diff --git a/packages/webpack5/src/transformers/AngularHMR/index.ts b/packages/webpack5/src/transformers/AngularHMR/index.ts new file mode 100644 index 000000000..65b3dba38 --- /dev/null +++ b/packages/webpack5/src/transformers/AngularHMR/index.ts @@ -0,0 +1 @@ +// todo diff --git a/packages/webpack5/src/transformers/NativeClass/index.ts b/packages/webpack5/src/transformers/NativeClass/index.ts new file mode 100644 index 000000000..71b832c1c --- /dev/null +++ b/packages/webpack5/src/transformers/NativeClass/index.ts @@ -0,0 +1,51 @@ +import ts from 'typescript'; + +/** + * A TypeScript transform that compiles classes marked with @NativeClass as es5 & commonjs + * + * @param ctx + */ +export default function (ctx: ts.TransformationContext) { + function isNativeClassExtension(node: ts.ClassDeclaration) { + return ( + node.decorators && + node.decorators.filter((d) => { + const fullText = d.getFullText().trim(); + return fullText.indexOf('@NativeClass') > -1; + }).length > 0 + ); + } + function visitNode(node: ts.Node): ts.Node { + if (ts.isClassDeclaration(node) && isNativeClassExtension(node)) { + return createHelper(node); + } + return ts.visitEachChild(node, visitNode, ctx); + } + + function createHelper(node: ts.Node) { + // we remove the decorator for now! + return ts.createIdentifier( + ts + .transpileModule( + node.getText().replace(/@NativeClass(\((.|\n)*?\))?/gm, ''), + { + compilerOptions: { + noEmitHelpers: true, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES5, + }, + } + ) + .outputText.replace( + /(Object\.defineProperty\(.*?{.*?)(enumerable:\s*false)(.*?}\))/gs, + '$1enumerable: true$3' + ) + ); + } + + return (source: ts.SourceFile) => + ts.updateSourceFileNode( + source, + ts.visitNodes(source.statements, visitNode) + ); +} diff --git a/packages/webpack5/tsconfig.build.json b/packages/webpack5/tsconfig.build.json new file mode 100644 index 000000000..6d90f5603 --- /dev/null +++ b/packages/webpack5/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/webpack5/tsconfig.json b/packages/webpack5/tsconfig.json new file mode 100644 index 000000000..57f24b976 --- /dev/null +++ b/packages/webpack5/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "target": "es2017", + "module": "commonjs", + "outDir": "./dist", + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es2017"], + "sourceMap": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "diagnostics": true, + "paths": { + "@nativescript/webpack": ["src"] + }, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "stripInternal": true + }, + "include": ["src", "scripts", "__tests__"], + "exclude": ["node_modules"] +} diff --git a/tools/workspace-scripts.js b/tools/workspace-scripts.js index 53fb315a2..2ff7477e7 100644 --- a/tools/workspace-scripts.js +++ b/tools/workspace-scripts.js @@ -115,6 +115,13 @@ module.exports = { test: { script: 'nx run webpack:test', description: '@nativescript/webpack: Unit tests' + }, + }, + // @nativescript/webpack (5) + webpack5: { + build: { + script: 'nx run webpack5:build', + description: '@nativescript/webpack(5): Build for npm' }, }, }, diff --git a/workspace.json b/workspace.json index 232a842a2..baf7de078 100644 --- a/workspace.json +++ b/workspace.json @@ -253,6 +253,31 @@ } } } + }, + "webpack5": { + "root": "packages/webpack5", + "sourceRoot": "packages/webpack5", + "projectType": "library", + "schematics": {}, + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [] + } + }, + "build": { + "builder": "@nrwl/workspace:run-commands", + "outputs": ["dist/packages"], + "options": { + "commands": [ + "npm run build" + ], + "cwd": "packages/webpack5", + "parallel": false + } + } + } } }, "cli": {