Webpack + vue-loader + postcss-modules results in empty $style object - vue.js

In my app I'm initializing a Vue app, which uses single file .vue components.
I use Webpack to bundle, and vue-loader + postcss-modules to generate scoped classes.
But for some reason I can't access the generated classes inside my components ($style object is empty). I'll explain the problem below and created this repo as an example.
My hello.vue component looks like this:
<template>
<div :class="$style.hello">
Hello World!
</div>
</template>
<script>
export default {
name: "hello",
created() {
console.log(this.$style); // <- empty object :(
}
};
</script>
<style module>
.hello {
background: lime;
}
</style>
hello.vue.json is generated as expected (CSS Modules mapping):
{"hello":"_hello_23p9g_17"}
Scoped styles are appended in the document head, and when using mini-css-extract-plugin it is bundled in app.css:
._hello_23p9g_17 {
background: lime;
}
Does anyone know what the problem is and possibly how to fix this?
Below my config files.
webpack.config.js (trimmed for readability)
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
entry: {
app: "./src/index.js"
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build")
},
resolve: {
extensions: [".js", ".vue", ".json", ".css"],
alias: {
vue: "vue/dist/vue.esm.js"
}
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader"
}
},
{
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.css$/,
use: [
"vue-style-loader",
// MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
// modules: true,
importLoaders: 1
}
},
"postcss-loader"
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: "[name].css"
}),
new VueLoaderPlugin()
]
};
postcss.config.js
module.exports = {
ident: "postcss",
plugins: {
"postcss-preset-env": { stage: 0 },
"postcss-modules": {}
}
};
EDIT:
FYI, setting modules: true in the css-loader options works in that it populates the $style object (see docs):
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: true
}
}
]
}
But in our app we use postcss-loader (as per docs) that takes care of all transformations including scoping. Enabling both modules: true and postcss-modules conflicts and breaks the classes/mapping (as expected).
In other words, I'm looking for a way to omit the modules: true option and enable css modules using postcss-modules instead.

Found a workaround: manually import the styles from the JSON file.
If anyone knows a better way please let me know :)
hello.vue.json
{"hello":"_hello_fgtjb_30"}
hello.vue
<template>
<div :class="style.hello">
Hello World!
</div>
</template>
<script>
import style from "./hello.vue.json";
export default {
name: "hello",
beforeCreate() {
this.style = style;
},
created() {
console.log(this.style);
}
};
</script>
<style module>
.hello {
background: lime;
}
</style>
This only works when using postcss-modules (loaded from config by postcss-loader in my case) instead of using modules: true:
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
importLoaders: 1
}
},
"postcss-loader"
]
};
See this PR as a full example.

Related

Storybook: Unable to override vuetify sass variables using variables.scss

I have created some custom components on top of vuetify components. In order to match my design I have overridden the vuetify sass variables by creating a .scss file in path src/scss/variables.scss and written custom .scss as well. This works absolutely fine but when I try to import the components in my storybook I am unable to see the overridden design.
// .storybook/main.js
const path = require("path");
module.exports = {
stories: [
// "../src/**/*.stories.mdx",
"../src/**/*.stories.#(js|jsx|ts|tsx)"
],
addons: [
"#storybook/addon-links",
{
name: "#storybook/addon-essentials",
options: {
docs: true
}
}
],
webpackFinal: async (config, { configType }) => {
config.resolve.alias["~storybook"] = path.resolve(__dirname);
config.resolve.alias["#"] = path.resolve(__dirname, "..", "src");
config.module.rules.push({
test: /\.s(c|a)ss$/,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
implementation: require("sass")
prependData: "#import '#/scss/variables.scss';", // ISSUE IS HERE
sassOptions: {
indentedSyntax: false // Tried with both true and false
},
}
}
],
include: path.resolve(__dirname, "../")
});
return config;
}
};

Can't compile sass while using Vue Storybook

