From a07187fadc1974b55cd0edb2f173dbf037154aca Mon Sep 17 00:00:00 2001 From: barak igal Date: Mon, 1 Nov 2021 16:43:30 +0200 Subject: [PATCH] feat(webpack-extensions): add ability to generate css vars named exports in manifest index (3.x) (#2119) --- .../src/create-metadata-stylesheet.ts | 24 ++------ .../src/stylable-manifest-plugin.ts | 50 ++++++++++----- .../src/stylable-metadata-loader.ts | 3 +- packages/webpack-extensions/src/types.ts | 14 +++++ .../test/e2e/manifest.css-vars.spec.ts | 61 +++++++++++++++++++ .../manifest-plugin/Button.comp.st.css | 1 + .../webpack.css-vars.config.js | 27 ++++++++ 7 files changed, 145 insertions(+), 35 deletions(-) create mode 100644 packages/webpack-extensions/test/e2e/manifest.css-vars.spec.ts create mode 100644 packages/webpack-extensions/test/e2e/projects/manifest-plugin/webpack.css-vars.config.js diff --git a/packages/webpack-extensions/src/create-metadata-stylesheet.ts b/packages/webpack-extensions/src/create-metadata-stylesheet.ts index 72d475e84..2b7c6a249 100644 --- a/packages/webpack-extensions/src/create-metadata-stylesheet.ts +++ b/packages/webpack-extensions/src/create-metadata-stylesheet.ts @@ -1,22 +1,15 @@ -import { - Stylable, - StylableMeta, - valueMapping, - Imported, - CSSResolve, - JSResolve, -} from '@stylable/core'; -import { Rule, ChildNode, AtRule } from 'postcss'; +import { Stylable, StylableMeta, valueMapping } from '@stylable/core'; +import type { Rule, ChildNode, AtRule } from 'postcss'; +import type { Metadata, ResolvedImport } from './types'; import { hashContent } from './hash-content-util'; export function createMetadataForStylesheet( stylable: Stylable, content: string, resourcePath: string, - exposeNamespaceMapping = true -) { - const meta = stylable.fileProcessor.processContent(content, resourcePath); - + exposeNamespaceMapping = true, + meta = stylable.fileProcessor.processContent(content, resourcePath) +): Metadata { const usedMeta = collectDependenciesDeep(stylable, meta); const hashes = createContentHashPerMeta(usedMeta.keys()); @@ -110,11 +103,6 @@ export function createContentHashPerMeta(usedMeta: Iterable) { return hashes; } -export type ResolvedImport = { - stImport: Imported; - resolved: CSSResolve | JSResolve | null; -}; - export function collectDependenciesDeep( stylable: Stylable, meta: StylableMeta, diff --git a/packages/webpack-extensions/src/stylable-manifest-plugin.ts b/packages/webpack-extensions/src/stylable-manifest-plugin.ts index 4c67da255..d4b678c49 100644 --- a/packages/webpack-extensions/src/stylable-manifest-plugin.ts +++ b/packages/webpack-extensions/src/stylable-manifest-plugin.ts @@ -2,12 +2,12 @@ import { basename } from 'path'; import { EOL } from 'os'; import webpack from 'webpack'; import { RawSource } from 'webpack-sources'; -import { Stylable } from '@stylable/core'; +import { Stylable, StylableMeta } from '@stylable/core'; import { resolveNamespace } from '@stylable/node'; import { createMetadataForStylesheet } from './create-metadata-stylesheet'; import { hashContent } from './hash-content-util'; import { ComponentsMetadata } from './component-metadata-builder'; -import { Metadata, Manifest } from './types'; +import { MetadataList, Manifest } from './types'; export interface Options { outputType: 'manifest' | 'fs-manifest'; @@ -15,6 +15,7 @@ export interface Options { packageAlias: Record; contentHashLength?: number; exposeNamespaceMapping: boolean; + generateCSSVarsExports: boolean; resolveNamespace(namespace: string, filePath: string): string; filterComponents(resourcePath: string): boolean; getCompId(resourcePath: string): string; @@ -30,6 +31,7 @@ const defaultOptions: Options = { packageAlias: {}, resolveNamespace, exposeNamespaceMapping: true, + generateCSSVarsExports: false, filterComponents(resourcePath) { return resourcePath.endsWith('.comp.st.css'); }, @@ -41,6 +43,12 @@ const defaultOptions: Options = { }, }; +export function generateCssVarsNamedExports(name: string, meta: StylableMeta) { + return Object.keys(meta.cssVars) + .map((varName) => `${varName} as --${name}-${varName.slice(2)}`) + .join(','); +} + export class StylableManifestPlugin { private options: Options; constructor(options: Partial = {}) { @@ -61,31 +69,39 @@ export class StylableManifestPlugin { resolveNamespace: this.options.resolveNamespace, }); - compiler.hooks.done.tap(this.constructor.name + ' stylable.initCache', () => stylable.initCache()); + compiler.hooks.done.tap(this.constructor.name + ' stylable.initCache', () => + stylable.initCache() + ); - let metadata: Array<{ compId: string; metadata: Metadata }>; + let metadataList: MetadataList; compiler.hooks.compilation.tap(this.constructor.name, (compilation) => { compilation.hooks.optimizeModules.tap(this.constructor.name, (modules) => { - metadata = this.createModulesMetadata(compiler, stylable, modules); + metadataList = this.createModulesMetadata(compiler, stylable, [...modules]); }); + + compiler.hooks.emit.tap(this.constructor.name, (compilation) => + this.emitManifest(metadataList, compilation) + ); }); compiler.hooks.emit.tap(this.constructor.name, (compilation) => - this.emitManifest(metadata, compilation) + this.emitManifest(metadataList, compilation) ); } - private emitManifest( - metadata: { compId: string; metadata: Metadata }[], - compilation: webpack.compilation.Compilation - ) { - const manifest = metadata.reduce( - (manifest, { compId, metadata }) => { + private emitManifest(metadataList: MetadataList, compilation: webpack.compilation.Compilation) { + const manifest = metadataList.reduce( + (manifest, { meta, compId, metadata }) => { + const cssVars = this.options.generateCSSVarsExports + ? generateCssVarsNamedExports(compId, meta) + : null; Object.assign(manifest.stylesheetMapping, metadata.stylesheetMapping); Object.assign(manifest.namespaceMapping, metadata.namespaceMapping); manifest.componentsEntries[compId] = metadata.entry; manifest.componentsIndex += `:import{-st-from: ${JSON.stringify( metadata.entry - )};-st-default: ${compId};} ${compId}{}${EOL}`; + )};-st-default: ${compId};${ + cssVars ? `-st-named:${cssVars};` : `` + }} ${compId}{}${EOL}`; return manifest; }, { @@ -125,7 +141,7 @@ export class StylableManifestPlugin { compiler: webpack.compiler.Compiler, stylable: Stylable, modules: webpack.compilation.Module[] - ) { + ): MetadataList { const stylableComps = modules.filter((module) => { const resource = (module as any).resource; return resource && this.options.filterComponents(resource); @@ -134,14 +150,16 @@ export class StylableManifestPlugin { return stylableComps.map((module) => { const resource = (module as any).resource; const source = compiler.inputFileSystem.readFileSync(resource).toString(); - + const meta = stylable.fileProcessor.processContent(source, resource); return { + meta, compId: this.options.getCompId(resource), metadata: createMetadataForStylesheet( stylable, source, resource, - this.options.exposeNamespaceMapping + this.options.exposeNamespaceMapping, + meta ), }; }); diff --git a/packages/webpack-extensions/src/stylable-metadata-loader.ts b/packages/webpack-extensions/src/stylable-metadata-loader.ts index 9172bab73..1d76d68c0 100644 --- a/packages/webpack-extensions/src/stylable-metadata-loader.ts +++ b/packages/webpack-extensions/src/stylable-metadata-loader.ts @@ -2,7 +2,8 @@ import { Stylable, StylableMeta, processNamespace } from '@stylable/core'; import { loader as webpackLoader } from 'webpack'; import findConfig from 'find-config'; import { getOptions } from 'loader-utils'; -import { createMetadataForStylesheet, ResolvedImport } from './create-metadata-stylesheet'; +import { createMetadataForStylesheet } from './create-metadata-stylesheet'; +import { ResolvedImport } from './types'; let stylable: Stylable; const getLocalConfig = loadLocalConfigLoader(); diff --git a/packages/webpack-extensions/src/types.ts b/packages/webpack-extensions/src/types.ts index 6a35c7bd7..2742092e5 100644 --- a/packages/webpack-extensions/src/types.ts +++ b/packages/webpack-extensions/src/types.ts @@ -1,5 +1,8 @@ +import type { CSSResolve, Imported, JSResolve, StylableMeta } from '@stylable/core'; + export interface Metadata { entry: string; + usedMeta: Map; stylesheetMapping: Record; namespaceMapping?: Record; } @@ -12,3 +15,14 @@ export interface Manifest { componentsEntries: Record; componentsIndex: string; } + +export type MetadataList = Array<{ + meta: StylableMeta; + compId: string; + metadata: Metadata; +}>; + +export type ResolvedImport = { + stImport: Imported; + resolved: CSSResolve | JSResolve | null; +}; diff --git a/packages/webpack-extensions/test/e2e/manifest.css-vars.spec.ts b/packages/webpack-extensions/test/e2e/manifest.css-vars.spec.ts new file mode 100644 index 000000000..5c4dbe78d --- /dev/null +++ b/packages/webpack-extensions/test/e2e/manifest.css-vars.spec.ts @@ -0,0 +1,61 @@ +import { StylableProjectRunner } from '@stylable/e2e-test-kit'; +import { expect } from 'chai'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { hashContent } from '@stylable/webpack-extensions'; +import { EOL } from 'os'; + +const project = 'manifest-plugin'; +const projectDir = dirname( + require.resolve(`@stylable/webpack-extensions/test/e2e/projects/${project}/webpack.config`) +); + +describe(`${project} - manifest`, () => { + const projectRunner = StylableProjectRunner.mochaSetup( + { + projectDir, + launchOptions: { + // headless: false + }, + configName: 'webpack.css-vars.config', + }, + before, + afterEach, + after + ); + + it('Should generate manifest for the current build', () => { + const assets = projectRunner.getBuildAssets(); + const manifestKey = Object.keys(assets).find((key) => key.startsWith('stylable.manifest'))!; + const source = assets[manifestKey].source(); + + const compContent = readFileSync( + join(projectRunner.projectDir, 'Button.comp.st.css'), + 'utf-8' + ); + const commonContent = readFileSync( + join(projectRunner.projectDir, 'common.st.css'), + 'utf-8' + ); + const commonHash = hashContent(commonContent); + const compHash = hashContent(compContent); + + expect(JSON.parse(source)).to.deep.include({ + name: 'manifest-plugin-test', + version: '0.0.0-test', + componentsIndex: `:import{-st-from: "/${compHash}.st.css";-st-default: Button;-st-named:--myColor as --Button-myColor;} Button{}${EOL}`, + componentsEntries: { Button: `/${compHash}.st.css` }, + stylesheetMapping: { + [`/${compHash}.st.css`]: compContent.replace( + './common.st.css', + `/${commonHash}.st.css` + ), + [`/${commonHash}.st.css`]: commonContent, + }, + namespaceMapping: { + [`/${commonHash}.st.css`]: 'common911354609', + [`/${compHash}.st.css`]: 'Buttoncomp1090430236', + }, + }); + }); +}); diff --git a/packages/webpack-extensions/test/e2e/projects/manifest-plugin/Button.comp.st.css b/packages/webpack-extensions/test/e2e/projects/manifest-plugin/Button.comp.st.css index 17b363340..30601cacc 100644 --- a/packages/webpack-extensions/test/e2e/projects/manifest-plugin/Button.comp.st.css +++ b/packages/webpack-extensions/test/e2e/projects/manifest-plugin/Button.comp.st.css @@ -4,5 +4,6 @@ } .root { + --myColor: red; color: value(myColor) } diff --git a/packages/webpack-extensions/test/e2e/projects/manifest-plugin/webpack.css-vars.config.js b/packages/webpack-extensions/test/e2e/projects/manifest-plugin/webpack.css-vars.config.js new file mode 100644 index 000000000..7702cf6af --- /dev/null +++ b/packages/webpack-extensions/test/e2e/projects/manifest-plugin/webpack.css-vars.config.js @@ -0,0 +1,27 @@ +const { stylableLoaders } = require('@stylable/experimental-loader'); +const { StylableManifestPlugin } = require('@stylable/webpack-extensions'); + +/** @type {import('webpack').Configuration} */ +module.exports = { + mode: 'development', + context: __dirname, + devtool: 'source-map', + entry: require.resolve('./index'), + output: { + library: 'metadata', + }, + plugins: [ + new StylableManifestPlugin({ + package: require('./package.json'), + generateCSSVarsExports: true, + }), + ], + module: { + rules: [ + { + test: /\.st\.css?$/, + use: [stylableLoaders.transform({ exportsOnly: true })], + }, + ], + }, +};