ESM library generated with rollup-plugin-postcss throws Cannot find module '../node_modules/style-inject/dist/style-inject.es.js' - rollup

We are maintaining an internal library which is exporting ESM modules using Rollup. We have just recently switched to using CSS modules, which we have set with rollup-plugin-postcss. We want to inject these styles into the head rather than have an external file.
Our built bundle generates the ESM file with:
import styleInject from '../node_modules/style-inject/dist/style-inject.es.js';
Our consuming library then fails with
Uncaught Error: Cannot find module '../node_modules/style-inject/dist/style-inject.es.js'
I would expect the ESM export to import styleInject from 'style-inject' and style-inject to be included in the package-lock.json as a dependency. What is the correct way of using CSS Modules and injecting into the head for the consumer of a library?
rollup.config.js
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import babel from '#rollup/plugin-babel';
import json from '#rollup/plugin-json';
import postcss from 'rollup-plugin-postcss';
import pkg from './package.json';
import fg from 'fast-glob';
import path from 'path';
export default [
{
input: 'src/index.js',
external: external(),
output: [
{
name: '#my/packageName',
file: pkg.module,
format: 'es',
sourcemap: true,
},
],
plugins: [
{
name: 'watch-external',
async buildStart() {
const files = await fg(['src/index.d.ts', 'playground/**/*']);
for (let file of files) {
this.addWatchFile(path.resolve(file));
}
},
},
json(),
postcss({
modules: true,
}),
babel({
exclude: /node_modules/,
babelHelpers: 'runtime',
babelrc: false,
presets: [
[
'#babel/preset-env',
{
modules: false,
useBuiltIns: 'entry',
corejs: 3,
targets: {
ie: 11,
},
},
],
'#babel/preset-react',
],
plugins: [
'#babel/plugin-transform-runtime',
'#babel/plugin-proposal-class-properties',
'#babel/plugin-proposal-export-namespace-from',
],
}),
commonjs(),
],
},
];
function external() {
const { dependencies = {}, peerDependencies = {} } = pkg;
const externals = [
...Object.keys(dependencies),
...Object.keys(peerDependencies),
];
return id =>
// match 'lodash' and 'lodash/fp/isEqual' for example
externals.some(dep => id === dep || id.startsWith(`${dep}/`));
}

There is a bug in the library where the import is relative rather than being the module name.
There is an open pr, but the library looks like it is no longer being maintained. There are two recommended solutions in the comments:
String replace the built output
Add a custom rollup plugin to do this

This is not a bug at all. It works properly because stylesInject is just a function from 'styles-inject' package, so we can allow it to be bundled. But it is not bundled because most likely you have this setting in your rollup.config.js:
external: [
/node_modules/
]
So, replace it with a code bellow and it will work:
external: (id) => {
if (/style-inject/.test(id)) return false;
if (/node_modules/.test(id)) return true;
return false;
},
also, you can write a regexp for it instead