I'm trying to create storybook on vue. My components written using sass. So, I made this in .storybook/main.js:
webpackFinal: (config) => {
config.module.rules.push({
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
],
})
return config
}
And the styles looks like this:
<style lang="sass" scoped>
button
background-color: red
</style>
So I'm getting this error when trying to compile:
SassError: Invalid CSS after "": expected 1 selector or at-rule, was "button"
on line 1 of C:\Code\testproj\src\components\UI\TestComponent.vue
And if I change my style to this:
<style lang="sass" scoped>
button {
background-color: red
}
</style>
All works, but that's not a sass syntax.
I was having this exact same issue and I was able to solve it. The issue is from the webpack config. If you're using SASS, your webpack.config.js file in your .storybook folder should look like this:
module.exports = ({ config }) => {
config.module.rules.push({
test: /\.sass$/,
use: [
require.resolve("vue-style-loader"),
require.resolve("css-loader"),
{
loader: require.resolve("sass-loader"),
options: {
sassOptions: {
indentedSyntax: true
}
}
}
],
});
return config;
};
And if you're using SCSS, then it should be like this:
module.exports = ({ config }) => {
config.module.rules.push({
test: /\.scss$/,
use: [
require.resolve("vue-style-loader"),
require.resolve("css-loader"),
require.resolve("sass-loader"),
],
});
return config;
};
I was able to figure this out while reading the Vue Loader Docs
The problem is that in your webpack config, you are telling webpack that only use sass-loader when de extension of your file was test: /.s[ac]ss$/i That is to say .sass or .scss. However the extension of your file is .vue, because you are using sass in the vue file of your component.
With that configuration, try to put your sass style in a .sass file and check if works
For anyone using React 17+ with Storybook 6.4.9+, I had a similar problem where the Storybook/webpack build was not including my SCSS files. This configuration in .storybook/main.js worked for me:
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.#(js|jsx|ts|tsx)"
],
"addons": [
"#storybook/addon-links",
"#storybook/addon-essentials"
],
"framework": "#storybook/react",
webpackFinal: async (config) => {
// add SCSS support for CSS Modules
config.module.rules.push({
test: /\.scss$/,
use: [
require.resolve("style-loader"),
require.resolve("css-loader"),
require.resolve("sass-loader"),
],
});
return config;
}
}
I spend a lot of time to find better solution. It is may main.js config file for Storybook 6.4.9:
const path = require('path');
module.exports = {
"stories": [
"../src/**/*.stories.#(js|jsx|ts|tsx|mdx)"
],
"addons": [
"#storybook/addon-essentials",
"#storybook/addon-actions",
"#storybook/addon-controls",
"#storybook/addon-links",
{
name: '#storybook/preset-scss',
options: {
sassLoaderOptions: {
implementation: require('node-sass'), // ATTENTION: We need to use "node-sass" instead "sass/dart-sass"
sassOptions: {
indentedSyntax: true
},
},
}
},
"#storybook/addon-postcss",
],
"framework": "#storybook/vue",
features: {
babelModeV7: true,
},
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
// 'PRODUCTION' is used when building the static version of storybook.
// ATTENTION: Need to preload "global.sass" style for all elements;
config.module.rules.map(rule => {
if (rule.test instanceof RegExp && rule.test.toString() === '/\\.s[ca]ss$/') {
rule.use.push({
loader: require.resolve('sass-resources-loader'),
options: {
resources: [
path.resolve(__dirname, '../src/styles/global.sass')
]
}
})
}
return rule
})
// ATTENTION: Need to compile "Pug" templates.
config.module.rules.push(
{
test: /\.pug$/,
oneOf: [
// this applies to `<template lang="pug">` in Vue components
{
resourceQuery: /^\?vue/,
use: ['pug-plain-loader']
},
// this applies to pug imports inside JavaScript
{
use: ['raw-loader', 'pug-plain-loader']
}
]
}
);
// Return the altered config
return config;
},
}

ERROR in ./src/auth/AuthService.js Module parse failed: Unexpected token (7:16)

