diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap index 350d9e624..01aaba5cb 100644 --- a/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap +++ b/packages/webpack5/__tests__/configuration/__snapshots__/react.spec.ts.snap @@ -24,6 +24,7 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR 'react-dom': 'react-nativescript' }, extensions: [ + '.android.tsx', '.tsx', '.android.ts', '.ts', @@ -39,7 +40,7 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -92,9 +93,9 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { @@ -199,6 +200,7 @@ exports[`react configuration > android > base config 1`] = ` 'react-dom': 'react-nativescript' }, extensions: [ + '.android.tsx', '.tsx', '.android.ts', '.ts', @@ -214,7 +216,7 @@ exports[`react configuration > android > base config 1`] = ` }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -267,9 +269,9 @@ exports[`react configuration > android > base config 1`] = ` { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { @@ -367,6 +369,7 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena 'react-dom': 'react-nativescript' }, extensions: [ + '.ios.tsx', '.tsx', '.ios.ts', '.ts', @@ -382,7 +385,7 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -435,9 +438,9 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { @@ -545,6 +548,7 @@ exports[`react configuration > ios > base config 1`] = ` 'react-dom': 'react-nativescript' }, extensions: [ + '.ios.tsx', '.tsx', '.ios.ts', '.ts', @@ -560,7 +564,7 @@ exports[`react configuration > ios > base config 1`] = ` }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -613,9 +617,9 @@ exports[`react configuration > ios > base config 1`] = ` { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { diff --git a/packages/webpack5/__tests__/configuration/__snapshots__/vue.spec.ts.snap b/packages/webpack5/__tests__/configuration/__snapshots__/vue.spec.ts.snap index 4da8a273a..cae33ac1d 100644 --- a/packages/webpack5/__tests__/configuration/__snapshots__/vue.spec.ts.snap +++ b/packages/webpack5/__tests__/configuration/__snapshots__/vue.spec.ts.snap @@ -40,7 +40,7 @@ exports[`vue configuration for android 1`] = ` }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -84,9 +84,9 @@ exports[`vue configuration for android 1`] = ` { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { @@ -212,7 +212,7 @@ exports[`vue configuration for ios 1`] = ` }, resolveLoader: { modules: [ - '@nativescript/webpack/dist/loaders', + 'node_modules/@nativescript/webpack/dist/loaders', 'node_modules' ] }, @@ -256,9 +256,9 @@ exports[`vue configuration for ios 1`] = ` { test: /\\\\.css$/, use: [ - /* config.module.rule('css').use('css2json-loader') */ + /* config.module.rule('css').use('apply-css-loader') */ { - loader: 'css2json-loader' + loader: 'apply-css-loader' }, /* config.module.rule('css').use('css-loader') */ { diff --git a/packages/webpack5/package.json b/packages/webpack5/package.json index 1525e78e0..b629dfbf1 100644 --- a/packages/webpack5/package.json +++ b/packages/webpack5/package.json @@ -17,20 +17,30 @@ "babel-loader": "^8.2.1", "clean-webpack-plugin": "^3.0.0", "cli-highlight": "^2.1.4", + "css": "^3.0.0", + "css-loader": "^5.0.1", + "loader-utils": "^2.0.0", + "scss": "^0.2.4", + "scss-loader": "^0.0.1", + "source-map": "^0.7.3", "terser-webpack-plugin": "^5.0.3", + "ts-dedent": "^2.0.0", + "ts-loader": "^8.0.11", "vue-loader": "^15.9.5", - "webpack": "^5.4.0", + "webpack": "^5.6.0", "webpack-chain": "^6.5.1", "webpack-cli": "^4.2.0", "webpack-merge": "^5.4.0", "worker-plugin": "^5.0.0" }, "devDependencies": { + "@types/css": "^0.0.31", "@types/jest": "^26.0.15", + "@types/loader-utils": "^2.0.1", "@types/terser-webpack-plugin": "^5.0.2", "jest": "^26.6.3", "ts-jest": "^26.4.4", - "typescript": "^4.0.5" + "typescript": "^4.1.2" }, "peerDependencies": { "nativescript-vue-template-compiler": "^2.8.1" diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index ab8328a6d..c6798f003 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -8,6 +8,8 @@ import { getPlatform, } from '../helpers/project'; +import TransformNativeClass from '../transformers/NativeClass'; + import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import { DefinePlugin } from 'webpack'; import { WatchStateLoggerPlugin } from '../plugins/WatchStateLoggerPlugin'; @@ -55,10 +57,10 @@ export default function (config: Config, env: IWebpackEnv): Config { ]); // look for loaders in - // - @nativescript/webpack/loaders + // - node_modules/@nativescript/webpack/dist/loaders // - node_modules config.resolveLoader.modules - .add('@nativescript/webpack/dist/loaders') + .add('node_modules/@nativescript/webpack/dist/loaders') .add('node_modules'); // inspector_modules @@ -105,9 +107,7 @@ export default function (config: Config, env: IWebpackEnv): Config { }, getCustomTransformers() { return { - before: [ - // todo: transform NativeClass - ], + before: [TransformNativeClass], }; }, }); @@ -124,8 +124,8 @@ export default function (config: Config, env: IWebpackEnv): Config { config.module .rule('css') .test(/\.css$/) - .use('css2json-loader') - .loader('css2json-loader') + .use('apply-css-loader') + .loader('apply-css-loader') .end() .use('css-loader') .loader('css-loader'); diff --git a/packages/webpack5/src/configuration/react.ts b/packages/webpack5/src/configuration/react.ts index ac44c3934..db2e330a2 100644 --- a/packages/webpack5/src/configuration/react.ts +++ b/packages/webpack5/src/configuration/react.ts @@ -2,15 +2,17 @@ import base from './base'; import { env as _env, IWebpackEnv } from '@nativescript/webpack'; import Config from 'webpack-chain'; import { merge } from 'webpack-merge'; +import { getPlatform } from '../helpers/project'; export default function (config: Config, env: IWebpackEnv = _env): Config { base(config, env); + const platform = getPlatform(); // todo: use env let isAnySourceMapEnabled = true; let production = false; - config.resolve.extensions.prepend('.tsx'); + config.resolve.extensions.prepend('.tsx').prepend(`.${platform}.tsx`); config.resolve.alias.set('react-dom', 'react-nativescript'); config.module @@ -34,7 +36,9 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { * 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(production ? 'production' : 'development'), + 'process.env.NODE_ENV': JSON.stringify( + production ? 'production' : 'development' + ), }); return args; @@ -42,21 +46,23 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // todo: env flag to forceEnable? config.when(env.hmr && !production, (config) => { - config.plugin('ReactRefreshWebpackPlugin').use(function ReactRefreshWebpackPlugin() {}, [ - { - /** - * 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, - }, - ]); + config + .plugin('ReactRefreshWebpackPlugin') + .use(function ReactRefreshWebpackPlugin() {}, [ + { + /** + * 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/loaders/apply-css-loader/index.ts b/packages/webpack5/src/loaders/apply-css-loader/index.ts new file mode 100644 index 000000000..b41657bb0 --- /dev/null +++ b/packages/webpack5/src/loaders/apply-css-loader/index.ts @@ -0,0 +1,44 @@ +import { dedent } from 'ts-dedent'; + +const cssLoaderWarning = dedent` + The apply-css-loader requires the file to be pre-processed by css-loader. + Make sure css-loader is applied before apply-css-loader. +`; + +function hasCssLoader(loaders: any[], loaderIndex: number) { + return loaders + ?.slice(loaderIndex) + .some(({ path }) => path.includes('css-loader')); +} + +export default function loader(content, map) { + // if (this.request.match(/\/app\.(css|scss|less|sass)$/)) { + // return content; + // } + + // Emit a warning if the file was not processed by the css-loader. + if (!hasCssLoader(this.loaders, this.loaderIndex)) { + this.emitWarning(new Error(cssLoaderWarning)); + } + + content = dedent` + /* CSS START */ + ${content} + /* CSS END */ + + /* APPLY CSS */ + const { Application } = require("@nativescript/core"); + 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 + Application.addCss(cssExport[1]); + } + }); + } + `; + + this.callback(null, content, map); +} diff --git a/packages/webpack5/src/loaders/css2json-loader/index.ts b/packages/webpack5/src/loaders/css2json-loader/index.ts index 8337712ea..15052a361 100644 --- a/packages/webpack5/src/loaders/css2json-loader/index.ts +++ b/packages/webpack5/src/loaders/css2json-loader/index.ts @@ -1 +1,72 @@ -// +import { parse, Import, Stylesheet } from 'css'; +import { urlToRequest } from 'loader-utils'; + +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); + + let dependencies = []; + getImportRules(ast) + .map(extractUrlFromRule) + .map(createRequireUri) + .forEach(({ uri, requireURI }) => { + dependencies.push( + `global.registerModule("${uri}", () => require("${requirePrefix}${requireURI}"));` + ); + + // Call registerModule with requireURI to handle cases like @import "~@nativescript/theme/css/blue.css"; + if (uri !== requireURI) { + dependencies.push( + `global.registerModule("${requireURI}", () => require("${requirePrefix}${requireURI}"));` + ); + } + }); + + const str = JSON.stringify(ast, (k, v) => (k === 'position' ? undefined : v)); + + // map.mappings = map.mappings.replace(/;{2,}/, '') + + this.callback( + null, + `${dependencies.join('\n')}module.exports = ${str};`, + null + ); +} + +function getImportRules(ast: Stylesheet): Import[] { + if (!ast || (ast).type !== 'stylesheet' || !ast.stylesheet) { + return []; + } + return ( + ast.stylesheet.rules.filter( + (rule) => rule.type === 'import' && (rule).import + ) + ); +} + +/** + * 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/transformers/NativeClass/index.ts b/packages/webpack5/src/transformers/NativeClass/index.ts index 65b3dba38..4d16ae658 100644 --- a/packages/webpack5/src/transformers/NativeClass/index.ts +++ b/packages/webpack5/src/transformers/NativeClass/index.ts @@ -1 +1,46 @@ -// todo +import ts from 'typescript'; + +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) + ); +}