diff --git a/package.json b/package.json index 1656de49..9a541c34 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,9 @@ "@types/qunit": "^2.19.9", "autoprefixer": "^10.4.16", "backburner.js": "^2.8.0", + "express": "^4.18.2", + "glint-environment-gxt": "file:./glint-environment-gxt", + "happy-dom": "^13.0.6", "nyc": "^15.1.0", "postcss": "^8.4.33", "prettier": "^3.1.1", @@ -93,16 +96,17 @@ "vite-plugin-circular-dependency": "^0.2.1", "vite-plugin-dts": "^3.7.0", "vitest": "^1.1.1", - "zx": "^7.2.3", - "express": "^4.18.2", - "happy-dom": "^13.0.6", - "glint-environment-gxt": "file:./glint-environment-gxt" + "zx": "^7.2.3" }, "dependencies": { "@babel/core": "^7.23.6", - "decorator-transforms": "1.1.0", "@babel/preset-typescript": "^7.23.3", "@glimmer/syntax": "^0.87.1", - "content-tag": "^1.2.2" + "@prettier/sync": "^0.5.1", + "code-red": "^1.0.4", + "content-tag": "^1.2.2", + "decorator-transforms": "1.1.0", + "magic-string": "^0.30.7", + "svelte": "^4.2.10" } } diff --git a/plugins/__snapshots__/svlelte-compiler.test.ts.snap b/plugins/__snapshots__/svlelte-compiler.test.ts.snap new file mode 100644 index 00000000..8a66c50c --- /dev/null +++ b/plugins/__snapshots__/svlelte-compiler.test.ts.snap @@ -0,0 +1,390 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler > compile sample case #1 1`] = ` +"import { + $_fin, + $_tag, + $_if, + $_each, + $_eachSync, + $_slot, + $_edp, + $_args, + $_text, + $_c, + $_dc, + $SLOTS_SYMBOL, + $PROPS_SYMBOL, + $_GET_SLOTS, + $_GET_ARGS, + $_GET_FW, + $_componentHelper, + $_modifierHelper, + $_helperHelper, + $template, + $_hasBlockParams, + $_hasBlock, + $nodes, + $args, + $_maybeHelper, + $_maybeModifier, + $_inElement, + $_ucw, + $__if, + $__eq, + $__debugger, + $__log, + $__array, + $__hash, + $__fn, +} from "@lifeart/gxt"; + +export default function unknown(args) { + const $fw = $_GET_FW(this, arguments); + const $slots = $_GET_SLOTS(this, arguments); + const $ctx = {}; + const roots = [ + "\\n", + $_tag( + "div", + [ + [], + [ + ["id", "12"], + ["name", () => 3 + 2], + ["label", () => ["1", 2].join("")], + ], + [], + ], + [], + this, + ), + "\\n", + $_tag("button", [[], [["disabled", () => !clickable]], []], ["..."], this), + "\\n", + $_tag("button", [[], [["disabled", true]], []], ["can't touch this"], this), + "\\n", + $_tag("input", [[], [["type", "checkbox"]], []], [], this), + "\\n", + $_tag( + "input", + [ + [], + [ + ["required", () => false], + ["placeholder", "This input field is not required"], + ], + [], + ], + [], + this, + ), + "\\n", + $_tag( + "div", + [[], [["title", () => null]], []], + ["This div has no title attribute"], + this, + ), + "\\n", + $_tag( + "button", + [[], [["disabled", () => number !== 42]], []], + ["..."], + this, + ), + "\\n", + $_tag("button", [[], [["disabled", () => disabled]], []], ["..."], this), + "\\n", + $_c( + Widget, + { + foo: () => bar, + answer: () => 42, + text: "hello", + [$SLOTS_SYMBOL]: { + default: () => [], + }, + }, + this, + ), + "\\n", + $_tag( + "p", + [[], [], []], + [() => a, " + ", () => b, " = ", () => a + b, "."], + this, + ), + "\\n", + $_tag( + "div", + [[], [], []], + [() => (/^[A-Za-z ]+$/.test(value) ? x : y)], + this, + ), + "\\n", + $_if( + () => expression, + () => ["..."], + () => ["..."], + ), + "\\n", + $_if( + () => answer === 42, + () => [$_tag("p", [[], [], []], ["what was the question?"], this)], + null, + ), + "\\n-----\\n", + $_if( + () => porridge.temperature > 100, + () => [$_tag("p", [[], [], []], ["too hot!"], this)], + () => [ + $_if( + () => 80 > porridge.temperature, + () => [$_tag("p", [[], [], []], ["too cold!"], this)], + () => [$_tag("p", [[], [], []], ["just right!"], this)], + ), + ], + ), + "\\n---\\n", + $_each( + () => expression, + (name, $index, $key = "@identity") => ["..."], + this, + ), + "\\n\\n", + $_tag( + "ul", + [[], [], []], + [ + "\\n\\t", + $_each( + () => items, + (item, $index, $key = "@identity") => [ + $_tag( + "li", + [[], [], []], + [() => item.name, " x ", () => item.qty], + this, + ), + ], + this, + ), + "\\n", + ], + this, + ), + "\\n\\n\\n", + $_each( + () => items, + (item, i, $key = "@identity") => [ + $_tag( + "li", + [[], [], []], + [() => i + 1, ": ", () => item.name, " x ", () => item.qty], + this, + ), + ], + this, + ), + "\\n\\n", + $_each( + () => items, + (item, $index, $key = (item) => item.id) => [ + $_tag( + "li", + [[], [], []], + [() => item.name, " x ", () => item.qty], + this, + ), + ], + this, + ), + "\\n\\n", + $_each( + () => items, + (item, i, $key = (item) => item.id) => [ + $_tag( + "li", + [[], [], []], + [() => i + 1, ": ", () => item.name, " x ", () => item.qty], + this, + ), + ], + this, + ), + "\\n\\n", + $_c( + MyComponent, + { + ...rest, + [$SLOTS_SYMBOL]: { + default: () => [], + }, + }, + this, + ), + "\\n\\n", + $_tag( + "button", + [[], [], [["click", () => handleClick()]]], + ["\\n\\tcount: ", () => count, "\\n"], + this, + ), + "\\n\\n", + $_tag("div", [[["", "name"]], [], []], [], this), + "\\n", + $_tag( + "div", + [[["", () => (isActive ? "active" : "")]], [], []], + ["..."], + this, + ), + "\\n", + $_tag( + "div", + [[["", () => (isActive ? "active" : "")]], [], []], + ["..."], + this, + ), + "\\n", + $_tag( + "div", + [ + [ + ["", () => (active ? "active" : "")], + ["", () => (!active ? "inactive" : "")], + ["", () => (isAdmin ? "isAdmin" : "")], + ], + [], + [], + ], + ["..."], + this, + ), + "\\n", + $_tag("div", [[], [["color", () => myColor]], []], ["..."], this), + "\\n\\n", + $_tag( + "a", + [[], [["href", () => ["page/", p].join("")]], []], + ["page ", () => p], + this, + ), + "\\n\\n", + $_slot("item", () => [], $slots, this), + "\\n\\n", + $_c( + FancyList, + { + [$SLOTS_SYMBOL]: { + footer: () => ["Copyright (c) 2019 Svelte Industries"], + default: () => ["\\n ", "\\n"], + }, + }, + this, + ), + ]; + return $_fin(roots, this); +} +" +`; + +exports[`compiler > compile sample case #2 1`] = ` +"import { + $_fin, + $_tag, + $_if, + $_each, + $_eachSync, + $_slot, + $_edp, + $_args, + $_text, + $_c, + $_dc, + $SLOTS_SYMBOL, + $PROPS_SYMBOL, + $_GET_SLOTS, + $_GET_ARGS, + $_GET_FW, + $_componentHelper, + $_modifierHelper, + $_helperHelper, + $template, + $_hasBlockParams, + $_hasBlock, + $nodes, + $args, + $_maybeHelper, + $_maybeModifier, + $_inElement, + $_ucw, + $__if, + $__eq, + $__debugger, + $__log, + $__array, + $__hash, + $__fn, +} from "@lifeart/gxt"; + +export default function unknown(args) { + const $fw = $_GET_FW(this, arguments); + const $slots = $_GET_SLOTS(this, arguments); + const $ctx = {}; + const name = "Hello World"; + const roots = ["\\n\\n", $_tag("div", [[], [], []], [() => name], this)]; + return $_fin(roots, this); +} +" +`; + +exports[`compiler > compile sample case #3 1`] = ` +"import { + $_fin, + $_tag, + $_if, + $_each, + $_eachSync, + $_slot, + $_edp, + $_args, + $_text, + $_c, + $_dc, + $SLOTS_SYMBOL, + $PROPS_SYMBOL, + $_GET_SLOTS, + $_GET_ARGS, + $_GET_FW, + $_componentHelper, + $_modifierHelper, + $_helperHelper, + $template, + $_hasBlockParams, + $_hasBlock, + $nodes, + $args, + $_maybeHelper, + $_maybeModifier, + $_inElement, + $_ucw, + $__if, + $__eq, + $__debugger, + $__log, + $__array, + $__hash, + $__fn, +} from "@lifeart/gxt"; + +export default function unknown(args) { + const $fw = $_GET_FW(this, arguments); + const $slots = $_GET_SLOTS(this, arguments); + const $ctx = {}; + const roots = ["\\n", $_tag("input", [[], [], []], [], this)]; + return $_fin(roots, this); +} +" +`; diff --git a/plugins/bebel-svelte.ts b/plugins/bebel-svelte.ts new file mode 100644 index 00000000..075fa9a7 --- /dev/null +++ b/plugins/bebel-svelte.ts @@ -0,0 +1,123 @@ +import type Babel from '@babel/core'; + +export default function babelSvelteTransform(babel: typeof Babel) { + /* + transforming + export let foo = bar; + + to Object.defineProperty(ctx, 'foo', { + get() { + return args['foo] ?? bar; + } + }); + + and all it's usages from + foo.bar + to + ctx['foo'].bar + +*/ + + const { types: t } = babel; + type State = { + exportedIdentifiers: Map; + }; + + const contextName = '$ctx'; + + return { + name: 'svelte-export-transform', + visitor: { + Program: { + enter(_: any, state: State) { + state.exportedIdentifiers = new Map(); + }, + exit(path: any, state: State) { + path.traverse({ + ReferencedIdentifier(path: any) { + const identifierName = path.node.name; + if (state.exportedIdentifiers.has(identifierName)) { + // We're referencing an exported identifier; replace it. + const ctxIdentifier = t.memberExpression( + t.identifier(contextName), + t.stringLiteral(identifierName), + true, + ); + if ( + path.parentPath.isOptionalMemberExpression({ + object: path.node, + }) || + path.parentPath.isMemberExpression({ object: path.node }) + ) { + // The identifier is the object of a member expression. + // Replace the identifier, but keep the rest of the expression as is. + path.replaceWith(ctxIdentifier); + } else if ( + path.findParent((p: any) => p.isOptionalMemberExpression()) + ) { + // Inside an optional chain, but not the object itself. + // This case might be less common based on the current requirements. + // Adjust or extend as necessary for your specific use cases. + } else { + // Not part of a member expression; replace directly. + path.replaceWith(ctxIdentifier); + } + } + }, + }); + }, + }, + ExportNamedDeclaration(path: any, state: State) { + if ( + path.node.declaration && + path.node.declaration.type === 'VariableDeclaration' + ) { + const declarations = path.node.declaration.declarations; + declarations.forEach((declaration: any) => { + // TODO: support it without init + if (declaration.id && declaration.init) { + const varName = declaration.id.name; + // Mark this variable name as exported. + state.exportedIdentifiers.set(varName, true); + + // Replace the export statement with the appropriate Object.defineProperty call. + const definePropertyCall = t.callExpression( + t.memberExpression( + t.identifier('Object'), + t.identifier('defineProperty'), + ), + [ + t.identifier(contextName), + t.stringLiteral(varName), + t.objectExpression([ + t.objectProperty( + t.identifier('get'), + t.functionExpression( + null, + [], + t.blockStatement([ + t.returnStatement( + t.logicalExpression( + '??', + t.memberExpression( + t.identifier('args'), + t.stringLiteral(varName), + true, + ), + declaration.init, + ), + ), + ]), + ), + ), + ]), + ], + ); + path.replaceWith(definePropertyCall); + } + }); + } + }, + }, + }; +} diff --git a/plugins/compiler.ts b/plugins/compiler.ts index e41a7eee..a905b667 100644 --- a/plugins/compiler.ts +++ b/plugins/compiler.ts @@ -4,7 +4,7 @@ import { transform } from './test'; import { MAIN_IMPORT } from './symbols'; import { type Flags, defaultFlags } from './flags.ts'; import { HMR, fixExportsForHMR, shouldHotReloadFile } from './hmr.ts'; - +import { compile as compileSvelte } from './svelte-compiler.js'; export { stripGXTDebug } from './babel.ts'; const p = new Preprocessor(); @@ -26,6 +26,7 @@ const extensionsToResolve = [ ]; const templateFileRegex = /\.(gts|gjs)$/; +const svelteTemplateFileRegex = /\.(svelte)$/; const scriptFileRegex = /\.(ts|js)$/; type Options = { authorMode?: boolean; @@ -59,7 +60,10 @@ export function compiler(mode: string, options: Options = {}): Plugin { }; }, transform(code: string, file: string) { - if (templateFileRegex.test(file)) { + if (svelteTemplateFileRegex.test(file)) { + const result = compileSvelte(code, file); + return result; + } else if (templateFileRegex.test(file)) { const intermediate = fixContentTagOutput(p.process(code, file)); if (mode === 'development') { diff --git a/plugins/svelte-compiler.ts b/plugins/svelte-compiler.ts new file mode 100644 index 00000000..72375a45 --- /dev/null +++ b/plugins/svelte-compiler.ts @@ -0,0 +1,401 @@ +// @ts-check + +import * as parser from 'svelte/compiler'; +import { print } from 'code-red'; +import { SYMBOLS, MAIN_IMPORT } from './symbols.js'; +import MagicString from 'magic-string'; +import plugin from './bebel-svelte'; +import { transformSync } from '@babel/core'; +import synchronizedPrettier from '@prettier/sync'; + +import type { + Attribute, + BaseDirective, + BaseExpressionDirective, + BaseNode, + Element, + MustacheTag, + SpreadAttribute, + Text, +} from 'svelte/types/compiler/interfaces'; + +const importsPrefix = ` + import { ${Object.values(SYMBOLS).join(',')} } from "${MAIN_IMPORT}"; +`; +let magic = new MagicString(''); + +function applyBabelTransform(code: string) { + let babelResult = null; + try { + babelResult = transformSync(code, { + plugins: [plugin], + filename: 'item.ts', + presets: [ + [ + '@babel/preset-typescript', + { allExtensions: true, onlyRemoveTypeImports: true }, + ], + ], + }); + } catch (e) { + // loc: Position { line: 3, column: 731, index: 739 } + // let's extract error code based on loc + // @ts-expect-error loc may not exist + const loc = e.loc; + const lines = code.split('\n'); + const line = lines[loc.line - 1]; + const start = loc.column - 30; + const end = loc.column + 20; + // we need to wrap error with << and >> to make it visible + const errorPrefix = line.substring(start, loc.column); + const errorSuffix = line.substring(loc.column + 2, end); + console.log( + // @ts-expect-error + e.reasonCode, + errorPrefix + + '>>' + + line.substring(loc.column + 1, loc.column + 2) + + '<<' + + errorSuffix, + ); + } + + const txt = babelResult?.code ?? ''; + + return txt; +} + +export function compile(code = '', fileName = 'unknown') { + const componentName = fileName.split('.svelte').pop(); + magic = new MagicString(code, { + filename: fileName, + }); + // code-red + const result = parser.compile(code, { + dev: true, + preserveWhitespace: false, + preserveComments: false, + enableSourcemap: false, + generate: 'dom', + varsReport: 'strict', + immutable: false, + hydratable: false, + legacy: false, + css: 'external', + }); + + // console.log('ast', JSON.stringify(result.ast, null, 2)); + + const script = result.ast.instance + ? print(result.ast.instance.content).code + : ''; + + const template = result.ast.html.children + ? result.ast.html.children.map(transform).join(',') + : ''; + + const imports = script.split('\n').filter((el) => el.includes('import ')); + const content = script.split('\n').filter((el) => !el.includes('import ')); + + magic.prepend(` + ${importsPrefix} + ${imports.join('\n')} + `); + + let map = magic.generateMap({ + source: fileName, + file: fileName + '.map', + includeContent: true, + }); + + let codeResult = ` + ${importsPrefix} + ${imports.join('\n')} + export default function ${componentName}(args) { + const $fw = ${SYMBOLS.$_GET_FW}(this, arguments); + const $slots = ${SYMBOLS.$_GET_SLOTS}(this, arguments); + const $ctx = {}; + ${applyBabelTransform(` + ${content.join('\n')} + const roots = [${template}]; + `)} + return ${SYMBOLS.FINALIZE_COMPONENT}(roots, this); + }`; + const prettyCode = synchronizedPrettier.format(codeResult, { + parser: 'babel', + }); + return { code: prettyCode, map }; +} + +const p = (expression: any) => print(expression).code; + +const getContext = () => 'this'; + +const t = { + isElement: (element: BaseNode): element is Element => + element.type === 'Element', + isText: (element: BaseNode): element is Text => element.type === 'Text', + isAttributeShorthand: (element: BaseNode) => + element.type === 'AttributeShorthand', + isInlineComponent: (element: BaseNode): element is Element => + element.type === 'InlineComponent', + isIfBlock: (element: BaseNode): element is BaseExpressionDirective => + element.type === 'IfBlock', + isElseBlock: (element: BaseNode): element is BaseExpressionDirective => + element.type === 'ElseBlock', + isEachBlock: (element: BaseNode): element is BaseExpressionDirective => + element.type === 'EachBlock', + isEventHandler: (element: BaseNode): element is BaseDirective => + element.type === 'EventHandler', + isStyleDirective: (element: BaseNode): element is BaseDirective => + element.type === 'isStyleDirective', + isClass: (element: BaseNode): element is BaseDirective => + element.type === 'Class', + isAttribute: (element: BaseNode): element is Attribute => + element.type === 'Attribute', + isMustacheTag: (element: BaseNode): element is MustacheTag => + element.type === 'MustacheTag', + isSpread: (element: BaseNode): element is SpreadAttribute => + element.type === 'Spread', + isSlot: (element: BaseNode): element is Element => element.type === 'Slot', +}; + +function transformElement(element: Element): string { + const modifiers = element.attributes.filter( + (el) => t.isEventHandler(el) || t.isStyleDirective(el), + ); + const properties = element.attributes.filter( + (el) => t.isClass(el) || (t.isAttribute(el) && el.name === 'class'), + ); + // TODO: deal with spread ($fw) + const spread = element.attributes.filter((el) => t.isSpread(el)); + const attributes = element.attributes.filter( + (el) => + !modifiers.includes(el) && + !properties.includes(el) && + !spread.includes(el), + ); + const compiledProperties = `[${properties + .map((el) => { + if (t.isClass(el)) { + return `['',()=>${p(el.expression)} ? ${escapeText(el.name)} : '']`; + } + if (Array.isArray(el.value)) { + if (el.value.length !== 1) { + throw new Error('Unknown attribute type'); + } + if (t.isMustacheTag(el.value[0])) { + return `['',()=>${transform(el.value[0])}]`; + } + return `['',${transform(el.value[0])}]`; + } else { + throw new Error('Unknown attribute type'); + } + }) + .join(',')}]`; + const compiledModifiers = `[${modifiers + .map((el) => { + if (t.isStyleDirective(el)) { + return `[2,${escapeText(el.name)},()=>${p(el.value[0].expression)}]`; + } + const value = p(el.expression); + if (el.expression.type === 'Identifier') { + return `[${escapeText(el.name)},()=>${value}()]`; + } + return `[${escapeText(el.name)},${value}]`; + }) + .join(',')}]`; + return `${SYMBOLS.TAG}(${escapeText( + element.name, + )}, [${compiledProperties}, [${attributes.map( + transformAttribute, + )}], ${compiledModifiers}], [${ + element.children + ?.map((node) => { + if (t.isMustacheTag(node)) { + return `()=>${transform(node)}`; + } else { + return transform(node); + } + }) + .join(',') ?? '' + }],${getContext()})`; +} +function transformArgument( + attribute: BaseDirective | Attribute | SpreadAttribute, +): string { + if (!Array.isArray(attribute.value)) { + if (t.isSpread(attribute)) { + return `...${p(attribute.expression)}`; + } + return `${escapeText(attribute.name)}:${transform(attribute.value)}`; + } + if (attribute.value.length === 1) { + const node = attribute.value[0]; + if (t.isMustacheTag(node)) { + return `${escapeText(attribute.name)}:()=>${transform(node)}`; + } else { + return `${escapeText(attribute.name)}:${transform(node)}`; + } + } else { + return `${escapeText(attribute.name)}:()=>[${attribute.value.map((el) => { + return transform(el); + })}].join('')`; + } +} +function transformAttribute( + attribute: BaseDirective | Attribute | SpreadAttribute, +): string { + if (t.isSpread(attribute)) { + throw new Error('Spread attributes are not supported'); + } + if (!Array.isArray(attribute.value)) { + return `[${escapeText(attribute.name)},${transform(attribute.value)}]`; + } + if (attribute.value.length === 1) { + const node = attribute.value[0]; + if (t.isMustacheTag(node)) { + return `[${escapeText(attribute.name)},()=>${transform(node)}]`; + } else { + return `[${escapeText(attribute.name)},${transform(node)}]`; + } + } else { + return `[${escapeText(attribute.name)},()=>[${attribute.value.map((el) => { + return transform(el); + })}].join('')]`; + } +} + +function transformMustacheTag(node: MustacheTag): string { + return p(node.expression); +} + +function escapeText(text: string): string { + return JSON.stringify(text); +} +function hasAttribute(element: Element, attrName: string) { + return element.attributes.find( + (el) => t.isAttribute(el) && el.name === attrName, + ); +} + +function transformInlineComponent(node: Element): string { + const argsArray = node.attributes.map((attr) => { + const result = transformArgument(attr); + magic.update(attr.start, attr.end, result); + return result; + }); + const slotNodes = + node.children?.filter( + (el) => t.isElement(el) && hasAttribute(el, 'slot'), + ) ?? []; + + const slots = slotNodes.map((el) => { + const slotName = + el.attributes.find( + (attr: BaseDirective | Attribute | SpreadAttribute) => + attr.name === 'slot', + ).value[0].data ?? 'default'; + return `${slotName}:()=>[${el.children?.map(transform).join(',') ?? ''}]`; + }); + + const children = node.children?.filter((el) => !slotNodes.includes(el)); + + if (children) { + slots.push(`default:()=>[${children.map(transform).join(',')}]`); + } + if (slots.length) { + argsArray.push(`[${SYMBOLS.$SLOTS_SYMBOL}]:{${slots.join(',')}}`); + } + return `${SYMBOLS.COMPONENT}(${node.name},{${argsArray.join( + ',', + )}},${getContext()})`; +} +function transformAttributeShorthand(node: any) { + return `()=>${p(node.expression)}`; +} + +function transformIfBlock(node: BaseExpressionDirective): string { + return `${SYMBOLS.IF}(()=>${p(node.expression)},()=>[${ + node.children?.map(transform).join(',') ?? '' + }], ${node.else ? `${transform(node.else)}` : null})`; +} +function transformElseBlock(node: BaseExpressionDirective): string { + return `()=>[${node.children?.map(transform).join(',') ?? ''}]`; +} +function transformSlot(node: Element): string { + // slot definition + const slotName = + node.attributes.find((attr) => attr.name === 'name')?.value?.[0]?.data ?? + 'default'; + // todo - add params support + const paramNames: string[] = []; + return `${SYMBOLS.SLOT}(${escapeText(slotName)},() => [${paramNames.join( + ',', + )}], $slots, ${getContext()})`; +} + +function transformEachBlock(node: BaseExpressionDirective): string { + const item = p(node.context); + const index = node.index ? node.index : '$index'; + const key = node.key + ? `$key = (${item})=>${p(node.key)}` + : `$key = '@identity'`; + return `${SYMBOLS.EACH}(()=>${p( + node.expression, + )},(${item},${index},${key})=>[${ + node.children?.map(transform).join(',') ?? '' + }],${getContext()})`; +} + +function transform( + node: BaseNode | boolean | string | number | null | undefined, +) { + if (node === null) { + return 'null'; + } else if (typeof node !== 'object') { + return node; + } else if (t.isElement(node)) { + const result = transformElement(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isAttribute(node)) { + const result = transformAttribute(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isText(node)) { + const result = escapeText(node.data); + magic.update(node.start, node.end, result); + return result; + } else if (t.isMustacheTag(node)) { + const result = transformMustacheTag(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isAttributeShorthand(node)) { + const result = transformAttributeShorthand(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isInlineComponent(node)) { + const result = transformInlineComponent(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isIfBlock(node)) { + const result = transformIfBlock(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isElseBlock(node)) { + const result = transformElseBlock(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isEachBlock(node)) { + const result = transformEachBlock(node); + magic.update(node.start, node.end, result); + return result; + } else if (t.isSlot(node)) { + const result = transformSlot(node); + magic.update(node.start, node.end, result); + return result; + } + + throw new Error(`Unknown node type: ${node.type}`); +} diff --git a/plugins/svlelte-compiler.test.ts b/plugins/svlelte-compiler.test.ts new file mode 100644 index 00000000..14fe154c --- /dev/null +++ b/plugins/svlelte-compiler.test.ts @@ -0,0 +1,94 @@ +import { expect, test, describe } from 'vitest'; +import { compile } from './svelte-compiler'; + +let sample1 = ` +
+ + + + +
This div has no title attribute
+ + + +

