From 4b71f45928ae7f6bc4f4e321ab948ed393cc0538 Mon Sep 17 00:00:00 2001 From: Dylan Piercey Date: Thu, 17 Dec 2020 08:46:52 -0700 Subject: [PATCH] feat(translator-fluurt): extract some translate logic to analyze stage (#63) --- package-lock.json | 27 ++- package.json | 2 +- packages/translator/src/analyze/index.ts | 24 +++ .../src/analyze/nested-attribute-tags.ts | 88 ++++++++ .../translator/src/analyze/tag-name-type.ts | 193 +++++++++++++++++ packages/translator/src/core/translate-tag.ts | 3 +- packages/translator/src/index.ts | 4 +- packages/translator/src/tag/attribute-tag.ts | 199 ++++++------------ packages/translator/src/tag/custom-tag.ts | 3 +- packages/translator/src/tag/index.ts | 11 +- .../translator/src/tag/native-tag/html.ts | 13 +- .../translator/src/util/analyze-tag-name.ts | 175 --------------- .../translator/src/util/attrs-to-object.ts | 46 +--- .../snapshots/html-compiled-expected.js | 4 +- 14 files changed, 416 insertions(+), 376 deletions(-) create mode 100644 packages/translator/src/analyze/index.ts create mode 100644 packages/translator/src/analyze/nested-attribute-tags.ts create mode 100644 packages/translator/src/analyze/tag-name-type.ts delete mode 100644 packages/translator/src/util/analyze-tag-name.ts diff --git a/package-lock.json b/package-lock.json index 5908dd02d..c344b5f01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2980,9 +2980,9 @@ } }, "@marko/compiler": { - "version": "5.0.0-next.69", - "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.0.0-next.69.tgz", - "integrity": "sha512-vhAtqWHpz3GDgxP4ug3tIhbpGri17mbxOL/0v+FFFL5P4KR1AT/h3EbVsgLKILEmDmXA2V3q1kxRsQ5UrgPdKg==", + "version": "5.0.0-next.70", + "resolved": "https://registry.npmjs.org/@marko/compiler/-/compiler-5.0.0-next.70.tgz", + "integrity": "sha512-ATeT/fWT+QBArIaQrE+4uVxi7JoYMvhuFmdMODeLxlp3ABXoYzj8wCNA6DGhoJynmAgTWohUR/T59l1sklCTRw==", "dev": true, "requires": { "@babel/code-frame": "^7.5.5", @@ -2995,7 +2995,7 @@ "@babel/types": "^7.7.4", "@marko/babel-types": "^5.0.0-next.67", "@marko/babel-utils": "^5.0.0-next.68", - "@marko/translator-default": "^5.0.0-next.69", + "@marko/translator-default": "^5.0.0-next.70", "complain": "^1.6.0", "enhanced-resolve": "5.0.0", "he": "^1.1.0", @@ -3007,12 +3007,31 @@ "strip-ansi": "^5.2.0" }, "dependencies": { + "@marko/translator-default": { + "version": "5.0.0-next.70", + "resolved": "https://registry.npmjs.org/@marko/translator-default/-/translator-default-5.0.0-next.70.tgz", + "integrity": "sha512-biu+5sE8f1+xO1m6x1dUU8mmiqHu+N2tcaL2/sKNzSI0V9+naYoUPbvD7XZIcQ5Brit4oRnq6m6v3yqDMqkwBg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.7", + "@marko/babel-types": "^5.0.0-next.67", + "@marko/babel-utils": "^5.0.0-next.68", + "escape-string-regexp": "^4.0.0", + "self-closing-tags": "^1.0.1" + } + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", diff --git a/package.json b/package.json index 6be928c93..960ccc46d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@commitlint/config-conventional": "^11.0.0", "@commitlint/config-lerna-scopes": "^11.0.0", "@istanbuljs/nyc-config-typescript": "^1.0.1", - "@marko/compiler": "^5.0.0-next.69", + "@marko/compiler": "^5.0.0-next.70", "@rollup/plugin-replace": "2.3.4", "@types/jsdom": "^16.2.5", "@types/mocha": "^8.0.4", diff --git a/packages/translator/src/analyze/index.ts b/packages/translator/src/analyze/index.ts new file mode 100644 index 000000000..b2e6c6a7f --- /dev/null +++ b/packages/translator/src/analyze/index.ts @@ -0,0 +1,24 @@ +import { Visitor } from "@marko/babel-types"; +import analyzeTagNameType, { TagNameTypes } from "./tag-name-type"; +import analyzeNestedAttributeTags from "./nested-attribute-tags"; + +declare module "@marko/babel-types" { + // This is extended by individual helpers. + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface MarkoTagExtra {} + + export interface MarkoTag { + extra: MarkoTagExtra & Record; + } +} + +export default { + MarkoTag(tag) { + const extra = (tag.node.extra ??= {} as typeof tag.node.extra); + analyzeTagNameType(tag); + + if (extra.tagNameType !== TagNameTypes.NativeTag) { + analyzeNestedAttributeTags(tag); + } + } +} as Visitor; diff --git a/packages/translator/src/analyze/nested-attribute-tags.ts b/packages/translator/src/analyze/nested-attribute-tags.ts new file mode 100644 index 000000000..f77f2aeb0 --- /dev/null +++ b/packages/translator/src/analyze/nested-attribute-tags.ts @@ -0,0 +1,88 @@ +import { types as t, NodePath } from "@marko/babel-types"; +import { + isAttributeTag, + isTransparentTag, + isLoopTag +} from "@marko/babel-utils"; + +type Lookup = Record< + string, + { + identifier?: t.Identifier; + dynamic: boolean; + repeated: boolean; + } +>; + +declare module "@marko/babel-types" { + export interface MarkoTagExtra { + hoistedControlFlows: number; + nestedAttributeTags: Lookup; + } +} + +export default function analyzeAttributeTags(tag: NodePath) { + const { extra } = tag.node; + extra.nestedAttributeTags = {}; + extra.hoistedControlFlows = 0; + analyzeChildren(extra, false, false, tag); +} + +function analyzeChildren( + rootExtra: t.MarkoTag["extra"], + repeated: boolean, + dynamic: boolean, + tag: NodePath +) { + let hasAttributeTags = false; + for (const child of tag.get("body").get("body")) { + if (child.isMarkoTag()) { + if ( + analyzeChild( + rootExtra, + repeated, + dynamic, + child as NodePath + ) + ) { + hasAttributeTags = true; + } + } + } + + return hasAttributeTags; +} + +function analyzeChild( + rootExtra: t.MarkoTag["extra"], + repeated: boolean, + dynamic: boolean, + tag: NodePath +) { + if (isTransparentTag(tag)) { + if (analyzeChildren(rootExtra, repeated || isLoopTag(tag), true, tag)) { + if ( + !isTransparentTag(tag.parentPath.parentPath as NodePath) + ) { + rootExtra.hoistedControlFlows++; + } + return true; + } + } else if (isAttributeTag(tag)) { + const attrName = (tag.node.name as t.StringLiteral).value.slice(1); + const lookup = rootExtra.nestedAttributeTags; + const existing = lookup[attrName]; + const info = + existing || + (lookup[attrName] = { + dynamic: false, + repeated: false + }); + + info.dynamic ||= dynamic; + info.repeated ||= repeated || existing !== undefined; + return true; + } + + return false; +} diff --git a/packages/translator/src/analyze/tag-name-type.ts b/packages/translator/src/analyze/tag-name-type.ts new file mode 100644 index 000000000..7d3c19354 --- /dev/null +++ b/packages/translator/src/analyze/tag-name-type.ts @@ -0,0 +1,193 @@ +import { types as t, NodePath } from "@marko/babel-types"; +import { isNativeTag } from "@marko/babel-utils"; + +declare module "@marko/babel-types" { + export interface MarkoTagExtra { + tagNameType: TagNameTypes; + tagNameNullable: boolean; + tagNameDynamic: boolean; + } +} + +export const enum TagNameTypes { + NativeTag, + CustomTag, + DynamicTag, + AttributeTag +} + +const MARKO_FILE_REG = /^<.*>$|\.marko$/; + +export default function analyzeTagNameType(tag: NodePath) { + const name = tag.get("name"); + const { extra } = tag.node; + + if (name.isStringLiteral()) { + extra.tagNameType = + name.node.value[0] === "@" + ? TagNameTypes.AttributeTag + : isNativeTag(tag) + ? TagNameTypes.NativeTag + : TagNameTypes.CustomTag; + + extra.tagNameNullable = extra.tagNameNullable = false; + return; + } + + const pending = [name] as NodePath[]; + let path: typeof pending[0] | undefined; + let type: TagNameTypes | undefined = undefined; + let nullable = false; + + while ((path = pending.pop()) && type !== TagNameTypes.DynamicTag) { + switch (path.type) { + case "ConditionalExpression": { + const curPath = path as NodePath; + pending.push(curPath.get("consequent")); + + if (curPath.node.alternate) { + pending.push(curPath.get("alternate")); + } + break; + } + + case "LogicalExpression": { + const curPath = path as NodePath; + if (curPath.node.operator === "||") { + pending.push(curPath.get("left")); + } else { + nullable = true; + } + + pending.push(curPath.get("right")); + break; + } + + case "AssignmentExpression": + pending.push((path as NodePath).get("right")); + break; + + case "BinaryExpression": + type = + (path as NodePath).node.operator !== "+" || + (type !== undefined && type !== TagNameTypes.NativeTag) + ? TagNameTypes.DynamicTag + : TagNameTypes.NativeTag; + + break; + + case "StringLiteral": + case "TemplateLiteral": + type = + type !== undefined && type !== TagNameTypes.NativeTag + ? TagNameTypes.DynamicTag + : TagNameTypes.NativeTag; + break; + + case "NullLiteral": + nullable = true; + break; + + case "Identifier": { + const curPath = path as NodePath; + if (curPath.node.name === "undefined") { + nullable = true; + break; + } + + const binding = curPath.scope.getBinding(curPath.node.name); + + if (!binding) { + type = TagNameTypes.DynamicTag; + break; + } + + if (binding.kind === "module") { + const decl = binding.path.parent as t.ImportDeclaration; + if ( + MARKO_FILE_REG.test(decl.source.value) && + decl.specifiers.some(it => t.isImportDefaultSpecifier(it)) + ) { + type = + type !== undefined && type !== TagNameTypes.CustomTag + ? TagNameTypes.DynamicTag + : TagNameTypes.CustomTag; + } else { + type = TagNameTypes.DynamicTag; + } + + break; + } + + const bindingPath = binding.path; + const bindingTag = bindingPath.parentPath as NodePath; + + if (bindingTag.isMarkoTag() && bindingPath.key === "var") { + const bindingTagName = (bindingTag.get("name") + .node as t.StringLiteral).value; + if (bindingTagName === "tag") { + // treat as a custom tag. + type = + type !== undefined && type !== TagNameTypes.CustomTag + ? TagNameTypes.DynamicTag + : TagNameTypes.CustomTag; + break; + } + + if (bindingTagName === "const") { + pending.push( + (bindingTag.get( + "attributes" + )[0] as NodePath).get("value") + ); + break; + } + + if (bindingTagName === "let") { + const defaultAttr = bindingTag.get("attributes")[0]; + + if (defaultAttr.node) { + pending.push( + (defaultAttr as NodePath).get("value") + ); + } else { + nullable = true; + } + + const assignments = binding.constantViolations; + for (let i = assignments.length; i--; ) { + const assignment = assignments[ + i + ] as NodePath; + const { operator } = assignment.node; + if (operator === "=") { + pending.push(assignment.get("right")); + } else if (operator === "+=") { + type = + type !== undefined && type !== TagNameTypes.NativeTag + ? TagNameTypes.DynamicTag + : TagNameTypes.NativeTag; + } else { + type = TagNameTypes.DynamicTag; + break; + } + } + } + + break; + } + + type = TagNameTypes.DynamicTag; + break; + } + + default: + type = TagNameTypes.DynamicTag; + break; + } + } + + extra.tagNameType = type!; + extra.tagNameNullable = nullable; + extra.tagNameDynamic = true; +} diff --git a/packages/translator/src/core/translate-tag.ts b/packages/translator/src/core/translate-tag.ts index 50b5e019e..5b8c71b67 100644 --- a/packages/translator/src/core/translate-tag.ts +++ b/packages/translator/src/core/translate-tag.ts @@ -1,5 +1,4 @@ import { types as t, NodePath } from "@marko/babel-types"; -import { markIdentifierAsComponent } from "../util/analyze-tag-name"; import { flushBefore, flushInto } from "../util/html-flush"; import toFirstExpressionOrBlock from "../util/to-first-expression-or-block"; @@ -28,7 +27,7 @@ export function exit(tag: NodePath) { tag.replaceWith( t.variableDeclaration("const", [ t.variableDeclarator( - markIdentifierAsComponent(tag.node.var! as t.Identifier), + tag.node.var!, t.arrowFunctionExpression( tag.node.params || [], toFirstExpressionOrBlock(tag.node.body) diff --git a/packages/translator/src/index.ts b/packages/translator/src/index.ts index a9e5f3f39..f0b561f66 100644 --- a/packages/translator/src/index.ts +++ b/packages/translator/src/index.ts @@ -13,7 +13,9 @@ export const taglibs = [ [require.resolve("./core/marko.json"), require("./core/marko.json")] ]; -export const visitor: Visitor = { +export { default as analyze } from "./analyze"; + +export const translate: Visitor = { Program, ImportDeclaration, MarkoDocumentType, diff --git a/packages/translator/src/tag/attribute-tag.ts b/packages/translator/src/tag/attribute-tag.ts index 58934b5b9..62c539cfb 100644 --- a/packages/translator/src/tag/attribute-tag.ts +++ b/packages/translator/src/tag/attribute-tag.ts @@ -1,37 +1,21 @@ import { types as t, NodePath } from "@marko/babel-types"; -import { - findParentTag, - isTransparentTag, - isLoopTag, - isAttributeTag, - assertNoVar -} from "@marko/babel-utils"; -import analyzeTagName, { TagNameTypes } from "../util/analyze-tag-name"; +import { findParentTag, assertNoVar } from "@marko/babel-utils"; +import { TagNameTypes } from "../analyze/tag-name-type"; import attrsToObject from "../util/attrs-to-object"; import { flushInto, hasPendingHTML } from "../util/html-flush"; -const HOISTED_NODES = new WeakSet(); -const HOISTED_CHILDREN = new WeakSet>(); -const PARENT_TAGS = new WeakMap, NodePath>(); -const LOOKUPS = new WeakMap, Lookup>(); -type Lookup = Record< - string, - { - identifier?: t.Identifier; - dynamic: boolean; - repeated: boolean; - } ->; - -export function hasHoistedChildren(tag: NodePath) { - return HOISTED_CHILDREN.has(tag); -} - -export function isHoistedNode(node: t.Node) { - return HOISTED_NODES.has(node); -} - export function enter(tag: NodePath) { + if (hasPendingHTML(tag)) { + throw tag + .get("name") + .buildCodeFrameError("Dynamic @tags cannot be mixed with body content."); + } +} + +export function exit(tag: NodePath) { + assertNoVar(tag); + flushInto(tag); + const parentTag = findParentTag(tag); if (!parentTag) { @@ -40,84 +24,63 @@ export function enter(tag: NodePath) { .buildCodeFrameError("@tags must be nested within another tag."); } - if (analyzeTagName(parentTag).type === TagNameTypes.NativeTag) { + const parentExtra = parentTag.node.extra; + + if (parentExtra.tagNameType === TagNameTypes.NativeTag) { throw tag .get("name") .buildCodeFrameError("@tags cannot be nested under native tags."); } - if (hasPendingHTML(tag)) { - throw tag - .get("name") - .buildCodeFrameError("Dynamic @tags cannot be mixed with body content."); - } - - PARENT_TAGS.set(tag, parentTag); - - if (!LOOKUPS.has(parentTag)) { - const lookup = analyzeRoot(parentTag); - LOOKUPS.set(parentTag, lookup); - (parentTag.node as any).exampleAttributeTag = tag.node; // Used by @marko/babel-utils assertNoAttributeTags. - - for (const attrName in lookup) { - const info = lookup[attrName]; - if (info.dynamic) { - info.identifier = parentTag.scope.generateUidIdentifier(attrName); - parentTag.insertBefore( - info.repeated - ? t.variableDeclaration("const", [ - t.variableDeclarator(info.identifier, t.arrayExpression([])) - ]) - : t.variableDeclaration("let", [ - t.variableDeclarator(info.identifier) - ]) - ); - - parentTag.pushContainer( - "attributes", - t.markoAttribute(attrName, info.identifier) - ); - - HOISTED_CHILDREN.add(parentTag); - } else if (info.repeated) { - parentTag.pushContainer( - "attributes", - t.markoAttribute(attrName, t.arrayExpression([])) - ); - } - } - } -} - -export function exit(tag: NodePath) { - assertNoVar(tag); - flushInto(tag); - const attrName = (tag.node.name as t.StringLiteral).value.slice(1); - const parentTag = PARENT_TAGS.get(tag)!; - const info = LOOKUPS.get(parentTag)![attrName]; + const info = parentExtra.nestedAttributeTags[attrName]; const attrsObject = attrsToObject(tag, true) || t.objectExpression([]); - if (info.identifier) { - const replacement = t.expressionStatement( - info.repeated - ? t.callExpression( - t.memberExpression(info.identifier, t.identifier("push")), - [attrsObject] - ) - : t.assignmentExpression("=", info.identifier, attrsObject) - ); + if (info.dynamic) { + if (!info.identifier) { + info.identifier = parentTag.scope.generateUidIdentifier(attrName); + parentTag.insertBefore( + info.repeated + ? t.variableDeclaration("const", [ + t.variableDeclarator(info.identifier, t.arrayExpression([])) + ]) + : t.variableDeclaration("let", [ + t.variableDeclarator(info.identifier) + ]) + ); - HOISTED_NODES.add(replacement); - tag.replaceWith(replacement); - } else if (info.repeated) { - (parentTag - .get("attributes") - .find(attr => (attr.node as t.MarkoAttribute).name === attrName)! - .get("value") as NodePath).pushContainer( - "elements", - attrsObject + parentTag.pushContainer( + "attributes", + t.markoAttribute(attrName, info.identifier) + ); + } + + tag.replaceWith( + t.expressionStatement( + info.repeated + ? t.callExpression( + t.memberExpression(info.identifier, t.identifier("push")), + [attrsObject] + ) + : t.assignmentExpression("=", info.identifier, attrsObject) + ) ); + } else if (info.repeated) { + const existingAttr = parentTag + .get("attributes") + .find(attr => (attr.node as t.MarkoAttribute).name === attrName) as + | NodePath + | undefined; + + if (existingAttr) { + existingAttr.pushContainer("elements", attrsObject); + } else { + parentTag.pushContainer( + "attributes", + t.markoAttribute(attrName, t.arrayExpression([attrsObject])) + ); + } + tag.remove(); } else { parentTag.pushContainer( @@ -127,45 +90,3 @@ export function exit(tag: NodePath) { tag.remove(); } } - -function analyzeRoot(tag: NodePath) { - const lookup = {} as Lookup; - analyzeChildren(lookup, false, false, tag); - return lookup; -} - -function analyzeChildren( - lookup: Lookup, - repeated: boolean, - dynamic: boolean, - tag: NodePath -) { - for (const child of tag.get("body").get("body")) { - if (child.isMarkoTag()) { - analyzeChild(lookup, repeated, dynamic, child as NodePath); - } - } -} - -function analyzeChild( - lookup: Lookup, - repeated: boolean, - dynamic: boolean, - tag: NodePath -) { - if (isTransparentTag(tag)) { - analyzeChildren(lookup, repeated || isLoopTag(tag), true, tag); - } else if (isAttributeTag(tag)) { - const attrName = (tag.node.name as t.StringLiteral).value.slice(1); - const existing = lookup[attrName]; - const info = - existing || - (lookup[attrName] = { - dynamic: false, - repeated: false - }); - - info.dynamic ||= dynamic; - info.repeated ||= repeated || existing !== undefined; - } -} diff --git a/packages/translator/src/tag/custom-tag.ts b/packages/translator/src/tag/custom-tag.ts index 4d330af4d..f6a93ab8a 100644 --- a/packages/translator/src/tag/custom-tag.ts +++ b/packages/translator/src/tag/custom-tag.ts @@ -6,7 +6,6 @@ import { } from "@marko/babel-utils"; import attrsToObject, { getRenderBodyProp } from "../util/attrs-to-object"; import { flushBefore, flushInto } from "../util/html-flush"; -import analyzeTagName from "../util/analyze-tag-name"; import translateVar from "../util/translate-var"; export function enter(tag: NodePath) { @@ -46,7 +45,7 @@ export function exit(tag: NodePath) { const attrsObject = attrsToObject(tag, true); - if (analyzeTagName(tag).nullable) { + if (node.extra.tagNameNullable) { const renderBodyProp = getRenderBodyProp(attrsObject); let renderBodyId: t.Identifier | undefined = undefined; let renderTagExpr: t.Expression = callExpression( diff --git a/packages/translator/src/tag/index.ts b/packages/translator/src/tag/index.ts index b2f18a567..57f631338 100644 --- a/packages/translator/src/tag/index.ts +++ b/packages/translator/src/tag/index.ts @@ -6,7 +6,7 @@ import { Plugin } from "@marko/babel-utils"; import { require as markoRequire } from "@marko/compiler/modules"; -import analyzeTagName, { TagNameTypes } from "../util/analyze-tag-name"; +import { TagNameTypes } from "../analyze/tag-name-type"; import * as hooks from "../util/plugin-hooks"; import * as NativeTag from "./native-tag"; import * as CustomTag from "./custom-tag"; @@ -21,6 +21,7 @@ declare module "@marko/babel-utils" { export function enter(tag: NodePath) { const tagDef = getTagDef(tag); + const extra = tag.node.extra; assertNoArgs(tag); @@ -57,9 +58,7 @@ export function enter(tag: NodePath) { } } - const analyzed = analyzeTagName(tag); - - if (analyzed.dynamic && analyzed.nullable) { + if (extra.tagNameDynamic && extra.tagNameNullable) { if (!tag.get("name").isIdentifier()) { const tagNameId = tag.scope.generateUidIdentifier("tagName"); const [tagNameVarPath] = tag.insertBefore( @@ -73,7 +72,7 @@ export function enter(tag: NodePath) { } } - switch (analyzed.type) { + switch (extra.tagNameType) { case TagNameTypes.NativeTag: NativeTag.enter(tag); break; @@ -97,7 +96,7 @@ export function exit(tag: NodePath) { return; } - switch (analyzeTagName(tag).type) { + switch (tag.node.extra.tagNameType) { case TagNameTypes.NativeTag: NativeTag.exit(tag); break; diff --git a/packages/translator/src/tag/native-tag/html.ts b/packages/translator/src/tag/native-tag/html.ts index c97f970e4..0cc39eac1 100644 --- a/packages/translator/src/tag/native-tag/html.ts +++ b/packages/translator/src/tag/native-tag/html.ts @@ -1,6 +1,5 @@ import { types as t, NodePath } from "@marko/babel-types"; import { getTagDef } from "@marko/babel-utils"; -import analyzeTagName from "../../util/analyze-tag-name"; import attrsToObject from "../../util/attrs-to-object"; import { consumeHTML, flushBefore, flushInto } from "../../util/html-flush"; import { writeHTML } from "../../util/html-write"; @@ -8,13 +7,13 @@ import { callRuntime, getHTMLRuntime } from "../../util/runtime"; import translateVar from "../../util/translate-var"; export function enter(tag: NodePath) { + const { extra } = tag.node; const name = tag.get("name"); const attrs = tag.get("attributes"); const tagDef = getTagDef(tag); const hasSpread = attrs.some(attr => attr.isMarkoSpreadAttribute()); - const { nullable } = analyzeTagName(tag); - if (nullable) { + if (extra.tagNameNullable) { flushBefore(tag); } @@ -87,7 +86,7 @@ export function enter(tag: NodePath) { emptyBody = true; } - if (nullable) { + if (extra.tagNameNullable) { tag.insertBefore(t.ifStatement(name.node, consumeHTML(tag)!))[0].skip(); } @@ -97,9 +96,9 @@ export function enter(tag: NodePath) { } export function exit(tag: NodePath) { - const { nullable } = analyzeTagName(tag); + const { extra } = tag.node; - if (nullable) { + if (extra.tagNameNullable) { flushInto(tag); } @@ -107,7 +106,7 @@ export function exit(tag: NodePath) { writeHTML(tag)``; - if (nullable) { + if (extra.tagNameNullable) { tag.insertBefore(t.ifStatement(tag.node.name, consumeHTML(tag)!))[0].skip(); } diff --git a/packages/translator/src/util/analyze-tag-name.ts b/packages/translator/src/util/analyze-tag-name.ts deleted file mode 100644 index bc580afd1..000000000 --- a/packages/translator/src/util/analyze-tag-name.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { types as t, NodePath } from "@marko/babel-types"; -import { isNativeTag } from "@marko/babel-utils"; - -export const enum TagNameTypes { - NativeTag, - CustomTag, - DynamicTag, - AttributeTag -} -type TagNameInfo = { type: TagNameTypes; nullable: boolean; dynamic: boolean }; -const HANDLE_BINDINGS = ["module", "var", "let", "const"]; -const MARKO_FILE_REG = /^<.*>$|\.marko$/; -const CACHE = new WeakMap, TagNameInfo>(); -const KNOWN_COMPONENTS = new WeakSet(); - -export function markIdentifierAsComponent(node: t.Identifier) { - KNOWN_COMPONENTS.add(node); - return node; -} - -export default function analyzeTagName(tag: NodePath) { - let cached = CACHE.get(tag); - if (cached) { - return cached; - } - - const name = tag.get("name"); - - if (name.isStringLiteral()) { - cached = { - type: - name.node.value[0] === "@" - ? TagNameTypes.AttributeTag - : isNativeTag(tag) - ? TagNameTypes.NativeTag - : TagNameTypes.CustomTag, - nullable: false, - dynamic: false - }; - } else { - const pending = [name] as NodePath[]; - let path: typeof pending[0] | undefined; - let type: TagNameTypes | undefined = undefined; - let nullable = false; - - while ((path = pending.pop()) && type !== TagNameTypes.DynamicTag) { - switch (path.type) { - case "ConditionalExpression": { - const curPath = path as NodePath; - pending.push(curPath.get("consequent")); - - if (curPath.node.alternate) { - pending.push(curPath.get("alternate")); - } - break; - } - - case "LogicalExpression": { - const curPath = path as NodePath; - if (curPath.node.operator === "||") { - pending.push(curPath.get("left")); - } else { - nullable = true; - } - - pending.push(curPath.get("right")); - break; - } - - case "AssignmentExpression": - pending.push((path as NodePath).get("right")); - break; - - case "BinaryExpression": - type = - (path as NodePath).node.operator !== "+" || - (type !== undefined && type !== TagNameTypes.NativeTag) - ? TagNameTypes.DynamicTag - : TagNameTypes.NativeTag; - - break; - - case "StringLiteral": - case "TemplateLiteral": - if (type === undefined || type === TagNameTypes.NativeTag) { - type = TagNameTypes.NativeTag; - } else { - type = TagNameTypes.DynamicTag; - } - type = - type !== undefined && type !== TagNameTypes.NativeTag - ? TagNameTypes.DynamicTag - : TagNameTypes.NativeTag; - break; - - case "NullLiteral": - nullable = true; - break; - - case "Identifier": { - const curPath = path as NodePath; - if (curPath.node.name === "undefined") { - nullable = true; - } else { - const binding = curPath.scope.getBinding(curPath.node.name); - - if (!binding) { - type = TagNameTypes.DynamicTag; - } else if ( - KNOWN_COMPONENTS.has(binding.path.node as t.Identifier) - ) { - type = - type !== undefined && type !== TagNameTypes.CustomTag - ? TagNameTypes.DynamicTag - : TagNameTypes.CustomTag; - } else if (!HANDLE_BINDINGS.includes(binding.kind)) { - type = TagNameTypes.DynamicTag; - } else if (binding.kind === "module") { - const decl = binding.path.parent as t.ImportDeclaration; - if ( - MARKO_FILE_REG.test(decl.source.value) && - decl.specifiers.some(it => t.isImportDefaultSpecifier(it)) - ) { - type = - type !== undefined && type !== TagNameTypes.CustomTag - ? TagNameTypes.DynamicTag - : TagNameTypes.CustomTag; - } else { - type = TagNameTypes.DynamicTag; - } - } else { - const initialValue = (binding.path as NodePath).get( - "init" - ); - if (initialValue.node) { - pending.push(initialValue); - } else { - nullable = true; - } - - const assignments = binding.constantViolations; - for (let i = assignments.length; i--; ) { - const assignment = assignments[ - i - ] as NodePath; - const { operator } = assignment.node; - if (operator === "=") { - pending.push(assignment.get("right")); - } else if (operator === "+=") { - type = - type !== undefined && type !== TagNameTypes.NativeTag - ? TagNameTypes.DynamicTag - : TagNameTypes.NativeTag; - } else { - type = TagNameTypes.DynamicTag; - break; - } - } - } - } - break; - } - - default: - type = TagNameTypes.DynamicTag; - break; - } - } - - cached = { type: type!, nullable, dynamic: true }; - } - - CACHE.set(tag, cached); - return cached; -} diff --git a/packages/translator/src/util/attrs-to-object.ts b/packages/translator/src/util/attrs-to-object.ts index 791569a28..c7fb17b8b 100644 --- a/packages/translator/src/util/attrs-to-object.ts +++ b/packages/translator/src/util/attrs-to-object.ts @@ -1,20 +1,6 @@ -import { types as t, NodePath, Visitor } from "@marko/babel-types"; -import { hasHoistedChildren, isHoistedNode } from "../tag/attribute-tag"; +import { types as t, NodePath } from "@marko/babel-types"; import toPropertyName from "./to-property-name"; -type HoistedVisitorState = { isHoisted: boolean }; -const HOISTED_CHILDREN_VISITOR: Visitor = { - ExpressionStatement( - path: NodePath, - state: HoistedVisitorState - ) { - if (isHoistedNode(path.node)) { - state.isHoisted = true; - path.stop(); - } - } -}; - export default function attrsToObject( tag: NodePath, withRenderBody = false @@ -35,31 +21,17 @@ export default function attrsToObject( } if (withRenderBody) { - if (hasHoistedChildren(tag)) { - const state: HoistedVisitorState = { isHoisted: false }; - const children = tag.get("body").get("body"); - const len = children.length; + let hoistedControlFlows = node.extra.hoistedControlFlows; - for (let i = len; i--; ) { - const child = children[i]; + if (hoistedControlFlows) { + for (const child of tag.get("body").get("body")) { + tag.insertBefore(child.node); + child.remove(); - if (isHoistedNode(child.node)) { - state.isHoisted = true; - } else { - child.traverse(HOISTED_CHILDREN_VISITOR, state); - } - - if (state.isHoisted) { - const renderBodyStartIndex = i + 1; - - if (renderBodyStartIndex === len) { - tag.insertBefore(tag.node.body.body); - tag.node.body.body = []; - } else { - tag.insertBefore(tag.node.body.body.slice(0, renderBodyStartIndex)); - tag.node.body.body = tag.node.body.body.slice(renderBodyStartIndex); + if (child.isConditional() || child.isLoop()) { + if (!--hoistedControlFlows) { + break; } - break; } } } diff --git a/packages/translator/test/fixtures/at-tags-dynamic/snapshots/html-compiled-expected.js b/packages/translator/test/fixtures/at-tags-dynamic/snapshots/html-compiled-expected.js index 4dfc840d4..622537fb5 100644 --- a/packages/translator/test/fixtures/at-tags-dynamic/snapshots/html-compiled-expected.js +++ b/packages/translator/test/fixtures/at-tags-dynamic/snapshots/html-compiled-expected.js @@ -63,10 +63,10 @@ const _renderer = _register("packages/translator/test/fixtures/at-tags-dynamic/t }); _hello({ - col: _col, list: { item: _item - } + }, + col: _col }); });