From a5ad3a131b31dd5fae59bf60a4635b62aa601b74 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Sun, 16 Feb 2025 22:33:36 +0200 Subject: [PATCH 1/5] refactor: simplify generator --- package-lock.json | 3 + package.json | 3 + .../react-components-pro/tsconfig.build.json | 5 +- packages/react-components-pro/tsconfig.json | 8 + packages/react-components/package.json | 4 +- packages/react-components/tsconfig.build.json | 5 +- packages/react-components/tsconfig.json | 8 + scripts/generator.ts | 420 ++++-------------- tsconfig.json | 6 +- 9 files changed, 120 insertions(+), 342 deletions(-) create mode 100644 packages/react-components-pro/tsconfig.json create mode 100644 packages/react-components/tsconfig.json diff --git a/package-lock.json b/package-lock.json index a2368f2..206ae9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,9 @@ "type-fest": "4.30.1", "typescript": "^5.7.2", "vite": "^5.4.11" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index 4e335ab..3a1f8a0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "type": "module", "version": "24.7.0-alpha9", "private": true, + "engines": { + "node": ">=22.0.0" + }, "scripts": { "build": "npm run build:load-schema && npm run build -w packages/react-components && npm run build -w packages/react-components-pro", "build:load-schema": "tsx scripts/schema-loader.ts", diff --git a/packages/react-components-pro/tsconfig.build.json b/packages/react-components-pro/tsconfig.build.json index f9ab96f..0b102cc 100644 --- a/packages/react-components-pro/tsconfig.build.json +++ b/packages/react-components-pro/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "outDir": ".", "module": "NodeNext", @@ -7,6 +7,5 @@ "noEmit": false, "emitDeclarationOnly": true }, - "include": ["./src"], - "exclude": ["node_modules", "dist"] + "include": ["src"] } diff --git a/packages/react-components-pro/tsconfig.json b/packages/react-components-pro/tsconfig.json new file mode 100644 index 0000000..6d69ee5 --- /dev/null +++ b/packages/react-components-pro/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2021", + "lib": ["es2021", "dom"] + }, + "include": ["src"] +} diff --git a/packages/react-components/package.json b/packages/react-components/package.json index bab15ad..6f1a715 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -17,7 +17,7 @@ "build": "npm run build:dev && npm run build:code && npm run build:update-packagejson", "build:dev": "npm run build:generate && npm run build:generate-css", "build:generate": "tsx ../../scripts/generator.ts", - "build:generate-css": "tsx --experimental-vm-modules ../../scripts/css-generator.ts", + "build:generate-css": "tsx ../../scripts/css-generator.ts", "build:update-packagejson": "tsx ../../scripts/package-json-update.ts", "build:code": "concurrently npm:build:code:*", "build:code:ts": "tsx ../../scripts/build.ts", @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} \ No newline at end of file +} diff --git a/packages/react-components/tsconfig.build.json b/packages/react-components/tsconfig.build.json index f9ab96f..0b102cc 100644 --- a/packages/react-components/tsconfig.build.json +++ b/packages/react-components/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "outDir": ".", "module": "NodeNext", @@ -7,6 +7,5 @@ "noEmit": false, "emitDeclarationOnly": true }, - "include": ["./src"], - "exclude": ["node_modules", "dist"] + "include": ["src"] } diff --git a/packages/react-components/tsconfig.json b/packages/react-components/tsconfig.json new file mode 100644 index 0000000..a710e2b --- /dev/null +++ b/packages/react-components/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2021", + "lib": ["es2021", "dom"], + }, + "include": ["src"] +} diff --git a/scripts/generator.ts b/scripts/generator.ts index 4cc2058..f51b780 100644 --- a/scripts/generator.ts +++ b/scripts/generator.ts @@ -1,42 +1,12 @@ -import { unlink, writeFile, readFile } from 'node:fs/promises'; +import { unlink, writeFile, readFile, glob } from 'node:fs/promises'; import { relative, resolve, basename } from 'node:path'; -import ts, { - type ExpressionStatement, - type Identifier, - type Node, - type SourceFile, - type TypeAliasDeclaration, - type VariableDeclaration, -} from 'typescript'; +import { createPrinter, createSourceFile, NewLineKind, ScriptKind, ScriptTarget, type SourceFile } from 'typescript'; import type { HtmlElement as SchemaHTMLElement, JSONSchemaForWebTypes } from '../types/schema.js'; import { extractElementsFromDescriptions, loadDescriptions } from './descriptions.js'; import { generatedDir, nodeModulesDir, utilsDir, packageDir } from './utils/config.js'; import { ElementNameMissingError } from './utils/errors.js'; -import fromAsync from './utils/fromAsync.js'; -import { fswalk } from './utils/fswalk.js'; -import { - camelCase, - createImportPath, - createSourceFile, - type NamedGenericJsContribution, - pickNamedEvents, - search, - template, - transform, - convertElementNameToClassName, -} from './utils/misc.js'; -import { eventSettings, genericElements, NonGenericInterface } from './utils/settings.js'; - -// Placeholders -const CALL_EXPRESSION = '$CALL_EXPRESSION$'; -const COMPONENT_NAME = '$COMPONENT_NAME$'; -const COMPONENT_TAG = '$COMPONENT_TAG$'; -const CREATE_COMPONENT_PATH = '$CREATE_COMPONENT_PATH$'; -const EVENT_MAP = '$EVENT_MAP$'; -const EVENT_MAP_REF_IN_EVENTS = '$EVENT_MAP_REF_IN_EVENTS$'; -const EVENTS_DECLARATION = '$EVENTS_DECLARATION$'; -const LIT_REACT_PATH = '@lit/react'; -const MODULE_PATH = '$MODULE_PATH$'; +import { camelCase, createImportPath, pickNamedEvents, search, convertElementNameToClassName } from './utils/misc.js'; +import { eventSettings, type GenericElementInfo, genericElements, NonGenericInterface } from './utils/settings.js'; type ElementData = Readonly<{ packageName: string; @@ -44,7 +14,9 @@ type ElementData = Readonly<{ }>; // Remove all existing files -await fromAsync(fswalk(generatedDir), ([path]) => unlink(path)); +for await (const path of glob('**/*', { cwd: generatedDir })) { + await unlink(path); +} const packagePath = resolve(packageDir, './package.json'); const packageJson = JSON.parse(await readFile(packagePath, 'utf8')); @@ -76,226 +48,40 @@ async function prepareElementFiles( } const descriptions = await loadDescriptions(); -const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed, + +const printer = createPrinter({ + newLine: NewLineKind.LineFeed, removeComments: false, }); -function createGenericTypeNames(numberOfGenerics: number) { - return Array.from({ length: numberOfGenerics }, (_, i) => ts.factory.createIdentifier(`T${i + 1}`)); -} - -function isEventMapDeclaration(node: Node): node is TypeAliasDeclaration { - return ts.isTypeAliasDeclaration(node) && node.name.text === EVENT_MAP; -} - -function isEventListDeclaration(node: Node): node is Identifier { - return ts.isIdentifier(node) && node.text === EVENTS_DECLARATION; -} - -function isEventMapReferenceInEventsDeclaration(node: Node): node is Identifier { - return ts.isIdentifier(node) && node.text === EVENT_MAP_REF_IN_EVENTS; +function createIfElse(condition: boolean) { + return (positive = '', negative = '') => (condition ? positive : negative); } -function isComponentPropsDeclaration(node: Node): node is TypeAliasDeclaration { - return ts.isTypeAliasDeclaration(node) && node.name.text === `${COMPONENT_NAME}Props`; -} +function createGenerics({ numberOfGenerics, typeConstraints, nonGenericInterfaces }: GenericElementInfo) { + const typeArguments = Array.from({ length: numberOfGenerics }, (_, i) => `T${i + 1}`); + const typeParameters = typeArguments + .map((generic, index) => [generic, typeConstraints?.[index]] as const) + .map(([generic, constraint]) => (constraint ? `${generic} extends ${constraint}` : generic)); -function isComponentDeclaration(node: Node): node is VariableDeclaration { - return ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === COMPONENT_NAME; -} + let eventMapTypeArguments: typeof typeArguments = []; + let eventMapTypeParameters: typeof typeParameters = []; -function createEventMapDeclaration( - originalNode: TypeAliasDeclaration, - elementName: string, - events: readonly NamedGenericJsContribution[] | undefined, -): Node { - const { remove: eventsToRemove, makeUnknown: eventsToBeUnknown } = eventSettings.get(elementName) ?? {}; - const eventNameTypeId = ts.factory.createIdentifier('EventName'); - const eventMapId = ts.factory.createIdentifier(EVENT_MAP); - - return ts.factory.createTypeAliasDeclaration( - originalNode.modifiers, - eventMapId, - undefined, - ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Readonly'), [ - ts.factory.createTypeLiteralNode( - events - ?.filter(({ name: eventName }) => (eventsToRemove ? !eventsToRemove.includes(eventName) : true)) - .map(({ name: eventName }) => - ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier(`on${camelCase(eventName!)}`), - undefined, - ts.factory.createTypeReferenceNode( - eventNameTypeId, - eventsToBeUnknown?.includes(eventName!) - ? [ - ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('CustomEvent'), [ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ]), - ] - : [ - ts.factory.createIndexedAccessTypeNode( - ts.factory.createTypeReferenceNode(`_${elementName}EventMap`), - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(eventName!)), - ), - ], - ), - ), - ), - ), - ]), - ); -} - -function createEventList(elementName: string, events: readonly NamedGenericJsContribution[] | undefined): Node { - const { remove: eventsToRemove } = eventSettings.get(elementName) ?? {}; - - return ts.factory.createObjectLiteralExpression( - events - ?.filter(({ name: eventName }) => (eventsToRemove ? !eventsToRemove.includes(eventName) : true)) - .map(({ name: eventName }) => { - const eventString = ts.factory.createStringLiteral(eventName); - return ts.factory.createPropertyAssignment( - ts.factory.createIdentifier(`on${camelCase(eventName)}`), - eventString, - ); - }), - ); -} - -function removeAllEventRelated(node: Node, hasEvents: boolean, hasKnownEvents: boolean): Node | undefined { - if (hasEvents && hasKnownEvents) { - return node; + if (!nonGenericInterfaces?.includes(NonGenericInterface.EVENT_MAP)) { + eventMapTypeArguments = typeArguments; + eventMapTypeParameters = typeParameters; } - if (!hasEvents && ts.isImportSpecifier(node) && node.name.text === 'EventName') { - return undefined; - } - - if (!hasKnownEvents && ts.isImportSpecifier(node) && node.name.text === `_${COMPONENT_NAME}EventMap`) { - return undefined; - } - - return node; + return { + typeParameters: toGenericsString(typeParameters), + typeArguments: toGenericsString(typeArguments), + eventMapTypeArguments: toGenericsString(eventMapTypeArguments), + eventMapTypeParameters: toGenericsString(eventMapTypeParameters), + }; } -function addGenerics(node: Node, elementName: string) { - const genericElementInfo = genericElements.get(elementName); - - if (!genericElementInfo?.numberOfGenerics) { - return node; - } - - const genericTypeNames = createGenericTypeNames(genericElementInfo.numberOfGenerics); - - const typeParameters = genericTypeNames.map((id, index) => { - const typeConstraint = genericElementInfo.typeConstraints?.[index]; - const typeReference = typeConstraint ? ts.factory.createTypeReferenceNode(typeConstraint) : undefined; - // If typeConstraint is provided, use it as constraint and default type for the type parameter. - return ts.factory.createTypeParameterDeclaration(undefined, id, typeReference, typeReference); - }); - const typeArguments = genericTypeNames.map((id) => ts.factory.createTypeReferenceNode(id)); - - const isEventMapGeneric = !genericElementInfo.nonGenericInterfaces?.includes(NonGenericInterface.EVENT_MAP); - - if (isEventMapDeclaration(node) && isEventMapGeneric) { - // export type GridEventMap = Readonly<{ - // ^ adding this type argument - // onActiveItemChanged: EventName<_GridEventMap["active-item-changed"]>; - // ... - // }> - const declaration = ts.factory.createTypeAliasDeclaration(node.modifiers, node.name, typeParameters, node.type); - - return ts.transform(declaration, [ - // export type GridEventMap = Readonly<{ - // onActiveItemChanged: EventName<_GridEventMap["active-item-changed"]>; - // ^ adding these type arguments - // ... - // }> - transform((node) => - ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text.endsWith('EventMap') - ? ts.factory.createTypeReferenceNode(node.typeName, typeArguments) - : node, - ), - ]).transformed[0]; - } - - if (isEventMapReferenceInEventsDeclaration(node)) { - // const events = { - // onActiveItemChanged: "active-item-changed", - // ... - // } as GridEventMap; - // ^ adding this type argument - return ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(EVENT_MAP), - isEventMapGeneric - ? genericTypeNames.map(() => ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)) - : undefined, - ); - } - - if (isComponentPropsDeclaration(node)) { - // export type GridProps = WebComponentProps, GridEventMap>; - // ^ adding this type parameter - const declaration = ts.factory.createTypeAliasDeclaration(node.modifiers, node.name, typeParameters, node.type); - - return ts.transform(declaration, [ - // export type GridProps = WebComponentProps, GridEventMap>; - // ^ adding this type argument - transform((node) => - ts.isTypeReferenceNode(node) && - ts.isIdentifier(node.typeName) && - node.typeName.text === `${COMPONENT_NAME}Element` - ? ts.factory.createTypeReferenceNode(node.typeName, typeArguments) - : node, - ), - ...(isEventMapGeneric - ? [ - // export type GridProps = WebComponentProps, GridEventMap>; - // ^ adding this type argument - transform((node) => - ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === EVENT_MAP - ? ts.factory.createTypeReferenceNode(node.typeName, typeArguments) - : node, - ), - ] - : []), - ]).transformed[0]; - } - - if (isComponentDeclaration(node)) { - const { initializer: originalInitializer } = node; - - const asExpression = template( - ` -${CALL_EXPRESSION} as ( - props: ${COMPONENT_NAME}Props & React.RefAttributes<${COMPONENT_NAME}Element>, -) => React.ReactElement | null - `, - (statements) => (statements[0] as ExpressionStatement).expression, - [ - transform((node) => - ts.isTypeReferenceNode(node) && - ts.isIdentifier(node.typeName) && - (node.typeName.text === `${COMPONENT_NAME}Props` || node.typeName.text === `${COMPONENT_NAME}Element`) - ? ts.factory.createTypeReferenceNode(node.typeName, typeArguments) - : node, - ), - transform((node) => - ts.isFunctionTypeNode(node) - ? ts.factory.createFunctionTypeNode(typeParameters, node.parameters, node.type) - : node, - ), - transform((node) => (ts.isIdentifier(node) && node.text === CALL_EXPRESSION ? originalInitializer : node)), - ], - ); - - return ts.factory.createVariableDeclaration(node.name, undefined, undefined, asExpression); - } - - return node; +function toGenericsString(generics: readonly string[]) { + return generics.length > 0 ? `<${generics.join(', ')}>` : ''; } function generateReactComponent({ name, js }: SchemaHTMLElement, { packageName, path }: ElementData): SourceFile { @@ -305,105 +91,77 @@ function generateReactComponent({ name, js }: SchemaHTMLElement, { packageName, const elementName = convertElementNameToClassName(name); const elementModulePath = createImportPath(relative(nodeModulesDir, path), false); - const eventMapId = ts.factory.createIdentifier(`${elementName}EventMap`); - const elementClassNameId = ts.factory.createIdentifier(`${elementName}Element`); - const componentTagLiteral = ts.factory.createStringLiteral(name); const createComponentPath = createImportPath(relative(generatedDir, resolve(utilsDir, './createComponent.js')), true); - const hasEvents = !!js?.events && js.events.length > 0; + const hasEvents = createIfElse(!!js?.events && js.events.length > 0); const events = js?.events; const eventNameMissingLogger = () => console.error(`[${packageName}]: event name is missing`); const namedEvents = pickNamedEvents(events, eventNameMissingLogger); const { remove: eventsToRemove, makeUnknown: eventsToBeUnknown } = eventSettings.get(elementName) ?? {}; - const hasKnownEvents = - namedEvents?.some(({ name }) => !eventsToRemove?.includes(name) && !eventsToBeUnknown?.includes(name)) || false; + const existingEvents = namedEvents?.filter(({ name: eventName }) => + eventsToRemove ? !eventsToRemove.includes(eventName) : true, + ); + const hasKnownEvents = createIfElse( + !!namedEvents?.some(({ name }) => !eventsToRemove?.includes(name) && !eventsToBeUnknown?.includes(name)), + ); const genericElementInfo = genericElements.get(elementName); - - const ast = template( - ` -import type { EventName } from "${LIT_REACT_PATH}"; + const { + typeArguments = '', + typeParameters = '', + eventMapTypeArguments = '', + eventMapTypeParameters = '', + } = genericElementInfo ? createGenerics(genericElementInfo) : {}; + + const code = `${hasEvents(`import type { EventName } from '@lit/react';`)} import { - ${COMPONENT_NAME} as ${COMPONENT_NAME}Element - type ${COMPONENT_NAME}EventMap as _${COMPONENT_NAME}EventMap, - ${[...new Set(genericElementInfo?.typeConstraints || [])].map((constraint) => `type ${constraint}`)} -} from "${MODULE_PATH}"; -import * as React from "react"; -import { createComponent, type WebComponentProps } from "${CREATE_COMPONENT_PATH}"; + ${elementName} as ${elementName}Element, + ${hasKnownEvents(`type ${elementName}EventMap as _${elementName}EventMap,`)} + ${Array.from(new Set(genericElementInfo?.typeConstraints || []), (constraint) => `type ${constraint}`).join(',\n')}, +} from '${elementModulePath}'; +import * as React from 'react'; +import { createComponent, type WebComponentProps } from '${createComponentPath}'; -export * from "${MODULE_PATH}"; +export * from '${elementModulePath}'; export { - ${COMPONENT_NAME}Element, + ${elementName}Element, }; -export type ${EVENT_MAP}; -const events = ${EVENTS_DECLARATION} as ${EVENT_MAP_REF_IN_EVENTS}; -export type ${COMPONENT_NAME}Props = WebComponentProps<${COMPONENT_NAME}Element, ${EVENT_MAP}>; -export const ${COMPONENT_NAME} = createComponent({ - elementClass: ${COMPONENT_NAME}Element, - events, +${hasEvents(`export type ${elementName}EventMap${eventMapTypeParameters} = Readonly<{ + ${existingEvents + ?.map( + ({ name: eventName }) => + `on${camelCase(eventName!)}: EventName<${ + eventsToBeUnknown?.includes(eventName!) + ? 'CustomEvent' + : `_${elementName}EventMap${eventMapTypeArguments}['${eventName!}']` + }>`, + ) + .join(',\n')} +}>; + +const events = { + ${existingEvents?.map(({ name: eventName }) => `'on${camelCase(eventName!)}': '${eventName}'`).join(',\n')} +} as ${elementName}EventMap${eventMapTypeArguments ? '' : ''};`)} + +export type ${elementName}Props${typeParameters} = WebComponentProps<${elementName}Element${typeArguments}${hasKnownEvents(`, ${elementName}EventMap${eventMapTypeArguments}`)}>; +export const ${elementName} = createComponent({ + elementClass: ${elementName}Element, + events${hasEvents(undefined, ': {}')}, react: React, - tagName: ${COMPONENT_TAG} -}); -`, - (statements) => statements, - [ - transform((node) => removeAllEventRelated(node, hasEvents, hasKnownEvents)), - transform((node) => - isEventMapDeclaration(node) ? createEventMapDeclaration(node, elementName, namedEvents) : node, - ), - transform((node) => (isEventListDeclaration(node) ? createEventList(elementName, namedEvents) : node)), - transform((node) => addGenerics(node, elementName)), - transform((node) => { - if (!ts.isStringLiteral(node)) { - return node; - } - - switch (node.text) { - case CREATE_COMPONENT_PATH: - return ts.factory.createStringLiteral(createComponentPath); - case MODULE_PATH: - return ts.factory.createStringLiteral(elementModulePath); - default: - // When createSourceFile hass setParentNodes flag, original string - // literals are not emitted for some reason. Copy as a workaroud. - return ts.factory.createStringLiteral(node.text); - } - }), - transform((node) => - isEventMapReferenceInEventsDeclaration(node) ? ts.factory.createIdentifier(EVENT_MAP) : node, - ), - transform((node) => - ts.isIdentifier(node) && node.text === `${COMPONENT_NAME}Element` ? elementClassNameId : node, - ), - transform((node) => (ts.isIdentifier(node) && node.text === COMPONENT_TAG ? componentTagLiteral : node)), - transform((node) => (ts.isIdentifier(node) && node.text === EVENT_MAP ? eventMapId : node)), - transform((node) => - ts.isIdentifier(node) && node.text.includes(COMPONENT_NAME) - ? ts.factory.createIdentifier(node.text.replaceAll(COMPONENT_NAME, elementName)) - : node, - ), - // TODO: remove the workaround for printing `@deprecated` JSDoc tags. - // TypeScript compiler API does not support .jsDoc attached to nodes, - // and on top of it, emitting @deprecated JSDocDeprecatedTag is skipped, - // see https://github.com/microsoft/TypeScript/issues/17146. - transform((node) => { - if (!('jsDoc' in node)) { - return node; - } - - const tag = (node.jsDoc as ts.JSDoc[] | undefined)?.[0]?.tags?.[0]; - if (!tag || !ts.isJSDocDeprecatedTag(tag)) { - return node; - } - - ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, '* @deprecated ', false); - return node; - }), - ], + tagName: '${name}' +}) as ${typeParameters}( + props: ${elementName}Props${typeArguments} & React.RefAttributes<${elementName}Element${typeArguments}>, +) => React.ReactElement | null; +`; + + return createSourceFile( + resolve(generatedDir, `${elementName}.ts`), + code, + ScriptTarget.ES2019, + undefined, + ScriptKind.TS, ); - - return createSourceFile(ast, resolve(generatedDir, `${elementName}.ts`)); } const elementFilesMap = await prepareElementFiles(descriptions); @@ -415,12 +173,12 @@ const elementNames = sourceFiles.map(({ fileName }) => basename(fileName, '.ts') function generateIndexFile(elementNames: readonly string[], extension: string): SourceFile { const sourceLines = [...elementNames.map((elementName) => `export * from './${elementName}.js';`)]; - return ts.createSourceFile( + return createSourceFile( resolve(packageDir, `index.${extension}`), sourceLines.join('\n'), - ts.ScriptTarget.ES2019, + ScriptTarget.ES2019, undefined, - ts.ScriptKind.TS, + ScriptKind.TS, ); } diff --git a/tsconfig.json b/tsconfig.json index d140c96..ab119e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,10 @@ "declarationMap": true, "esModuleInterop": true, "jsx": "react-jsx", - "target": "es2021", + "target": "esnext", "module": "esnext", "moduleResolution": "bundler", - "lib": ["es2021", "dom"], + "lib": ["esnext", "dom"], "noEmit": true, "skipLibCheck": true, "skipDefaultLibCheck": true, @@ -21,6 +21,6 @@ "useUnknownInCatchVariables": true, "verbatimModuleSyntax": true }, - "include": ["src", "scripts", "test", "dev", "types", "karma.config.cjs", "vite.config.ts"], + "include": ["scripts", "test", "dev", "types", "karma.config.cjs", "vite.config.ts"], "exclude": ["node_modules", "dist"] } From 5e2a0f2e5b3b287a40099eba38fec7d04c2c9898 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Sun, 16 Feb 2025 23:33:41 +0200 Subject: [PATCH 2/5] fix: use correct path for glob --- packages/react-components/package.json | 4 ++-- scripts/generator.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 6f1a715..bab15ad 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -17,7 +17,7 @@ "build": "npm run build:dev && npm run build:code && npm run build:update-packagejson", "build:dev": "npm run build:generate && npm run build:generate-css", "build:generate": "tsx ../../scripts/generator.ts", - "build:generate-css": "tsx ../../scripts/css-generator.ts", + "build:generate-css": "tsx --experimental-vm-modules ../../scripts/css-generator.ts", "build:update-packagejson": "tsx ../../scripts/package-json-update.ts", "build:code": "concurrently npm:build:code:*", "build:code:ts": "tsx ../../scripts/build.ts", @@ -538,4 +538,4 @@ "./renderers/useSimpleRenderer.js": "./renderers/useSimpleRenderer.js", "./renderers/useSimpleRenderer.js.map": "./renderers/useSimpleRenderer.js.map" } -} +} \ No newline at end of file diff --git a/scripts/generator.ts b/scripts/generator.ts index f51b780..12a2ca7 100644 --- a/scripts/generator.ts +++ b/scripts/generator.ts @@ -14,7 +14,7 @@ type ElementData = Readonly<{ }>; // Remove all existing files -for await (const path of glob('**/*', { cwd: generatedDir })) { +for await (const path of glob(resolve(generatedDir, '**/*'))) { await unlink(path); } From 5a4e42ec387553c930ff603223fd48d447e6050c Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Fri, 28 Feb 2025 13:33:11 +0200 Subject: [PATCH 3/5] chore: replace utils with libs --- package-lock.json | 14 +++++++ package.json | 3 +- packages/react-components/package.json | 20 ++++++++++ scripts/copy-dts.ts | 15 +++----- scripts/descriptions.ts | 3 +- scripts/generator.ts | 3 +- scripts/package-json-update.ts | 52 +++++++++++--------------- scripts/polyfills.ts | 8 ++++ scripts/types.ts | 4 ++ scripts/utils/filterEmptyItems.ts | 5 --- scripts/utils/fromAsync.ts | 12 ------ scripts/utils/fswalk.ts | 31 --------------- scripts/utils/misc.ts | 14 ++----- 13 files changed, 83 insertions(+), 101 deletions(-) create mode 100644 scripts/polyfills.ts create mode 100644 scripts/types.ts delete mode 100644 scripts/utils/filterEmptyItems.ts delete mode 100644 scripts/utils/fromAsync.ts delete mode 100644 scripts/utils/fswalk.ts diff --git a/package-lock.json b/package-lock.json index 206ae9e..9ae410f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/react-dom": "^18.3.3", "@types/sinon": "^17.0.3", "@vitejs/plugin-react": "^4.3.4", + "array-from-async": "^3.0.0", "chai-as-promised": "^8.0.1", "chai-dom": "^1.12.0", "concurrently": "^9.1.0", @@ -2755,6 +2756,13 @@ "dequal": "^2.0.3" } }, + "node_modules/array-from-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-from-async/-/array-from-async-3.0.0.tgz", + "integrity": "sha512-gV8/L4y2QB5JTXL9DMdtspGyed2M3V6nMnSN+nNg8ejyUlAAbKAjRS6pfWWINjU/MuFJFMGWPazHPor7hThXQw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -10038,6 +10046,12 @@ "dequal": "^2.0.3" } }, + "array-from-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-from-async/-/array-from-async-3.0.0.tgz", + "integrity": "sha512-gV8/L4y2QB5JTXL9DMdtspGyed2M3V6nMnSN+nNg8ejyUlAAbKAjRS6pfWWINjU/MuFJFMGWPazHPor7hThXQw==", + "dev": true + }, "assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", diff --git a/package.json b/package.json index 3a1f8a0..a6bcd1d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "24.7.0-alpha9", "private": true, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" }, "scripts": { "build": "npm run build:load-schema && npm run build -w packages/react-components && npm run build -w packages/react-components-pro", @@ -46,6 +46,7 @@ "@types/react-dom": "^18.3.3", "@types/sinon": "^17.0.3", "@vitejs/plugin-react": "^4.3.4", + "array-from-async": "^3.0.0", "chai-as-promised": "^8.0.1", "chai-dom": "^1.12.0", "concurrently": "^9.1.0", diff --git a/packages/react-components/package.json b/packages/react-components/package.json index bab15ad..4194938 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -477,11 +477,28 @@ "./css/lumo/Font.css": "./css/lumo/Font.css", "./css/lumo/FontIcons.css": "./css/lumo/FontIcons.css", "./css/lumo/Globals.css": "./css/lumo/Globals.css", + "./css/lumo/mixins/FieldButton.css": "./css/lumo/mixins/FieldButton.css", + "./css/lumo/mixins/MenuOverlay.css": "./css/lumo/mixins/MenuOverlay.css", + "./css/lumo/mixins/MenuOverlayCore.css": "./css/lumo/mixins/MenuOverlayCore.css", + "./css/lumo/mixins/Overlay.css": "./css/lumo/mixins/Overlay.css", + "./css/lumo/mixins/RequiredField.css": "./css/lumo/mixins/RequiredField.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/utilities/Accessibility.module.css": "./css/lumo/utilities/Accessibility.module.css", + "./css/lumo/utilities/Background.module.css": "./css/lumo/utilities/Background.module.css", + "./css/lumo/utilities/Border.module.css": "./css/lumo/utilities/Border.module.css", + "./css/lumo/utilities/Filter.module.css": "./css/lumo/utilities/Filter.module.css", + "./css/lumo/utilities/FlexboxAndGrid.module.css": "./css/lumo/utilities/FlexboxAndGrid.module.css", + "./css/lumo/utilities/FlexboxGrid.module.css": "./css/lumo/utilities/FlexboxGrid.module.css", + "./css/lumo/utilities/Layout.module.css": "./css/lumo/utilities/Layout.module.css", + "./css/lumo/utilities/Shadows.module.css": "./css/lumo/utilities/Shadows.module.css", + "./css/lumo/utilities/Sizing.module.css": "./css/lumo/utilities/Sizing.module.css", + "./css/lumo/utilities/Spacing.module.css": "./css/lumo/utilities/Spacing.module.css", + "./css/lumo/utilities/Transition.module.css": "./css/lumo/utilities/Transition.module.css", + "./css/lumo/utilities/Typography.module.css": "./css/lumo/utilities/Typography.module.css", "./css/lumo/Utility.module.css": "./css/lumo/Utility.module.css", "./css/Material.css": "./css/Material.css", "./css/material/Color.css": "./css/material/Color.css", @@ -490,6 +507,9 @@ "./css/material/ColorLight.css": "./css/material/ColorLight.css", "./css/material/Font.css": "./css/material/Font.css", "./css/material/FontIcons.css": "./css/material/FontIcons.css", + "./css/material/mixins/FieldButton.css": "./css/material/mixins/FieldButton.css", + "./css/material/mixins/Overlay.css": "./css/material/mixins/Overlay.css", + "./css/material/mixins/RequiredField.css": "./css/material/mixins/RequiredField.css", "./css/material/Shadow.css": "./css/material/Shadow.css", "./css/material/Typography.css": "./css/material/Typography.css", "./css/material/UserColors.css": "./css/material/UserColors.css", diff --git a/scripts/copy-dts.ts b/scripts/copy-dts.ts index 0b8eed9..ec1ab8b 100644 --- a/scripts/copy-dts.ts +++ b/scripts/copy-dts.ts @@ -1,12 +1,9 @@ -import { glob } from 'glob'; +import { globIterate as glob } from 'glob'; import { constants, copyFile, mkdir } from 'node:fs/promises'; import { packageURL, srcURL } from './utils/config.js'; -const files = await glob(['**/*.d.ts'], { cwd: srcURL }); -await Promise.all( - files.map(async (file) => { - const dest = new URL(file, packageURL); - await mkdir(new URL('./', dest), { recursive: true }); - return copyFile(new URL(file, srcURL), dest, constants.COPYFILE_FICLONE); - }), -); +for await (const file of glob(['**/*.d.ts'], { cwd: srcURL })) { + const dest = new URL(file, packageURL); + await mkdir(new URL('./', dest), { recursive: true }); + await copyFile(new URL(file, srcURL), dest, constants.COPYFILE_FICLONE); +} diff --git a/scripts/descriptions.ts b/scripts/descriptions.ts index faf0ff9..7dc82b6 100644 --- a/scripts/descriptions.ts +++ b/scripts/descriptions.ts @@ -2,7 +2,6 @@ import { readdir, readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import type { HtmlElement as SchemaHTMLElement, JSONSchemaForWebTypes } from '../types/schema.js'; import { nodeModulesDir } from './utils/config.js'; -import filterEmptyItems from './utils/filterEmptyItems.js'; export type SchemaElementWithPackage = readonly [packageName: string, element: SchemaHTMLElement]; @@ -22,7 +21,7 @@ export async function loadDescriptions(): Promise schema != null); } export function* extractElementsFromDescriptions( diff --git a/scripts/generator.ts b/scripts/generator.ts index 12a2ca7..f6552de 100644 --- a/scripts/generator.ts +++ b/scripts/generator.ts @@ -1,5 +1,6 @@ -import { unlink, writeFile, readFile, glob } from 'node:fs/promises'; +import { unlink, writeFile, readFile } from 'node:fs/promises'; import { relative, resolve, basename } from 'node:path'; +import { globIterate as glob } from 'glob'; import { createPrinter, createSourceFile, NewLineKind, ScriptKind, ScriptTarget, type SourceFile } from 'typescript'; import type { HtmlElement as SchemaHTMLElement, JSONSchemaForWebTypes } from '../types/schema.js'; import { extractElementsFromDescriptions, loadDescriptions } from './descriptions.js'; diff --git a/scripts/package-json-update.ts b/scripts/package-json-update.ts index 5a68626..b54a5b6 100644 --- a/scripts/package-json-update.ts +++ b/scripts/package-json-update.ts @@ -1,13 +1,13 @@ +import './polyfills.js'; +import { globIterate as glob } from 'glob'; import { readFile, writeFile } from 'node:fs/promises'; -import { basename, extname, relative, resolve, sep } from 'node:path'; +import { basename, extname, resolve } from 'node:path'; import { existsSync } from 'node:fs'; import type { PackageJson } from 'type-fest'; import { packageDir, srcDir } from './utils/config.js'; -import fromAsync from './utils/fromAsync.js'; -import { fswalk } from './utils/fswalk.js'; const packageJsonPath = resolve(packageDir, 'package.json'); -const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); +const packageJson: PackageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); const exports: Record = { '.': { @@ -26,9 +26,9 @@ function compareExportsPaths([entrypointA]: ExportsEntry, [entrypointB]: Exports return collator.compare(entrypointA, entrypointB); } -const moduleNames = await fromAsync(fswalk(srcDir), async ([path]) => { - return basename(path, extname(path)); -}); +const moduleNames = await Array.fromAsync(glob('*', { cwd: srcDir, nodir: true }), async (path) => + basename(path, extname(path)), +); Object.assign( exports, @@ -36,10 +36,7 @@ Object.assign( moduleNames .map( (moduleName) => - [ - `./${moduleName}.js`, - { types: `./${moduleName}.d.ts`, default: `./${moduleName}.js` }, - ] as ConditionalExportsEntry, + [`./${moduleName}.js`, { types: `./${moduleName}.d.ts`, default: `./${moduleName}.js` }] as const, ) .sort(compareExportsPaths), ), @@ -51,9 +48,7 @@ Object.assign( Object.assign( exports, Object.fromEntries( - moduleNames - .map((moduleName) => [`./${moduleName}`, `./${moduleName}.js`] as PlainExportsEntry) - .sort(compareExportsPaths), + moduleNames.map((moduleName) => [`./${moduleName}`, `./${moduleName}.js`] as const).sort(compareExportsPaths), ), ); @@ -65,11 +60,10 @@ if (existsSync(outCssDir)) { exports, Object.fromEntries( ( - await fromAsync(fswalk(outCssDir, { recursive: true }), async ([path]) => { - const cssPath = relative(outCssDir, path).replaceAll(sep, '/'); - - return [`./css/${cssPath}`, `./css/${cssPath}`] as PlainExportsEntry; - }) + await Array.fromAsync( + glob('**/*', { cwd: outCssDir, posix: true, nodir: true }), + async (path) => [`./css/${path}`, `./css/${path}`] as const, + ) ).sort(compareExportsPaths), ), ); @@ -82,11 +76,10 @@ if (existsSync(outUtilsDir)) { exports, Object.fromEntries( ( - await fromAsync(fswalk(outUtilsDir, { recursive: true }), async ([path]) => { - const utilsPath = relative(outUtilsDir, path).replaceAll(sep, '/'); - - return [`./utils/${utilsPath}`, `./utils/${utilsPath}`] as PlainExportsEntry; - }) + await Array.fromAsync( + glob('**/*', { cwd: outUtilsDir, posix: true, nodir: true }), + async (path) => [`./utils/${path}`, `./utils/${path}`] as const, + ) ).sort(compareExportsPaths), ), ); @@ -99,16 +92,15 @@ if (existsSync(outRenderersDir)) { exports, Object.fromEntries( ( - await fromAsync(fswalk(outRenderersDir, { recursive: true }), async ([path]) => { - const renderersPath = relative(outRenderersDir, path).replaceAll(sep, '/'); - - return [`./renderers/${renderersPath}`, `./renderers/${renderersPath}`] as PlainExportsEntry; - }) + await Array.fromAsync( + glob('**/*', { cwd: outRenderersDir, posix: true, nodir: true }), + async (path) => [`./renderers/${path}`, `./renderers/${path}`] as const, + ) ).sort(compareExportsPaths), ), ); } -packageJson['exports'] = exports; +packageJson.exports = exports; await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); diff --git a/scripts/polyfills.ts b/scripts/polyfills.ts new file mode 100644 index 0000000..e393cc5 --- /dev/null +++ b/scripts/polyfills.ts @@ -0,0 +1,8 @@ +if (!('fromAsync' in Array)) { + const { fromAsync } = await import('array-from-async'); + Object.defineProperty(Array, 'fromAsync', { + value: fromAsync, + }); +} + +export {}; diff --git a/scripts/types.ts b/scripts/types.ts new file mode 100644 index 0000000..590e1c7 --- /dev/null +++ b/scripts/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/unambiguous +declare module 'array-from-async' { + export const fromAsync: typeof Array.fromAsync; +} diff --git a/scripts/utils/filterEmptyItems.ts b/scripts/utils/filterEmptyItems.ts deleted file mode 100644 index 6d54266..0000000 --- a/scripts/utils/filterEmptyItems.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function filterEmptyItems(arr: Array): I[]; -export default function filterEmptyItems(arr: ReadonlyArray): readonly I[]; -export default function filterEmptyItems(arr: ReadonlyArray): readonly unknown[] { - return arr.filter(Boolean); -} diff --git a/scripts/utils/fromAsync.ts b/scripts/utils/fromAsync.ts deleted file mode 100644 index 174e8ad..0000000 --- a/scripts/utils/fromAsync.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default async function fromAsync( - asyncIterable: AsyncIterable, - mapperFn: (value: T) => U, -): Promise>> { - const arr: Array> = []; - - for await (const item of asyncIterable) { - arr.push(await mapperFn(item)); - } - - return arr; -} diff --git a/scripts/utils/fswalk.ts b/scripts/utils/fswalk.ts deleted file mode 100644 index 42304eb..0000000 --- a/scripts/utils/fswalk.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Dirent } from 'fs'; -import { opendir } from 'node:fs/promises'; -import { join } from 'node:path'; - -export type WalkOptions = Readonly<{ - recursive?: boolean; - yieldDirs?: boolean; -}>; -export type FsWalkResult = readonly [path: string, entry: Dirent]; - -export async function* fswalk(dir: string, options?: WalkOptions): AsyncGenerator { - const subdirs: Dirent[] = []; - - for await (const entry of await opendir(dir)) { - if (entry.isDirectory()) { - subdirs.push(entry); - - if (options?.yieldDirs) { - yield [join(dir, entry.name), entry]; - } - } else if (entry.isFile()) { - yield [join(dir, entry.name), entry]; - } - } - - if (options?.recursive) { - for (const subdir of subdirs) { - yield* fswalk(join(dir, subdir.name)); - } - } -} diff --git a/scripts/utils/misc.ts b/scripts/utils/misc.ts index 5cec198..fd21fe2 100644 --- a/scripts/utils/misc.ts +++ b/scripts/utils/misc.ts @@ -1,6 +1,7 @@ import { constants } from 'node:fs'; import { access } from 'node:fs/promises'; import { join } from 'node:path'; +import { globIterate as glob } from 'glob'; import type { SetRequired } from 'type-fest'; import ts, { type Node, @@ -10,7 +11,6 @@ import ts, { type TransformerFactory, } from 'typescript'; import type { GenericJsContribution } from '../../types/schema.js'; -import { fswalk, type WalkOptions } from './fswalk.js'; import { elementsWithMissingEntrypoint, elementToClassNamingConventionViolations } from './settings.js'; export function camelCase(str: string) { @@ -58,19 +58,13 @@ export async function exists(file: string): Promise { } } -export type SearchOptions = WalkOptions; - -export async function search(elementName: string, dir: string, options?: SearchOptions): Promise { +export async function search(elementName: string, dir: string): Promise { if (elementsWithMissingEntrypoint.has(elementName)) { dir = join(dir, 'src'); } - const fileName = `${elementName}.js`; - for await (const [path, entry] of fswalk(dir, options)) { - if (entry.name === fileName) { - return path; - } - } + const { value } = await glob(`**/${elementName}.js`, { cwd: dir, absolute: true }).next(); + return value as string | undefined; } export function hasOverrideKey([overrideKey]: readonly string[]) { From 22d9c33f88798887c0c7c890eca764b4aef523cf Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Fri, 28 Feb 2025 22:44:12 +0200 Subject: [PATCH 4/5] chore: fix polyfill import --- scripts/polyfills.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/polyfills.ts b/scripts/polyfills.ts index e393cc5..4b1ee42 100644 --- a/scripts/polyfills.ts +++ b/scripts/polyfills.ts @@ -1,5 +1,5 @@ if (!('fromAsync' in Array)) { - const { fromAsync } = await import('array-from-async'); + const { default: fromAsync } = await import('array-from-async'); Object.defineProperty(Array, 'fromAsync', { value: fromAsync, }); From b8d25c3284ab053240278aacb9b19b22f1fab719 Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Fri, 21 Mar 2025 12:34:31 +0200 Subject: [PATCH 5/5] Update package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index baa8bb1..1700660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "vite": "^5.4.11" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" } }, "node_modules/@ampproject/remapping": {