I have setup vuejs with webpack 4 and it's working fine now I have added auth0 file in my vuejs application and include it into the login page view It may be a webpack issue as in console the export default not working. Whenever I tried to work with vue-cli it works fine as well in the example. I want it with a webpack 4.
ERROR in ./src/auth/AuthService.js
Module parse failed: Unexpected token (7:16)
You may need an appropriate loader to handle this file type.
|
| export default class AuthService {
| authenticated = this.isAuthenticated()
| authNotifier = new EventEmitter()
|
# ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib/selector.js?type=script&index=0!./src/views/session/Login.vue 59:0-49
# ./src/views/session/Login.vue
# ./src/router/index.js
# ./src/index.js
# multi (webpack)-dev-server/client?http://localhost:8080 babel-polyfill ./src/index.js
This is my login vue file.
<template>
<div>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Auth0 - Vue</a>
<router-link :to="'/'"
class="btn btn-primary btn-margin">
Home
</router-link>
<button
class="btn btn-primary btn-margin"
v-if="!authenticated"
#click="login()">
Log In
</button>
<button
class="btn btn-primary btn-margin"
v-if="authenticated"
#click="logout()">
Log Out
</button>
</div>
</div>
</nav>
<div class="container">
<router-view
:auth="auth"
:authenticated="authenticated">
</router-view>
</div>
</div>
</template>
<script>
import AuthService from './auth/AuthService'
const auth = new AuthService()
const { login, logout, authenticated, authNotifier } = auth
export default {
name: 'app',
data () {
authNotifier.on('authChange', authState => {
this.authenticated = authState.authenticated
})
return {
auth,
authenticated
}
},
methods: {
login,
logout
}
}
</script>
<style>
#import 'bootstrap/dist/css/bootstrap.css';
.btn-margin {
margin-top: 7px
}
</style>
And this is my webpack config file.
const path = require('path');
const fs = require('fs');
const webpack = require('webpack');
// plugins
const HtmlWebPackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');
// the path(s) that should be cleaned
let pathsToClean = [
'dist'
]
// the clean options to use
let cleanOptions = {
root: __dirname,
verbose: false, // Write logs to console.
dry: false
}
// Webpack uses `publicPath` to determine where the app is being served from.
// In development, we always serve from the root. This makes config easier.
const publicPath = '/';
// Make sure any symlinks in the project folder are resolved:
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: ["babel-polyfill", "./src/index.js"],
output: {
// The build folder.
path: resolveApp('dist'),
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: publicPath
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json']
},
devServer: {
contentBase: false,
compress: true,
port: 8080 // port number
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: { minimize: true }
}
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/img/[name].[hash:7].[ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/fonts/[name].[hash:7].[ext]'
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /\.scss$/,
use: [
{
loader: "style-loader"
},
{
loader: "css-loader"
},
{
loader: "sass-loader"
}
]
}
]
},
plugins: [
new CleanWebpackPlugin(pathsToClean, cleanOptions),
new HtmlWebPackPlugin({
template: "./index.html",
filename: "./index.html",
favicon: './static/favicon.png'
}),
new CopyWebpackPlugin([{
from: 'static/img',
to: 'static/img'
}]),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].css"
}),
//jquery plugin
new webpack.ProvidePlugin({
$: 'jquery',
jquery: 'jquery',
'window.jQuery': 'jquery',
jQuery: 'jquery'
})
]
}
I had the same error, and I resolved it translating AuthService.js Module in Babel:
https://babeljs.io/repl
However, is not the solution that everybody want, so, you can try:
Unable to load stage-3 javascript file using babel-loader from webpack
I do not resolved with this solution.

Vue of Typescript. Error: Module not found: Error: Can't resolve './components/mycomponent.vue'

