Skip to content

Commit

Permalink
[zero][webpack] Setup next js plugin package
Browse files Browse the repository at this point in the history
  • Loading branch information
brijeshb42 committed Sep 7, 2023
1 parent 076c4c3 commit fe244d0
Show file tree
Hide file tree
Showing 20 changed files with 1,100 additions and 15 deletions.
7 changes: 4 additions & 3 deletions apps/zero-runtime-vite-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ You can either run `yarn release:build` command to build all the packages, or yo

1. `@mui/zero-runtime`
2. `@mui/zero-tag-processor`
3. `@mui/zero-vite-plugin`

Make sure you have also run `yarn release:build` at least once because we also use `@mui/material` and `@mui/system` packages. On subsequent runs, you can only build the above 2 packages using -
Make sure you have also run `yarn release:build` at least once because we also use `@mui/material` and `@mui/system` packages. On subsequent runs, you can only build the above packages using -

```bash
yarn build
Expand All @@ -19,7 +20,7 @@ yarn build
After building, you can run the project by changing into the directory and then

1. Install dependencies using `yarn install`
2. Start the dev server using `yarn vite`
3. Build the code using `yarn vite build`
2. Start the dev server using `yarn dev`
3. Build the code using `yarn build`

Optionally, before running the dev server, you can run `yarn vite optimize --force` if it logged some error during `yarn vite`.
5 changes: 5 additions & 0 deletions packages/zero-next-plugin/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/consistent-type-imports": "error"
}
}
21 changes: 21 additions & 0 deletions packages/zero-next-plugin/LICENSE.next-with-linaria.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Dario Lehmhus

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions packages/zero-next-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @mui/zero-vite-plugin

Vite plugin to support MUI's `styled` processor.
65 changes: 65 additions & 0 deletions packages/zero-next-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@mui/zero-next-plugin",
"version": "0.0.1-alpha.1",
"private": true,
"author": "MUI Team",
"description": "Vite plugin for MUI zero styled implementation.",
"main": "./src/index.ts",
"keywords": [
"zero runtime",
"css-in-js",
"mui"
],
"repository": {
"type": "git",
"url": "https://github.com/mui/material-ui.git",
"directory": "packages/zero-webpack-plugin"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/mui/material-ui/issues"
},
"homepage": "@TODO",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"scripts": {
"build": "yarn build:legacy && yarn build:modern && yarn build:node && yarn build:stable && yarn build:types && yarn build:copy-files",
"build:legacy": "node ../../scripts/build.mjs legacy",
"build:modern": "node ../../scripts/build.mjs modern",
"build:node": "node ../../scripts/build.mjs node",
"build:stable": "node ../../scripts/build.mjs stable",
"build:copy-files": "node ../../scripts/copyFiles.mjs",
"build:types": "node ../../scripts/buildTypes.mjs",
"prebuild": "rimraf build tsconfig.build.tsbuildinfo",
"release": "yarn build && npm publish build",
"test": "cd ../../ && cross-env NODE_ENV=test mocha 'packages/zero-babel-plugin/**/*.test.{js,ts,tsx}'",
"typescript": "tslint -p tsconfig.json \"{src,test}/**/*.{spec,d}.{ts,tsx}\" && tsc -p tsconfig.json",
"typescript:module-augmentation": "node scripts/testModuleAugmentation.js"
},
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/plugin-syntax-jsx": "^7.22.5",
"@babel/preset-typescript": "^7.22.10",
"@linaria/babel-preset": "^4.5.4",
"@mui/zero-tag-processor": "0.0.1-alpha.1",
"file-system-cache": "2.0.2"
},
"devDependencies": {
"@types/babel__core": "^7.20.1",
"@types/loader-utils": "^2.0.3",
"next": "^13.4.19",
"webpack": "^5.88.2"
},
"peerDependencies": {
"next": ">=12.0.0 <14.0.0"
},
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=12.0.0"
}
}
78 changes: 78 additions & 0 deletions packages/zero-next-plugin/src/VirtualModuleStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Cache, { type FileSystemCache } from 'file-system-cache';
import path from 'path';
import type * as Webpack from 'webpack';

import VirtualModulesPlugin from './plugins/webpack-virtual-modules';
import { isFSCache } from './utils';

type CachedFile = {
path: string;
value: {
content: string;
path: string;
};
};

export default class VirtualModuleStore {
private vmInstances: Map<string, VirtualModulesPlugin> = new Map();

private initialCachedFiles: Record<string, string> = {};

private cssCache: FileSystemCache | undefined;

private dependencyCache: FileSystemCache | undefined;

constructor(config: Webpack.Configuration) {
this.addModule = this.addModule.bind(this);

if (isFSCache(config.cache)) {
const baseDir = config.cache.cacheDirectory || path.join(process.cwd(), '.linaria-cache');

const cachePath = path.join(baseDir, `linaria-${config.mode}`);
this.cssCache = Cache({
basePath: cachePath,
ns: config.cache.version,
});

this.cssCache.load().then((cachedFiles) => {
cachedFiles.files.forEach(({ value: { content, path: filePath } }: CachedFile) => {
this.initialCachedFiles[filePath] = content;
this.addModule(filePath, content, false);
});
});

this.dependencyCache = Cache({
basePath: cachePath,
ns: `${config.cache.version}-deps`,
});
}
}

public createStore(name = 'default') {
const vm = new VirtualModulesPlugin(this.initialCachedFiles);
this.vmInstances.set(name, vm);
return vm;
}

public addModule(filePath: string, content: string, addToCache = true) {
this.vmInstances.forEach((vm) => {
vm.writeModule(filePath, content);
});
if (this.cssCache && addToCache) {
this.cssCache.set(filePath, { content, path: filePath });
}
}

public addModuleDependencies(modulePath: string, deps: string[]) {
if (this.dependencyCache) {
this.dependencyCache.set(modulePath, deps);
}
}

public async getModuleDependencies(modulePath: string): Promise<string[] | null> {
if (this.dependencyCache) {
return this.dependencyCache.get(modulePath);
}
return null;
}
}
165 changes: 165 additions & 0 deletions packages/zero-next-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import type { NextConfig } from 'next';
import type * as NextServer from 'next/dist/server/config-shared';
import path from 'path';
import type * as Webpack from 'webpack';
import { generateCss } from '@mui/zero-tag-processor/generateCss';

import type { PluginOptions, Preprocessor } from '@linaria/babel-preset';
import type { LinariaLoaderOptions } from './loaders/transformLoader';
import { regexLinariaCSS, regexLinariaGlobalCSS } from './loaders/transformLoader';
import ErrorPlugin from './plugins/errorPlugin';
import { isCssLoader, isCssModule } from './utils';
import VirtualModuleStore from './VirtualModuleStore';

export interface ZeroPluginOptions extends PluginOptions {
/**
* An object of the themes that you want passed in as an argument in the callback argument of `styled`.
*/
theme: unknown;
/**
* Prefix string to use in the generated css variables.
*/
cssVariablesPrefix?: string;
/**
* Whether the css variables for the default theme should target the :root selector or not.
* @default true
*/
injectDefaultThemeInRoot?: boolean;
// exclude?: FilterPattern;
sourceMap?: boolean;
preprocessor?: Preprocessor;
}

// Thanks https://github.com/Mistereo/next-linaria/blob/de4fd15269bd059e35797bb7250ce84cc8c5067c/index.js#L3
// for the inspiration
function traverseLoaders(rules: Webpack.RuleSetRule[]) {
for (let i = 0; i < rules.length; i += 1) {
const rule = rules[i];
if (isCssLoader(rule)) {
if (isCssModule(rule)) {
const nextGetLocalIdent = rule.options.modules.getLocalIdent;
const nextMode = rule.options.modules.mode;

// allow global css for *.linaria.global.css files
rule.options.modules.mode = (innerPath) => {
const isGlobal = regexLinariaGlobalCSS.test(innerPath);
if (isGlobal) {
return 'local';
}
return typeof nextMode === 'function' ? nextMode(innerPath) : nextMode;
};

// We don't want the default css-loader to generate classnames
// for linaria modules, since those are generated by linaria.
rule.options.modules.getLocalIdent = (context, _, exportName, ...rest) => {
if (regexLinariaCSS.test(context.resourcePath)) {
return exportName;
}
return nextGetLocalIdent(context, _, exportName, ...rest);
};
}
}
if (typeof rule.use === 'object') {
// FIXME: Can we do it without the typecast?
const useRules = rule.use as Webpack.RuleSetRule | Webpack.RuleSetRule[];
traverseLoaders(Array.isArray(useRules) ? useRules : [useRules]);
}
if (Array.isArray(rule.oneOf)) {
traverseLoaders(rule.oneOf as Webpack.RuleSetRule[]);
}
}
}

let moduleStore: VirtualModuleStore;

export default function withLinaria(nextConfig: NextConfig, zeroConfig: ZeroPluginOptions) {
const {
theme,
cssVariablesPrefix = 'mui',
injectDefaultThemeInRoot = true,
babelOptions = {},
sourceMap = false,
displayName = false,
} = zeroConfig;

const webpack = (config: Webpack.Configuration, options: NextServer.WebpackConfigContext) => {
if (config.module?.rules && config.plugins) {
traverseLoaders(config.module.rules as Webpack.RuleSetRule[]);

// Add our store for virtual linaria css modules
if (!moduleStore) {
moduleStore = new VirtualModuleStore(config);
}
const vmPlugin = moduleStore.createStore(config.name);
vmPlugin.writeModule(
'node_modules/@mui/zero-runtime/styles.css',
generateCss(
{
cssVariablesPrefix,
themeArgs: {
theme,
},
},
{
defaultThemeKey: 'theme',
injectInRoot: injectDefaultThemeInRoot,
},
),
);
config.plugins.push(vmPlugin);

// Show message when linaria cache is out of sync with webpack
config.plugins.push(new ErrorPlugin());

// Add css output loader with access to the module store
// in order to set the correct dependencies
config.module.rules.push({
test: regexLinariaCSS,
exclude: /node_modules/,
use: [
{
loader: path.resolve(__dirname, './loaders/outputCssLoader'),
options: {
moduleStore,
},
},
],
});

// Add linaria loader to transform files
const linariaLoaderOptions: LinariaLoaderOptions = {
...zeroConfig,
babelOptions: {
...babelOptions,
presets: [...(babelOptions.presets ?? []), 'next/babel', '@linaria'],
},
themeArgs: {
theme,
},
sourceMap: sourceMap ?? process.env.NODE_ENV !== 'production',
displayName: displayName ?? process.env.NODE_ENV !== 'production',
moduleStore,
};
config.module.rules.push({
test: /\.(tsx|ts|js|mjs|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: path.resolve(__dirname, './loaders/transformLoader'),
options: linariaLoaderOptions,
},
],
});
}

if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
};

return {
...nextConfig,
webpack,
};
}
32 changes: 32 additions & 0 deletions packages/zero-next-plugin/src/loaders/outputCssLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { RawLoaderDefinitionFunction } from 'webpack';
import type VirtualModuleStore from '../VirtualModuleStore';

type OutputLoaderOptions = {
moduleStore: VirtualModuleStore;
};

type LoaderType = RawLoaderDefinitionFunction<OutputLoaderOptions>;

const cssOutputLoader: LoaderType = function cssOutputLoader(content, inputSourceMap) {
this.async();

const { moduleStore } = this.getOptions();
moduleStore
.getModuleDependencies(this.resourcePath)
.then((deps) => {
if (deps) {
deps.forEach((dep) => {
this.addDependency(dep);
});
}
})
.catch((err) => {
this.emitError(err);
console.error(`Error getting dependencies for ${this.resourcePath}`);
})
.finally(() => {
this.callback(null, content, inputSourceMap);
});
};

export default cssOutputLoader;
Loading

0 comments on commit fe244d0

Please sign in to comment.