diff --git a/packages/react-components/package.json b/packages/react-components/package.json index d991903..049e004 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -476,17 +476,6 @@ "./Upload": "./Upload.js", "./VerticalLayout": "./VerticalLayout.js", "./VirtualList": "./VirtualList.js", - "./css/Lumo.css": "./css/Lumo.css", - "./css/lumo/Badge.css": "./css/lumo/Badge.css", - "./css/lumo/Color.css": "./css/lumo/Color.css", - "./css/lumo/ColorBase.css": "./css/lumo/ColorBase.css", - "./css/lumo/Font.css": "./css/lumo/Font.css", - "./css/lumo/FontIcons.css": "./css/lumo/FontIcons.css", - "./css/lumo/Sizing.css": "./css/lumo/Sizing.css", - "./css/lumo/Spacing.css": "./css/lumo/Spacing.css", - "./css/lumo/Style.css": "./css/lumo/Style.css", - "./css/lumo/Typography.css": "./css/lumo/Typography.css", - "./css/lumo/UserColors.css": "./css/lumo/UserColors.css", "./css/lumo/Utility.module.css": "./css/lumo/Utility.module.css", "./utils/createComponent.d.ts": "./utils/createComponent.d.ts", "./utils/createComponent.d.ts.map": "./utils/createComponent.d.ts.map", diff --git a/scripts/css-generator.ts b/scripts/css-generator.ts index 3ae1748..053b771 100644 --- a/scripts/css-generator.ts +++ b/scripts/css-generator.ts @@ -1,16 +1,18 @@ -import * as themableMixinModule from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; -import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; -import { dirname, posix, relative, resolve, sep } from 'node:path'; -import { createContext, type Module as VmModule, SourceTextModule, SyntheticModule } from 'node:vm'; -import { nodeModulesDir, packageDir, stylePackages } from './utils/config.js'; - -const themePackage = '@vaadin/vaadin-lumo-styles'; - -function getJsPath(moduleId: string): string { - const jsSpecifier = posix.join(...relative(nodeModulesDir, moduleId).split(sep)); - return jsSpecifier.replace(themePackage, './lumo/'); -} +/** + * This script exposes Lumo utility styles as CSS modules for backwards compatibility. + * It copies individual utility CSS files from the @vaadin/vaadin-lumo-styles package + * and processes the main utility.css file to reference these CSS modules. File names + * and paths are adjusted to match the previous file paths. + */ +import { copyFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { nodeModulesDir, packageDir } from './utils/config.js'; + +const themePackage = resolve(nodeModulesDir, '@vaadin/vaadin-lumo-styles'); +const utilitiesSourceDir = resolve(themePackage, 'src', 'utilities'); + +const outputDir = resolve(packageDir, 'css', 'lumo'); +const utilitiesOutputDir = resolve(outputDir, 'utilities'); function toCamelCase(dashSeparated: string): string { return dashSeparated @@ -19,333 +21,34 @@ function toCamelCase(dashSeparated: string): string { .join(''); } -function getCssPath(moduleId: string, name?: string): string { - const jsPath = getJsPath(moduleId); - const dirname = posix.dirname(jsPath); - const cssName = toCamelCase(name || posix.basename(jsPath, '.js')); - const suffix = cssName === 'Utility' || dirname.endsWith('/utilities') ? '.module' : ''; - - return posix.join(dirname, `${cssName}${suffix}.css`); -} - -function resolveSpecifier(specifier: string, moduleId: string) { - const require = createRequire(moduleId); - return require.resolve(specifier); -} - -function storeItem(storage: Map, moduleId: string, item: T) { - const items = storage.get(moduleId) || []; - const index = items.length; - storage.set(moduleId, [...items, item]); - return index; -} - -const globalContents: Map = new Map(); - -const globalWindow = { __moduleId: '' }; - -const vmGlobal = { - window: globalWindow, - document: { - createElement(localName: string) { - const el = { localName }; - - if (localName === 'template') { - return { - ...el, - content: '', - get innerHTML() { - return this.content; - }, - set innerHTML(value) { - this.content = value; - }, - }; - } - - return el; - }, - head: { - childNodes: [], - appendChild(content: string) { - // @ts-ignore - storeItem(globalContents, globalWindow.__moduleId, content); - }, - insertAdjacentElement(where: InsertPosition, el: { localName: string; id?: string; textContent: string }) { - if (el.localName === 'style') { - // @ts-ignore - storeItem(globalContents, globalWindow.__moduleId, el.textContent); - } - }, - }, - }, -}; - -const context = createContext(vmGlobal); - -const shimModules: Map = new Map(); - -function shimModule(specifier: string, moduleNamespaceObject: object) { - const moduleId = resolveSpecifier(specifier, import.meta.url); - const module = new SyntheticModule( - Object.keys(moduleNamespaceObject), - function () { - for (const [exportName, exportContent] of Object.entries(moduleNamespaceObject)) { - this.setExport(exportName, exportContent); - } - }, - { - identifier: moduleId, - context, - }, - ); - shimModules.set(moduleId, module); - return module; -} - -function unsafeCSS() { - throw new Error('forbidden'); -} - -const cssLiterals: Map = new Map(); - -class CSSResult { - public readonly index: number = 0; - public readonly moduleId: string; - public readonly strings: readonly string[]; - public readonly values: ReadonlyArray; - public name?: string; - public hasGlobalReference: boolean = false; - - constructor(moduleId: string, strings: readonly string[], values: ReadonlyArray) { - this.moduleId = moduleId; - this.strings = strings; - this.values = values; - this.index = storeItem(cssLiterals, this.moduleId, this); - } - - toString(): string { - return `@import url(css:${this.moduleId}?${this.index});`; - } -} - -type CSSResultGroup = CSSResult | readonly CSSResult[] | readonly CSSResultGroup[]; +// Create output directories +mkdirSync(utilitiesOutputDir, { recursive: true }); -type CSSResultTransformer = (cssResult: CSSResult) => R; - -function transformCss(css: CSSResultGroup, transformer: CSSResultTransformer): readonly R[] { - if (Array.isArray(css)) { - return css.flatMap((item) => transformCss(item, transformer)); - } else if (css instanceof CSSResult) { - return [transformer(css)]; - } else return []; -} +// Copy individual utility CSS files to CSS modules +readdirSync(utilitiesSourceDir) + .filter((file) => file.endsWith('.css')) + .forEach((file) => { + const moduleName = toCamelCase(file.replace(/\.css$/, '')) + '.module.css'; -const registerStyles = '@vaadin/vaadin-themable-mixin/register-styles.js'; -const registerStylesId = resolveSpecifier(registerStyles, import.meta.url); + const sourceFilePath = resolve(utilitiesSourceDir, file); + const outputFilePath = resolve(utilitiesOutputDir, moduleName); -function shimRegisterStyles(moduleId: string) { - return shimModule(registerStyles, { - css(strings: readonly string[], ...values: ReadonlyArray) { - return new CSSResult(moduleId, strings, values); - }, - registerStyles() {}, - unsafeCSS, - addGlobalThemeStyles(id: string, ...values: ReadonlyArray) { - transformCss(values, (cssResult) => (cssResult.hasGlobalReference = true)); - }, + copyFileSync(sourceFilePath, outputFilePath); }); -} - -const themableMixin = '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; -const themableMixinId = resolveSpecifier(themableMixin, import.meta.url); - -function shimThemableMixin(moduleId: string) { - return shimModule(themableMixin, { - ...themableMixinModule, - css(strings: readonly string[], ...values: ReadonlyArray) { - const result: readonly string[] = moduleId.endsWith('font-icons.js') - ? strings.map((string) => string.replace(/'\\\\([a-z0-9]+)'/g, "'\\$1'")) - : strings; - return new CSSResult(moduleId, result, values); - }, - registerStyles() {}, - unsafeCSS, - }); -} - -const escapePattern = /\\/g; -function escape(str: string) { - return str.replaceAll(escapePattern, '\\\\'); -} - -async function loadModule(moduleId: string): Promise { - if (shimModules.has(moduleId)) { - return shimModules.get(moduleId)!; - } - - const source = await readFile(moduleId, { encoding: 'utf-8' }); - return new SourceTextModule(`window.__moduleId = '${escape(moduleId)}';\n${escape(source)}`, { - identifier: moduleId, - context, - initializeImportMeta(meta) { - meta.url = `file://${moduleId}`; - }, - }); -} - -const moduleMap: Map> = new Map(); -const moduleDependencies: Map = new Map(); - -async function linker(specifier: string, referencingModule: VmModule): Promise { - const moduleId = resolveSpecifier(specifier, referencingModule.identifier); - if (moduleId === registerStylesId) { - return shimRegisterStyles(referencingModule.identifier); - } - - if (moduleId === themableMixinId) { - return shimThemableMixin(referencingModule.identifier); - } - - storeItem(moduleDependencies, referencingModule.identifier, moduleId); - - if (moduleMap.has(moduleId)) { - return moduleMap.get(moduleId)!; - } - - const modulePromise = loadModule(moduleId); - moduleMap.set(moduleId, modulePromise); - const module = await modulePromise; - return module; -} - -// TODO: support icon sets defined with HTML -shimModule(`@vaadin/vaadin-lumo-styles/vaadin-iconset.js`, {}); - -async function parseStylePackage(packageName: string) { - shimModule(`${packageName}/version.js`, {}); - - const entryFile = `${packageName}/all-imports.js`; - const moduleId = resolveSpecifier(entryFile, import.meta.url); - const modulePromise = loadModule(moduleId); - moduleMap.set(moduleId, modulePromise); - const module = await modulePromise; - await module.link(linker); - await module.evaluate({ - timeout: 5 * 60 * 1000, - }); -} - -// Parse css packages -await Promise.all(stylePackages.map(parseStylePackage)); - -function renderCssResult(cssPath: string, cssResult: CSSResult): string { - const resultPath = getCssPath(cssResult.moduleId, cssResult.name); - const cssContents: string[] = []; - if (cssPath === resultPath) { - cssContents.push(cssResult.strings[0]); - for (let i = 0; i < cssResult.values.length; i++) { - const value = cssResult.values[i]; - if (value instanceof CSSResult) { - cssContents.push(renderCssResult(resultPath, value)); - } else if (typeof value === 'number') { - cssContents.push(value.toString()); - } - cssContents.push(cssResult.strings[i + 1]); - } - } else { - cssContents.push(`@import url(./${posix.relative(posix.dirname(cssPath), resultPath)});\n`); - emitCssFile(resultPath, cssResult); - } - return cssContents.join('\n').replace(':host', 'html'); -} - -function renderCss(cssPath: string, css: CSSResultGroup): string { - return transformCss(css, (cssResult) => renderCssResult(cssPath, cssResult)).join('\n'); -} - -const output: Map = new Map(); - -function emitCssFile(cssPath: string, css: CSSResultGroup) { - if (output.has(cssPath)) { - return; - } - - const cssContents: string[] = ['/* Generated file, do not edit */\n']; - cssContents.push(renderCss(cssPath, css)); - output.set(cssPath, cssContents.join('\n')); -} - -// Assign export names to CSSResult -for (const [, modulePromise] of moduleMap) { - const module = await modulePromise; - for (const [name, value] of Object.entries(module.namespace)) { - if (value instanceof CSSResult) { - value.name = name; - } - } -} - -// Process global styles -for (const [moduleId, contents] of globalContents) { - for (const htmlContent of contents) { - if (typeof htmlContent === 'string') { - const styleContent = htmlContent.replaceAll(/(.*)<\/style>/gis, '$1'); - const urlMatch = styleContent.match(/^@import url\(css:(.*)\?(.*)\);$/); - if (urlMatch) { - // Mark global CSSResult reference - const moduleId = urlMatch[1] as string, - index = Number(urlMatch[2] as string); - const result = cssLiterals.get(moduleId)![index]; - result.hasGlobalReference = true; - } else { - // Add synthetic CSSResult from global style content string - const result = new CSSResult(moduleId, [styleContent], []); - result.hasGlobalReference = true; - storeItem(cssLiterals, moduleId, result); - } - } else if ( - typeof htmlContent === 'object' && - (htmlContent as any).localName === 'link' && - (htmlContent as any).rel === 'stylesheet' - ) { - // Add synthentic CSSResult import from global - const url = (htmlContent as any).href as string; - const result = new CSSResult(moduleId, [`@import url(${url});`], []); - result.hasGlobalReference = true; - storeItem(cssLiterals, moduleId, result); - } - } -} - -// Emit .css files from CSSResult literals -for (const [moduleId, cssResults] of cssLiterals) { - emitCssFile(getCssPath(moduleId), cssResults); -} - -// Emit Theme.css entrypoint files from global references -for (const stylePackage of stylePackages) { - const name = 'lumo'; - const stylePackageDir = resolve(nodeModulesDir, stylePackage); - const globalCssResults: Set = new Set(); - - for (const [moduleId, cssResults] of cssLiterals) { - if (!moduleId.startsWith(stylePackageDir)) { - continue; - } - - const referencedCssResults = cssResults.filter((result) => result.hasGlobalReference); - referencedCssResults.forEach((result) => globalCssResults.add(result)); - } - - emitCssFile(`./${toCamelCase(name)}.css`, Array.from(globalCssResults)); -} - -// Write files -const cssDir = resolve(packageDir, 'css'); -for (const [cssPath, contents] of output) { - const filePath = resolve(cssDir, cssPath); - await mkdir(dirname(filePath), { recursive: true }); - await writeFile(filePath, contents, { encoding: 'utf-8' }); -} +// Process utility.css entry point to CSS module +const entryPointSourceFile = resolve(themePackage, 'utility.css'); +const entryPointOutputFile = resolve(outputDir, 'Utility.module.css'); +const entryPointCss = readFileSync(entryPointSourceFile, 'utf-8').replace( + /@import\s+['"]([^'"]+)['"];/g, + (_match, path) => { + const fileName = path + .split('/') + .pop() + .replace(/\.css$/, ''); + const moduleName = toCamelCase(fileName) + '.module.css'; + + return `@import './utilities/${moduleName}';`; + }, +); +writeFileSync(entryPointOutputFile, entryPointCss, 'utf-8'); diff --git a/scripts/utils/config.ts b/scripts/utils/config.ts index 0a5caea..5529597 100644 --- a/scripts/utils/config.ts +++ b/scripts/utils/config.ts @@ -19,5 +19,3 @@ export const srcURL = pathToFileURL(`${srcDir}/`); export const generatedURL = pathToFileURL(`${generatedDir}/`); await Promise.all([mkdir(generatedDir, { recursive: true }), mkdir(typesDir, { recursive: true })]); - -export const stylePackages = ['@vaadin/vaadin-lumo-styles'];