diff --git a/cli/wca.js b/cli/wca.js new file mode 100644 index 00000000000..93b8719897b --- /dev/null +++ b/cli/wca.js @@ -0,0 +1,155 @@ +import { spawn } from 'node:child_process'; +import { glob as globFiles, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve, relative } from 'node:path'; + +const customElementsFileName = 'custom-elements.json'; + +function findObjectEnd(content, start) { + let depth = 0; + let mode = 'code'; + let quote; + + for (let index = start; index < content.length; index++) { + const character = content[index]; + const nextCharacter = content[index + 1]; + + if (mode === 'line-comment') { + if (character === '\n') mode = 'code'; + continue; + } + if (mode === 'block-comment') { + if (character === '*' && nextCharacter === '/') { + mode = 'code'; + index++; + } + continue; + } + if (mode === 'string' || mode === 'template') { + if (character === '\\') { + index++; + } else if (character === quote) { + mode = 'code'; + } + continue; + } + + if (character === '/' && nextCharacter === '/') { + mode = 'line-comment'; + index++; + } else if (character === '/' && nextCharacter === '*') { + mode = 'block-comment'; + index++; + } else if (character === '\'' || character === '"' || character === '`') { + mode = character === '`' ? 'template' : 'string'; + quote = character; + } else if (character === '{') { + depth++; + } else if (character === '}' && --depth === 0) { + return index; + } + } + + throw new Error('Could not find the end of a static properties object.'); +} + +function transformProperties(content) { + const propertyPattern = /\bstatic\s+properties\s*=/g; + let result = ''; + let sourceIndex = 0; + let match; + + while ((match = propertyPattern.exec(content)) !== null) { + const objectStart = content.indexOf('{', propertyPattern.lastIndex); + if (objectStart === -1) throw new Error('Could not find a static properties object.'); + + const objectEnd = findObjectEnd(content, objectStart); + const semicolon = content[objectEnd + 1] === ';' ? 1 : 0; + const declaration = content.slice(match.index, objectEnd + 1 + semicolon); + const object = content.slice(objectStart, objectEnd + 1); + + result += content.slice(sourceIndex, match.index); + result += declaration.replace(/\bstatic\s+properties\s*=\s*/, 'static get properties() { return ') + .replace(object, `${object}; }`); + sourceIndex = objectEnd + 1 + semicolon; + propertyPattern.lastIndex = sourceIndex; + } + + return result + content.slice(sourceIndex); +} + +async function transformFiles(files, temporaryDirectory) { + for (const file of files) { + const sourcePath = resolve(file); + const relativePath = relative(process.cwd(), sourcePath); + const destinationPath = join(temporaryDirectory, relativePath); + + await mkdir(dirname(destinationPath), { recursive: true }); + const content = await readFile(sourcePath, 'utf8'); + await writeFile(destinationPath, transformProperties(content)); + } +} + +function restorePaths(value, temporaryPath) { + if (Array.isArray(value)) { + value.forEach(item => restorePaths(item, temporaryPath)); + } else if (value && typeof value === 'object') { + Object.entries(value).forEach(([key, item]) => { + if (key === 'path' && typeof item === 'string') { + value[key] = item.replace(temporaryPath, '.'); + } else { + restorePaths(item, temporaryPath); + } + }); + } +} + +async function restoreCustomElementsPaths(temporaryDirectory) { + const customElements = JSON.parse(await readFile(customElementsFileName, 'utf8')); + const temporaryPath = `./${relative(process.cwd(), temporaryDirectory)}`; + restorePaths(customElements, temporaryPath); + await writeFile(customElementsFileName, `${JSON.stringify(customElements, null, 2)}\n`); +} + +function runWca(src) { + return new Promise((resolve, reject) => { + const child = spawn('wca', [ + 'analyze', + src, + '--format', 'json', + '--outFile', customElementsFileName + ], { stdio: 'inherit' }); + child.once('error', reject); + child.once('close', (exitCode, signal) => { + if (exitCode === 0) { + resolve(); + } else { + reject(new Error(`wca exited with ${signal ? `signal ${signal}` : `code ${exitCode}`}`)); + } + }); + }); +} + +async function main() { + const [glob] = process.argv.slice(2); + if (!glob) throw new Error('Usage: node ./cli/wca.js '); + + const temporaryDirectory = await mkdtemp(join(tmpdir(), 'wca-')); + + try { + const files = await Array.fromAsync(globFiles(glob, { nodir: true })); + + await transformFiles(files, temporaryDirectory); + + await runWca(join(temporaryDirectory, '**/*.js')); + + await restoreCustomElementsPaths(temporaryDirectory); + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); diff --git a/components/button/button-mixin.js b/components/button/button-mixin.js index c62c430ef86..fb42bc4697f 100644 --- a/components/button/button-mixin.js +++ b/components/button/button-mixin.js @@ -2,74 +2,72 @@ import { FocusMixin } from '../../mixins/focus/focus-mixin.js'; export const ButtonMixin = superclass => class extends FocusMixin(superclass) { - static get properties() { - return { - /** - * @ignore - */ - ariaExpanded: { type: String, reflect: true, attribute: 'aria-expanded' }, - /** - * @ignore - */ - ariaHaspopup: { type: String, reflect: true, attribute: 'aria-haspopup' }, - /** - * @ignore - */ - ariaLabel: { type: String, reflect: true, attribute: 'aria-label' }, - /** - * @ignore - */ - // eslint-disable-next-line lit/no-native-attributes - autofocus: { type: Boolean, reflect: true }, - /** - * Wether the controlled element is expanded. Replaces 'aria-expanded' - * @type {'true' | 'false'} - */ - expanded: { type: String, reflect: true, attribute: 'expanded' }, - /** - * Disables the button - * @type {boolean} - */ - disabled: { type: Boolean, reflect: true }, - /** - * Tooltip text when disabled - * @type {string} - */ - disabledTooltip: { type: String, attribute: 'disabled-tooltip' }, - /** - * @ignore - */ - form: { type: String, reflect: true }, - /** - * @ignore - */ - formaction: { type: String, reflect: true }, - /** - * @ignore - */ - formenctype: { type: String, reflect: true }, - /** - * @ignore - */ - formmethod: { type: String, reflect: true }, - /** - * @ignore - */ - formnovalidate: { type: String, reflect: true }, - /** - * @ignore - */ - formtarget: { type: String, reflect: true }, - /** - * @ignore - */ - name: { type: String, reflect: true }, - /** - * @ignore - */ - type: { type: String, reflect: true } - }; - } + static properties = { + /** + * @ignore + */ + ariaExpanded: { type: String, reflect: true, attribute: 'aria-expanded' }, + /** + * @ignore + */ + ariaHaspopup: { type: String, reflect: true, attribute: 'aria-haspopup' }, + /** + * @ignore + */ + ariaLabel: { type: String, reflect: true, attribute: 'aria-label' }, + /** + * @ignore + */ + // eslint-disable-next-line lit/no-native-attributes + autofocus: { type: Boolean, reflect: true }, + /** + * Wether the controlled element is expanded. Replaces 'aria-expanded' + * @type {'true' | 'false'} + */ + expanded: { type: String, reflect: true, attribute: 'expanded' }, + /** + * Disables the button + * @type {boolean} + */ + disabled: { type: Boolean, reflect: true }, + /** + * Tooltip text when disabled + * @type {string} + */ + disabledTooltip: { type: String, attribute: 'disabled-tooltip' }, + /** + * @ignore + */ + form: { type: String, reflect: true }, + /** + * @ignore + */ + formaction: { type: String, reflect: true }, + /** + * @ignore + */ + formenctype: { type: String, reflect: true }, + /** + * @ignore + */ + formmethod: { type: String, reflect: true }, + /** + * @ignore + */ + formnovalidate: { type: String, reflect: true }, + /** + * @ignore + */ + formtarget: { type: String, reflect: true }, + /** + * @ignore + */ + name: { type: String, reflect: true }, + /** + * @ignore + */ + type: { type: String, reflect: true } + }; constructor() { super(); diff --git a/components/demo/demo-passthrough-mixin.js b/components/demo/demo-passthrough-mixin.js index 99396c01cd1..a0d59f7396f 100644 --- a/components/demo/demo-passthrough-mixin.js +++ b/components/demo/demo-passthrough-mixin.js @@ -8,9 +8,7 @@ import { LitElement } from 'lit'; * @param { String } target The element name or other selector string of the element to pass properties to. */ export const DemoPassthroughMixin = (superclass, target) => class extends LitElement { - static get properties() { - return Object.fromEntries(superclass.elementProperties); - } + static properties = Object.fromEntries(superclass.elementProperties); firstUpdated() { this.target = this.shadowRoot.querySelector(target); diff --git a/components/selection/selection-action-mixin.js b/components/selection/selection-action-mixin.js index 288446c316c..681f803f0f6 100644 --- a/components/selection/selection-action-mixin.js +++ b/components/selection/selection-action-mixin.js @@ -5,21 +5,19 @@ import { SelectionObserverMixin } from './selection-observer-mixin.js'; export const SelectionActionMixin = superclass => class extends LocalizeCoreElement(SelectionObserverMixin(superclass)) { - static get properties() { - return { - /** - * Disables bulk actions in the list or table controls once the selection limit is reached, but does not prevent further selection - * @type {number} - */ - maxSelectionCount: { type: Number, attribute: 'max-selection-count' }, - /** - * Whether the action requires one or more selected items - * @type {boolean} - */ - requiresSelection: { type: Boolean, attribute: 'requires-selection', reflect: true }, - _disabledTooltip: { state: true } - }; - } + static properties = { + /** + * Disables bulk actions in the list or table controls once the selection limit is reached, but does not prevent further selection + * @type {number} + */ + maxSelectionCount: { type: Number, attribute: 'max-selection-count' }, + /** + * Whether the action requires one or more selected items + * @type {boolean} + */ + requiresSelection: { type: Boolean, attribute: 'requires-selection', reflect: true }, + _disabledTooltip: { state: true } + }; constructor() { super(); diff --git a/package.json b/package.json index ed640796330..db1972e1619 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:icons": "node ./cli/icon-generator.js", "build:illustrations": "node ./cli/empty-state-illustration-generator.js", "build:sass": "sass ./test/sass.scss > ./test/sass.output.css", - "build:wca": "wca analyze \"{components,templates}/**/*.js\" --format json --outFile custom-elements.json", + "build:wca": "node ./cli/wca.js \"{components,mixins,templates}/**/*.js\"", "build": "npm run build:clean && npm run build:icons && npm run build:illustrations && npm run build:sass && npm run build:wca", "build-static": "rollup -c ./rollup/rollup.config.js", "lint": "npm run lint:eslint && npm run lint:style",