From 4dd44316e4a30b00d83ee93caa6bfc0a76a3e102 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Wed, 8 Nov 2023 22:54:30 +0100 Subject: [PATCH 01/13] feat(ParserKaz): implement state setter magic string --- packages/kaz-ast/src/index.ts | 2 + packages/kaz-ast/src/lib/traverse.ts | 33 ++++ packages/parser-kaz/package.json | 9 +- packages/parser-kaz/src/parser-kaz.ts | 4 + packages/parser-kaz/src/transform-ast.ts | 142 ++++++++++++++++++ packages/parser-kaz/tests/fixtures/Input.kaz | 19 +++ packages/parser-kaz/tests/parser-kaz.test.ts | 21 +++ packages/transform-utils/build.config.ts | 0 packages/transform-utils/package.json | 30 ++++ packages/transform-utils/src/index.ts | 3 + packages/transform-utils/src/magic-strings.ts | 32 ++++ packages/transform-utils/tsconfig.json | 11 ++ packages/transformer-typescript/package.json | 1 + .../src/handlers/stateInstruction.ts | 7 +- .../src/transformer-typescript.ts | 13 +- 15 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 packages/kaz-ast/src/lib/traverse.ts create mode 100644 packages/parser-kaz/src/transform-ast.ts create mode 100644 packages/transform-utils/build.config.ts create mode 100644 packages/transform-utils/package.json create mode 100644 packages/transform-utils/src/index.ts create mode 100644 packages/transform-utils/src/magic-strings.ts create mode 100644 packages/transform-utils/tsconfig.json diff --git a/packages/kaz-ast/src/index.ts b/packages/kaz-ast/src/index.ts index 4a69dc2..ac653e3 100644 --- a/packages/kaz-ast/src/index.ts +++ b/packages/kaz-ast/src/index.ts @@ -14,3 +14,5 @@ export const parse = (tokens: Token[]) => { export const semanticTokensLegend = getSemanticTokensLegend(config.tokens) export * from './types/KazAst' + +export { traverse } from './lib/traverse' diff --git a/packages/kaz-ast/src/lib/traverse.ts b/packages/kaz-ast/src/lib/traverse.ts new file mode 100644 index 0000000..daf1adb --- /dev/null +++ b/packages/kaz-ast/src/lib/traverse.ts @@ -0,0 +1,33 @@ +import type { z } from 'zod' + +import type * as schemas from '../types/KazAst' + +type AllSchemas = typeof schemas[keyof typeof schemas] +type AllInferedSchemas = z.infer }>>> +type Visitor = { + [T in AllInferedSchemas['$type']]?: (node: Extract) => void +} & { + $default?: (node: AllInferedSchemas) => void +} + +export const traverse = (ast: AllInferedSchemas, visitor: Visitor) => { + const traverse = (node: AllInferedSchemas) => { + (visitor[node.$type] ?? visitor.$default)?.(node as never) + + for (const key in node) { + const value = node[key as keyof typeof node] as unknown + + if (Array.isArray(value)) { + for (const item of value) { + if (typeof item === 'object' && item !== null) + traverse(item) + } + } + else if (typeof value === 'object' && value !== null && '$type' in value) { + traverse(value as AllInferedSchemas) + } + } + } + + traverse(ast) +} diff --git a/packages/parser-kaz/package.json b/packages/parser-kaz/package.json index ffee5ed..ab8a9de 100644 --- a/packages/parser-kaz/package.json +++ b/packages/parser-kaz/package.json @@ -22,11 +22,18 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@babel/parser": "^7.22.5", + "@babel/traverse": "^7.22.5", "@whitebird/kaz-ast": "workspace:*", "@whitebird/kazam-parser-base": "workspace:*", - "glob": "10.2.1" + "@whitebird/kazam-transform-utils": "workspace:*", + "@whitebird/kazam-transformer-typescript": "workspace:*", + "glob": "10.2.1", + "magic-string": "^0.30.5" }, "devDependencies": { + "@babel/types": "^7.22.5", + "@types/babel__traverse": "^7.20.1", "@types/node": "^18.11.9", "@vitest/coverage-v8": "^0.34.1", "@whitebird/kazam-transformer-base": "workspace:*", diff --git a/packages/parser-kaz/src/parser-kaz.ts b/packages/parser-kaz/src/parser-kaz.ts index 2c44ccd..f50de78 100644 --- a/packages/parser-kaz/src/parser-kaz.ts +++ b/packages/parser-kaz/src/parser-kaz.ts @@ -5,6 +5,8 @@ import { parse, tokenize } from '@whitebird/kaz-ast' import { ParserBase } from '@whitebird/kazam-parser-base' import { glob } from 'glob' +import { transformAst } from './transform-ast' + export class ParserKaz extends ParserBase<{ pathRelativeToInputPath: string inputPath: string @@ -58,6 +60,8 @@ export class ParserKaz extends ParserBase<{ if (ast === undefined) throw new Error(`Could not parse file ${filePath}`) + transformAst(ast, { fileName: filePath }) + kazAsts[pathRelativeToInputPath] = { ast, sourceAbsoluteFilePath: filePath, diff --git a/packages/parser-kaz/src/transform-ast.ts b/packages/parser-kaz/src/transform-ast.ts new file mode 100644 index 0000000..727f904 --- /dev/null +++ b/packages/parser-kaz/src/transform-ast.ts @@ -0,0 +1,142 @@ +import { parse as parseTypescript } from '@babel/parser' +import traverseTypescriptAst, { type NodePath } from '@babel/traverse' +import type { AssignmentExpression, UpdateExpression } from '@babel/types' +import { type KazAst, traverse as traverseKazAst } from '@whitebird/kaz-ast' +import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' +import { TransformerTypescript } from '@whitebird/kazam-transformer-typescript' +import StringManipulator from 'magic-string' + +export const transformAst = (astKaz: KazAst, options: { + fileName: string +}): void => { + const transformerTypescript = new TransformerTypescript({ + [options.fileName]: { + ast: astKaz, + sourceAbsoluteFilePath: options.fileName, + getTransformedOutputFilePath: (filePath: string) => filePath, + }, + }, { + withKazamInternalJsDoc: true, + }) + + const tsFiles = transformerTypescript.transformAndGenerateMappings() + const tsFile = tsFiles[`${options.fileName}.ts`] + + if (tsFile === undefined) + throw new Error('tsFile is undefined') + + const astTypescript = parseTypescript(tsFile.content, { + sourceType: 'module', + plugins: [ + 'typescript', + ], + }) + + // TODO: Currently, we mark the state setters. We should also mark the state getters and computed getters. + // We could traverse all identifiers instead of AssignmentExpression and UpdateExpression. + // We could traverse identifiers, check if their binding has a trailing comment with the magic string, + // and then categorize them as state setters, state getters, or computed getters. (if they are state setters, we should categorize the AssignmentExpression or UpdateExpression as a state setter, not the identifier). + // To categorize them, we could check if the identifier's parent is an AssignmentExpression or UpdateExpression. If it is, we could categorize it as a state setter. Otherwise, we could categorize it as a state getter or computed getter according to the comment. + + const updateAndAssignmentExpressions: NodePath[] = [] + traverseTypescriptAst(astTypescript, { + AssignmentExpression(path) { + updateAndAssignmentExpressions.push(path) + }, + UpdateExpression(path) { + updateAndAssignmentExpressions.push(path) + }, + }) + + traverseKazAst(astKaz, { + $default(node) { + const getExpression = (n: typeof node): { $range: [number, number]; $value: string } | undefined => { + const expressionKey = Object.keys(n).find(key => key.toLowerCase().includes('expression')) as keyof typeof n | undefined + + if (expressionKey === undefined) { + for (const key in n) { + const value = n[key as keyof typeof n] + + if (Array.isArray(value)) { + for (const item of value) { + if ('$type' in item) { + const expression = getExpression(item) + if (expression !== undefined) + return expression + } + } + } + else if (typeof value === 'object' && value !== null && '$type' in value) { + const expression = getExpression(value) + if (expression !== undefined) + return expression + } + } + + return undefined + } + + const expression = node[expressionKey] + + if (expression === undefined || typeof expression !== 'object' || expression === null) + return undefined + + return expression + } + + const expression = getExpression(node) + + if (expression === undefined) + return + + const matchedTypescriptExpressionNodePath = tsFile.mapping.find((mapping) => { + return mapping.sourceRange[0] === expression.$range[0] && mapping.sourceRange[1] === expression.$range[1] + }) + + if (matchedTypescriptExpressionNodePath === undefined) + return + + const typescriptExpressionNodePaths = updateAndAssignmentExpressions.filter((updateOrAssignmentExpression) => { + const { start, end } = updateOrAssignmentExpression.node + if (start === undefined || end === undefined || start === null || end === null) + return false + + return matchedTypescriptExpressionNodePath.generatedRange[0] <= start && end <= matchedTypescriptExpressionNodePath.generatedRange[1] + }) + + if (typescriptExpressionNodePaths === undefined) + return + + const expressionStringManipulator = new StringManipulator(expression.$value) + + for (const typescriptExpressionNodePath of typescriptExpressionNodePaths) { + const identifier = typescriptExpressionNodePath.isAssignmentExpression() + ? typescriptExpressionNodePath.node.left + : (typescriptExpressionNodePath.isUpdateExpression() && typescriptExpressionNodePath.node.argument) || undefined + + if (identifier === undefined || identifier.type !== 'Identifier') + return + + const binding = typescriptExpressionNodePath.scope.getBinding(identifier.name) + + if (binding?.identifier.trailingComments?.some((comment) => { + return new RegExp(`^ *\\* *${kazamMagicStrings.kazStateJSDoc.regexp.source} *$`).test(comment.value) + })) { + const startExpressionSetter = typescriptExpressionNodePath.node.start! - matchedTypescriptExpressionNodePath.generatedRange[0] + const endExpressionSetter = typescriptExpressionNodePath.node.end! - matchedTypescriptExpressionNodePath.generatedRange[0] + + expressionStringManipulator.update( + startExpressionSetter, + endExpressionSetter, + kazamMagicStrings.setState.create( + identifier.name, + expression.$value.slice(startExpressionSetter, endExpressionSetter), + ), + ) + } + } + + expression.$value = expressionStringManipulator.toString() + }, + }) +} diff --git a/packages/parser-kaz/tests/fixtures/Input.kaz b/packages/parser-kaz/tests/fixtures/Input.kaz index e69de29..16d3c62 100644 --- a/packages/parser-kaz/tests/fixtures/Input.kaz +++ b/packages/parser-kaz/tests/fixtures/Input.kaz @@ -0,0 +1,19 @@ +- state value: string = '' +- state old: string = '' +- onMount () => { + console.log('Hello') + console.log('Mounted') +} + +input(on:change={(event) => { + console.log('1', event.target.value) + let value2 = event.target.value + value3 = value + value = event.target.value + old = value + value3 = 'test' + value2 = 'yeya' + console.log('2', value) +}}) + +div(class={value}) diff --git a/packages/parser-kaz/tests/parser-kaz.test.ts b/packages/parser-kaz/tests/parser-kaz.test.ts index 6d58df4..97c8868 100644 --- a/packages/parser-kaz/tests/parser-kaz.test.ts +++ b/packages/parser-kaz/tests/parser-kaz.test.ts @@ -1,5 +1,6 @@ import * as path from 'node:path' +import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' import { describe, expect, test } from 'vitest' import { ParserKaz } from '../src' @@ -38,5 +39,25 @@ describe('parser-kaz', () => { 'buttons/SecondaryButton.kaz': expect.any(Object), }) }) + + test('should have the magic string in the expression that sets a state', async () => { + const result = await parser.loadAndParse(config) + + const inputElement = result['Input.kaz'].ast.template[0] + + if (inputElement.$type !== 'Tag') + throw new Error('inputElement.$type !== "Tag"') + + const onChangeAttribute = inputElement.attributes[0] + + if (onChangeAttribute.$type !== 'TagEventAttribute') + throw new Error('onChangeAttribute.$type !== "TagEventAttribute"') + + const setStateMagicStrings = Array.from(onChangeAttribute.expression.$value.matchAll(kazamMagicStrings.setState.regexp)) + + expect(setStateMagicStrings).toHaveLength(2) + expect(kazamMagicStrings.setState.parse(setStateMagicStrings[0][0])).toEqual(['value', 'value = event.target.value']) + expect(kazamMagicStrings.setState.parse(setStateMagicStrings[1][0])).toEqual(['old', 'old = value']) + }) }) }) diff --git a/packages/transform-utils/build.config.ts b/packages/transform-utils/build.config.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/transform-utils/package.json b/packages/transform-utils/package.json new file mode 100644 index 0000000..52e80ba --- /dev/null +++ b/packages/transform-utils/package.json @@ -0,0 +1,30 @@ +{ + "name": "@whitebird/kazam-transform-utils", + "type": "module", + "version": "0.0.0", + "license": "MIT", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "vitest run", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@whitebird/kazam-test-web-transformer": "workspace:*" + }, + "devDependencies": { + "@types/node": "^18.11.9" + } +} diff --git a/packages/transform-utils/src/index.ts b/packages/transform-utils/src/index.ts new file mode 100644 index 0000000..b4369be --- /dev/null +++ b/packages/transform-utils/src/index.ts @@ -0,0 +1,3 @@ +export * from '@whitebird/kazam-test-web-transformer' + +export { magicStrings as kazamMagicStrings } from './magic-strings' diff --git a/packages/transform-utils/src/magic-strings.ts b/packages/transform-utils/src/magic-strings.ts new file mode 100644 index 0000000..e0df562 --- /dev/null +++ b/packages/transform-utils/src/magic-strings.ts @@ -0,0 +1,32 @@ +import { Buffer } from 'node:buffer' + +class MagicString { + constructor(private readonly magicString: string) { } + + create(...strings: Parameters) { + const encodedString = strings.map(string => Buffer.from(string).toString('base64')).join('&') + + return `${this.magicString}__${encodedString}` + } + + parse(magicString: string): Parameters { + const [guid, encodedString] = magicString.split('__') + + if (guid !== this.magicString) + throw new Error(`The magic string ${magicString} is not a ${this.magicString} magic string.`) + + const encodedStrings = encodedString?.split('&') ?? [] + + return encodedStrings.map(encodedString => Buffer.from(encodedString, 'base64').toString('utf-8')) as unknown as Parameters + } + + get regexp() { + const base64RegExp = '(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?' + return new RegExp(`${this.magicString}__(?:${base64RegExp}&?)*`, 'g') + } +} + +export const magicStrings = { + setState: new MagicString<[stateName: string, setter: string]>('cb7c9a40-0971-4ecd-87c5-0572727d1a5b'), + kazStateJSDoc: new MagicString('5c46e186-3ca8-45f5-9971-4012b174b715'), +} diff --git a/packages/transform-utils/tsconfig.json b/packages/transform-utils/tsconfig.json new file mode 100644 index 0000000..8913473 --- /dev/null +++ b/packages/transform-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "extends": "@whitebird/tsconfig/node.json", + "compilerOptions": { + "module": "ES6", + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/transformer-typescript/package.json b/packages/transformer-typescript/package.json index 06ce7af..047b1a8 100644 --- a/packages/transformer-typescript/package.json +++ b/packages/transformer-typescript/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@whitebird/kaz-ast": "workspace:*", + "@whitebird/kazam-transform-utils": "workspace:*", "@whitebird/kazam-transformer-base": "workspace:*", "zod": "^3.21.4" }, diff --git a/packages/transformer-typescript/src/handlers/stateInstruction.ts b/packages/transformer-typescript/src/handlers/stateInstruction.ts index fe20a79..87d198f 100644 --- a/packages/transformer-typescript/src/handlers/stateInstruction.ts +++ b/packages/transformer-typescript/src/handlers/stateInstruction.ts @@ -1,9 +1,14 @@ +import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' + import type { IHandler } from '../transformer-typescript' -export const handleStateInstruction: IHandler<'stateInstruction'> = (stateInstruction, { addGeneratedContent }) => { +export const handleStateInstruction: IHandler<'stateInstruction'> = (stateInstruction, { addGeneratedContent, options }) => { addGeneratedContent('let ') addGeneratedContent(stateInstruction.name) + if (options.withKazamInternalJsDoc) + addGeneratedContent(` /** ${kazamMagicStrings.kazStateJSDoc.create()} */`) + if (stateInstruction.type !== undefined) { addGeneratedContent(': ') addGeneratedContent(stateInstruction.type) diff --git a/packages/transformer-typescript/src/transformer-typescript.ts b/packages/transformer-typescript/src/transformer-typescript.ts index d88d80c..180bccf 100644 --- a/packages/transformer-typescript/src/transformer-typescript.ts +++ b/packages/transformer-typescript/src/transformer-typescript.ts @@ -32,12 +32,16 @@ type ISchemaHandlers = { $value: string }) => void componentMeta: IComponentMeta + options: typeof TransformerTypescript.prototype.options }) => THandlerReturnType } export type IHandler = ISchemaHandlers[T] -export class TransformerTypescript extends TransformerBase<{ outputFileNameFormat: `${string}.ts` }> { +export class TransformerTypescript extends TransformerBase< + { outputFileNameFormat: `${string}.ts` }, + { withKazamInternalJsDoc: boolean } +> { private handlers: ISchemaHandlers = { ast: handlers.handleKaz, computedInstruction: handlers.handleComputedInstruction, @@ -71,7 +75,7 @@ export class TransformerTypescript extends TransformerBase<{ outputFileNameForma } transformAndGenerateMappings(): { - [key: string]: { content: string; mapping: Mapping[] } + [key: `${string}.ts`]: { content: string; mapping: Mapping[] } } { for (const componentName in this.input) { const component = this.input[componentName] @@ -83,9 +87,9 @@ export class TransformerTypescript extends TransformerBase<{ outputFileNameForma } return Object.entries(this.generatedContent).reduce<{ - [key: string]: { content: string; mapping: Mapping[] } + [key: `${string}.ts`]: { content: string; mapping: Mapping[] } }>((acc, [componentName, content]) => { - acc[componentName] = { + acc[`${componentName}.ts`] = { content, mapping: this.mappings[componentName] ?? [], } @@ -107,6 +111,7 @@ export class TransformerTypescript extends TransformerBase<{ outputFileNameForma handle: input => this.handle(input, componentMeta), addGeneratedContent: content => this.addGeneratedContent(content, componentMeta), componentMeta, + options: this.options, }) } } From c99ed648fe1ba5722d5915a5780c8e5f39097003 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Thu, 9 Nov 2023 19:46:10 +0100 Subject: [PATCH 02/13] feat(ParserKaz): implement state getter and computed getter magic string --- packages/parser-kaz/src/transform-ast.ts | 117 ++++++++++++------ packages/parser-kaz/tests/parser-kaz.test.ts | 2 +- packages/transform-utils/src/magic-strings.ts | 3 + .../src/handlers/computedInstruction.ts | 7 +- 4 files changed, 91 insertions(+), 38 deletions(-) diff --git a/packages/parser-kaz/src/transform-ast.ts b/packages/parser-kaz/src/transform-ast.ts index 727f904..529f092 100644 --- a/packages/parser-kaz/src/transform-ast.ts +++ b/packages/parser-kaz/src/transform-ast.ts @@ -1,6 +1,6 @@ import { parse as parseTypescript } from '@babel/parser' import traverseTypescriptAst, { type NodePath } from '@babel/traverse' -import type { AssignmentExpression, UpdateExpression } from '@babel/types' +import type { Identifier } from '@babel/types' import { type KazAst, traverse as traverseKazAst } from '@whitebird/kaz-ast' import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' import { TransformerTypescript } from '@whitebird/kazam-transformer-typescript' @@ -32,19 +32,52 @@ export const transformAst = (astKaz: KazAst, options: { ], }) - // TODO: Currently, we mark the state setters. We should also mark the state getters and computed getters. - // We could traverse all identifiers instead of AssignmentExpression and UpdateExpression. - // We could traverse identifiers, check if their binding has a trailing comment with the magic string, - // and then categorize them as state setters, state getters, or computed getters. (if they are state setters, we should categorize the AssignmentExpression or UpdateExpression as a state setter, not the identifier). - // To categorize them, we could check if the identifier's parent is an AssignmentExpression or UpdateExpression. If it is, we could categorize it as a state setter. Otherwise, we could categorize it as a state getter or computed getter according to the comment. + const replacements: ( + { type: 'stateSetter' | 'computedGetter' | 'stateGetter'; identifierPath: NodePath; pathToReplace?: NodePath } + )[] = [] - const updateAndAssignmentExpressions: NodePath[] = [] traverseTypescriptAst(astTypescript, { - AssignmentExpression(path) { - updateAndAssignmentExpressions.push(path) - }, - UpdateExpression(path) { - updateAndAssignmentExpressions.push(path) + Identifier(path) { + if (path.parentPath.isVariableDeclarator()) + return + + if (path.parentPath.isMemberExpression() && path.parentPath.node.property === path.node) + return + + const binding = path.scope.getBinding(path.node.name) + + if (binding === undefined) + return + + let type: 'state' | 'computed' | undefined + for (const comment of binding?.identifier.trailingComments ?? []) { + if (new RegExp(`^ *\\* *${kazamMagicStrings.kazStateJSDoc.regexp.source} *$`).test(comment.value)) + type = 'state' + else if (new RegExp(`^ *\\* *${kazamMagicStrings.kazComputedJSDoc.regexp.source} *$`).test(comment.value)) + type = 'computed' + + if (type !== undefined) + break + } + + if (type === undefined) + return + + switch (type) { + case 'state': { + if (path.parentPath.isUpdateExpression()) + replacements.push({ type: 'stateSetter', identifierPath: path, pathToReplace: path.parentPath }) + else if (path.parentPath.isAssignmentExpression() && path.parentPath.node.left === path.node) + replacements.push({ type: 'stateSetter', identifierPath: path, pathToReplace: path.parentPath }) + else + replacements.push({ type: 'stateGetter', identifierPath: path }) + break + } + case 'computed': { + replacements.push({ type: 'computedGetter', identifierPath: path }) + break + } + } }, }) @@ -96,8 +129,8 @@ export const transformAst = (astKaz: KazAst, options: { if (matchedTypescriptExpressionNodePath === undefined) return - const typescriptExpressionNodePaths = updateAndAssignmentExpressions.filter((updateOrAssignmentExpression) => { - const { start, end } = updateOrAssignmentExpression.node + const typescriptExpressionNodePaths = replacements.filter((replacement) => { + const { start, end } = (replacement.pathToReplace ?? replacement.identifierPath).node if (start === undefined || end === undefined || start === null || end === null) return false @@ -109,31 +142,43 @@ export const transformAst = (astKaz: KazAst, options: { const expressionStringManipulator = new StringManipulator(expression.$value) + typescriptExpressionNodePaths.sort((a) => { + if (a.type.toLowerCase().endsWith('getter')) + return -1 + + return 1 + }) + for (const typescriptExpressionNodePath of typescriptExpressionNodePaths) { - const identifier = typescriptExpressionNodePath.isAssignmentExpression() - ? typescriptExpressionNodePath.node.left - : (typescriptExpressionNodePath.isUpdateExpression() && typescriptExpressionNodePath.node.argument) || undefined - - if (identifier === undefined || identifier.type !== 'Identifier') - return - - const binding = typescriptExpressionNodePath.scope.getBinding(identifier.name) - - if (binding?.identifier.trailingComments?.some((comment) => { - return new RegExp(`^ *\\* *${kazamMagicStrings.kazStateJSDoc.regexp.source} *$`).test(comment.value) - })) { - const startExpressionSetter = typescriptExpressionNodePath.node.start! - matchedTypescriptExpressionNodePath.generatedRange[0] - const endExpressionSetter = typescriptExpressionNodePath.node.end! - matchedTypescriptExpressionNodePath.generatedRange[0] - - expressionStringManipulator.update( - startExpressionSetter, - endExpressionSetter, - kazamMagicStrings.setState.create( - identifier.name, - expression.$value.slice(startExpressionSetter, endExpressionSetter), - ), + const startExpressionSetter = (typescriptExpressionNodePath.pathToReplace ?? typescriptExpressionNodePath.identifierPath).node.start! - matchedTypescriptExpressionNodePath.generatedRange[0] + const endExpressionSetter = (typescriptExpressionNodePath.pathToReplace ?? typescriptExpressionNodePath.identifierPath).node.end! - matchedTypescriptExpressionNodePath.generatedRange[0] + + let magicString: string | undefined + if (typescriptExpressionNodePath.type === 'stateSetter') { + magicString = kazamMagicStrings.setState.create( + typescriptExpressionNodePath.identifierPath.node.name, + expressionStringManipulator.slice(startExpressionSetter, endExpressionSetter), ) } + else if (typescriptExpressionNodePath.type === 'stateGetter') { + magicString = kazamMagicStrings.getState.create( + typescriptExpressionNodePath.identifierPath.node.name, + ) + } + else if (typescriptExpressionNodePath.type === 'computedGetter') { + magicString = kazamMagicStrings.getComputed.create( + typescriptExpressionNodePath.identifierPath.node.name, + ) + } + + if (magicString === undefined) + continue + + expressionStringManipulator.update( + startExpressionSetter, + endExpressionSetter, + magicString, + ) } expression.$value = expressionStringManipulator.toString() diff --git a/packages/parser-kaz/tests/parser-kaz.test.ts b/packages/parser-kaz/tests/parser-kaz.test.ts index 97c8868..2fc1774 100644 --- a/packages/parser-kaz/tests/parser-kaz.test.ts +++ b/packages/parser-kaz/tests/parser-kaz.test.ts @@ -57,7 +57,7 @@ describe('parser-kaz', () => { expect(setStateMagicStrings).toHaveLength(2) expect(kazamMagicStrings.setState.parse(setStateMagicStrings[0][0])).toEqual(['value', 'value = event.target.value']) - expect(kazamMagicStrings.setState.parse(setStateMagicStrings[1][0])).toEqual(['old', 'old = value']) + expect(kazamMagicStrings.setState.parse(setStateMagicStrings[1][0])).toEqual(['old', `old = ${kazamMagicStrings.getState.create('value')}`]) }) }) }) diff --git a/packages/transform-utils/src/magic-strings.ts b/packages/transform-utils/src/magic-strings.ts index e0df562..4cdcb91 100644 --- a/packages/transform-utils/src/magic-strings.ts +++ b/packages/transform-utils/src/magic-strings.ts @@ -28,5 +28,8 @@ class MagicString { export const magicStrings = { setState: new MagicString<[stateName: string, setter: string]>('cb7c9a40-0971-4ecd-87c5-0572727d1a5b'), + getState: new MagicString<[stateName: string]>('153c1b2d-d22f-439a-976c-20af6d7659b3'), + getComputed: new MagicString<[computedName: string]>('28ddb00a-552b-4c53-98a4-54a8eebdaf43'), kazStateJSDoc: new MagicString('5c46e186-3ca8-45f5-9971-4012b174b715'), + kazComputedJSDoc: new MagicString('d1216e22-94df-4b98-828a-87c53bc24b8a'), } diff --git a/packages/transformer-typescript/src/handlers/computedInstruction.ts b/packages/transformer-typescript/src/handlers/computedInstruction.ts index 592e7c8..7e281ea 100644 --- a/packages/transformer-typescript/src/handlers/computedInstruction.ts +++ b/packages/transformer-typescript/src/handlers/computedInstruction.ts @@ -1,9 +1,14 @@ +import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' + import type { IHandler } from '../transformer-typescript' -export const handleComputedInstruction: IHandler<'computedInstruction'> = (computedInstruction, { addGeneratedContent }) => { +export const handleComputedInstruction: IHandler<'computedInstruction'> = (computedInstruction, { addGeneratedContent, options }) => { addGeneratedContent('const ') addGeneratedContent(computedInstruction.name) + if (options.withKazamInternalJsDoc) + addGeneratedContent(` /** ${kazamMagicStrings.kazComputedJSDoc.create()} */`) + if (computedInstruction.type !== undefined) { addGeneratedContent(': ') addGeneratedContent(computedInstruction.type) From 457bc9432b487e549e3379b0b0b00d62020c95b3 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Thu, 9 Nov 2023 21:34:04 +0100 Subject: [PATCH 03/13] refactor(ParserKaz): rewrite transformAst to make it more readable/maintainable --- packages/parser-kaz/src/transform-ast.ts | 390 +++++++++++++++-------- 1 file changed, 257 insertions(+), 133 deletions(-) diff --git a/packages/parser-kaz/src/transform-ast.ts b/packages/parser-kaz/src/transform-ast.ts index 529f092..5888a53 100644 --- a/packages/parser-kaz/src/transform-ast.ts +++ b/packages/parser-kaz/src/transform-ast.ts @@ -6,37 +6,66 @@ import { kazamMagicStrings } from '@whitebird/kazam-transform-utils' import { TransformerTypescript } from '@whitebird/kazam-transformer-typescript' import StringManipulator from 'magic-string' -export const transformAst = (astKaz: KazAst, options: { - fileName: string -}): void => { - const transformerTypescript = new TransformerTypescript({ +type PathType = 'stateSetter' | 'computedGetter' | 'stateGetter' + +export const transformAst = ( + kazAst: KazAst, + options: { fileName: string }, +): void => { + const typescriptFile = generateTypescriptFileAndMappings(kazAst, options) + + const typescriptAst = parseTypescript(typescriptFile.content, { + sourceType: 'module', + plugins: ['typescript'], + }) + + const pathsToReplace = findPathsToReplace(typescriptAst) + + replacePaths( + kazAst, + pathsToReplace, + typescriptFile, + ) +} + +function createTransformerTypescript( + kazAst: KazAst, + options: { fileName: string }, +) { + return new TransformerTypescript({ [options.fileName]: { - ast: astKaz, + ast: kazAst, sourceAbsoluteFilePath: options.fileName, getTransformedOutputFilePath: (filePath: string) => filePath, }, }, { withKazamInternalJsDoc: true, }) +} + +function generateTypescriptFileAndMappings( + kazAst: KazAst, + options: { fileName: string }, +) { + const transformerTypescript = createTransformerTypescript(kazAst, options) - const tsFiles = transformerTypescript.transformAndGenerateMappings() - const tsFile = tsFiles[`${options.fileName}.ts`] + const typescriptFiles = transformerTypescript.transformAndGenerateMappings() + const typescriptFile = typescriptFiles[`${options.fileName}.ts`] - if (tsFile === undefined) + if (typescriptFile === undefined) throw new Error('tsFile is undefined') - const astTypescript = parseTypescript(tsFile.content, { - sourceType: 'module', - plugins: [ - 'typescript', - ], - }) + return typescriptFile +} - const replacements: ( - { type: 'stateSetter' | 'computedGetter' | 'stateGetter'; identifierPath: NodePath; pathToReplace?: NodePath } - )[] = [] +function findPathsToReplace(typescriptAst: ReturnType) { + const pathsToReplace = new Set<{ + type: PathType + identifierPath: NodePath + pathToReplace?: NodePath + }>() - traverseTypescriptAst(astTypescript, { + traverseTypescriptAst(typescriptAst, { Identifier(path) { if (path.parentPath.isVariableDeclarator()) return @@ -44,144 +73,239 @@ export const transformAst = (astKaz: KazAst, options: { if (path.parentPath.isMemberExpression() && path.parentPath.node.property === path.node) return - const binding = path.scope.getBinding(path.node.name) + const pathType = getPathType(path) - if (binding === undefined) + if (pathType === undefined) return - let type: 'state' | 'computed' | undefined - for (const comment of binding?.identifier.trailingComments ?? []) { - if (new RegExp(`^ *\\* *${kazamMagicStrings.kazStateJSDoc.regexp.source} *$`).test(comment.value)) - type = 'state' - else if (new RegExp(`^ *\\* *${kazamMagicStrings.kazComputedJSDoc.regexp.source} *$`).test(comment.value)) - type = 'computed' + addPathToReplace(pathsToReplace, path, pathType) + }, + }) + + return pathsToReplace +} + +function getPathType(path: NodePath) { + const binding = path.scope.getBinding(path.node.name) + + if (binding === undefined) + return + + for (const comment of binding?.identifier.trailingComments ?? []) { + if (new RegExp(`^ *\\* *${kazamMagicStrings.kazStateJSDoc.regexp.source} *$`).test(comment.value)) + return 'state' - if (type !== undefined) - break + if (new RegExp(`^ *\\* *${kazamMagicStrings.kazComputedJSDoc.regexp.source} *$`).test(comment.value)) + return 'computed' + } + + return undefined +} + +function addPathToReplace( + pathsToReplace: Set<{ + type: PathType + identifierPath: NodePath + pathToReplace?: NodePath + }>, + path: NodePath, + type: NonNullable>, +) { + switch (type) { + case 'state': { + if ( + path.parentPath.isUpdateExpression() + || (path.parentPath.isAssignmentExpression() && path.parentPath.node.left === path.node) + ) { + pathsToReplace.add({ + type: 'stateSetter', + identifierPath: path, + pathToReplace: path.parentPath, + }) + break } - if (type === undefined) + pathsToReplace.add({ + type: 'stateGetter', + identifierPath: path, + }) + break + } + case 'computed': { + pathsToReplace.add({ + type: 'computedGetter', + identifierPath: path, + }) + break + } + } +} + +function replacePaths( + kazAst: KazAst, + pathsToReplace: Set<{ + type: PathType + identifierPath: NodePath + pathToReplace?: NodePath + }>, + typescriptFile: ReturnType, +) { + traverseKazAst(kazAst, { + $default(node) { + const expression = getKazExpression(node) + + if (expression === undefined) return - switch (type) { - case 'state': { - if (path.parentPath.isUpdateExpression()) - replacements.push({ type: 'stateSetter', identifierPath: path, pathToReplace: path.parentPath }) - else if (path.parentPath.isAssignmentExpression() && path.parentPath.node.left === path.node) - replacements.push({ type: 'stateSetter', identifierPath: path, pathToReplace: path.parentPath }) - else - replacements.push({ type: 'stateGetter', identifierPath: path }) - break - } - case 'computed': { - replacements.push({ type: 'computedGetter', identifierPath: path }) - break - } - } + const typescriptMappedRange = getTypescriptMappedRange(typescriptFile, expression) + + const pathsInRange = sortPathsByType( + findPathsInRange(pathsToReplace, typescriptMappedRange), + ['stateGetter', 'computedGetter', 'stateSetter'], + ) + + const expressionStringManipulator = new StringManipulator(expression.$value) + + applyPathsReplacements( + pathsInRange, + expressionStringManipulator, + typescriptMappedRange, + ) + + expression.$value = expressionStringManipulator.toString() }, }) +} - traverseKazAst(astKaz, { - $default(node) { - const getExpression = (n: typeof node): { $range: [number, number]; $value: string } | undefined => { - const expressionKey = Object.keys(n).find(key => key.toLowerCase().includes('expression')) as keyof typeof n | undefined - - if (expressionKey === undefined) { - for (const key in n) { - const value = n[key as keyof typeof n] - - if (Array.isArray(value)) { - for (const item of value) { - if ('$type' in item) { - const expression = getExpression(item) - if (expression !== undefined) - return expression - } - } - } - else if (typeof value === 'object' && value !== null && '$type' in value) { - const expression = getExpression(value) - if (expression !== undefined) - return expression - } - } - - return undefined - } - - const expression = node[expressionKey] - - if (expression === undefined || typeof expression !== 'object' || expression === null) - return undefined +function getKazExpression( + node: Parameters[1]['$default']>>[0], +): { $range: [number, number]; $value: string } | undefined { + const expressionKey = ( + Object.keys(node) + .find(key => key.toLowerCase().includes('expression')) + ) as keyof typeof node | undefined - return expression - } + if (expressionKey !== undefined) { + const expression = node[expressionKey] as never - const expression = getExpression(node) + if (expression === undefined || typeof expression !== 'object' || expression === null) + return - if (expression === undefined) - return + return expression + } - const matchedTypescriptExpressionNodePath = tsFile.mapping.find((mapping) => { - return mapping.sourceRange[0] === expression.$range[0] && mapping.sourceRange[1] === expression.$range[1] - }) + for (const key in node) { + const subNode = node[key as keyof typeof node] - if (matchedTypescriptExpressionNodePath === undefined) - return + if (typeof subNode === 'object' && subNode !== null && '$type' in subNode) { + const expression = getKazExpression(subNode) + if (expression !== undefined) + return expression + } + } - const typescriptExpressionNodePaths = replacements.filter((replacement) => { - const { start, end } = (replacement.pathToReplace ?? replacement.identifierPath).node - if (start === undefined || end === undefined || start === null || end === null) - return false + return undefined +} - return matchedTypescriptExpressionNodePath.generatedRange[0] <= start && end <= matchedTypescriptExpressionNodePath.generatedRange[1] - }) +function getTypescriptMappedRange( + typescriptFile: ReturnType, + expression: NonNullable>, +) { + const mappedRange = typescriptFile.mapping.find((mapping) => { + return ( + mapping.sourceRange[0] === expression.$range[0] + && mapping.sourceRange[1] === expression.$range[1] + ) + }) - if (typescriptExpressionNodePaths === undefined) - return + if (mappedRange === undefined) + throw new Error(`mappedRange === undefined for range: ${JSON.stringify(expression.$range)}`) - const expressionStringManipulator = new StringManipulator(expression.$value) + return mappedRange.generatedRange +} - typescriptExpressionNodePaths.sort((a) => { - if (a.type.toLowerCase().endsWith('getter')) - return -1 +function findPathsInRange( + pathsToReplace: Set<{ + type: PathType + identifierPath: NodePath + pathToReplace?: NodePath + }>, + range: [number, number], +) { + return Array.from(pathsToReplace).filter((path) => { + const { start, end } = (path.pathToReplace ?? path.identifierPath).node + if (start === undefined || end === undefined || start === null || end === null) + return false + + return range[0] <= start && end <= range[1] + }) +} - return 1 - }) +function sortPathsByType( + paths: ReturnType, + types: typeof paths[number]['type'][], +) { + return [...paths].sort((a, b) => { + let aIndex = types.indexOf(a.type) + let bIndex = types.indexOf(b.type) - for (const typescriptExpressionNodePath of typescriptExpressionNodePaths) { - const startExpressionSetter = (typescriptExpressionNodePath.pathToReplace ?? typescriptExpressionNodePath.identifierPath).node.start! - matchedTypescriptExpressionNodePath.generatedRange[0] - const endExpressionSetter = (typescriptExpressionNodePath.pathToReplace ?? typescriptExpressionNodePath.identifierPath).node.end! - matchedTypescriptExpressionNodePath.generatedRange[0] - - let magicString: string | undefined - if (typescriptExpressionNodePath.type === 'stateSetter') { - magicString = kazamMagicStrings.setState.create( - typescriptExpressionNodePath.identifierPath.node.name, - expressionStringManipulator.slice(startExpressionSetter, endExpressionSetter), - ) - } - else if (typescriptExpressionNodePath.type === 'stateGetter') { - magicString = kazamMagicStrings.getState.create( - typescriptExpressionNodePath.identifierPath.node.name, - ) - } - else if (typescriptExpressionNodePath.type === 'computedGetter') { - magicString = kazamMagicStrings.getComputed.create( - typescriptExpressionNodePath.identifierPath.node.name, - ) - } - - if (magicString === undefined) - continue - - expressionStringManipulator.update( - startExpressionSetter, - endExpressionSetter, - magicString, - ) - } + if (aIndex === -1) + aIndex = types.length - expression.$value = expressionStringManipulator.toString() - }, + if (bIndex === -1) + bIndex = types.length + + return aIndex - bIndex }) } + +function applyPathsReplacements( + paths: ReturnType, + expressionStringManipulator: StringManipulator, + range: [number, number], +) { + for (const path of paths) { + const startExpressionSetter = (path.pathToReplace ?? path.identifierPath).node.start! - range[0] + const endExpressionSetter = (path.pathToReplace ?? path.identifierPath).node.end! - range[0] + + const magicString = getMagicString( + path, + expressionStringManipulator.slice(startExpressionSetter, endExpressionSetter), + ) + + if (magicString === undefined) + continue + + expressionStringManipulator.update( + startExpressionSetter, + endExpressionSetter, + magicString, + ) + } +} + +function getMagicString( + path: Parameters[0][number], + expression: string, +) { + if (path.type === 'stateSetter') { + return kazamMagicStrings.setState.create( + path.identifierPath.node.name, + expression, + ) + } + + if (path.type === 'stateGetter') { + return kazamMagicStrings.getState.create( + path.identifierPath.node.name, + ) + } + + if (path.type === 'computedGetter') { + return kazamMagicStrings.getComputed.create( + path.identifierPath.node.name, + ) + } + + return undefined +} From 3c24dd03ea6504488f2f7d92d9a13b3de0c4feb3 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Thu, 9 Nov 2023 21:36:50 +0100 Subject: [PATCH 04/13] refactor(KazAst): rename `$default` visitor to `enter` in `traverse` function --- packages/kaz-ast/src/lib/traverse.ts | 4 ++-- packages/parser-kaz/src/transform-ast.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kaz-ast/src/lib/traverse.ts b/packages/kaz-ast/src/lib/traverse.ts index daf1adb..54338cf 100644 --- a/packages/kaz-ast/src/lib/traverse.ts +++ b/packages/kaz-ast/src/lib/traverse.ts @@ -7,12 +7,12 @@ type AllInferedSchemas = z.infer) => void } & { - $default?: (node: AllInferedSchemas) => void + enter?: (node: AllInferedSchemas) => void } export const traverse = (ast: AllInferedSchemas, visitor: Visitor) => { const traverse = (node: AllInferedSchemas) => { - (visitor[node.$type] ?? visitor.$default)?.(node as never) + (visitor[node.$type] ?? visitor.enter)?.(node as never) for (const key in node) { const value = node[key as keyof typeof node] as unknown diff --git a/packages/parser-kaz/src/transform-ast.ts b/packages/parser-kaz/src/transform-ast.ts index 5888a53..79ea445 100644 --- a/packages/parser-kaz/src/transform-ast.ts +++ b/packages/parser-kaz/src/transform-ast.ts @@ -151,7 +151,7 @@ function replacePaths( typescriptFile: ReturnType, ) { traverseKazAst(kazAst, { - $default(node) { + enter(node) { const expression = getKazExpression(node) if (expression === undefined) @@ -178,7 +178,7 @@ function replacePaths( } function getKazExpression( - node: Parameters[1]['$default']>>[0], + node: Parameters[1]['enter']>>[0], ): { $range: [number, number]; $value: string } | undefined { const expressionKey = ( Object.keys(node) From 9e74008d0da513130e23701cfab5484a77408b2d Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Fri, 10 Nov 2023 14:40:48 +0100 Subject: [PATCH 05/13] refactor(TransformerReact): transform expressions by replacing magic strings --- packages/test-web-transformer/package.json | 5 +- .../create-test-web-transformer-fixture.ts | 50 ++-- packages/transform-utils/package.json | 4 +- packages/transform-utils/src/index.ts | 7 +- packages/transform-utils/src/magic-strings.ts | 32 +++ packages/transformer-react/package.json | 1 + .../functions/handlers/computedInstruction.ts | 7 +- .../handlers/lifecycleEventInstruction.ts | 2 +- .../functions/handlers/propInstruction.ts | 7 +- .../functions/handlers/stateInstruction.ts | 2 +- .../functions/handlers/templateExpression.ts | 7 +- .../handlers/templateTagAttribute.ts | 7 +- .../handlers/templateTagEventAttribute.ts | 7 +- .../functions/handlers/watchInstruction.ts | 2 +- .../functions/transform-expression.ts | 226 ------------------ .../transform-service/transform-service.ts | 3 - .../transformer-react/transform/transform.ts | 13 + pnpm-lock.yaml | 77 ++++-- 18 files changed, 165 insertions(+), 294 deletions(-) delete mode 100644 packages/transformer-react/src/transformer-react/transform/transform-service/functions/transform-expression.ts diff --git a/packages/test-web-transformer/package.json b/packages/test-web-transformer/package.json index 7cfc51f..8473b7c 100644 --- a/packages/test-web-transformer/package.json +++ b/packages/test-web-transformer/package.json @@ -32,12 +32,13 @@ }, "dependencies": { "looks-same": "^8.1.0", - "playwright": "^1.35.0" + "playwright": "^1.35.0", + "tmp-promise": "^3.0.3" }, "devDependencies": { "@types/dedent": "^0.7.0", "@types/node": "^18.11.9", - "@whitebird/kaz-ast": "workspace:*", + "@whitebird/kazam-parser-kaz": "workspace:*", "@whitebird/kazam-transformer-base": "workspace:*", "dedent": "^0.7.0", "rimraf": "^5.0.1" diff --git a/packages/test-web-transformer/src/utils/create-test-web-transformer-fixture.ts b/packages/test-web-transformer/src/utils/create-test-web-transformer-fixture.ts index 6da32b4..20be802 100644 --- a/packages/test-web-transformer/src/utils/create-test-web-transformer-fixture.ts +++ b/packages/test-web-transformer/src/utils/create-test-web-transformer-fixture.ts @@ -1,6 +1,10 @@ -import { parse, tokenize } from '@whitebird/kaz-ast' +import * as fs from 'node:fs/promises' +import * as path from 'node:path' + +import { ParserKaz } from '@whitebird/kazam-parser-kaz' import type { TransformerInput } from '@whitebird/kazam-transformer-base' import type { Page } from 'playwright' +import { dir } from 'tmp-promise' export interface TestWebTransformerFixture { fixtureDirectory: string @@ -18,19 +22,37 @@ export const createTestWebTransformerFixture = async (fixture: TestWebTransforme return { ...fixture, input: Object.fromEntries( - Object.entries(fixture.input).map(([key, value]) => { - const tokens = tokenize(value) - const ast = parse(tokens) - - if (ast instanceof Error || ast === undefined) - throw new Error(`Failed to parse ${key} in ${fixture.fixtureDirectory}`) - - return [key, { - ast, - sourceAbsoluteFilePath: '', - getTransformedOutputFilePath: (filePath: string) => filePath, - }] as const - }), + await Promise.all( + Object.entries(fixture.input).map(async ([key, value]) => { + const { path: directoryPath, cleanup: cleanupDirectory } = await dir({ + unsafeCleanup: true, + tmpdir: fixture.fixtureDirectory, + }) + await fs.writeFile(path.join(directoryPath, `${key}.kaz`), value) + + const parser = new ParserKaz() + const parseResult = await parser.loadAndParse({ + input: [ + directoryPath, + ], + output: 'dist', + rootDir: directoryPath, + }) + + await cleanupDirectory() + + const ast = parseResult[`${key}.kaz`]?.ast + + if (ast === undefined) + throw new Error(`Failed to parse ${key} in ${fixture.fixtureDirectory}`) + + return [key, { + ast, + sourceAbsoluteFilePath: '', + getTransformedOutputFilePath: (filePath: string) => filePath, + }] as const + }), + ), ) as TestWebTransformerFixture['input'], } } diff --git a/packages/transform-utils/package.json b/packages/transform-utils/package.json index 52e80ba..1d7fc30 100644 --- a/packages/transform-utils/package.json +++ b/packages/transform-utils/package.json @@ -5,6 +5,7 @@ "license": "MIT", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } @@ -21,9 +22,6 @@ "test": "vitest run", "test:coverage": "vitest run --coverage" }, - "dependencies": { - "@whitebird/kazam-test-web-transformer": "workspace:*" - }, "devDependencies": { "@types/node": "^18.11.9" } diff --git a/packages/transform-utils/src/index.ts b/packages/transform-utils/src/index.ts index b4369be..1bc3377 100644 --- a/packages/transform-utils/src/index.ts +++ b/packages/transform-utils/src/index.ts @@ -1,3 +1,4 @@ -export * from '@whitebird/kazam-test-web-transformer' - -export { magicStrings as kazamMagicStrings } from './magic-strings' +export { + magicStrings as kazamMagicStrings, + replaceMagicStrings as replaceKazamMagicStrings, +} from './magic-strings' diff --git a/packages/transform-utils/src/magic-strings.ts b/packages/transform-utils/src/magic-strings.ts index 4cdcb91..3e0ca95 100644 --- a/packages/transform-utils/src/magic-strings.ts +++ b/packages/transform-utils/src/magic-strings.ts @@ -20,6 +20,12 @@ class MagicString { return encodedStrings.map(encodedString => Buffer.from(encodedString, 'base64').toString('utf-8')) as unknown as Parameters } + replaceAllInString(string: string, replacer: (match: string, ...parameters: Parameters) => string) { + return string.replace(this.regexp, (match) => { + return replacer(match, ...this.parse(match)) + }) + } + get regexp() { const base64RegExp = '(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?' return new RegExp(`${this.magicString}__(?:${base64RegExp}&?)*`, 'g') @@ -33,3 +39,29 @@ export const magicStrings = { kazStateJSDoc: new MagicString('5c46e186-3ca8-45f5-9971-4012b174b715'), kazComputedJSDoc: new MagicString('d1216e22-94df-4b98-828a-87c53bc24b8a'), } + +export const replaceMagicStrings = ( + string: string, + replacers: { + [T in keyof typeof magicStrings]?: (match: string, ...parameters: typeof magicStrings[T] extends MagicString ? Parameters : never) => string + }, +) => { + let result = string + + for (const magicStringKey in magicStrings) { + const magicString = magicStrings[magicStringKey as keyof typeof magicStrings] + const replacer = replacers[magicStringKey as keyof typeof magicStrings] + + result = magicString.replaceAllInString(result, (match, ...parameters) => { + // @ts-expect-error Parameters is a tuple, but TypeScript thinks it's an array. + return replacer?.(match, ...parameters) ?? match + }) + } + + const remainingMagicStrings = Object.keys(magicStrings).some(magicStringKey => magicStrings[magicStringKey as keyof typeof magicStrings].regexp.test(result)) + + if (remainingMagicStrings) + replaceMagicStrings(result, replacers) + + return result +} diff --git a/packages/transformer-react/package.json b/packages/transformer-react/package.json index 69c495a..0b2a496 100644 --- a/packages/transformer-react/package.json +++ b/packages/transformer-react/package.json @@ -27,6 +27,7 @@ "@types/lodash": "^4.14.191", "@types/prettier": "^2.7.2", "@whitebird/kaz-ast": "workspace:*", + "@whitebird/kazam-transform-utils": "workspace:*", "@whitebird/kazam-transformer-base": "workspace:*", "@whitebird/kazam-transformer-typescript": "workspace:*", "effect": "2.0.0-next.48", diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/computedInstruction.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/computedInstruction.ts index fd39859..734905b 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/computedInstruction.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/computedInstruction.ts @@ -1,17 +1,14 @@ import { Effect } from 'effect' -import { TransformService } from '../../transform-service' import type { Handle } from '../handle' export const handleComputedInstruction: Handle<'computedInstruction', string> = computedInstruction => - Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - + Effect.gen(function* () { return String.prototype.concat( 'const ', computedInstruction.name.$value, computedInstruction.type !== undefined ? `: ${computedInstruction.type.$value}` : '', ' = ', - yield * _(transformService.transformExpression(computedInstruction.computeValue.expression)), + computedInstruction.computeValue.expression.$value, ) }) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/lifecycleEventInstruction.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/lifecycleEventInstruction.ts index 469af79..18c62b4 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/lifecycleEventInstruction.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/lifecycleEventInstruction.ts @@ -15,7 +15,7 @@ export const handleLifecycleEventInstruction: Handle<'lifecycleEventInstruction' case 'mount': { return String.prototype.concat( 'useEffect(() => {', - yield * _(transformService.transformExpression(callbackExpression)), + callbackExpression.$value, '}, [])', ) } diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/propInstruction.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/propInstruction.ts index da0ae56..75a5885 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/propInstruction.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/propInstruction.ts @@ -1,20 +1,17 @@ import { Effect } from 'effect' -import { TransformService } from '../../transform-service' import type { Handle } from '../handle' export const handlePropInstruction: Handle<'propInstruction', { declaration: string type: string }> = propInstruction => - Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - + Effect.gen(function* () { return { declaration: String.prototype.concat( propInstruction.name.$value, propInstruction.defaultValue !== undefined - ? ` = ${yield * _(transformService.transformExpression(propInstruction.defaultValue.expression))}` + ? ` = ${propInstruction.defaultValue.expression.$value}` : '', ), type: String.prototype.concat( diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/stateInstruction.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/stateInstruction.ts index 577826b..8239719 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/stateInstruction.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/stateInstruction.ts @@ -24,7 +24,7 @@ export const handleStateInstruction: Handle<'stateInstruction', string> = stateI : '', '(', stateInstruction.defaultValue !== undefined - ? yield * _(transformService.transformExpression(stateInstruction.defaultValue.expression)) + ? stateInstruction.defaultValue.expression.$value : '', ')', ) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateExpression.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateExpression.ts index f358c8d..31868be 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateExpression.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateExpression.ts @@ -1,15 +1,12 @@ import { Effect } from 'effect' -import { TransformService } from '../../transform-service' import type { Handle } from '../handle' export const handleTemplateExpression: Handle<'templateExpression', string> = templateExpression => - Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - + Effect.gen(function* () { return String.prototype.concat( '{', - yield * _(transformService.transformExpression(templateExpression.expression)), + templateExpression.expression.$value, '}', ) }) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagAttribute.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagAttribute.ts index 05048c7..8e8702f 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagAttribute.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagAttribute.ts @@ -1,12 +1,9 @@ import { Effect, pipe } from 'effect' -import { TransformService } from '../../transform-service' import type { Handle } from '../handle' export const handleTemplateTagAttribute: Handle<'templateTagAttribute', string> = templateTagAttribute => Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - switch (templateTagAttribute.name.$value) { case 'class': { templateTagAttribute.name.$value = 'className' @@ -31,7 +28,7 @@ export const handleTemplateTagAttribute: Handle<'templateTagAttribute', string> return `{${value}}` }) - const getValue = () => Effect.gen(function* (_) { + const getValue = () => Effect.gen(function* () { if ('value' in templateTagAttribute) { if (typeof templateTagAttribute.value === 'boolean') return String(templateTagAttribute.value) @@ -40,7 +37,7 @@ export const handleTemplateTagAttribute: Handle<'templateTagAttribute', string> } if ('expression' in templateTagAttribute) - return yield * _(transformService.transformExpression(templateTagAttribute.expression)) + return templateTagAttribute.expression.$value return 'true' }) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagEventAttribute.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagEventAttribute.ts index c0dbca1..286c55a 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagEventAttribute.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/templateTagEventAttribute.ts @@ -1,19 +1,16 @@ import { Effect } from 'effect' import { upperFirst } from 'lodash' -import { TransformService } from '../../transform-service' import type { Handle } from '../handle' export const handleTemplateTagEventAttribute: Handle<'templateTagEventAttribute', string> = templateTagEventAttribute => - Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - + Effect.gen(function* () { return String.prototype.concat( 'on', upperFirst(templateTagEventAttribute.name.$value), '=', '{', - yield * _(transformService.transformExpression(templateTagEventAttribute.expression)), + templateTagEventAttribute.expression.$value, '}', ) }) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/watchInstruction.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/watchInstruction.ts index dd2ab58..35ddd13 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/watchInstruction.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/handlers/watchInstruction.ts @@ -14,7 +14,7 @@ export const handleWatchInstruction: Handle<'watchInstruction', string> = watchI return String.prototype.concat( 'useEffect(() => {', - yield * _(transformService.transformExpression(watchInstruction.callbackExpression)), + watchInstruction.callbackExpression.$value, '}, [', watchInstruction.watchedVariables.map(variable => variable.name.$value).join(', '), '])', diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/transform-expression.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/functions/transform-expression.ts deleted file mode 100644 index 5ef12ca..0000000 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/functions/transform-expression.ts +++ /dev/null @@ -1,226 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ - -import * as babelParser from '@babel/parser' -import traverse, { type NodePath } from '@babel/traverse' -import type { AssignmentExpression, UpdateExpression } from '@babel/types' -import type { KazAst, kazStateInstructionSchema, kazTemplateExpressionSchema } from '@whitebird/kaz-ast' -import { TransformerTypescript } from '@whitebird/kazam-transformer-typescript' -import { Effect, pipe } from 'effect' -import { upperFirst } from 'lodash' -import type { z } from 'zod' - -import { TransformService } from '../transform-service' - -type KazExpression = z.infer['expression'] - -type StateInstruction = z.infer - -export const transformExpression = ( - kazExpression: KazExpression, -) => - pipe( - Effect.all([Effect.succeed(kazExpression), Effect.succeed(kazExpression.$value)]), - Effect.flatMap(transformSetStateInExpression), - // Add other transformations here - ).pipe( - Effect.map(([, fixedExpression]) => fixedExpression), - ) - -const transformToTypescript = () => - Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - - const metadata = yield * _(transformService.getMetadata()) - - const kazAst = metadata.input[metadata.filePath] - - if (kazAst === undefined) - throw new Error(`No AST found for file ${metadata.filePath}`) - - const transformerTypescript = new TransformerTypescript({ - [metadata.componentName]: kazAst, - }, {}) - - const result = transformerTypescript.transformAndGenerateMappings()[metadata.componentName] - - if (result === undefined) - throw new Error(`No result found for component ${metadata.componentName}`) - - const ast = babelParser.parse(result.content, { - sourceType: 'module', - plugins: ['typescript'], - }) - - const resultWithAst = { - ...result, - ast, - } - - return resultWithAst - }) - -const transformSetStateInExpression = ( - [kazExpression, fixedExpression]: [KazExpression, string], -) => Effect.gen(function* (_) { - const assignments = yield * _( - pipe( - // Find the corresponding Typescript expression range - findCorrespondingTypescriptExpressionRange(kazExpression), - // Find the node paths in the Typescript AST corresponding to the Kaz expression - Effect.flatMap(findCorrespondingTypescriptExpressionNodePaths), - // Find the assigments to a state variable - Effect.flatMap(findAssignmentsToStateVariable), - ), - ) - - let shift = 0 - - for (const assignmentIdentifier of assignments) { - // Find the range of the replacement expression - const [start, end] = yield * _(findReplacementExpressionRange(assignmentIdentifier)) - - const identifier = (() => { - if (assignmentIdentifier.isAssignmentExpression()) - return assignmentIdentifier.get('left') - - if (assignmentIdentifier.isUpdateExpression()) - return assignmentIdentifier.get('argument') - - throw new Error('Expected assignment or update expression') - })() - - if (!identifier.isIdentifier()) - throw new Error('Expected identifier') - - const replacement = `set${upperFirst(identifier.node.name)}((${identifier.node.name}) => { - ${fixedExpression.slice(start + shift, end + shift)}; - return ${identifier.node.name}; - })` - - fixedExpression = fixedExpression.slice(0, start + shift) + replacement + fixedExpression.slice(end + shift) - shift += replacement.length - (end - start) - } - - return [kazExpression, fixedExpression] as const -}) - -const findCorrespondingTypescriptExpressionRange = ( - kazExpression: KazExpression, -) => - Effect.gen(function* (_) { - const { mapping: typescriptMapping } = yield * _(transformToTypescript()) - - const typescriptExpression = typescriptMapping.find(mapping => - mapping.sourceRange[0] === kazExpression.$range[0] && mapping.sourceRange[1] === kazExpression.$range[1]) - - if (typescriptExpression === undefined) - throw new Error('Could not find corresponding Typescript expression') - - return typescriptExpression.generatedRange - }) - -const findCorrespondingTypescriptExpressionNodePaths = ( - searchRange: [number, number], -) => Effect.gen(function* (_) { - const nodePaths: NodePath[] = [] - - yield * _( - pipe( - transformToTypescript(), - Effect.map(({ ast: typescriptAst }) => typescriptAst), - Effect.map(ast => - traverse(ast, { - enter(path) { - const node = path.node - - if (node.start === undefined || node.start === null || node.end === undefined || node.end === null) - return - - if (node.start >= searchRange[0] && node.end <= searchRange[1]) - nodePaths.push(path) - }, - }), - ), - ), - ) - - return nodePaths -}) - -const findStates = (kazAst: KazAst) => - Effect.gen(function* () { - return kazAst.instructions.filter( - (instruction): instruction is StateInstruction => instruction.$type === 'StateInstruction', - ) - }) - -const findAssignmentsToStateVariable = ( - nodePaths: NodePath[], -) => Effect.gen(function* (_) { - const transformService = yield * _(TransformService) - - const metadata = yield * _(transformService.getMetadata()) - - const kazAst = metadata.input[metadata.filePath]?.ast - - if (kazAst === undefined) - throw new Error(`No AST found for file ${metadata.filePath}`) - - const stateInstructions = yield * _(findStates(kazAst)) - - const assignmentIdentifiers = new Set>() - - for (const nodePath of nodePaths) { - const parentNodePath = nodePath.parentPath - - if (parentNodePath === null) - continue - - if (!parentNodePath.isAssignmentExpression() && !parentNodePath.isUpdateExpression()) - continue - - const updatedNodePath = parentNodePath.isAssignmentExpression() - ? parentNodePath.get('left') - : parentNodePath.isUpdateExpression() - ? parentNodePath.get('argument') - : null - - if (updatedNodePath === null) - continue - - if (!updatedNodePath.isIdentifier()) - continue - - if (stateInstructions.some(stateInstruction => stateInstruction.name.$value === updatedNodePath.node.name)) - assignmentIdentifiers.add(parentNodePath) - } - - return assignmentIdentifiers -}) - -const findReplacementExpressionRange = ( - assignmentIdentifier: NodePath, -) => Effect.gen(function* (_) { - const { mapping: typescriptMapping } = yield * _(transformToTypescript()) - - const [assigmentStart, assignmentEnd] = [assignmentIdentifier.node.start, assignmentIdentifier.node.end] - - if (assigmentStart === null || assigmentStart === undefined || assignmentEnd === null || assignmentEnd === undefined) - throw new Error('Unexpected null or undefined value') - - const assignmentLength = assignmentEnd - assigmentStart - - const correspondingMapping = typescriptMapping.find(mapping => - mapping.generatedRange[0] <= assigmentStart && mapping.generatedRange[1] >= assignmentEnd, - ) - - if (correspondingMapping === undefined) - throw new Error('Could not find corresponding mapping') - - const [typescriptStart] = correspondingMapping.generatedRange - - const start = assigmentStart - typescriptStart - const end = start + assignmentLength - - return [start, end] as const -}) diff --git a/packages/transformer-react/src/transformer-react/transform/transform-service/transform-service.ts b/packages/transformer-react/src/transformer-react/transform/transform-service/transform-service.ts index c26f683..1084b8a 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform-service/transform-service.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform-service/transform-service.ts @@ -6,7 +6,6 @@ import { getComponentName } from './functions/get-component-name' import { getImportString } from './functions/get-import-string' import { getMetadata, setMetadata } from './functions/get-set-metadata' import { handle } from './functions/handle' -import { transformExpression } from './functions/transform-expression' export interface TransformService { handle: typeof handle @@ -16,7 +15,6 @@ export interface TransformService { addImport: typeof addImport getImportString: typeof getImportString autoImport: typeof autoImport - transformExpression: typeof transformExpression } // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -32,6 +30,5 @@ export const TransformServiceLive = Layer.succeed( addImport, getImportString, autoImport, - transformExpression, }), ) diff --git a/packages/transformer-react/src/transformer-react/transform/transform.ts b/packages/transformer-react/src/transformer-react/transform/transform.ts index 89b9554..90431c7 100644 --- a/packages/transformer-react/src/transformer-react/transform/transform.ts +++ b/packages/transformer-react/src/transformer-react/transform/transform.ts @@ -1,4 +1,6 @@ +import { replaceKazamMagicStrings } from '@whitebird/kazam-transform-utils' import { Effect, pipe } from 'effect' +import lodash from 'lodash' import * as prettier from 'prettier' import { createServices } from './create-services' @@ -28,6 +30,17 @@ export const transform = ({ input }: Pick const transformed = yield * _( pipe( transformService.handle(file.ast), + Effect.map(transformed => replaceKazamMagicStrings(transformed, { + getComputed(_match, computedName) { + return computedName + }, + getState(_match, stateName) { + return stateName + }, + setState(_match, stateName, setter) { + return `set${lodash.upperFirst(stateName)}((${stateName}) => { ${setter}; return ${stateName} })` + }, + })), Effect.map(transformed => prettier.format(transformed, { parser: 'babel-ts', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74721db..90c01ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,7 +168,7 @@ importers: version: 1.10.1 '@vscode/vsce': specifier: latest - version: 2.21.1 + version: 2.22.0 '@whitebird/kaz-ast': specifier: workspace:* version: link:../../packages/kaz-ast @@ -358,16 +358,37 @@ importers: packages/parser-kaz: dependencies: + '@babel/parser': + specifier: ^7.22.5 + version: 7.23.0 + '@babel/traverse': + specifier: ^7.22.5 + version: 7.23.0 '@whitebird/kaz-ast': specifier: workspace:* version: link:../kaz-ast '@whitebird/kazam-parser-base': specifier: workspace:* version: link:../parser-base + '@whitebird/kazam-transform-utils': + specifier: workspace:* + version: link:../transform-utils + '@whitebird/kazam-transformer-typescript': + specifier: workspace:* + version: link:../transformer-typescript glob: specifier: 10.2.1 version: 10.2.1 + magic-string: + specifier: ^0.30.5 + version: 0.30.5 devDependencies: + '@babel/types': + specifier: ^7.22.5 + version: 7.23.0 + '@types/babel__traverse': + specifier: ^7.20.1 + version: 7.20.2 '@types/node': specifier: ^18.11.9 version: 18.18.0 @@ -411,6 +432,9 @@ importers: playwright: specifier: ^1.35.0 version: 1.38.1 + tmp-promise: + specifier: ^3.0.3 + version: 3.0.3 devDependencies: '@types/dedent': specifier: ^0.7.0 @@ -418,9 +442,9 @@ importers: '@types/node': specifier: ^18.11.9 version: 18.18.0 - '@whitebird/kaz-ast': + '@whitebird/kazam-parser-kaz': specifier: workspace:* - version: link:../kaz-ast + version: link:../parser-kaz '@whitebird/kazam-transformer-base': specifier: workspace:* version: link:../transformer-base @@ -431,6 +455,12 @@ importers: specifier: ^5.0.1 version: 5.0.1 + packages/transform-utils: + devDependencies: + '@types/node': + specifier: ^18.11.9 + version: 18.18.0 + packages/transformer-base: dependencies: '@whitebird/kaz-ast': @@ -467,6 +497,9 @@ importers: '@whitebird/kaz-ast': specifier: workspace:* version: link:../kaz-ast + '@whitebird/kazam-transform-utils': + specifier: workspace:* + version: link:../transform-utils '@whitebird/kazam-transformer-base': specifier: workspace:* version: link:../transformer-base @@ -522,6 +555,9 @@ importers: '@whitebird/kaz-ast': specifier: workspace:* version: link:../kaz-ast + '@whitebird/kazam-transform-utils': + specifier: workspace:* + version: link:../transform-utils '@whitebird/kazam-transformer-base': specifier: workspace:* version: link:../transformer-base @@ -4482,7 +4518,7 @@ packages: express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.1.1 - magic-string: 0.30.3 + magic-string: 0.30.5 rollup: 3.29.3 typescript: 5.2.2 vite: 4.4.9(@types/node@18.18.0) @@ -4520,7 +4556,7 @@ packages: express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.1.1 - magic-string: 0.30.3 + magic-string: 0.30.5 rollup: 3.29.3 typescript: 5.2.2 vite: 4.5.0(@types/node@20.4.7) @@ -4873,7 +4909,7 @@ packages: '@storybook/builder-vite': 7.5.1(typescript@5.2.2)(vite@4.4.9) '@storybook/react': 7.5.1(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) '@vitejs/plugin-react': 3.1.0(vite@4.4.9) - magic-string: 0.30.3 + magic-string: 0.30.5 react: 18.2.0 react-docgen: 6.0.4 react-dom: 18.2.0(react@18.2.0) @@ -5740,7 +5776,7 @@ packages: /@vitest/snapshot@0.34.5: resolution: {integrity: sha512-+ikwSbhu6z2yOdtKmk/aeoDZ9QPm2g/ZO5rXT58RR9Vmu/kB2MamyDSx77dctqdZfP3Diqv4mbc/yw2kPT8rmA==} dependencies: - magic-string: 0.30.3 + magic-string: 0.30.5 pathe: 1.1.1 pretty-format: 29.7.0 dev: true @@ -5840,8 +5876,8 @@ packages: resolution: {integrity: sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==} dev: false - /@vscode/vsce@2.21.1: - resolution: {integrity: sha512-f45/aT+HTubfCU2oC7IaWnH9NjOWp668ML002QiFObFRVUCoLtcwepp9mmql/ArFUy+HCHp54Xrq4koTcOD6TA==} + /@vscode/vsce@2.22.0: + resolution: {integrity: sha512-8df4uJiM3C6GZ2Sx/KilSKVxsetrTBBIUb3c0W4B1EWHcddioVs5mkyDKtMNP0khP/xBILVSzlXxhV+nm2rC9A==} engines: {node: '>= 14'} hasBin: true dependencies: @@ -5895,7 +5931,7 @@ packages: '@vue/reactivity-transform': 3.3.4 '@vue/shared': 3.3.4 estree-walker: 2.0.2 - magic-string: 0.30.3 + magic-string: 0.30.5 postcss: 8.4.30 source-map-js: 1.0.2 dev: true @@ -5914,7 +5950,7 @@ packages: '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 estree-walker: 2.0.2 - magic-string: 0.30.3 + magic-string: 0.30.5 dev: true /@vue/reactivity@3.3.4: @@ -10635,6 +10671,12 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -11744,7 +11786,7 @@ packages: engines: {node: '>= 14'} hasBin: true dependencies: - '@vscode/vsce': 2.21.1 + '@vscode/vsce': 2.22.0 commander: 6.2.1 follow-redirects: 1.15.3 is-ci: 2.0.0 @@ -12913,7 +12955,7 @@ packages: rollup: ^3.0 typescript: ^4.1 || ^5.0 dependencies: - magic-string: 0.30.3 + magic-string: 0.30.5 rollup: 3.29.3 typescript: 5.2.2 optionalDependencies: @@ -13716,6 +13758,12 @@ packages: engines: {node: '>=12'} dev: false + /tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + dependencies: + tmp: 0.2.1 + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -13728,7 +13776,6 @@ packages: engines: {node: '>=8.17.0'} dependencies: rimraf: 3.0.2 - dev: true /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -14106,7 +14153,7 @@ packages: globby: 13.2.2 hookable: 5.5.3 jiti: 1.20.0 - magic-string: 0.30.3 + magic-string: 0.30.5 mkdist: 1.3.0(typescript@5.2.2) mlly: 1.4.2 mri: 1.2.0 From b7400dbc933a62fe84ac5315a7dfe540376703fd Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Fri, 10 Nov 2023 16:55:26 +0100 Subject: [PATCH 06/13] fix(KazAst): some node were not typed in traverse function --- packages/kaz-ast/src/lib/traverse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kaz-ast/src/lib/traverse.ts b/packages/kaz-ast/src/lib/traverse.ts index 54338cf..27be9e6 100644 --- a/packages/kaz-ast/src/lib/traverse.ts +++ b/packages/kaz-ast/src/lib/traverse.ts @@ -3,7 +3,7 @@ import type { z } from 'zod' import type * as schemas from '../types/KazAst' type AllSchemas = typeof schemas[keyof typeof schemas] -type AllInferedSchemas = z.infer }>>> +type AllInferedSchemas = Extract, { $type: string }> type Visitor = { [T in AllInferedSchemas['$type']]?: (node: Extract) => void } & { From 7bfe7bbd08b9b6890cf6735eb4f88643e10eb274 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Fri, 10 Nov 2023 16:56:04 +0100 Subject: [PATCH 07/13] fix(ParserKaz): handle getting expressions for if/elseif/for logical --- packages/parser-kaz/src/transform-ast.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/parser-kaz/src/transform-ast.ts b/packages/parser-kaz/src/transform-ast.ts index 79ea445..a6ad0d8 100644 --- a/packages/parser-kaz/src/transform-ast.ts +++ b/packages/parser-kaz/src/transform-ast.ts @@ -180,6 +180,26 @@ function replacePaths( function getKazExpression( node: Parameters[1]['enter']>>[0], ): { $range: [number, number]; $value: string } | undefined { + // Handle special cases (e.g. `IfLogical`, `ElseIfLogical`, `ForLogical`) + + if (node.$type === 'IfLogical' || node.$type === 'ElseLogical') { + const condition = node.$type === 'IfLogical' + ? node.condition + : 'if' in node + ? node.if.condition + : undefined + + if (condition === undefined) + return + + return condition + } + + if (node.$type === 'ForLogical') + return node.parameters + + // Handle generic cases + const expressionKey = ( Object.keys(node) .find(key => key.toLowerCase().includes('expression')) @@ -197,7 +217,7 @@ function getKazExpression( for (const key in node) { const subNode = node[key as keyof typeof node] - if (typeof subNode === 'object' && subNode !== null && '$type' in subNode) { + if (typeof subNode === 'object' && subNode !== null && !('$type' in subNode)) { const expression = getKazExpression(subNode) if (expression !== undefined) return expression From ad41929527834e3eb6d3a19b13721d3f920122b1 Mon Sep 17 00:00:00 2001 From: Arthur Fontaine <0arthur.fontaine@gmail.com> Date: Fri, 10 Nov 2023 16:56:18 +0100 Subject: [PATCH 08/13] refactor(TransformerVue): transform expressions by replacing magic strings --- packages/transformer-vue/package.json | 2 +- .../src/handlers/computedInstruction.ts | 3 +- packages/transformer-vue/src/handlers/kaz.ts | 2 +- .../src/handlers/lifecycleEventInstruction.ts | 3 +- .../src/handlers/propInstruction.ts | 3 +- .../src/handlers/stateInstruction.ts | 3 +- .../src/handlers/templateExpression.ts | 3 +- .../src/handlers/templateIf.ts | 3 +- .../src/handlers/templateTagAttribute.ts | 3 +- .../src/handlers/templateTagEventAttribute.ts | 3 +- .../src/handlers/watchInstruction.ts | 3 +- .../transformer-vue/src/transformer-vue.ts | 31 ++++++++---- .../src/utils/transform-vue-expression.ts | 47 ------------------- pnpm-lock.yaml | 17 ++++--- 14 files changed, 44 insertions(+), 82 deletions(-) delete mode 100644 packages/transformer-vue/src/utils/transform-vue-expression.ts diff --git a/packages/transformer-vue/package.json b/packages/transformer-vue/package.json index c6bbbf6..2f4417c 100644 --- a/packages/transformer-vue/package.json +++ b/packages/transformer-vue/package.json @@ -24,8 +24,8 @@ "dependencies": { "@types/lodash": "^4.14.191", "@types/prettier": "^2.7.2", - "@typescript-eslint/typescript-estree": "^5.59.9", "@whitebird/kaz-ast": "workspace:*", + "@whitebird/kazam-transform-utils": "workspace:*", "@whitebird/kazam-transformer-base": "workspace:*", "deepmerge": "^4.3.1", "estraverse": "^5.3.0", diff --git a/packages/transformer-vue/src/handlers/computedInstruction.ts b/packages/transformer-vue/src/handlers/computedInstruction.ts index 4af20c1..e4a3197 100644 --- a/packages/transformer-vue/src/handlers/computedInstruction.ts +++ b/packages/transformer-vue/src/handlers/computedInstruction.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleComputedInstruction: IHandler<'computedInstruction'> = (computedInstruction, { addImport }) => { addImport({ @@ -13,5 +12,5 @@ export const handleComputedInstruction: IHandler<'computedInstruction'> = (compu computedInstruction.type === undefined ? '' : `<${computedInstruction.type.$value}>` - }(() => ${transformVueExpression(computedInstruction.computeValue.expression.$value)})` + }(() => ${computedInstruction.computeValue.expression.$value})` } diff --git a/packages/transformer-vue/src/handlers/kaz.ts b/packages/transformer-vue/src/handlers/kaz.ts index e68b552..00c44e4 100644 --- a/packages/transformer-vue/src/handlers/kaz.ts +++ b/packages/transformer-vue/src/handlers/kaz.ts @@ -42,7 +42,7 @@ export const handleKaz: IHandler<'ast'> = (kaz, { addImport, handle }) => { ], ) ) - })(toRefs(props)) + })(props) }, }) ` diff --git a/packages/transformer-vue/src/handlers/lifecycleEventInstruction.ts b/packages/transformer-vue/src/handlers/lifecycleEventInstruction.ts index 2940afc..3ec31e7 100644 --- a/packages/transformer-vue/src/handlers/lifecycleEventInstruction.ts +++ b/packages/transformer-vue/src/handlers/lifecycleEventInstruction.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleLifecycleEventInstruction: IHandler<'lifecycleEventInstruction'> = (lifecycleEvent, { addImport }) => { addImport({ @@ -9,5 +8,5 @@ export const handleLifecycleEventInstruction: IHandler<'lifecycleEventInstructio path: 'vue', }) - return `onMounted(() => {${transformVueExpression(lifecycleEvent.callbackExpression.$value)}})` + return `onMounted(() => {${lifecycleEvent.callbackExpression.$value}})` } diff --git a/packages/transformer-vue/src/handlers/propInstruction.ts b/packages/transformer-vue/src/handlers/propInstruction.ts index f5abb35..7d083be 100644 --- a/packages/transformer-vue/src/handlers/propInstruction.ts +++ b/packages/transformer-vue/src/handlers/propInstruction.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handlePropInstruction: IHandler<'propInstruction'> = (propInstruction, { addImport }) => { addImport({ @@ -12,6 +11,6 @@ export const handlePropInstruction: IHandler<'propInstruction'> = (propInstructi return `${propInstruction.name.$value}: { type: undefined as unknown as PropType<${propInstruction.type?.$value ?? 'any'}>, skipCheck: true, - ${propInstruction.defaultValue !== undefined ? `default: ${transformVueExpression(propInstruction.defaultValue.expression.$value)},` : ''} + ${propInstruction.defaultValue !== undefined ? `default: ${propInstruction.defaultValue.expression.$value},` : ''} }` } diff --git a/packages/transformer-vue/src/handlers/stateInstruction.ts b/packages/transformer-vue/src/handlers/stateInstruction.ts index 51fbe44..2f4d741 100644 --- a/packages/transformer-vue/src/handlers/stateInstruction.ts +++ b/packages/transformer-vue/src/handlers/stateInstruction.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleStateInstruction: IHandler<'stateInstruction'> = (stateInstruction, { addImport }) => { addImport({ @@ -13,5 +12,5 @@ export const handleStateInstruction: IHandler<'stateInstruction'> = (stateInstru stateInstruction.type === undefined ? '' : `<${stateInstruction.type.$value}>` - }(${stateInstruction.defaultValue !== undefined ? transformVueExpression(stateInstruction.defaultValue.expression.$value) : ''})` + }(${stateInstruction.defaultValue !== undefined ? stateInstruction.defaultValue.expression.$value : ''})` } diff --git a/packages/transformer-vue/src/handlers/templateExpression.ts b/packages/transformer-vue/src/handlers/templateExpression.ts index ce2d71e..ffced3f 100644 --- a/packages/transformer-vue/src/handlers/templateExpression.ts +++ b/packages/transformer-vue/src/handlers/templateExpression.ts @@ -1,6 +1,5 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleTemplateExpression: IHandler<'templateExpression'> = (templateExpression) => { - return transformVueExpression(templateExpression.expression.$value) + return templateExpression.expression.$value } diff --git a/packages/transformer-vue/src/handlers/templateIf.ts b/packages/transformer-vue/src/handlers/templateIf.ts index ae95119..5cbe3f2 100644 --- a/packages/transformer-vue/src/handlers/templateIf.ts +++ b/packages/transformer-vue/src/handlers/templateIf.ts @@ -1,9 +1,8 @@ import type { IHandler } from '../transformer-vue' import { mergeTextChildren } from '../utils/merge-text-children' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleTemplateIf: IHandler<'templateIf'> = (templateIf, { handle }) => { - return `${transformVueExpression(templateIf.condition.$value)} + return `${templateIf.condition.$value} ? [${mergeTextChildren(templateIf.children).map(handle).join(',\n')}] : ${templateIf.else !== undefined ? handle(templateIf.else) diff --git a/packages/transformer-vue/src/handlers/templateTagAttribute.ts b/packages/transformer-vue/src/handlers/templateTagAttribute.ts index d5332bd..72f26cb 100644 --- a/packages/transformer-vue/src/handlers/templateTagAttribute.ts +++ b/packages/transformer-vue/src/handlers/templateTagAttribute.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleTemplateTagAttribute: IHandler<'templateTagAttribute'> = (templateTagAttribute) => { let value = '' @@ -9,7 +8,7 @@ export const handleTemplateTagAttribute: IHandler<'templateTagAttribute'> = (tem else if ('value' in templateTagAttribute && typeof templateTagAttribute.value === 'boolean') value = `${templateTagAttribute.value}` else if ('expression' in templateTagAttribute) - value = transformVueExpression(templateTagAttribute.expression.$value) + value = templateTagAttribute.expression.$value return `"${templateTagAttribute.name.$value}": ${value}` } diff --git a/packages/transformer-vue/src/handlers/templateTagEventAttribute.ts b/packages/transformer-vue/src/handlers/templateTagEventAttribute.ts index e9524fd..ceed723 100644 --- a/packages/transformer-vue/src/handlers/templateTagEventAttribute.ts +++ b/packages/transformer-vue/src/handlers/templateTagEventAttribute.ts @@ -1,7 +1,6 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' import { upperFirst } from '../utils/upperFirst' export const handleTemplateTagEventAttribute: IHandler<'templateTagEventAttribute'> = (templateTagEventAttribute) => { - return `on${upperFirst(templateTagEventAttribute.name.$value)}: ${transformVueExpression(templateTagEventAttribute.expression.$value)}` + return `on${upperFirst(templateTagEventAttribute.name.$value)}: ${templateTagEventAttribute.expression.$value}` } diff --git a/packages/transformer-vue/src/handlers/watchInstruction.ts b/packages/transformer-vue/src/handlers/watchInstruction.ts index 6b95692..4f96434 100644 --- a/packages/transformer-vue/src/handlers/watchInstruction.ts +++ b/packages/transformer-vue/src/handlers/watchInstruction.ts @@ -1,5 +1,4 @@ import type { IHandler } from '../transformer-vue' -import { transformVueExpression } from '../utils/transform-vue-expression' export const handleWatchInstruction: IHandler<'watchInstruction'> = (watchInstruction, { addImport }) => { addImport({ @@ -14,7 +13,7 @@ export const handleWatchInstruction: IHandler<'watchInstruction'> = (watchInstru ${watchInstruction.watchedVariables.map(variable => variable.name.$value).join(', ')} ], ([${watchInstruction.watchedVariables.map(variable => variable.name.$value).join(', ')}]) => { - ${transformVueExpression(watchInstruction.callbackExpression.$value)} + ${watchInstruction.callbackExpression.$value} } )` } diff --git a/packages/transformer-vue/src/transformer-vue.ts b/packages/transformer-vue/src/transformer-vue.ts index e79ac2d..05b53d7 100644 --- a/packages/transformer-vue/src/transformer-vue.ts +++ b/packages/transformer-vue/src/transformer-vue.ts @@ -1,6 +1,7 @@ import * as path from 'node:path' import * as schemas from '@whitebird/kaz-ast' +import { replaceKazamMagicStrings } from '@whitebird/kazam-transform-utils' import { TransformerBase } from '@whitebird/kazam-transformer-base' import prettier from 'prettier' import type { z } from 'zod' @@ -72,17 +73,29 @@ export class TransformerVue extends TransformerBase<{ const result = this.handle(component.ast, { name: componentName }) + const unformattedResult = ` + ${importsToString( + mergeImports(this.imports[componentName] ?? []), + component.sourceAbsoluteFilePath, + component.getTransformedOutputFilePath(`${componentName}.vue`), + )} + + ${result} + ` + this.generatedComponents[componentName] = `