{a} + {b} = {a + b}.

+
{(/^[A-Za-z ]+$/).test(value) ? x : y}
+{#if expression}...{:else}...{/if} +{#if answer === 42} +

what was the question?

+{/if} +----- +{#if porridge.temperature > 100} +

too hot!

+{:else if 80 > porridge.temperature} +

too cold!

+{:else} +

just right!

+{/if} +--- +{#each expression as name}...{/each} + + + + +{#each items as item, i} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} + +{#each items as item (item.id)} +
  • {item.name} x {item.qty}
  • +{/each} + +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} + + + + + +
    +
    ...
    +
    ...
    +
    ...
    +
    ...
    + +page {p} + + + + +

    Copyright (c) 2019 Svelte Industries

    +
    +`; + +let sample2 = ` + +
    {name}
    `; + +let sample3 = ` + +`; + +describe('compiler', () => { + test('compile sample case #1', () => { + const result = compile(sample1); + expect(result.code).toMatchSnapshot(); + }); + test('compile sample case #2', () => { + const result = compile(sample2); + expect(result.code).toMatchSnapshot(); + }); + test('compile sample case #3', () => { + const result = compile(sample3); + expect(result.code).toMatchSnapshot(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 374d14f9..794787e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,24 @@ dependencies: '@glimmer/syntax': specifier: ^0.87.1 version: 0.87.1 + '@prettier/sync': + specifier: ^0.5.1 + version: 0.5.1(prettier@3.1.1) + code-red: + specifier: ^1.0.4 + version: 1.0.4 content-tag: specifier: ^1.2.2 version: 1.2.2 decorator-transforms: specifier: 1.1.0 version: 1.1.0(@babel/core@7.23.6) + magic-string: + specifier: ^0.30.7 + version: 0.30.7 + svelte: + specifier: ^4.2.10 + version: 4.2.10 devDependencies: '@glint/core': @@ -2090,6 +2102,15 @@ packages: playwright: 1.40.1 dev: true + /@prettier/sync@0.5.1(prettier@3.1.1): + resolution: {integrity: sha512-tpF+A1e4ynO2U4fTH21Sjgm9EYENmqg4zmJCMLrmLVfzIzuDc1cKGXyxrxbFgcH8qQRfowyDCZFAUukwhiZlsw==} + peerDependencies: + prettier: '*' + dependencies: + make-synchronized: 0.2.8 + prettier: 3.1.1 + dev: false + /@rollup/pluginutils@5.1.0: resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} @@ -2447,7 +2468,6 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true /@types/fs-extra@11.0.4: resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -2545,7 +2565,7 @@ packages: /@vitest/snapshot@1.1.1: resolution: {integrity: sha512-WnMHjv4VdHLbFGgCdVVvyRkRPnOKN75JJg+LLTdr6ah7YnL75W+7CTIMdzPEPzaDxA8r5yvSVlc1d8lH3yE28w==} dependencies: - magic-string: 0.30.5 + magic-string: 0.30.7 pathe: 1.1.1 pretty-format: 29.7.0 dev: true @@ -2641,7 +2661,6 @@ packages: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -2837,6 +2856,12 @@ packages: sprintf-js: 1.0.3 dev: true + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: false + /array-binsearch@1.0.1: resolution: {integrity: sha512-KZw1m6nCIGsjuUHnY2e1mOZPxH7widuwutZChvgoXwe8+ZCKM7GiIBtgBMNiUKBycPoh6tLOnJBQApjm3wMelw==} dev: true @@ -2942,6 +2967,12 @@ packages: engines: {node: '>= 0.4'} dev: true + /axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + dependencies: + dequal: 2.0.3 + dev: false + /babel-import-util@0.2.0: resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} engines: {node: '>= 12.*'} @@ -3529,6 +3560,16 @@ packages: engines: {node: '>=0.10.0'} dev: true + /code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.5 + acorn: 8.11.2 + estree-walker: 3.0.3 + periscopic: 3.1.0 + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -3675,6 +3716,14 @@ packages: which: 2.0.2 dev: true + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: false + /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: true @@ -3826,6 +3875,11 @@ packages: engines: {node: '>= 0.8'} dev: true + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: false + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -4215,6 +4269,12 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: false + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5180,6 +5240,12 @@ packages: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true + /is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + dependencies: + '@types/estree': 1.0.5 + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -5556,6 +5622,10 @@ packages: pkg-types: 1.0.3 dev: true + /locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + dev: false + /locate-path@2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} @@ -5690,12 +5760,11 @@ packages: sourcemap-codec: 1.4.8 dev: true - /magic-string@0.30.5: - resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + /magic-string@0.30.7: + resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} @@ -5715,6 +5784,10 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /make-synchronized@0.2.8: + resolution: {integrity: sha512-jtXnKYCxjmGaXiZhXbDbGPbh4YyTvIIbOgcQjtAboc4RSm9k3nyhTFvFQB0cfs7QFKuZXKe2D2RvOkv1c+vpxg==} + dev: false + /map-stream@0.1.0: resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} dev: true @@ -5737,6 +5810,10 @@ packages: minimatch: 3.1.2 dev: true + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -6299,6 +6376,14 @@ packages: through: 2.3.8 dev: true + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.5 + estree-walker: 3.0.3 + is-reference: 3.0.2 + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -6451,7 +6536,6 @@ packages: resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} engines: {node: '>=14'} hasBin: true - dev: true /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} @@ -6996,7 +7080,6 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -7265,6 +7348,26 @@ packages: engines: {node: '>= 0.4'} dev: true + /svelte@4.2.10: + resolution: {integrity: sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA==} + engines: {node: '>=16'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + '@types/estree': 1.0.5 + acorn: 8.11.2 + aria-query: 5.3.0 + axobject-query: 4.0.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.2 + locate-character: 3.0.0 + magic-string: 0.30.7 + periscopic: 3.1.0 + dev: false + /symbol-observable@1.2.0: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} @@ -7912,7 +8015,7 @@ packages: execa: 8.0.1 happy-dom: 13.0.6 local-pkg: 0.5.0 - magic-string: 0.30.5 + magic-string: 0.30.7 pathe: 1.1.1 picocolors: 1.0.0 std-env: 3.7.0 diff --git a/src/components/World.svelte b/src/components/World.svelte new file mode 100644 index 00000000..0a8363c8 --- /dev/null +++ b/src/components/World.svelte @@ -0,0 +1,20 @@ + + + + diff --git a/src/components/pages/PageOne.gts b/src/components/pages/PageOne.gts index ea20beb6..e330aa4c 100644 --- a/src/components/pages/PageOne.gts +++ b/src/components/pages/PageOne.gts @@ -1,11 +1,13 @@ import { Component } from '@lifeart/gxt'; import { Smile } from './page-one/Smile'; import { Table } from './page-one/Table.gts'; +import HelloWorld from './../World.svelte'; export class PageOne extends Component {