From 4def8050a1c3d25b5c0213a6b9bcfdcdcf328d66 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 15:45:12 +1000 Subject: [PATCH 01/14] feat: add biome configuration and update dependencies - Introduced a new `biome.json` configuration file for Biome setup. - Added `@biomejs/biome` as a development dependency in `package.json` and `package-lock.json`. - Updated `main.ts` to improve code formatting and readability by adjusting indentation and line breaks. - Enhanced state management and event handling within the component to ensure better performance and maintainability. --- biome.json | 31 +++ package-lock.json | 165 +++++++++++++++ package.json | 1 + src/main.ts | 504 +++++++++++++++++++++++++++++++--------------- 4 files changed, 535 insertions(+), 166 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..089d265 --- /dev/null +++ b/biome.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 03fdc95..8c66f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { + "@biomejs/biome": "1.9.4", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", @@ -49,6 +50,170 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index aa7cc25..414ce86 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "MIT", "description": "", "devDependencies": { + "@biomejs/biome": "1.9.4", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", diff --git a/src/main.ts b/src/main.ts index e0aab45..85a1dba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,14 +9,28 @@ interface ComponentOptions { style?: string onMount?: (this: CustomElement) => void onUnmount?: () => void - onAttributeChanged?: (attrName: string, oldValue: string, newValue: string) => void + onAttributeChanged?: ( + attrName: string, + oldValue: string, + newValue: string, + ) => void states?: Record statesListeners?: { [key: string]: (value: any) => void } funcs?: { [key: string]: (...args: any[]) => void } } export default (options: ComponentOptions) => { - const { tag, template, style, onMount, onUnmount, onAttributeChanged, states, statesListeners, funcs } = options + const { + tag, + template, + style, + onMount, + onUnmount, + onAttributeChanged, + states, + statesListeners, + funcs, + } = options const componentRegistry = new Map() componentRegistry.set(tag, options) @@ -25,75 +39,92 @@ export default (options: ComponentOptions) => { private _stateToElementsMap: Record> = {} private _currentRenderingElement: HTMLElement | null = null private _statesListeners: Record = {} - private _textBindings: Array<{ node: Text, expr: string, originalContent: string }> = [] - private _attributeBindings: Array<{ element: Element, attrName: string, expr: string, template: string }> = [] - private _conditionalElements: Map = new Map() + private _textBindings: Array<{ + node: Text + expr: string + originalContent: string + }> = [] + private _attributeBindings: Array<{ + element: Element + attrName: string + expr: string + template: string + }> = [] + private _conditionalElements: Map< + Element, + { + expr: string + placeholder: Comment + isPresent: boolean + } + > = new Map() constructor() { super() // copy state from options - this._states = new Proxy({ ...(states || {}) }, { - set: (target: Record, keyPath: string, value: any) => { - const valueRoute = keyPath.split('.') - let currentTarget = target - for (let i in valueRoute) { - const key = valueRoute[i] - if (parseInt(i) === valueRoute.length - 1) { - currentTarget[key] = value - } else { - if (!currentTarget[key]) { - currentTarget[key] = {} + this._states = new Proxy( + { ...(states || {}) }, + { + set: (target: Record, keyPath: string, value: any) => { + const valueRoute = keyPath.split('.') + let currentTarget = target + for (let i in valueRoute) { + const key = valueRoute[i] + if (parseInt(i) === valueRoute.length - 1) { + currentTarget[key] = value + } else { + if (!currentTarget[key]) { + currentTarget[key] = {} + } + currentTarget = currentTarget[key] } - currentTarget = currentTarget[key] } - } - // trigger dom updates - this._triggerDomUpdates(keyPath) - if (this._statesListeners[keyPath]) - this._statesListeners[keyPath](value) + // trigger dom updates + this._triggerDomUpdates(keyPath) + if (this._statesListeners[keyPath]) + this._statesListeners[keyPath](value) - // trigger %if macros - if (this._conditionalElements.size > 0) - this._conditionalElements.forEach((info, element) => { - if (info.expr.includes(keyPath)) - this._evaluateIfCondition(element, info.expr) - }) + // trigger %if macros + if (this._conditionalElements.size > 0) + this._conditionalElements.forEach((info, element) => { + if (info.expr.includes(keyPath)) + this._evaluateIfCondition(element, info.expr) + }) - // trigger state update events - if (statesListeners && statesListeners[keyPath]) - statesListeners[keyPath](value) + // trigger state update events + if (statesListeners && statesListeners[keyPath]) + statesListeners[keyPath](value) - return true + return true + }, + get: (target: Record, keyPath: string) => { + // collect state dependencies + if (this._currentRenderingElement) { + if (!this._stateToElementsMap[keyPath]) + this._stateToElementsMap[keyPath] = new Set() + this._stateToElementsMap[keyPath].add( + this._currentRenderingElement, + ) + } + + const valueRoute = keyPath.split('.') + let currentTarget = target + for (let i in valueRoute) { + const key = valueRoute[i] + if (parseInt(i) === valueRoute.length - 1) { + return currentTarget[key] + } else { + if (!currentTarget[key]) { + currentTarget[key] = {} + } + currentTarget = currentTarget[key] + } + } + return undefined + }, }, - get: (target: Record, keyPath: string) => { - // collect state dependencies - if (this._currentRenderingElement) { - if (!this._stateToElementsMap[keyPath]) - this._stateToElementsMap[keyPath] = new Set() - this._stateToElementsMap[keyPath].add(this._currentRenderingElement) - } - - const valueRoute = keyPath.split('.') - let currentTarget = target - for (let i in valueRoute) { - const key = valueRoute[i] - if (parseInt(i) === valueRoute.length - 1) { - return currentTarget[key] - } else { - if (!currentTarget[key]) { - currentTarget[key] = {} - } - currentTarget = currentTarget[key] - } - } - return undefined - } - }) + ) // initialize dom tree and append to shadow root this._initialize() @@ -132,7 +163,7 @@ export default (options: ComponentOptions) => { if (this._stateToElementsMap[keyPath]) { const updateQueue = new Set() - this._stateToElementsMap[keyPath].forEach(element => { + this._stateToElementsMap[keyPath].forEach((element) => { updateQueue.add(element) }) @@ -141,17 +172,27 @@ export default (options: ComponentOptions) => { // Update text bindings that depend on this state if (this._textBindings) { - this._textBindings.forEach(binding => { - if (binding.expr === keyPath || binding.expr.startsWith(keyPath + '.')) { - this._updateTextNode(binding.node, binding.expr, binding.originalContent) + this._textBindings.forEach((binding) => { + if ( + binding.expr === keyPath || + binding.expr.startsWith(keyPath + '.') + ) { + this._updateTextNode( + binding.node, + binding.expr, + binding.originalContent, + ) } }) } // Update attribute bindings that depend on this state if (this._attributeBindings) { - this._attributeBindings.forEach(binding => { - if (binding.expr === keyPath || binding.expr.startsWith(keyPath + '.')) { + this._attributeBindings.forEach((binding) => { + if ( + binding.expr === keyPath || + binding.expr.startsWith(keyPath + '.') + ) { const value = this._getNestedState(binding.expr) if (value !== undefined) { binding.element.setAttribute(binding.attrName, String(value)) @@ -163,7 +204,7 @@ export default (options: ComponentOptions) => { private _scheduleUpdate(elements: Set) { requestAnimationFrame(() => { - elements.forEach(element => { + elements.forEach((element) => { this._updateElement(element) }) }) @@ -203,16 +244,21 @@ export default (options: ComponentOptions) => { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, - null + null, ) // Store nodes and expressions that need to be updated - const textBindings: Array<{ node: Text, expr: string, originalContent: string }> = [] - const ifDirectivesToProcess: Array<{ element: Element, expr: string }> = [] + const textBindings: Array<{ + node: Text + expr: string + originalContent: string + }> = [] + const ifDirectivesToProcess: Array<{ element: Element; expr: string }> = + [] // Traverse the DOM tree let currentNode: Node | null - while (currentNode = walker.nextNode()) { + while ((currentNode = walker.nextNode())) { // Handle text nodes if (currentNode.nodeType === Node.TEXT_NODE) { const textContent = currentNode.textContent || '' @@ -226,7 +272,7 @@ export default (options: ComponentOptions) => { // Record nodes and expressions that need to be updated const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g) if (matches) { - matches.forEach(match => { + matches.forEach((match) => { // Extract the expression content, removing {{ }} and spaces const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim() @@ -240,7 +286,9 @@ export default (options: ComponentOptions) => { if (!this._stateToElementsMap[expr]) this._stateToElementsMap[expr] = new Set() - this._stateToElementsMap[expr].add(textNode as unknown as HTMLElement) + this._stateToElementsMap[expr].add( + textNode as unknown as HTMLElement, + ) }) } } @@ -248,13 +296,12 @@ export default (options: ComponentOptions) => { // Handle element nodes (can extend to handle attribute bindings, etc.) else if (currentNode.nodeType === Node.ELEMENT_NODE) { - const currentElementNode = currentNode as Element // Renamed to avoid conflict with outer 'element' // Traverse all macro attributes // Detect :attr="" bindings, such as :src="imageUrl" - Array.from(currentElementNode.attributes).forEach(attr => { + Array.from(currentElementNode.attributes).forEach((attr) => { if (attr.name.startsWith(':')) { const attrName = attr.name.substring(1) // Remove ':' const expr = attr.value.trim() @@ -263,13 +310,20 @@ export default (options: ComponentOptions) => { currentElementNode.removeAttribute(attr.name) // Set up attribute binding - this._setupAttributeBinding(currentElementNode, attrName, expr, attr.value) + this._setupAttributeBinding( + currentElementNode, + attrName, + expr, + attr.value, + ) } }) // Process @event bindings, such as @click="handleClick" - const eventBindings = Array.from(currentElementNode.attributes).filter(attr => attr.name.startsWith('@')) - eventBindings.forEach(attr => { + const eventBindings = Array.from( + currentElementNode.attributes, + ).filter((attr) => attr.name.startsWith('@')) + eventBindings.forEach((attr) => { const eventName = attr.name.substring(1) // Remove '@' const handlerValue = attr.value.trim() @@ -279,22 +333,42 @@ export default (options: ComponentOptions) => { // Handle different types of event handlers if (handlerValue.includes('=>')) { // Handle arrow function: @click="e => setState('count', count + 1)" - this._setupArrowFunctionHandler(currentElementNode, eventName, handlerValue) - } else if (handlerValue.includes('(') && handlerValue.includes(')')) { + this._setupArrowFunctionHandler( + currentElementNode, + eventName, + handlerValue, + ) + } else if ( + handlerValue.includes('(') && + handlerValue.includes(')') + ) { // Handle function call: @click="increment(5)" - this._setupFunctionCallHandler(currentElementNode, eventName, handlerValue) + this._setupFunctionCallHandler( + currentElementNode, + eventName, + handlerValue, + ) } else if (typeof (this as any)[handlerValue] === 'function') { // Handle method reference: @click="handleClick" - currentElementNode.addEventListener(eventName, (this as any)[handlerValue].bind(this)) + currentElementNode.addEventListener( + eventName, + (this as any)[handlerValue].bind(this), + ) } else { // Handle simple expression: @click="count++" or @input="name = $event.target.value" - this._setupExpressionHandler(currentElementNode, eventName, handlerValue) + this._setupExpressionHandler( + currentElementNode, + eventName, + handlerValue, + ) } }) // Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items" - const macroBindings = Array.from(currentElementNode.attributes).filter(attr => attr.name.startsWith('%')) - macroBindings.forEach(attr => { + const macroBindings = Array.from( + currentElementNode.attributes, + ).filter((attr) => attr.name.startsWith('%')) + macroBindings.forEach((attr) => { const macroName = attr.name.substring(1) // Remove '%' const expr = attr.value.trim() @@ -302,19 +376,16 @@ export default (options: ComponentOptions) => { currentElementNode.removeAttribute(attr.name) // Handle different types of macros - if (macroName === 'connect') // Handle state connection: %connect="stateName" + if (macroName === 'connect') + // Handle state connection: %connect="stateName" this._setupTwoWayBinding(currentElementNode, expr) else if (macroName === 'if') ifDirectivesToProcess.push({ element: currentElementNode, expr }) else if (macroName === 'for') this._setupListRendering(currentElementNode, expr) - else if (macroName === 'key') - return - else - console.warn(`Unknown macro: %${macroName}`) + else if (macroName === 'key') return + else console.warn(`Unknown macro: %${macroName}`) }) - - } } @@ -336,7 +407,9 @@ export default (options: ComponentOptions) => { if (value !== undefined) element.setAttribute('data-laterano-connect', String(value)) else - console.error(`State \`${expr}\` not found in the component state. Although Laterano will try to work with it, it may has potentially unexpected behavior.`) + console.error( + `State \`${expr}\` not found in the component state. Although Laterano will try to work with it, it may has potentially unexpected behavior.`, + ) // Add event listener for input events element.addEventListener('input', (event: Event) => { @@ -359,20 +432,19 @@ export default (options: ComponentOptions) => { // Handle condition rendering (%if macro) private _setupConditionRendering(element: Element, expr: string) { - const placeholder = document.createComment(` %if: ${expr} `) element.parentNode?.insertBefore(placeholder, element) this._conditionalElements.set(element, { expr, placeholder, - isPresent: true + isPresent: true, }) this._evaluateIfCondition(element, expr) const statePaths = this._extractStatePathsFromExpression(expr) - statePaths.forEach(path => { + statePaths.forEach((path) => { if (!this._stateToElementsMap[path]) { this._stateToElementsMap[path] = new Set() } @@ -383,7 +455,9 @@ export default (options: ComponentOptions) => { // Handle list rendering (%for macro) private _setupListRendering(element: Element, expr: string) { // Parse the expression (e.g., "item in items" or "(item, index) in items") - const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/) + const match = expr.match( + /(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/, + ) if (!match) { console.error(`Invalid %for expression: ${expr}`) return @@ -404,9 +478,9 @@ export default (options: ComponentOptions) => { // Store current rendered items const renderedItems: Array<{ - element: Element, - key: any, - data: any, + element: Element + key: any + data: any index: number }> = [] @@ -414,7 +488,9 @@ export default (options: ComponentOptions) => { const updateList = () => { const collection = this._evaluateExpression(collectionExpr) if (!collection || !Array.isArray(collection)) { - console.warn(`Collection "${collectionExpr}" is not an array or does not exist`) + console.warn( + `Collection "${collectionExpr}" is not an array or does not exist`, + ) return } @@ -425,19 +501,22 @@ export default (options: ComponentOptions) => { } // Detach all currently rendered DOM items managed by this instance. - renderedItems.forEach(item => { + renderedItems.forEach((item) => { if (item.element.parentNode === parentNode) { - parentNode.removeChild(item.element); + parentNode.removeChild(item.element) } - }); + }) // Get key attribute if available const keyAttr = template.getAttribute('%key') - if (!keyAttr) console.warn(`%key attribute not found in the template, which is not a recommended practice.`) + if (!keyAttr) + console.warn( + `%key attribute not found in the template, which is not a recommended practice.`, + ) // Store a map of existing items by key for reuse const existingElementsByKey = new Map() - renderedItems.forEach(item => { + renderedItems.forEach((item) => { if (item.key !== undefined) { existingElementsByKey.set(item.key, item) } @@ -452,7 +531,15 @@ export default (options: ComponentOptions) => { // Create or update items in the list collection.forEach((item, index) => { // Determine the key for this item - const key = keyAttr ? this._evaluateExpressionWithItemContext(keyAttr ?? '', item, index, itemVar, indexVar ? indexVar : undefined) : index + const key = keyAttr + ? this._evaluateExpressionWithItemContext( + keyAttr ?? '', + item, + index, + itemVar, + indexVar ? indexVar : undefined, + ) + : index // Check if we can reuse an existing element const existingItem = existingElementsByKey.get(key) @@ -472,19 +559,21 @@ export default (options: ComponentOptions) => { element: itemElement, key, data: item, - index + index, }) // Create item context for this item const itemContext = { - [itemVar]: item + [itemVar]: item, } - if (indexVar) - itemContext[indexVar] = index + if (indexVar) itemContext[indexVar] = index // insert %key attribute, which dynamically bind the key if (keyAttr) { - const keyValue = this._evaluateExpressionWithItemContext(keyAttr, itemContext) + const keyValue = this._evaluateExpressionWithItemContext( + keyAttr, + itemContext, + ) itemElement.setAttribute('data-laterano-key', String(keyValue)) } @@ -503,7 +592,7 @@ export default (options: ComponentOptions) => { placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling) // Remove any remaining unused items - existingElementsByKey.forEach(item => { + existingElementsByKey.forEach((item) => { if (item.element.parentNode) { item.element.parentNode.removeChild(item.element) } @@ -519,7 +608,9 @@ export default (options: ComponentOptions) => { } // Using a unique identifier for this list rendering instance const listVirtualElement = document.createElement('div') - this._stateToElementsMap[collectionExpr].add(listVirtualElement as HTMLElement) + this._stateToElementsMap[collectionExpr].add( + listVirtualElement as HTMLElement, + ) // Add listener for state changes this._statesListeners[collectionExpr] = () => { @@ -528,9 +619,12 @@ export default (options: ComponentOptions) => { } // Recursively process the element and its children, applying the item context - private _processElementWithItemContext(element: Element, itemContext: Record) { + private _processElementWithItemContext( + element: Element, + itemContext: Record, + ) { // 1. Store the item context of the element so that subsequent updates can find it - (element as any)._itemContext = itemContext + ;(element as any)._itemContext = itemContext // 2. Process bindings in text nodes const processTextNodes = (node: Node) => { @@ -538,28 +632,37 @@ export default (options: ComponentOptions) => { const textContent = node.textContent || '' if (textContent.includes('{{')) { const textNode = node as Text - const updatedContent = textContent.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expr) => { - const value = this._evaluateExpressionWithItemContext(expr.trim(), itemContext) - return value !== undefined ? String(value) : '' - }) + const updatedContent = textContent.replace( + /\{\{\s*([^}]+)\s*\}\}/g, + (match, expr) => { + const value = this._evaluateExpressionWithItemContext( + expr.trim(), + itemContext, + ) + return value !== undefined ? String(value) : '' + }, + ) textNode.textContent = updatedContent } } } // Process the text nodes of the element itself - Array.from(element.childNodes).forEach(node => { + Array.from(element.childNodes).forEach((node) => { if (node.nodeType === Node.TEXT_NODE) { processTextNodes(node) } }) // 3. Process attribute bindings (:attr) - Array.from(element.attributes).forEach(attr => { + Array.from(element.attributes).forEach((attr) => { if (attr.name.startsWith(':')) { const attrName = attr.name.substring(1) const expr = attr.value.trim() - const value = this._evaluateExpressionWithItemContext(expr, itemContext) + const value = this._evaluateExpressionWithItemContext( + expr, + itemContext, + ) if (value !== undefined) { element.setAttribute(attrName, String(value)) @@ -571,7 +674,7 @@ export default (options: ComponentOptions) => { }) // 4. Process event bindings (@event) - Array.from(element.attributes).forEach(attr => { + Array.from(element.attributes).forEach((attr) => { if (attr.name.startsWith('@')) { const eventName = attr.name.substring(1) const handlerValue = attr.value.trim() @@ -587,14 +690,17 @@ export default (options: ComponentOptions) => { ...this._createHandlerContext(event, element), ...itemContext, $event: event, - $el: element + $el: element, } // Execute the expression const fnStr = `with(this) { ${handlerValue} }` new Function(fnStr).call(mergedContext) } catch (err) { - console.error(`Error executing event handler with item context: ${handlerValue}`, err) + console.error( + `Error executing event handler with item context: ${handlerValue}`, + err, + ) } }) } @@ -604,7 +710,7 @@ export default (options: ComponentOptions) => { let isConditional = false let shouldDisplay = true - Array.from(element.attributes).forEach(attr => { + Array.from(element.attributes).forEach((attr) => { if (attr.name === '%if') { isConditional = true const expr = attr.value.trim() @@ -613,12 +719,14 @@ export default (options: ComponentOptions) => { element.removeAttribute(attr.name) // Calculate the condition - const result = this._evaluateExpressionWithItemContext(expr, itemContext) + const result = this._evaluateExpressionWithItemContext( + expr, + itemContext, + ) shouldDisplay = Boolean(result) // Apply the condition (in the list item context, we use display style to simplify) - if (!shouldDisplay) - (element as HTMLElement).style.display = 'none' + if (!shouldDisplay) (element as HTMLElement).style.display = 'none' } }) @@ -630,7 +738,7 @@ export default (options: ComponentOptions) => { // 6. Process nested list rendering (%for) let hasForDirective = false - Array.from(element.attributes).forEach(attr => { + Array.from(element.attributes).forEach((attr) => { if (attr.name === '%for') { hasForDirective = true const forExpr = attr.value.trim() @@ -650,16 +758,22 @@ export default (options: ComponentOptions) => { } // 7. Recursively process all child elements - Array.from(element.children).forEach(child => { + Array.from(element.children).forEach((child) => { this._processElementWithItemContext(child, itemContext) }) } // Set up nested list rendering - private _setupNestedListRendering(element: Element, expr: string, parentItemContext: Record) { + private _setupNestedListRendering( + element: Element, + expr: string, + parentItemContext: Record, + ) { // Similar to _setupListRendering, but applies to nested situations // Parse the expression (e.g., "subItem in item.subItems") - const match = expr.match(/(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/) + const match = expr.match( + /(?:\(([^,]+),\s*([^)]+)\)|([^,\s]+))\s+in\s+(.+)/, + ) if (!match) { console.error(`Invalid nested %for expression: ${expr}`) return @@ -671,10 +785,15 @@ export default (options: ComponentOptions) => { const collectionExpr = match[4].trim() // Evaluate the collection expression, using the parent item context - const collection = this._evaluateExpressionWithItemContext(collectionExpr, parentItemContext) + const collection = this._evaluateExpressionWithItemContext( + collectionExpr, + parentItemContext, + ) if (!collection || !Array.isArray(collection)) { - console.warn(`Nested collection "${collectionExpr}" is not an array or does not exist`) + console.warn( + `Nested collection "${collectionExpr}" is not an array or does not exist`, + ) return } @@ -693,7 +812,7 @@ export default (options: ComponentOptions) => { // Create a nested item context, merging the parent context const nestedItemContext = { ...parentItemContext, - [itemVar]: item + [itemVar]: item, } if (indexVar) { @@ -705,14 +824,23 @@ export default (options: ComponentOptions) => { // TODO: detect list items existed inside the view, use replace instead of remove and re-add, // to improve performance - + // Insert the item element into the DOM - placeholder.parentNode?.insertBefore(itemElement, placeholder.nextSibling) + placeholder.parentNode?.insertBefore( + itemElement, + placeholder.nextSibling, + ) }) } // Evaluate expressions using the item context - private _evaluateExpressionWithItemContext(expression: string, itemContext: Record, index?: number, itemVar?: string, indexVar?: string): any { + private _evaluateExpressionWithItemContext( + expression: string, + itemContext: Record, + index?: number, + itemVar?: string, + indexVar?: string, + ): any { try { // Check if the expression directly references the item variable if (itemVar && expression === itemVar) { @@ -751,7 +879,10 @@ export default (options: ComponentOptions) => { const func = new Function(...contextKeys, `return ${expression}`) return func(...contextValues) } catch (error) { - console.error(`Error evaluating expression with item context: ${expression}`, error) + console.error( + `Error evaluating expression with item context: ${expression}`, + error, + ) return undefined } } @@ -765,10 +896,14 @@ export default (options: ComponentOptions) => { const shouldShow = Boolean(result) if (shouldShow !== info.isPresent) { - if (shouldShow) // Insert the element back into the DOM - info.placeholder.parentNode?.insertBefore(element, info.placeholder.nextSibling) - else // Remove the element from the DOM - element.parentNode?.removeChild(element) + if (shouldShow) + // Insert the element back into the DOM + info.placeholder.parentNode?.insertBefore( + element, + info.placeholder.nextSibling, + ) + // Remove the element from the DOM + else element.parentNode?.removeChild(element) // Update the state info.isPresent = shouldShow @@ -789,7 +924,9 @@ export default (options: ComponentOptions) => { const func = new Function(...stateKeys, `return ${expression}`) const execRes = func(...stateValues) if (typeof execRes !== 'boolean') - throw new Error(`The expression "${expression}" must return a boolean value.`) + throw new Error( + `The expression "${expression}" must return a boolean value.`, + ) return execRes } catch (error) { console.error(`Error evaluating expression: ${expression}`, error) @@ -799,13 +936,18 @@ export default (options: ComponentOptions) => { private _extractStatePathsFromExpression(expression: string): string[] { const matches = expression.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [] - return matches.filter(match => - !['true', 'false', 'null', 'undefined', 'this'].includes(match) + return matches.filter( + (match) => + !['true', 'false', 'null', 'undefined', 'this'].includes(match), ) } // Handle arrow function - private _setupArrowFunctionHandler(element: Element, eventName: string, handlerValue: string) { + private _setupArrowFunctionHandler( + element: Element, + eventName: string, + handlerValue: string, + ) { element.addEventListener(eventName, (event: Event) => { try { // Arrow function parsing @@ -860,7 +1002,10 @@ export default (options: ComponentOptions) => { handlerFn.apply(context, [event]) } } catch (err) { - console.error(`Error executing arrow function handler: ${handlerValue}`, err) + console.error( + `Error executing arrow function handler: ${handlerValue}`, + err, + ) } }) } @@ -881,21 +1026,30 @@ export default (options: ComponentOptions) => { $el: element, this: this, // Provide reference to the component instance setState: this.setState.bind(this), - getState: this.getState.bind(this) + getState: this.getState.bind(this), } // Add all methods of the component - Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach(name => { - if (typeof (this as any)[name] === 'function' && name !== 'constructor') { - context[name] = (this as any)[name].bind(this) - } - }) + Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach( + (name) => { + if ( + typeof (this as any)[name] === 'function' && + name !== 'constructor' + ) { + context[name] = (this as any)[name].bind(this) + } + }, + ) return context } // Handle function call, such as @click="increment(5)" - private _setupFunctionCallHandler(element: Element, eventName: string, handlerValue: string) { + private _setupFunctionCallHandler( + element: Element, + eventName: string, + handlerValue: string, + ) { element.addEventListener(eventName, (event: Event) => { try { // Create context object @@ -910,13 +1064,20 @@ export default (options: ComponentOptions) => { new Function(fnStr).call(context) } catch (err) { - console.error(`Error executing function call handler: ${handlerValue}`, err) + console.error( + `Error executing function call handler: ${handlerValue}`, + err, + ) } }) } // Handle simple expression, such as @click="count++" or @input="name = $event.target.value" - private _setupExpressionHandler(element: Element, eventName: string, handlerValue: string) { + private _setupExpressionHandler( + element: Element, + eventName: string, + handlerValue: string, + ) { element.addEventListener(eventName, (event: Event) => { try { // Create context object @@ -935,7 +1096,10 @@ export default (options: ComponentOptions) => { // If the expression returns a value, it can be used for two-way binding return result } catch (err) { - console.error(`Error executing expression handler: ${handlerValue}`, err) + console.error( + `Error executing expression handler: ${handlerValue}`, + err, + ) } }) } @@ -959,7 +1123,12 @@ export default (options: ComponentOptions) => { } // Set up attribute binding - private _setupAttributeBinding(element: Element, attrName: string, expr: string, template: string) { + private _setupAttributeBinding( + element: Element, + attrName: string, + expr: string, + template: string, + ) { // Initialize attribute value const value = this._getNestedState(expr) @@ -977,7 +1146,7 @@ export default (options: ComponentOptions) => { element, attrName, expr, - template + template, }) } @@ -1009,7 +1178,11 @@ export default (options: ComponentOptions) => { return ['data-attribute'] } - attributeChangedCallback(attrName: string, oldValue: string, newValue: string) { + attributeChangedCallback( + attrName: string, + oldValue: string, + newValue: string, + ) { if (onAttributeChanged) onAttributeChanged(attrName, oldValue, newValue) } @@ -1032,10 +1205,9 @@ export default (options: ComponentOptions) => { // function trigger triggerFunc(eventName: string, ...args: any[]) { - if (funcs && funcs[eventName]) - funcs[eventName].call(this, ...args) + if (funcs && funcs[eventName]) funcs[eventName].call(this, ...args) } } customElements.define(tag, CustomElementImpl) -} \ No newline at end of file +} -- 2.45.1 From afff93196ac41ad06d7961ed06223f32eef33920 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 15:52:11 +1000 Subject: [PATCH 02/14] chore: turn explicitly any declare check off to better focusing on other issues --- biome.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 089d265..ca5f101 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + } } }, "javascript": { -- 2.45.1 From 0ce566f50249e0483dd62ba46be4183c2e088af9 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:02:26 +1000 Subject: [PATCH 03/14] fix: refine type definitions and improve loop constructs for better clarity --- src/main.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main.ts b/src/main.ts index 85a1dba..7d9325b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,7 @@ export default (options: ComponentOptions) => { private _states: Record = {} private _stateToElementsMap: Record> = {} private _currentRenderingElement: HTMLElement | null = null - private _statesListeners: Record = {} + private _statesListeners: Record void> = {} private _textBindings: Array<{ node: Text expr: string @@ -69,9 +69,9 @@ export default (options: ComponentOptions) => { set: (target: Record, keyPath: string, value: any) => { const valueRoute = keyPath.split('.') let currentTarget = target - for (let i in valueRoute) { + for (const i in valueRoute) { const key = valueRoute[i] - if (parseInt(i) === valueRoute.length - 1) { + if (Number.parseInt(i) === valueRoute.length - 1) { currentTarget[key] = value } else { if (!currentTarget[key]) { @@ -110,9 +110,9 @@ export default (options: ComponentOptions) => { const valueRoute = keyPath.split('.') let currentTarget = target - for (let i in valueRoute) { + for (const i in valueRoute) { const key = valueRoute[i] - if (parseInt(i) === valueRoute.length - 1) { + if (Number.parseInt(i) === valueRoute.length - 1) { return currentTarget[key] } else { if (!currentTarget[key]) { @@ -533,12 +533,12 @@ export default (options: ComponentOptions) => { // Determine the key for this item const key = keyAttr ? this._evaluateExpressionWithItemContext( - keyAttr ?? '', - item, - index, - itemVar, - indexVar ? indexVar : undefined, - ) + keyAttr ?? '', + item, + index, + itemVar, + indexVar ? indexVar : undefined, + ) : index // Check if we can reuse an existing element @@ -624,7 +624,7 @@ export default (options: ComponentOptions) => { itemContext: Record, ) { // 1. Store the item context of the element so that subsequent updates can find it - ;(element as any)._itemContext = itemContext + ; (element as any)._itemContext = itemContext // 2. Process bindings in text nodes const processTextNodes = (node: Node) => { -- 2.45.1 From ba82252dd71024073338d07aa24f7b4a09584000 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:03:15 +1000 Subject: [PATCH 04/14] fix: simplify state update event triggering with optional chaining --- src/main.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 7d9325b..2a86b49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -93,8 +93,7 @@ export default (options: ComponentOptions) => { }) // trigger state update events - if (statesListeners && statesListeners[keyPath]) - statesListeners[keyPath](value) + statesListeners?.[keyPath]?.(value) return true }, -- 2.45.1 From 0e22cbf13040347cbdb068f1f84098f3e43c15a7 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:04:09 +1000 Subject: [PATCH 05/14] fix: streamline state retrieval logic by removing unnecessary else blocks --- src/main.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2a86b49..a83da4d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -111,14 +111,12 @@ export default (options: ComponentOptions) => { let currentTarget = target for (const i in valueRoute) { const key = valueRoute[i] - if (Number.parseInt(i) === valueRoute.length - 1) { + if (Number.parseInt(i) === valueRoute.length - 1) return currentTarget[key] - } else { - if (!currentTarget[key]) { - currentTarget[key] = {} - } - currentTarget = currentTarget[key] - } + + if (!currentTarget[key]) + currentTarget[key] = {} + currentTarget = currentTarget[key] } return undefined }, -- 2.45.1 From 75a37043b7914e3e0e3d31f861ad81683565c3b7 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:11:18 +1000 Subject: [PATCH 06/14] fix: optimize iteration methods and improve readability in state management --- src/main.ts | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/main.ts b/src/main.ts index a83da4d..24b83c9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -141,7 +141,7 @@ export default (options: ComponentOptions) => { const doc = parser.parseFromString(template, 'text/html') const mainContent = doc.body.firstElementChild - let rootElement + let rootElement: Element if (mainContent) { rootElement = document.importNode(mainContent, true) @@ -160,50 +160,47 @@ export default (options: ComponentOptions) => { if (this._stateToElementsMap[keyPath]) { const updateQueue = new Set() - this._stateToElementsMap[keyPath].forEach((element) => { + for (const element of this._stateToElementsMap[keyPath]) { updateQueue.add(element) - }) + } this._scheduleUpdate(updateQueue) } // Update text bindings that depend on this state if (this._textBindings) { - this._textBindings.forEach((binding) => { + // this._textBindings.forEach((binding) => { + for (const binding of this._textBindings) if ( binding.expr === keyPath || - binding.expr.startsWith(keyPath + '.') - ) { + binding.expr.startsWith(`${keyPath}.`) + ) this._updateTextNode( binding.node, binding.expr, binding.originalContent, ) - } - }) } // Update attribute bindings that depend on this state if (this._attributeBindings) { - this._attributeBindings.forEach((binding) => { + for (const binding of this._attributeBindings) if ( binding.expr === keyPath || - binding.expr.startsWith(keyPath + '.') + binding.expr.startsWith(`${keyPath}.`) ) { const value = this._getNestedState(binding.expr) - if (value !== undefined) { + if (value !== undefined) binding.element.setAttribute(binding.attrName, String(value)) - } } - }) + } } private _scheduleUpdate(elements: Set) { requestAnimationFrame(() => { - elements.forEach((element) => { + for (const element of elements) this._updateElement(element) - }) }) } @@ -255,7 +252,14 @@ export default (options: ComponentOptions) => { // Traverse the DOM tree let currentNode: Node | null - while ((currentNode = walker.nextNode())) { + let flag = true + while (flag) { + currentNode = walker.nextNode() + if (!currentNode) { + flag = false + break + } + // Handle text nodes if (currentNode.nodeType === Node.TEXT_NODE) { const textContent = currentNode.textContent || '' @@ -269,7 +273,7 @@ export default (options: ComponentOptions) => { // Record nodes and expressions that need to be updated const matches = textContent.match(/\{\{\s*([^}]+)\s*\}\}/g) if (matches) { - matches.forEach((match) => { + for (const match of matches) { // Extract the expression content, removing {{ }} and spaces const expr = match.replace(/\{\{\s*|\s*\}\}/g, '').trim() @@ -286,7 +290,7 @@ export default (options: ComponentOptions) => { this._stateToElementsMap[expr].add( textNode as unknown as HTMLElement, ) - }) + } } } } @@ -298,7 +302,7 @@ export default (options: ComponentOptions) => { // Traverse all macro attributes // Detect :attr="" bindings, such as :src="imageUrl" - Array.from(currentElementNode.attributes).forEach((attr) => { + for (const attr of Array.from(currentElementNode.attributes)) { if (attr.name.startsWith(':')) { const attrName = attr.name.substring(1) // Remove ':' const expr = attr.value.trim() @@ -314,13 +318,14 @@ export default (options: ComponentOptions) => { attr.value, ) } - }) + } // Process @event bindings, such as @click="handleClick" const eventBindings = Array.from( currentElementNode.attributes, ).filter((attr) => attr.name.startsWith('@')) - eventBindings.forEach((attr) => { + // eventBindings.forEach((attr) => { + for (const attr of eventBindings) { const eventName = attr.name.substring(1) // Remove '@' const handlerValue = attr.value.trim() @@ -359,12 +364,13 @@ export default (options: ComponentOptions) => { handlerValue, ) } - }) + } // Process %-started macros, such as %connect="stateName", %if="condition", %for="item in items" const macroBindings = Array.from( currentElementNode.attributes, ).filter((attr) => attr.name.startsWith('%')) + // biome-ignore lint/complexity/noForEach: TODO: will cause a bug, need to be fixed macroBindings.forEach((attr) => { const macroName = attr.name.substring(1) // Remove '%' const expr = attr.value.trim() -- 2.45.1 From aae4a85a49a59944a22bbc1f6f721358177a1f05 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:19:45 +1000 Subject: [PATCH 07/14] fix: enhance readability by replacing forEach with for-of loops in rendering logic --- src/main.ts | 111 ++++++++++++++++++++++++---------------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/src/main.ts b/src/main.ts index 24b83c9..8a8bd19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -447,12 +447,11 @@ export default (options: ComponentOptions) => { this._evaluateIfCondition(element, expr) const statePaths = this._extractStatePathsFromExpression(expr) - statePaths.forEach((path) => { - if (!this._stateToElementsMap[path]) { + for (const path of statePaths) { + if (!this._stateToElementsMap[path]) this._stateToElementsMap[path] = new Set() - } this._stateToElementsMap[path].add(element as HTMLElement) - }) + } } // Handle list rendering (%for macro) @@ -504,26 +503,23 @@ export default (options: ComponentOptions) => { } // Detach all currently rendered DOM items managed by this instance. - renderedItems.forEach((item) => { - if (item.element.parentNode === parentNode) { + for (const item of renderedItems) + if (item.element.parentNode === parentNode) parentNode.removeChild(item.element) - } - }) // Get key attribute if available const keyAttr = template.getAttribute('%key') if (!keyAttr) console.warn( - `%key attribute not found in the template, which is not a recommended practice.`, + '%key attribute not found in the template, which is not a recommended practice.' ) // Store a map of existing items by key for reuse const existingElementsByKey = new Map() - renderedItems.forEach((item) => { - if (item.key !== undefined) { + // renderedItems.forEach((item) => { + for (const item of renderedItems) + if (item.key !== undefined) existingElementsByKey.set(item.key, item) - } - }) // Clear rendered items renderedItems.length = 0 @@ -595,20 +591,19 @@ export default (options: ComponentOptions) => { placeholder.parentNode?.insertBefore(fragment, placeholder.nextSibling) // Remove any remaining unused items - existingElementsByKey.forEach((item) => { - if (item.element.parentNode) { + // existingElementsByKey.forEach((item) => { + for (const item of existingElementsByKey.values()) + if (item.element.parentNode) item.element.parentNode.removeChild(item.element) - } - }) } // Initial render updateList() // Set up state dependency for collection changes - if (!this._stateToElementsMap[collectionExpr]) { + if (!this._stateToElementsMap[collectionExpr]) this._stateToElementsMap[collectionExpr] = new Set() - } + // Using a unique identifier for this list rendering instance const listVirtualElement = document.createElement('div') this._stateToElementsMap[collectionExpr].add( @@ -651,14 +646,14 @@ export default (options: ComponentOptions) => { } // Process the text nodes of the element itself - Array.from(element.childNodes).forEach((node) => { - if (node.nodeType === Node.TEXT_NODE) { + // Array.from(element.childNodes).forEach((node) => { + for (const node of Array.from(element.childNodes)) + if (node.nodeType === Node.TEXT_NODE) processTextNodes(node) - } - }) // 3. Process attribute bindings (:attr) - Array.from(element.attributes).forEach((attr) => { + // Array.from(element.attributes).forEach((attr) => { + for (const attr of Array.from(element.attributes)) { if (attr.name.startsWith(':')) { const attrName = attr.name.substring(1) const expr = attr.value.trim() @@ -667,17 +662,17 @@ export default (options: ComponentOptions) => { itemContext, ) - if (value !== undefined) { + if (value !== undefined) element.setAttribute(attrName, String(value)) - } // Remove the original binding attribute (execute only for cloned templates once) element.removeAttribute(attr.name) } - }) + } // 4. Process event bindings (@event) - Array.from(element.attributes).forEach((attr) => { + // Array.from(element.attributes).forEach((attr) => { + for (const attr of Array.from(element.attributes)) { if (attr.name.startsWith('@')) { const eventName = attr.name.substring(1) const handlerValue = attr.value.trim() @@ -707,13 +702,14 @@ export default (options: ComponentOptions) => { } }) } - }) + } // 5. Process conditional rendering (%if) let isConditional = false let shouldDisplay = true - Array.from(element.attributes).forEach((attr) => { + // Array.from(element.attributes).forEach((attr) => { + for (const attr of Array.from(element.attributes)) { if (attr.name === '%if') { isConditional = true const expr = attr.value.trim() @@ -729,9 +725,10 @@ export default (options: ComponentOptions) => { shouldDisplay = Boolean(result) // Apply the condition (in the list item context, we use display style to simplify) - if (!shouldDisplay) (element as HTMLElement).style.display = 'none' + if (!shouldDisplay) + (element as HTMLElement).style.display = 'none' } - }) + } // If the condition evaluates to false, skip further processing of this element if (isConditional && !shouldDisplay) { @@ -741,7 +738,8 @@ export default (options: ComponentOptions) => { // 6. Process nested list rendering (%for) let hasForDirective = false - Array.from(element.attributes).forEach((attr) => { + // Array.from(element.attributes).forEach((attr) => { + for (const attr of Array.from(element.attributes)) { if (attr.name === '%for') { hasForDirective = true const forExpr = attr.value.trim() @@ -753,17 +751,16 @@ export default (options: ComponentOptions) => { // Note: We need to evaluate the collection expression through the current item context here this._setupNestedListRendering(element, forExpr, itemContext) } - }) - - // If this element is a list element, skip child element processing (they will be processed by the list processor) - if (hasForDirective) { - return } + // If this element is a list element, skip child element processing (they will be processed by the list processor) + if (hasForDirective) + return + // 7. Recursively process all child elements - Array.from(element.children).forEach((child) => { + // Array.from(element.children).forEach((child) => { + for (const child of Array.from(element.children)) this._processElementWithItemContext(child, itemContext) - }) } // Set up nested list rendering @@ -851,15 +848,14 @@ export default (options: ComponentOptions) => { } // Check if the expression is an item property path - if (itemVar && expression.startsWith(itemVar + '.')) { + if (itemVar && expression.startsWith(`${itemVar}.`)) { const propertyPath = expression.substring(itemVar.length + 1) const parts = propertyPath.split('.') let value = itemContext[itemVar] for (const part of parts) { - if (value === undefined || value === null) { + if (value === undefined || value === null) return undefined - } value = value[part] } @@ -959,11 +955,9 @@ export default (options: ComponentOptions) => { throw new Error(`Invalid arrow function syntax: ${handlerValue}`) } const paramsStr = (() => { - if (splitted[0].includes('(')) { + if (splitted[0].includes('(')) return splitted[0].trim() - } else { - return `(${splitted[0].trim()})` - } + return `(${splitted[0].trim()})` })() const bodyStr = splitted[1].trim() @@ -1033,16 +1027,14 @@ export default (options: ComponentOptions) => { } // Add all methods of the component - Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach( - (name) => { - if ( - typeof (this as any)[name] === 'function' && - name !== 'constructor' - ) { - context[name] = (this as any)[name].bind(this) - } - }, - ) + // Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach( + // (name) => { + for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) + if ( + typeof (this as any)[name] === 'function' && + name !== 'constructor' + ) + context[name] = (this as any)[name].bind(this) return context } @@ -1198,9 +1190,8 @@ export default (options: ComponentOptions) => { const parts = keyPath.split('.') let result = this._states for (const part of parts) { - if (result === undefined || result === null) { + if (result === undefined || result === null) return undefined - } result = result[part] } return result @@ -1208,7 +1199,7 @@ export default (options: ComponentOptions) => { // function trigger triggerFunc(eventName: string, ...args: any[]) { - if (funcs && funcs[eventName]) funcs[eventName].call(this, ...args) + funcs?.[eventName]?.call(this, ...args) } } -- 2.45.1 From b1dc96783a591a39acad8837dd63bd2efe9d0472 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 16:51:52 +1000 Subject: [PATCH 08/14] fix: improve readability by simplifying conditional statements and replacing forEach with for-of loops --- src/main.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main.ts b/src/main.ts index 8a8bd19..e09a1a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -214,9 +214,9 @@ export default (options: ComponentOptions) => { const result = renderFunction() // Update DOM - if (typeof result === 'string') { + if (typeof result === 'string') element.innerHTML = result - } else if (result instanceof Node) { + else if (result instanceof Node) { element.innerHTML = '' element.appendChild(result) } @@ -370,8 +370,9 @@ export default (options: ComponentOptions) => { const macroBindings = Array.from( currentElementNode.attributes, ).filter((attr) => attr.name.startsWith('%')) - // biome-ignore lint/complexity/noForEach: TODO: will cause a bug, need to be fixed - macroBindings.forEach((attr) => { + + // macroBindings.forEach((attr) => { + for (const attr of macroBindings) { const macroName = attr.name.substring(1) // Remove '%' const expr = attr.value.trim() @@ -382,13 +383,13 @@ export default (options: ComponentOptions) => { if (macroName === 'connect') // Handle state connection: %connect="stateName" this._setupTwoWayBinding(currentElementNode, expr) - else if (macroName === 'if') + else if (macroName === 'if') { ifDirectivesToProcess.push({ element: currentElementNode, expr }) - else if (macroName === 'for') + } else if (macroName === 'for') this._setupListRendering(currentElementNode, expr) - else if (macroName === 'key') return + else if (macroName === 'key') continue else console.warn(`Unknown macro: %${macroName}`) - }) + } } } -- 2.45.1 From fcbee7c0b2b968f1137ca325f2de73f76b10c20d Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:35:06 +1000 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20replace=20=E2=80=9Cany=E2=80=9D=20?= =?UTF-8?q?type=20declaration=20with=20=E2=80=9Cunknown=E2=80=9D=20or=20ot?= =?UTF-8?q?her=20specific=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biome.json | 5 +--- src/main.ts | 83 ++++++++++++++++++++++++++--------------------------- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/biome.json b/biome.json index ca5f101..089d265 100644 --- a/biome.json +++ b/biome.json @@ -19,10 +19,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "off" - } + "recommended": true } }, "javascript": { diff --git a/src/main.ts b/src/main.ts index e09a1a1..464c853 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ interface CustomElement extends HTMLElement { - setState(key_path: string, value: any): void - getState(key_path: string): any + setState(key_path: string, value: unknown): void + getState(key_path: string): unknown } interface ComponentOptions { @@ -14,9 +14,9 @@ interface ComponentOptions { oldValue: string, newValue: string, ) => void - states?: Record - statesListeners?: { [key: string]: (value: any) => void } - funcs?: { [key: string]: (...args: any[]) => void } + states?: Record + statesListeners?: { [key: string]: (value: unknown) => void } + funcs?: { [key: string]: (...args: unknown[]) => void } } export default (options: ComponentOptions) => { @@ -35,10 +35,10 @@ export default (options: ComponentOptions) => { componentRegistry.set(tag, options) class CustomElementImpl extends HTMLElement { - private _states: Record = {} + private _states: Record = {} private _stateToElementsMap: Record> = {} private _currentRenderingElement: HTMLElement | null = null - private _statesListeners: Record void> = {} + private _statesListeners: Record void> = {} private _textBindings: Array<{ node: Text expr: string @@ -66,7 +66,7 @@ export default (options: ComponentOptions) => { this._states = new Proxy( { ...(states || {}) }, { - set: (target: Record, keyPath: string, value: any) => { + set: (target: Record, keyPath: string, value: unknown) => { const valueRoute = keyPath.split('.') let currentTarget = target for (const i in valueRoute) { @@ -74,10 +74,9 @@ export default (options: ComponentOptions) => { if (Number.parseInt(i) === valueRoute.length - 1) { currentTarget[key] = value } else { - if (!currentTarget[key]) { + if (!currentTarget[key]) currentTarget[key] = {} - } - currentTarget = currentTarget[key] + currentTarget = currentTarget[key] as Record } } // trigger dom updates @@ -97,7 +96,7 @@ export default (options: ComponentOptions) => { return true }, - get: (target: Record, keyPath: string) => { + get: (target: Record, keyPath: string) => { // collect state dependencies if (this._currentRenderingElement) { if (!this._stateToElementsMap[keyPath]) @@ -116,7 +115,7 @@ export default (options: ComponentOptions) => { if (!currentTarget[key]) currentTarget[key] = {} - currentTarget = currentTarget[key] + currentTarget = currentTarget[key] as Record } return undefined }, @@ -205,7 +204,7 @@ export default (options: ComponentOptions) => { } private _updateElement(element: HTMLElement) { - const renderFunction = (element as any)._renderFunction + const renderFunction = (element as { _renderFunction?: () => string | Node })._renderFunction if (renderFunction) { // Set rendering context this._currentRenderingElement = element @@ -350,11 +349,11 @@ export default (options: ComponentOptions) => { eventName, handlerValue, ) - } else if (typeof (this as any)[handlerValue] === 'function') { + } else if (typeof (this as Record)[handlerValue] === 'function') { // Handle method reference: @click="handleClick" currentElementNode.addEventListener( eventName, - (this as any)[handlerValue].bind(this), + ((this as unknown as Record void>)[handlerValue]).bind(this), ) } else { // Handle simple expression: @click="count++" or @input="name = $event.target.value" @@ -425,12 +424,11 @@ export default (options: ComponentOptions) => { }) // Add event listener for state changes - this._statesListeners[expr] = (newValue: any) => { - if (element instanceof HTMLInputElement) { - element.value = newValue - } else { + this._statesListeners[expr] = (newValue: unknown) => { + if (element instanceof HTMLInputElement) + element.value = newValue as string + else element.setAttribute('data-laterano-connect', String(newValue)) - } } } @@ -482,8 +480,8 @@ export default (options: ComponentOptions) => { // Store current rendered items const renderedItems: Array<{ element: Element - key: any - data: any + key: unknown + data: unknown index: number }> = [] @@ -620,10 +618,10 @@ export default (options: ComponentOptions) => { // Recursively process the element and its children, applying the item context private _processElementWithItemContext( element: Element, - itemContext: Record, + itemContext: Record, ) { // 1. Store the item context of the element so that subsequent updates can find it - ; (element as any)._itemContext = itemContext + ; (element as { _itemContext?: Record })._itemContext = itemContext // 2. Process bindings in text nodes const processTextNodes = (node: Node) => { @@ -768,7 +766,7 @@ export default (options: ComponentOptions) => { private _setupNestedListRendering( element: Element, expr: string, - parentItemContext: Record, + parentItemContext: Record, ) { // Similar to _setupListRendering, but applies to nested situations // Parse the expression (e.g., "subItem in item.subItems") @@ -837,11 +835,11 @@ export default (options: ComponentOptions) => { // Evaluate expressions using the item context private _evaluateExpressionWithItemContext( expression: string, - itemContext: Record, + itemContext: Record, index?: number, itemVar?: string, indexVar?: string, - ): any { + ): unknown { try { // Check if the expression directly references the item variable if (itemVar && expression === itemVar) { @@ -857,7 +855,7 @@ export default (options: ComponentOptions) => { for (const part of parts) { if (value === undefined || value === null) return undefined - value = value[part] + value = (value as { [key: string]: unknown})[part] } return value @@ -911,7 +909,7 @@ export default (options: ComponentOptions) => { } } - private _evaluateExpression(expression: string): any { + private _evaluateExpression(expression: string): unknown { try { // get the state keys and values if (this._states[expression] !== undefined) @@ -1012,12 +1010,12 @@ export default (options: ComponentOptions) => { private _createHandlerContext(event: Event, element: Element) { // Basic context, including state const context: { - [key: string]: any + [key: string]: unknown $event: Event $el: Element this: CustomElementImpl // Provide reference to the component instance - setState: (keyPath: string, value: any) => void - getState: (keyPath: string) => any + setState: (keyPath: string, value: unknown) => void + getState: (keyPath: string) => unknown } = { ...this._states, $event: event, @@ -1032,10 +1030,10 @@ export default (options: ComponentOptions) => { // (name) => { for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) if ( - typeof (this as any)[name] === 'function' && + typeof (this as Record)[name] === 'function' && name !== 'constructor' ) - context[name] = (this as any)[name].bind(this) + context[name] = (this as unknown as Record void>)[name].bind(this) return context } @@ -1147,16 +1145,15 @@ export default (options: ComponentOptions) => { } // Get nested state value - private _getNestedState(path: string): any { + private _getNestedState(path: string): unknown { // Handle nested paths, such as "profile.name" const parts = path.split('.') let result = this._states for (const part of parts) { - if (result === undefined || result === null) { + if (result === undefined || result === null) return undefined - } - result = result[part] + result = (result as { [key: string]: Record })[part] } return result @@ -1183,23 +1180,23 @@ export default (options: ComponentOptions) => { } // state manager - setState(keyPath: string, value: any) { + setState(keyPath: string, value: unknown) { this._states[keyPath] = value } - getState(keyPath: string): any { + getState(keyPath: string): unknown { const parts = keyPath.split('.') let result = this._states for (const part of parts) { if (result === undefined || result === null) return undefined - result = result[part] + result = (result as { [key: string]: Record })[part] } return result } // function trigger - triggerFunc(eventName: string, ...args: any[]) { + triggerFunc(eventName: string, ...args: unknown[]) { funcs?.[eventName]?.call(this, ...args) } } -- 2.45.1 From 14c6c0a1ab14b9e79a2cb56be6b600e28a285854 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:43:42 +1000 Subject: [PATCH 10/14] chore: intergrate Biome for check code linting issue --- .gitea/workflows/publishToNpm.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitea/workflows/publishToNpm.yaml b/.gitea/workflows/publishToNpm.yaml index 1bf5ec7..a5bd09f 100644 --- a/.gitea/workflows/publishToNpm.yaml +++ b/.gitea/workflows/publishToNpm.yaml @@ -7,6 +7,18 @@ on: - dev jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . + publish: runs-on: ubuntu-latest -- 2.45.1 From a8b1a1512d946617d526bcc4a99a9d20f73dd4cc Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:48:50 +1000 Subject: [PATCH 11/14] chore: linting the files --- biome.json | 2 +- package.json | 64 +++++++------- rollup.config.js | 38 ++++----- src/main.ts | 92 ++++++++++---------- tsconfig.json | 218 +++++++++++++++++++++++------------------------ 5 files changed, 208 insertions(+), 206 deletions(-) diff --git a/biome.json b/biome.json index 089d265..c7f4c63 100644 --- a/biome.json +++ b/biome.json @@ -28,4 +28,4 @@ "semicolons": "asNeeded" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 414ce86..0d1b38a 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,34 @@ { - "name": "laterano", - "version": "0.0.1", - "main": "dist/main.min.js", - "types": "dist/types.d.ts", - "module": "dist/main.min.js", - "scripts": { - "build": "tsc && rollup -c && npm run cleanup-intermediate", - "prepare": "npm run build", - "cleanup-intermediate": "rimraf dist/main.js dist/types" - }, - "repository": { - "type": "git", - "url": "git@git.nas.astrian.moe:Astrian/Laterano.git" - }, - "author": "", - "license": "MIT", - "description": "", - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^12.1.2", - "dts-bundle-generator": "^9.5.1", - "i": "^0.3.7", - "npm": "^11.4.0", - "rimraf": "^6.0.1", - "rollup": "^4.40.2", - "rollup-plugin-dts": "^6.2.1", - "tslib": "^2.8.1", - "typescript": "^5.8.3" - }, - "type": "module" + "name": "laterano", + "version": "0.0.1", + "main": "dist/main.min.js", + "types": "dist/types.d.ts", + "module": "dist/main.min.js", + "scripts": { + "build": "tsc && rollup -c && npm run cleanup-intermediate", + "prepare": "npm run build", + "cleanup-intermediate": "rimraf dist/main.js dist/types" + }, + "repository": { + "type": "git", + "url": "git@git.nas.astrian.moe:Astrian/Laterano.git" + }, + "author": "", + "license": "MIT", + "description": "", + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", + "dts-bundle-generator": "^9.5.1", + "i": "^0.3.7", + "npm": "^11.4.0", + "rimraf": "^6.0.1", + "rollup": "^4.40.2", + "rollup-plugin-dts": "^6.2.1", + "tslib": "^2.8.1", + "typescript": "^5.8.3" + }, + "type": "module" } diff --git a/rollup.config.js b/rollup.config.js index b4dfc12..9479f56 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,23 +4,23 @@ import terser from '@rollup/plugin-terser' import dts from 'rollup-plugin-dts' export default [ - { - input: 'dist/main.js', - output: [ - { - file: 'dist/main.min.js', - format: 'esm', - plugins: [terser()], - }, - ], - plugins: [resolve(), typescript()], - }, - { - input: 'dist/types/main.d.ts', - output: { - file: 'dist/types.d.ts', - format: 'es', - }, - plugins: [dts()], - }, + { + input: 'dist/main.js', + output: [ + { + file: 'dist/main.min.js', + format: 'esm', + plugins: [terser()], + }, + ], + plugins: [resolve(), typescript()], + }, + { + input: 'dist/types/main.d.ts', + output: { + file: 'dist/types.d.ts', + format: 'es', + }, + plugins: [dts()], + }, ] diff --git a/src/main.ts b/src/main.ts index 464c853..9b3bd25 100644 --- a/src/main.ts +++ b/src/main.ts @@ -66,7 +66,11 @@ export default (options: ComponentOptions) => { this._states = new Proxy( { ...(states || {}) }, { - set: (target: Record, keyPath: string, value: unknown) => { + set: ( + target: Record, + keyPath: string, + value: unknown, + ) => { const valueRoute = keyPath.split('.') let currentTarget = target for (const i in valueRoute) { @@ -74,8 +78,7 @@ export default (options: ComponentOptions) => { if (Number.parseInt(i) === valueRoute.length - 1) { currentTarget[key] = value } else { - if (!currentTarget[key]) - currentTarget[key] = {} + if (!currentTarget[key]) currentTarget[key] = {} currentTarget = currentTarget[key] as Record } } @@ -113,8 +116,7 @@ export default (options: ComponentOptions) => { if (Number.parseInt(i) === valueRoute.length - 1) return currentTarget[key] - if (!currentTarget[key]) - currentTarget[key] = {} + if (!currentTarget[key]) currentTarget[key] = {} currentTarget = currentTarget[key] as Record } return undefined @@ -192,19 +194,19 @@ export default (options: ComponentOptions) => { if (value !== undefined) binding.element.setAttribute(binding.attrName, String(value)) } - } } private _scheduleUpdate(elements: Set) { requestAnimationFrame(() => { - for (const element of elements) - this._updateElement(element) + for (const element of elements) this._updateElement(element) }) } private _updateElement(element: HTMLElement) { - const renderFunction = (element as { _renderFunction?: () => string | Node })._renderFunction + const renderFunction = ( + element as { _renderFunction?: () => string | Node } + )._renderFunction if (renderFunction) { // Set rendering context this._currentRenderingElement = element @@ -213,8 +215,7 @@ export default (options: ComponentOptions) => { const result = renderFunction() // Update DOM - if (typeof result === 'string') - element.innerHTML = result + if (typeof result === 'string') element.innerHTML = result else if (result instanceof Node) { element.innerHTML = '' element.appendChild(result) @@ -349,11 +350,19 @@ export default (options: ComponentOptions) => { eventName, handlerValue, ) - } else if (typeof (this as Record)[handlerValue] === 'function') { + } else if ( + typeof (this as Record)[handlerValue] === + 'function' + ) { // Handle method reference: @click="handleClick" currentElementNode.addEventListener( eventName, - ((this as unknown as Record void>)[handlerValue]).bind(this), + ( + this as unknown as Record< + string, + (...args: unknown[]) => void + > + )[handlerValue].bind(this), ) } else { // Handle simple expression: @click="count++" or @input="name = $event.target.value" @@ -427,8 +436,7 @@ export default (options: ComponentOptions) => { this._statesListeners[expr] = (newValue: unknown) => { if (element instanceof HTMLInputElement) element.value = newValue as string - else - element.setAttribute('data-laterano-connect', String(newValue)) + else element.setAttribute('data-laterano-connect', String(newValue)) } } @@ -510,15 +518,14 @@ export default (options: ComponentOptions) => { const keyAttr = template.getAttribute('%key') if (!keyAttr) console.warn( - '%key attribute not found in the template, which is not a recommended practice.' + '%key attribute not found in the template, which is not a recommended practice.', ) // Store a map of existing items by key for reuse const existingElementsByKey = new Map() // renderedItems.forEach((item) => { for (const item of renderedItems) - if (item.key !== undefined) - existingElementsByKey.set(item.key, item) + if (item.key !== undefined) existingElementsByKey.set(item.key, item) // Clear rendered items renderedItems.length = 0 @@ -531,12 +538,12 @@ export default (options: ComponentOptions) => { // Determine the key for this item const key = keyAttr ? this._evaluateExpressionWithItemContext( - keyAttr ?? '', - item, - index, - itemVar, - indexVar ? indexVar : undefined, - ) + keyAttr ?? '', + item, + index, + itemVar, + indexVar ? indexVar : undefined, + ) : index // Check if we can reuse an existing element @@ -621,7 +628,8 @@ export default (options: ComponentOptions) => { itemContext: Record, ) { // 1. Store the item context of the element so that subsequent updates can find it - ; (element as { _itemContext?: Record })._itemContext = itemContext + ;(element as { _itemContext?: Record })._itemContext = + itemContext // 2. Process bindings in text nodes const processTextNodes = (node: Node) => { @@ -647,8 +655,7 @@ export default (options: ComponentOptions) => { // Process the text nodes of the element itself // Array.from(element.childNodes).forEach((node) => { for (const node of Array.from(element.childNodes)) - if (node.nodeType === Node.TEXT_NODE) - processTextNodes(node) + if (node.nodeType === Node.TEXT_NODE) processTextNodes(node) // 3. Process attribute bindings (:attr) // Array.from(element.attributes).forEach((attr) => { @@ -661,8 +668,7 @@ export default (options: ComponentOptions) => { itemContext, ) - if (value !== undefined) - element.setAttribute(attrName, String(value)) + if (value !== undefined) element.setAttribute(attrName, String(value)) // Remove the original binding attribute (execute only for cloned templates once) element.removeAttribute(attr.name) @@ -724,8 +730,7 @@ export default (options: ComponentOptions) => { shouldDisplay = Boolean(result) // Apply the condition (in the list item context, we use display style to simplify) - if (!shouldDisplay) - (element as HTMLElement).style.display = 'none' + if (!shouldDisplay) (element as HTMLElement).style.display = 'none' } } @@ -753,8 +758,7 @@ export default (options: ComponentOptions) => { } // If this element is a list element, skip child element processing (they will be processed by the list processor) - if (hasForDirective) - return + if (hasForDirective) return // 7. Recursively process all child elements // Array.from(element.children).forEach((child) => { @@ -853,9 +857,8 @@ export default (options: ComponentOptions) => { let value = itemContext[itemVar] for (const part of parts) { - if (value === undefined || value === null) - return undefined - value = (value as { [key: string]: unknown})[part] + if (value === undefined || value === null) return undefined + value = (value as { [key: string]: unknown })[part] } return value @@ -954,8 +957,7 @@ export default (options: ComponentOptions) => { throw new Error(`Invalid arrow function syntax: ${handlerValue}`) } const paramsStr = (() => { - if (splitted[0].includes('(')) - return splitted[0].trim() + if (splitted[0].includes('(')) return splitted[0].trim() return `(${splitted[0].trim()})` })() const bodyStr = splitted[1].trim() @@ -1028,12 +1030,16 @@ export default (options: ComponentOptions) => { // Add all methods of the component // Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach( // (name) => { - for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) + for (const name of Object.getOwnPropertyNames( + Object.getPrototypeOf(this), + )) if ( typeof (this as Record)[name] === 'function' && name !== 'constructor' ) - context[name] = (this as unknown as Record void>)[name].bind(this) + context[name] = ( + this as unknown as Record void> + )[name].bind(this) return context } @@ -1151,8 +1157,7 @@ export default (options: ComponentOptions) => { let result = this._states for (const part of parts) { - if (result === undefined || result === null) - return undefined + if (result === undefined || result === null) return undefined result = (result as { [key: string]: Record })[part] } @@ -1188,8 +1193,7 @@ export default (options: ComponentOptions) => { const parts = keyPath.split('.') let result = this._states for (const part of parts) { - if (result === undefined || result === null) - return undefined + if (result === undefined || result === null) return undefined result = (result as { [key: string]: Record })[part] } return result diff --git a/tsconfig.json b/tsconfig.json index c52e443..eb72527 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,120 +1,118 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["DOM", "ES2024"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "libReplacement": true, /* Enable lib replacement. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Language and Environment */ + "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "DOM", + "ES2024" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": ["./src/types"], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": ["./src/types"], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - "declarationDir": "./dist/types", /* Specify the output directory for generated declaration files. */ + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "declarationMap": true /* Create sourcemaps for d.ts files. */, + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + "declarationDir": "./dist/types" /* Specify the output directory for generated declaration files. */, - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": [ - "./src/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ], + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["./src/**/*"], + "exclude": ["node_modules", "dist"] } -- 2.45.1 From 7c7e0e1cf2b02502e798cb0313f4e84167fd549e Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:50:13 +1000 Subject: [PATCH 12/14] chore: fix linting --- rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.js b/rollup.config.js index 9479f56..0b4fb83 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,6 @@ import resolve from '@rollup/plugin-node-resolve' -import typescript from '@rollup/plugin-typescript' import terser from '@rollup/plugin-terser' +import typescript from '@rollup/plugin-typescript' import dts from 'rollup-plugin-dts' export default [ -- 2.45.1 From d5562a4d424441690908cb2fbae57ba038a0b608 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:52:26 +1000 Subject: [PATCH 13/14] chore: CI/CD publish requires quality check --- .gitea/workflows/publishToNpm.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/publishToNpm.yaml b/.gitea/workflows/publishToNpm.yaml index a5bd09f..41b50bd 100644 --- a/.gitea/workflows/publishToNpm.yaml +++ b/.gitea/workflows/publishToNpm.yaml @@ -12,16 +12,18 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Setup Biome - uses: biomejs/setup-biome@v2 + - name: Set up Node.js + uses: actions/setup-node@v2 with: - version: latest + node-version: '22' + - name: Install dependencies + run: npm ci - name: Run Biome run: biome ci . publish: runs-on: ubuntu-latest - + needs: quality steps: - name: Checkout repository uses: actions/checkout@v2 -- 2.45.1 From 7a55cdc92e2b8ee7718d28ae4774a71542654193 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Fri, 16 May 2025 19:56:05 +1000 Subject: [PATCH 14/14] chore: add new npm command for code quality check --- .gitea/workflows/publishToNpm.yaml | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/publishToNpm.yaml b/.gitea/workflows/publishToNpm.yaml index 41b50bd..ad1e230 100644 --- a/.gitea/workflows/publishToNpm.yaml +++ b/.gitea/workflows/publishToNpm.yaml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: npm ci - name: Run Biome - run: biome ci . + run: npm run quality-check publish: runs-on: ubuntu-latest diff --git a/package.json b/package.json index 0d1b38a..4c8ee6a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "scripts": { "build": "tsc && rollup -c && npm run cleanup-intermediate", "prepare": "npm run build", - "cleanup-intermediate": "rimraf dist/main.js dist/types" + "cleanup-intermediate": "rimraf dist/main.js dist/types", + "quality-check": "biome ci ." }, "repository": { "type": "git", -- 2.45.1