feat: css loading

This commit is contained in:
Igor Randjelovic
2020-11-20 19:56:54 +01:00
parent fa879ba49f
commit 288444c05c
8 changed files with 226 additions and 46 deletions

View File

@ -24,6 +24,7 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR
'react-dom': 'react-nativescript' 'react-dom': 'react-nativescript'
}, },
extensions: [ extensions: [
'.android.tsx',
'.tsx', '.tsx',
'.android.ts', '.android.ts',
'.ts', '.ts',
@ -39,7 +40,7 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -92,9 +93,9 @@ exports[`react configuration > android > adds ReactRefreshWebpackPlugin when HMR
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {
@ -199,6 +200,7 @@ exports[`react configuration > android > base config 1`] = `
'react-dom': 'react-nativescript' 'react-dom': 'react-nativescript'
}, },
extensions: [ extensions: [
'.android.tsx',
'.tsx', '.tsx',
'.android.ts', '.android.ts',
'.ts', '.ts',
@ -214,7 +216,7 @@ exports[`react configuration > android > base config 1`] = `
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -267,9 +269,9 @@ exports[`react configuration > android > base config 1`] = `
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {
@ -367,6 +369,7 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena
'react-dom': 'react-nativescript' 'react-dom': 'react-nativescript'
}, },
extensions: [ extensions: [
'.ios.tsx',
'.tsx', '.tsx',
'.ios.ts', '.ios.ts',
'.ts', '.ts',
@ -382,7 +385,7 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -435,9 +438,9 @@ exports[`react configuration > ios > adds ReactRefreshWebpackPlugin when HMR ena
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {
@ -545,6 +548,7 @@ exports[`react configuration > ios > base config 1`] = `
'react-dom': 'react-nativescript' 'react-dom': 'react-nativescript'
}, },
extensions: [ extensions: [
'.ios.tsx',
'.tsx', '.tsx',
'.ios.ts', '.ios.ts',
'.ts', '.ts',
@ -560,7 +564,7 @@ exports[`react configuration > ios > base config 1`] = `
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -613,9 +617,9 @@ exports[`react configuration > ios > base config 1`] = `
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {

View File

@ -40,7 +40,7 @@ exports[`vue configuration for android 1`] = `
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -84,9 +84,9 @@ exports[`vue configuration for android 1`] = `
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {
@ -212,7 +212,7 @@ exports[`vue configuration for ios 1`] = `
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
'@nativescript/webpack/dist/loaders', 'node_modules/@nativescript/webpack/dist/loaders',
'node_modules' 'node_modules'
] ]
}, },
@ -256,9 +256,9 @@ exports[`vue configuration for ios 1`] = `
{ {
test: /\\\\.css$/, test: /\\\\.css$/,
use: [ 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') */ /* config.module.rule('css').use('css-loader') */
{ {

View File

@ -17,20 +17,30 @@
"babel-loader": "^8.2.1", "babel-loader": "^8.2.1",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"cli-highlight": "^2.1.4", "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", "terser-webpack-plugin": "^5.0.3",
"ts-dedent": "^2.0.0",
"ts-loader": "^8.0.11",
"vue-loader": "^15.9.5", "vue-loader": "^15.9.5",
"webpack": "^5.4.0", "webpack": "^5.6.0",
"webpack-chain": "^6.5.1", "webpack-chain": "^6.5.1",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-merge": "^5.4.0", "webpack-merge": "^5.4.0",
"worker-plugin": "^5.0.0" "worker-plugin": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/css": "^0.0.31",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/loader-utils": "^2.0.1",
"@types/terser-webpack-plugin": "^5.0.2", "@types/terser-webpack-plugin": "^5.0.2",
"jest": "^26.6.3", "jest": "^26.6.3",
"ts-jest": "^26.4.4", "ts-jest": "^26.4.4",
"typescript": "^4.0.5" "typescript": "^4.1.2"
}, },
"peerDependencies": { "peerDependencies": {
"nativescript-vue-template-compiler": "^2.8.1" "nativescript-vue-template-compiler": "^2.8.1"

View File

@ -8,6 +8,8 @@ import {
getPlatform, getPlatform,
} from '../helpers/project'; } from '../helpers/project';
import TransformNativeClass from '../transformers/NativeClass';
import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import { DefinePlugin } from 'webpack'; import { DefinePlugin } from 'webpack';
import { WatchStateLoggerPlugin } from '../plugins/WatchStateLoggerPlugin'; import { WatchStateLoggerPlugin } from '../plugins/WatchStateLoggerPlugin';
@ -55,10 +57,10 @@ export default function (config: Config, env: IWebpackEnv): Config {
]); ]);
// look for loaders in // look for loaders in
// - @nativescript/webpack/loaders // - node_modules/@nativescript/webpack/dist/loaders
// - node_modules // - node_modules
config.resolveLoader.modules config.resolveLoader.modules
.add('@nativescript/webpack/dist/loaders') .add('node_modules/@nativescript/webpack/dist/loaders')
.add('node_modules'); .add('node_modules');
// inspector_modules // inspector_modules
@ -105,9 +107,7 @@ export default function (config: Config, env: IWebpackEnv): Config {
}, },
getCustomTransformers() { getCustomTransformers() {
return { return {
before: [ before: [TransformNativeClass],
// todo: transform NativeClass
],
}; };
}, },
}); });
@ -124,8 +124,8 @@ export default function (config: Config, env: IWebpackEnv): Config {
config.module config.module
.rule('css') .rule('css')
.test(/\.css$/) .test(/\.css$/)
.use('css2json-loader') .use('apply-css-loader')
.loader('css2json-loader') .loader('apply-css-loader')
.end() .end()
.use('css-loader') .use('css-loader')
.loader('css-loader'); .loader('css-loader');

View File

@ -2,15 +2,17 @@ import base from './base';
import { env as _env, IWebpackEnv } from '@nativescript/webpack'; import { env as _env, IWebpackEnv } from '@nativescript/webpack';
import Config from 'webpack-chain'; import Config from 'webpack-chain';
import { merge } from 'webpack-merge'; import { merge } from 'webpack-merge';
import { getPlatform } from '../helpers/project';
export default function (config: Config, env: IWebpackEnv = _env): Config { export default function (config: Config, env: IWebpackEnv = _env): Config {
base(config, env); base(config, env);
const platform = getPlatform();
// todo: use env // todo: use env
let isAnySourceMapEnabled = true; let isAnySourceMapEnabled = true;
let production = false; 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.resolve.alias.set('react-dom', 'react-nativescript');
config.module 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. * 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. * 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; return args;
@ -42,21 +46,23 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
// todo: env flag to forceEnable? // todo: env flag to forceEnable?
config.when(env.hmr && !production, (config) => { config.when(env.hmr && !production, (config) => {
config.plugin('ReactRefreshWebpackPlugin').use(function ReactRefreshWebpackPlugin() {}, [ 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 /**
*/ * Maybe one day we'll implement an Error Overlay, but the work involved is too daunting for now.
overlay: false, * @see https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/79#issuecomment-644324557
/** */
* If you (temporarily) want to enable HMR on a production build: overlay: false,
* 1) Set `forceEnable` to `true` /**
* 2) Remove the `!production` condition on `tsxRule` to ensure that babel-loader gets used. * If you (temporarily) want to enable HMR on a production build:
*/ * 1) Set `forceEnable` to `true`
forceEnable: false, * 2) Remove the `!production` condition on `tsxRule` to ensure that babel-loader gets used.
}, */
]); forceEnable: false,
},
]);
}); });
return config; return config;

View File

@ -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);
}

View File

@ -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 || (<any>ast).type !== 'stylesheet' || !ast.stylesheet) {
return [];
}
return <Import[]>(
ast.stylesheet.rules.filter(
(rule) => rule.type === 'import' && (<any>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),
};
}

View File

@ -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)
);
}