You need to do 2 things:
Add style-inject dependency in package.json
"dependencies": {
"style-inject": "^0.3.0"
},
Add following plugin to rollup.config.js
const plugins = [
...
{
/**
* - https://github.com/egoist/rollup-plugin-postcss/issues/381#issuecomment-880771065
* - https://lightrun.com/answers/egoist-rollup-plugin-postcss-esm-library-generated-with-rollup-plugin-postcss-throws-cannot-find-module-node_modulesstyle-in
*/
name: 'Custom Rollup Plugin`',
generateBundle: (options, bundle) => {
Object.entries(bundle).forEach((entry) => {
// early return if the file we're currently looking at doesn't need to be acted upon by this plugin
if (!entry[0].match(/.*(.scss.js)$/)) {
return;
}
// this line only runs for .scss.js files, which were generated by the postcss plugin.
// depending on the use-case, the relative path to style-inject might need to change
bundle[entry[0]].code = entry[1].code.replace(
/\.\.?\/[^\n"?:*<>|]+\/style-inject\/dist\/style-inject.es.js/g,
'style-inject',
);
});
},
}
];
References:
https://github.com/egoist/rollup-plugin-postcss/issues/381#issuecomment-880771065
https://lightrun.com/answers/egoist-rollup-plugin-postcss-esm-library-generated-with-rollup-plugin-postcss-throws-cannot-find-module-node_modulesstyle-in

Related

"You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file". Electron+Vue

I am trying to use the npm package "Typeorm" in my Electron + Vue project, but when I run it I have this error:
Module parse failed: Unexpected character '​' (2:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import { EntitySchema } from "typeorm"
> ​
| export default new EntitySchema({
| name: "Post", // Will use table name `post` as default behaviour.
But I don't even have a webpack.config.js in my project. Not even inside files from frameworks that i'm using. So I can't follow the tips of this https://webpack.js.org/concepts#loaders.
The file that contains this import:
Category.js
import { EntitySchema } from "typeorm"
export default new EntitySchema({
name: "Category", // Will use table name `category` as default behaviour.
tableName: "categories", // Optional: Provide `tableName` property to override the default behaviour for table name.
columns: {
id: {
primary: true,
type: "int",
generated: true,
},
name: {
type: "varchar",
},
},
})
This is my Babel.config:
module.exports = {
presets: [
'#vue/cli-plugin-babel/preset'
]
}
To try to make it work, in my vue.config.js I added this "transpileDependecies", as suggested in #vue/cli-plugin-babel/preset . But it stills the same error.
vue.config.js
const { defineConfig } = require('#vue/cli-service')
module.exports = defineConfig({
transpileDependencies: [
'typeorm'
],
configureWebpack: {
devtool: 'source-map',
},
pluginOptions: {
vuetify: {
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vuetify-loader
}
}
})
Other similar questions did not help..

How to deal with the problem of TS2367 error after the replacement code?

I use rollup-replace as the variable switch of the builder in the project, but I have encountered a problem when using it with TypeScript, and I always give me errors like this:
semantic error TS2367: This condition will always return'true' since the types'boolean' and'string' have no overlap.
I will write code like this in my code
// a.ts
import Prod from './prod';
import Pre from './pre';
If(process.env.PROD === ’true’){
console.log(‘is prod’);
Prod.init();
} else {
console.log(‘is pre’);
Pre.init();
}
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import replace from '#rollup/plugin-replace';
export default [
{
input: 'src/a.ts',
output: { file: 'dist/a.js', format: 'iife' },
plugins: [
replace({ values: { 'process.env.PROD': JSON.stringify(true) }, preventAssignment: true }),
resolve({ browser: true }),
commonjs(),
typescript(),
]
}
]
In the above build code, replace needs to precede resolve, because it is necessary to use replace to avoid useless JS from being imported into the bundle file.
Who can help me solve this meaningless error message?
In the rollup document, I found the advanced usage of treeshake.moduleSideEffects, which talked about the ability I want to achieve.
import typescript from 'rollup-plugin-typescript2';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import replace from '#rollup/plugin-replace';
export default [
{
input: 'src/a.ts',
output: { file: 'dist/a.js', format: 'iife' },
plugins: [
replace({ values: { 'process.env.PROD': JSON.stringify(true) }, preventAssignment: true }),
resolve({ browser: true }),
commonjs(),
typescript(),
],
treeshake: {
moduleSideEffects: false, // Prune unused pure external imports
},
}
]

Webpack using wrong node_modules folder

I have a vue client project that uses a vue library project (the vue library project is also using some 3rd party packages like vue-material).
They are linked via the client project's Package.json like this "lib": "file:../lib" and I am importing components in the client project using import Comp from "lib/src/components/Comp";
The problem is that when I build the client project using Webpack, the files in my library use lib/node_modules/vue instead of node_modules/vue which causes double vue instancing.
Anyone has any idea why when I am using webpack build from the client folder, it looks for vue package in my library folder? and is there a way to get around that?
My webpack.config
"use strict";
const path = require("path");
const utils = require("./utils");
const config = require("../config");
const vueLoaderConfig = require("./vue-loader.conf");
function resolve(dir) {
return path.join(__dirname, "..", dir);
}
module.exports = {
entry: {
app: ["babel-polyfill", "./src/main.js"]
},
output: {
path: config.build.assetsRoot,
filename: "[name].js",
publicPath: process.env.NODE_ENV === "production" ? config.build.assetsPublicPath : config.dev.assetsPublicPath
},
resolve: {
extensions: [".js", ".vue", ".json"],
alias: {
vue$: "vue/dist/vue.esm.js",
"#": resolve("src"),
src: resolve("src"),
assets: resolve("src/assets"),
components: resolve("src/components"),
utilities: resolve("src/utilities"),
directives: resolve("src/directives"),
plugins: resolve("src/plugins"),
data: resolve("src/data"),
"vuex-store": resolve("src/store"),
"lib": resolve("node_modules/lib")
}
},
module: {
rules: [
{
test: /\.(js|vue)$/,
loader: "eslint-loader",
enforce: "pre",
include: [resolve("src")],
options: {
formatter: require("eslint-friendly-formatter")
}
},
{
test: /\.vue$/,
loader: "vue-loader",
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: "babel-loader",
include: [resolve("src"), resolve("../lib/src")]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: utils.assetsPath("img/[name].[hash:7].[ext]")
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10000,
name: utils.assetsPath("media/[name].[hash:7].[ext]")
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: "base64-font-loader",
options: {
limit: 10000,
name: utils.assetsPath("fonts/[name].[hash:7].[ext]")
}
},
{
test: /\.ico$/,
loader: "file-loader?name=[name].[ext]"
}
]
}
};
My client's main entry
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
// Core Imports
import Vue from 'vue'
import App from './App'
// Plugins
import { ComponentsPlugin } from "lib/src/components";
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
As I ran out of solutions to this problem I decided to debug webpack's compiler.
It seems webpack uses a library called 'enhanced-resolve' (We actually send parameters to that library in our webpack's resolve:{} section). And using the resolve.alias property we can redirect any require to any folder we want.
So to redirect any require("vue") to my own vue.esm.js, all I need is to add an entry like this (I originally had that line but it was pointing to a relative path rather than an absolute path):
resolve: {
alias: {
vue$: resolve("node_modules/vue/dist/vue.esm.js"),
}
}
Note the $ at the end of the library name, It signals enhanced-resolve that the package only has 1 module so any require whos name is "vue" or a sub-directory of "vue" should be parsed using the alias.
Enhanced-Resolve - ResolverFactory.js
if(/\$$/.test(alias)) {
onlyModule = true;
....
}
Enhanced-Resolve - AliasPlugin.js
// InnerRequest is the path (vue/dist/vue.esm.js)
// name is the alias name without the $ (vue)
if(innerRequest === name || (!onlyModule && startsWith(innerRequest, name + "/"))) {
continue resolving....
}

Why are my js files being loaded as `index.html` instead of the actual js files created by webpack?

I'm creating a simple build from webpack, using typescript, jade, and stylus. When the final index.html file is spit out, however, it seems to think the js files are just the index.html file and not the actual js files bundled up by webpack and dynamically inserted at the bottom of the html body.
My project directory structure looks like this:
- dist (compiled/transpiled files)
- server
- dependencies
- index.js
- app.js
- app.[hash].js
- polyfills.[hash].js
- node_modules
- src
- server
- dependencies
- index.ts
- app.ts
- client (ng2 ts files)
- index.jade
This is my webpack build:
'use strict';
const webpack = require('webpack');
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const WebpackShellPlugin = require('webpack-shell-plugin');
const rootDir = __dirname;
/**
* Resolve paths so that we don't have to use relative paths when importing dependencies.
* Very helpful when scaling an application and changing the location of a file that my require another file
* in the same directory as the one it used to be in
*/
const pathResolves = [path.resolve(rootDir, 'src'), path.resolve(rootDir, 'node_modules')];
console.log('path', path.resolve(rootDir, 'src/server'));
module.exports = {
entry: {
'app': path.resolve(rootDir, 'src/client/main.ts'),
'polyfills': [
'core-js/es6',
'core-js/es7/reflect',
'zone.js/dist/zone'
]
},
output: {
path: path.resolve(rootDir, 'dist'),
filename: '[name].[hash].js'
},
module: {
rules: [
{
test: /\.component.ts$/,
use: [
{
loader: 'angular2-template-loader'
},
{
loader: 'ts-loader',
options: {
configFileName: path.resolve(rootDir, 'tsconfig.client.json')
}
}],
include: [path.resolve(rootDir, 'src/client')]
},
{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
options: {
configFileName: path.resolve(rootDir, 'tsconfig.client.json')
}
}
],
exclude: /\.component.ts$/
},
{
test: /\.jade$/,
use: ['pug-ng-html-loader']
},
{
test: /\.styl$/,
use: [
{ loader: 'raw-loader' },
{ loader: 'stylus-loader' }
]
}
]
},
resolve: {
extensions: ['.js', '.ts', '.jade', '.styl'],
modules: pathResolves
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'polyfills'
}),
new HTMLWebpackPlugin({
template: path.resolve(rootDir, 'dist/index.html')
}),
/**
* Define any environment variables for client
*/
new webpack.DefinePlugin({
APP_ENV: JSON.stringify(process.env.APP_ENVIRONMENT || 'development')
}),
/**
* This plugin is required because webpack 2.0 has some issues compiling angular 2.
* The angular CLI team implemented this quick regexp fix to get around compilation errors
*/
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
'./'
)
]
};
And finally, this is the src/server/app.ts file that serves up index.html:
import * as express from 'express';
import * as fs from 'fs';
import * as morgan from 'morgan';
import {
Config
}
from './dependencies/config';
export
function app(Container) {
const app = express();
const config: Config = Container.get(Config);
if (config.log.dev) {
app.use(morgan('combined'));
}
app.get('/', (req: express.Request, res: express.Response) => {
const indexPath: string = `dist/index.html`;
const encodeType: string = `utf-8`;
const html = fs.readFile(indexPath, encodeType, (err: Error, result: string) => {
if (err) {
return res.status(500).json(err);
}
return res.send(result);
});
});
return app;
}
The browser console shows the following 404 error messages (they're red in the browser console) when i go to localhost:3000:
GET http://localhost:3000/polyfills.9dcbd04127bb957ccf5e.js
GET http://localhost:3000/app.9dcbd04127bb957ccf5e.js
I know it's supposed to be getting the js files from dist/[file].[hash].js, but can't seem to make it work with webpack. Also, I should note that I set NODE_PATH to ./ in my gulp nodemon config. Any ideas why this isn't working?
Figured it out on my own. Forgot to add app.use(express.static('dist')) middleware to the app.ts file.

How would I import a module within an npm package subfolder with webpack?

Lets say theres a package in node_modules called foo and I want to import a module within a library such as foo/module via webpack & babel...
import Foo from 'foo'; works
import SomeOtherModule from 'foo/module'; fails with the following:
Module not found: Error: Cannot resolve module 'foo/module' in
/Users/x/Desktop/someproject/js
Which makes make it seem like webpack is looking for the file in the wrong place instead of node_modules
My webpack.config looks like this:
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: ['babel-polyfill','./js/script.js'],
output: {
path: __dirname,
filename: './build/script.js'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
query: {
cacheDirectory: true,
presets: ['es2015']
}
}
],
},
plugins: [
new webpack.NoErrorsPlugin()
],
stats: {
colors: true
},
devtool: 'source-map'
};
It should work with import 'foo/module';. It will resolve file ./node_modules/foo/module.js or ./node_modules/foo/module/index.js and not something like ./node_modules/foo/node_modules/module/index.js if it expected (in that case you better to install module via npm).
You can define a custom path using the module attribute in your package.json. Example:
{
...
"module": "dist/mylib.min.js",
...
}
See What is the "module" package.json field for?