I am using vue of typescript as well as typescript in Express. Everything worked until i want to use single file component in vue.js.
Here is my component,
<template>
<div>This is a simple component. {{msg}}</div>
</template>
<script lang='ts'>
export default {
data() {
return {
msg: "you"
}
}
}
</script>
My webpack file,
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPugPlugin = require('html-webpack-pug-plugin');
const Webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
bundle: [
'./node_modules/bootstrap/dist/js/bootstrap.js',
'./dist/web/views/common/client/ts/_main.js',
'./web/views/common/client/style/main.scss'
]
},
output: {
path: path.resolve(__dirname, 'public'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
ts: 'ts-loader'
},
esModule: true
}
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
minimize: false
}
}, {
loader: 'sass-loader'
}]
})
},
{
test: require.resolve('jquery'),
loader: 'expose-loader?$!expose-loader?jQuery'
}
]
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
plugins: [
new ExtractTextPlugin('styles.css'),
new HtmlWebpackPlugin({
template: './web/views/common/_layout.pug',
filename: '../web/views/common/layout.pug',
filetype: 'pug'
}),
new HtmlWebpackPugPlugin(),
new Webpack.ProvidePlugin({
jQuery: 'jquery',
$: 'jquery',
Popper: 'popper.js'
})
]
};
If I don't use single file component, everything works, but when i introduce .vue file, it will show this error,
ERROR in ./dist/web/views/about/client/ts/_aboutController.js
Module not found: Error: Can't resolve './components/mycomponent.vue' in '/Users/george/github/bochure/dist/web/views/about/client/ts'
# ./dist/web/views/about/client/ts/_aboutController.js 4:24-63
# ./dist/web/views/common/client/ts/_main.js
# multi ./node_modules/bootstrap/dist/js/bootstrap.js ./dist/web/views/common/client/ts/_main.js ./web/views/common/client/style/main.scss
Can anyone help me? You can also download my source at github and help me out. Many thanks.
git#github.com:geforcesong/bochure.git
I'd suggest you to check the component path first.
If you are embedding one vue component into another then check if both are on the same directory level.
If they are then you can use './component-name.vue' and you will be good to go.
Try to change
'./components/mycomponent.vue'
'../components/mycomponent.vue'
This might work in some cases.

Cannot Find Children When Using Enzyme JS Mount

