I'm trying to remove a lot of unused css within my sass files in my Angular 6 project.
I'm learned that there is a webpack plugin called PurifyCss.
Currently now, I'm unable to eject the webpack config in my angular project so I'm using ngw to help add the necessary plugins (to angular's webpack config) needed to extract the unused css in my sass files.
ngw.config.ts
import * as webpack from 'webpack';
import { Path } from '#angular-devkit/core';
import { NormalizedBrowserBuilderSchema } from '#angular-devkit/build-angular';
import * as PurifyCSSPlugin from 'purifycss-webpack';
import * as path from 'path';
import * as glob from 'glob';
export type WebpackOptions<T = NormalizedBrowserBuilderSchema> = {
root: Path,
projectRoot: Path,
options: T;
};
const command = process.argv[2].toLowerCase();
export default function (config: webpack.Configuration, options: WebpackOptions) {
if (command === 'test') {
console.log('Test configuration is running');
}
console.log('To modify webpack build, you can use ngw.config.ts');
console.log('check path:', glob.sync(path.join(__dirname, 'src/**/*.html')));
config.plugins.push(
new PurifyCSSPlugin({
// This was suggested to help it actually remove the css from: https://github.com/webpack-contrib/purifycss-webpack/issues/54
// Although there is an error of: Error: data['resolveExtensions'] should NOT have additional properties
resolveExtensions: ['.html', '.js'],
// This causes the build to run but does not remove the unused css
paths: glob.sync(path.join(__dirname, 'src/**/*.html'))
}),
);
return config;
}
Using the paths property alone doesn't work and it was suggested to add resolveExtensions from here.
Although this leads to the error below when doing a ngw prod build:
Error: data['resolveExtensions'] should NOT have additional properties
How can I configure the PurifyCSSPlugin to remove unused css within sass files in an Angular-6-cli environement?
Note: I don't have much experience with webpack, so I'm not sure if this config only works with css files instead of scss files. (If so please correct me).
Thanks
Related
Situation
I would like to run some Database code (mongoDB(mongoose)) on server startup / during builds. Considering next js doesn't have any lifecycle hooks that you can hook into in an easy manner, I was trying to perform the database actions in my webpack (next.config.mjs) configuration. However I ran into some problems with importing local files.
Current setup
This is the code of my current next.config.mjs file. (PS. I have also tried the CommonJS way of requiring the needed files, but that also fails with error meessage "module not found".)
None of the lines that import a local typescript file appear to succeed and I have checked the paths multiple times. They always end up with the error message "ERR_MODULE_NOT_FOUND". Only if a node_module package is imported, it works as expected (the mongoose npm package).
Code
/** #type {import('next').NextConfig} */
const { EmployeesSchema } = await import("./mongodb_schemas/employee_schema");
import { EmployeesSchema } from "./mongodb_schemas/employee_schema";
import "./util/test"
import mongoose from "mongoose";
const nextConfig = {
experimental: {
externalDir: true,
},
reactStrictMode: true,
swcMinify: true,
images: {
domains: ["*", "**", "www.google.com"],
},
webpack: (
config,
{ buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }
) => {
if (isServer) {
console.log(process.cwd());
}
return config;
},
};
export default nextConfig;
Anyone got a clue to why this might end up happening / have any possible solutions to the problem? I have also tried with a normal JavaScript file instead of a Typescript file, which also didn't work. I have found some similar asked questions on Stack Overflow but which were all left unanswered.
My guess for the reason why this occurs: during the build of the project, so when "npm run dev" is ran, the next.config.mjs is copied to a different location into the file structure, which means that the relative paths aren't correct anymore and thus the files can't be found.
PS. My apologize if the question is unclear / in an unusual format, it is my first post so not used to it.
I'm working on an extension system for my web app. Third-party developers should be able to extend the app by providing named AMD modules exporting constants and functions following a predefined spec and bundled into a single .js JavaScript file.
Example JavaScript bundle:
define('module1', ['exports', 'module3'], (function (exports, module3) {
exports.spec = 'http://example.com/spec/extension/v1'
exports.onRequest = function (request) { return module3.respond('Hello, World.') }
}));
define('module2', ['exports', 'module3'], (function (exports, module3) {
exports.spec = 'http://example.com/spec/extension/v1'
exports.onRequest = function (request) { return module3.respond('Foo. Bar.') }
}));
define('module3', ['exports'], (function (exports) {
exports.respond = function (message) { return { type: 'message', message: message } }
}));
In the above example module1 and module2 are extension modules (identified by the spec export) and module3 is a shared dependency (e.g. coming from an NPM package). Extension bundles will be loaded in a worker within a sandboxed iframe to seal of the untrusted code in the browser.
Example TypeScript source:
// module1.ts
import respond from 'module3'
export const spec = 'http://example.com/spec/extension/v1'
export const onRequest = (request: Request): Response => respond('Hello, World.')
// module2.ts
import respond from 'module3'
export const spec = 'http://example.com/spec/extension/v1'
export const onRequest = (request: Request): Response => respond('Foo. Bar.')
// module3.ts
import dep from 'some-npm-package'
export respond = (message: string) => dep.createMessageObject(message)
Here is my list of requirements to bundling:
All necessary dependencies (e.g. shared module, NPM package logic) must be included in the bundle
The source code needs to be transpiled to browser compatible code if necessary
The AMD format is required by the custom extension loader implementation
The AMD modules must not be anonymous as the module file names are lost while bundling
No relative paths must be used among dependencies (e.g. ./path/to/module3 instead of module3)
The result should be one JavaScript bundle, thus ONE JavaScript file and ONE sourcemaps file
What's the easiest way to do this?
This is the closest solution I found using rollup and the following rollup.config.js:
import { nodeResolve } from '#rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import typescript from '#rollup/plugin-typescript'
export default {
input: [
'src/module1.ts',
'src/module2.ts'
],
output: {
dir: 'dist',
format: 'amd',
sourcemap: true,
amd: {
autoId: true
}
},
plugins: [
typescript(),
nodeResolve(),
terser()
]
}
From this I get the desired named AMD modules (one for each entry point and chunk) in separate .js files. Problems:
Some dependencies are referenced by ./module3 while being named module3.
The modules appear in separate JavaScript and Sourcemap files instead of being concatenated into a single bundle.
Questions:
Is there an easy fix to the above rollup.config.js config to solve the problem?
I tried to write a small rollup plugin but I failed to get the final AMD module code within it to concatenate it to a bundle. Only the transpiled code is available to me. In addition I don't know how to handle sourcemaps during concatenation.
Is there an alternative to rollup better suited to this bundling scenario?
The big picture: Am I completely on the wrong track when it comes to building an extension system? Is AMD the wrong choice?
I found a way to extend the rollup.config.js mentioned in the question with a custom concatChunks rollup plugin to bundle multiple AMD chunks within a single file and having the source maps rendered, too. The only issue I didn't find an answer to was the relative module names that kept popping up. However, this may be resolved in the AMD loader.
Here's the full rollup.config.js that worked for me:
import Concat from 'concat-with-sourcemaps'
import glob from 'glob'
import typescript from '#rollup/plugin-typescript'
import { nodeResolve } from '#rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
const concatChunks = (
fileName = 'bundle.js',
sourceMapFileName = 'bundle.js.map'
) => {
return {
name: 'rollup-plugin-concat-chunks',
generateBundle: function (options, bundle, isWrite) {
const concat = new Concat(true, fileName, '\n')
// Go through each chunk in the bundle
let hasSourceMaps = false
Object.keys(bundle).forEach(fileId => {
const fileInfo = bundle[fileId]
if (fileInfo.type === 'chunk') {
let hasSourceMap = fileInfo.map !== null
hasSourceMaps = hasSourceMaps || hasSourceMap
// Concat file content and source maps with bundle
concat.add(
fileInfo.fileName,
fileInfo.code,
hasSourceMap ? JSON.stringify(fileInfo.map) : null
)
// Prevent single chunks from being emitted
delete bundle[fileId]
}
})
// Emit concatenated chunks
this.emitFile({
type: 'asset',
name: fileName,
fileName: fileName,
source: concat.content
})
// Emit concatenated source maps, if any
if (hasSourceMaps) {
this.emitFile({
type: 'asset',
name: sourceMapFileName,
fileName: sourceMapFileName,
source: concat.sourceMap
})
}
}
}
}
export default {
input: glob.sync('./src/*.{ts,js}'),
output: {
dir: 'dist',
format: 'amd',
sourcemap: true,
amd: {
autoId: true
}
},
plugins: [
typescript(),
nodeResolve(),
terser(),
concatChunks()
]
}
Please make sure you npm install the dependencies referenced in the import statements to make this work.
Considering the big picture, i.e. the extension system itself, I am moving away from a "one AMD module equals one extension/contribution" approach, as current developer tools and JavaScript bundlers are not ready for that (as this question shows). I'll go with an approach similar to the Visual Studio Code Extension API and will use a single "default" module with an activate export to register contributions a bundle has to offer. I hope that this will make extension bundling an easy task no matter what tools or languages are being used.
I'm trying and failing to write a loader for webpack that allows me to do some custom image processing on loaded images. The webpack config is generated by Vue CLI, so I have to integrate the loader into chainWebpack somehow. The simplest example I could think of was one that shouldn't change the images at all:
vue.config.js
const path = require("path");
const { defineConfig } = require('#vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: config => {
config.module
.rule("resize-image")
.test(/\.(jpe?g|png|webp)$/i)
.use("test-loader")
.loader(path.resolve("./test_loader.js"));
}
});
test_loader.js
function testLoader(resourcePath) {
return resourcePath;
}
module.exports = testLoader;
Unfortunately adding this "no-op" loader already breaks the loading of images completely and I'm left wondering if I have some sort of fundamental misunderstanding of how Webpack loaders work. I would think that doing absolutely nothing to the input data shouldn't change the outcome, but it doesn't seem to be the case. Am I missing something obvious here? Why is my basic loader not working?
I have a question regarding vue-cli's target build lib. I have an app that produces a custom element following this documentation. Check:
/* ce.js */
import { defineCustomElement } from 'vue';
import Chat from './App.ce.vue';
const element = defineCustomElement(Chat);
customElements.define('chat-widget', element);
The build command looks as follows:
/* package.json */
"build-wc": "vue-cli-service build --target lib --inline-vue --name chat-widget --dest ./wwwroot src/ce.js"
This is actually working all fine but not exactly how i want it. My images are all generated inline which totally bloats my generated umd file. Also when i put my app on a server it refuses to load the images when inline because of Content-Security-Policy issues (another discussion).
Is there a way to tell webpack / vue-cli that I want my images in separate folder? Preferably in the destination folder under /img.
Figured it out! Just set the maxSize of the image parser to very low and the images will be dropped in /img in the output folder. Cheers!
const { defineConfig } = require("#vue/cli-service");
module.exports = defineConfig({
...
chainWebpack: (config) => {
...
config.module.rule("images").set("parser", {
dataUrlCondition: {
maxSize: 4 * 1024, // 4KiB
},
});
...
},
});
In our app we are using absolute paths for import modules. We have react folder into our resolve root:
Folder structure
We are using webpack for build and develop app and it works ok, with the next options:
resolve: {
modules: [
'node_modules',
path.resolve('src')
]
},
I'm working on integration of storybook and found, that it can't find any module from this react folder.
ERROR in ./stories/index.stories.js
Module not found: Error: Can't resolve 'react/components/Button' in 'project_name/stories'
# ./stories/index.stories.js
for the next line:
import Button from 'react/components/Button';
As mark: I added resolve/modules to .storybook/webpack config and also if I try to import anything other from, for example services/xxx - it works.
Issues
react folder name conflicts with actual React package location: node_modules/react. Webpack tries to resolve to .resolution(default is node_modules) if the file does not exist in the path.
.resolution is not appropriate for this sort of usage. it is mostly used for package resolution because it can't tell source strings.
to change path selectively, use alias instead.
Solution
change your component folder's name so that it does not collide with node_modules/react. a good example is view/components/Button.
add alias to .storybook/main.js setting
// .storybook/main.js
const path = require('path');
module.exports = {
/* ... other settings goes here ... */
/**
* #param {import('webpack').Configuration} config
* */
webpackFinal: async (config, { configType }) => {
if (!config.resolve) config.resolve = {};
// this config allows to resolve `view/...` as `src/view/...`
config.resolve.alias = {
...(config.resolve.alias || {}),
view: path.resolve(__dirname, '../src/view'),
};
return config;
},
};
change storybook code in accordance with (1)
// Button.stories.jsx
import Button from 'view/components/Button';
//...