My expectation is that when I use Enzyme's mount function, I should be able to not only query for nodes in the top level element, but also nodes/elements that are rendered in child components.
Here are the tests exhibited in the matter:
import React from 'react';
import chai from 'chai'
import chaiEnzyme from 'chai-enzyme'
chai.use(chaiEnzyme());
const expect = chai.expect;
import {
shallow,
mount
} from 'enzyme';
class Child extends React.Component {
render() {
return <div id='child'>
CHILD
<button onClick={() => console.log('hit me')}>HIT ME</button>
</div>
}
}
class Parent extends React.Component {
render() {
return <div id='root'>
<Child aprop={1}/>
</div>
}
}
describe('example', function() {
describe('mounted', function() {
it('should find the button', function() {
const wrapper = mount(<Parent/>);
expect(wrapper.find('button').length).to.equal(1);
});
it('should find Child component', function() {
const wrapper = mount(<Parent/>);
console.log(wrapper.debug());
expect(wrapper.find(Child).length).to.equal(1);
});
});
describe('shallow', function() {
it('should not find the button', function() {
const wrapper = shallow(<Parent/>);
expect(wrapper.find('button').length).to.equal(0);
});
it('should find Child component', function() {
const wrapper = shallow(<Parent/>);
expect(wrapper.find(Child).length).to.equal(1);
});
});
});
The output is:
example
mounted
✗ should find the button
expected 0 to equal 1
AssertionError#webpack:///~/assertion-error/index.js?6193**:74:0 <- test/components/mount.spec.js:373:25
assert#webpack:///~/chai/lib/chai/assertion.js?fee1**:107:0 <- test/components/mount.spec.js:4705:32
assertEqual#webpack:///~/chai/lib/chai/core/assertions.js?b343**:487:0 <- test/components/mount.spec.js:5222:19
webpack:///~/chai/lib/chai/utils/addMethod.js?ca90**:41:0 <- test/components/mount.spec.js:4292:31
webpack:///test/components/mount.spec.js:35:53 <- test/components/mount.spec.js:153:54
LOG LOG: '<Parent />'
✗ should find Child component
expected 0 to equal 1
AssertionError#webpack:///~/assertion-error/index.js?6193**:74:0 <- test/components/mount.spec.js:373:25
assert#webpack:///~/chai/lib/chai/assertion.js?fee1**:107:0 <- test/components/mount.spec.js:4705:32
assertEqual#webpack:///~/chai/lib/chai/core/assertions.js?b343**:487:0 <- test/components/mount.spec.js:5222:19
webpack:///~/chai/lib/chai/utils/addMethod.js?ca90**:41:0 <- test/components/mount.spec.js:4292:31
webpack:///test/components/mount.spec.js:41:50 <- test/components/mount.spec.js:159:51
shallow
✓ should not find the button
✓ should find Child component
Either mount is not working as expected, or my understanding isn't correct.
Here is my Karma/Webpack file if it helps:
const webpack = require('webpack');
var argv = require('yargs').argv;
var path = require('path');
let srcPath = path.join(__dirname, '/../src/');
const webpackConfig = {
devtool: 'inline-source-map',
resolve: {
// allow us to import components in tests like:
// import Example from 'components/Example';
root: path.resolve(__dirname, './src'),
// allow us to avoid including extension name
extensions: ['', '.js', '.jsx', '.css', '.scss', '.json'],
// required for enzyme to work properly
alias: {
'sinon': 'sinon/pkg/sinon',
}
},
module: {
// don't run babel-loader through the sinon module
noParse: [
/node_modules\/sinon\//
],
isparta: {
embedSource: true,
noAutoWrap: true,
// these babel options will be passed only to isparta and not to babel-loader
babel: {
presets: ['es2015', 'stage-0', 'react']
}
},
// run babel loader for our tests
loaders: [
{
test: /\.(js|jsx)$/,
loader: 'babel',
exclude: path.resolve(__dirname, 'node_modules'),
query: {
plugins: ['transform-decorators-legacy'],
presets: ['es2015', 'airbnb', 'stage-1', 'react']
}
},
{
test: /\.json$/, loader: 'json'
},
{
test: /\.scss$/,
exclude: /[\/\\](node_modules|bower_components|public)[\/\\]/,
loaders: [
'style?sourceMap',
'css?modules&importLoaders=1&localIdentName=[local]',
'postcss',
'sass'
]
},
{
test: /\.css$/,
exclude: /[\/\\](node_modules|bower_components|public)[\/\\]/,
loaders: [
'style?sourceMap',
'css?modules&importLoaders=1&localIdentName=[local]'
]
}
],
preLoaders: [ { //delays coverage til after tests are run, fixing transpiled source coverage error
test: /\.(jsx|js)$/,
include: path.resolve('src/'),
loader: 'isparta',
} ]
},
plugins: [
new webpack.IgnorePlugin(/node-fetch/)
],
// required for enzyme to work properly
externals: {
'jsdom': 'window',
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': 'window',
'react/addons': true
},
};
module.exports = function(config) {
config.set({
browsers: ['PhantomJS'],
singleRun: !argv.watch,
frameworks: ['mocha', 'chai'],
reporters: ['spec', 'coverage'],
// include some polyfills for babel and phantomjs
files: [
'node_modules/whatwg-fetch/fetch.js',
'node_modules/babel-polyfill/dist/polyfill.js',
'./node_modules/phantomjs-polyfill/bind-polyfill.js',
'test/**/*.js'
],
preprocessors: {
// add webpack as preprocessor
'src/**/*.js': ['webpack', 'sourcemap'],
'test/**/*.js': ['webpack', 'sourcemap']
},
// A lot of people will reuse the same webpack config that they use
// in development for karma but remove any production plugins like UglifyJS etc.
// I chose to just re-write the config so readers can see what it needs to have
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
coverageReporter: {
dir: 'coverage/',
reporters: [
{ type: 'html' },
{ type: 'text' }
]
},
// tell karma all the plugins we're going to be using to prevent warnings
plugins: [
'karma-mocha',
'karma-chai',
'karma-webpack',
'karma-phantomjs-launcher',
'karma-spec-reporter',
'karma-sourcemap-loader',
'karma-coverage'
]
});
};