diff --git a/.changeset/modern-feet-move.md b/.changeset/modern-feet-move.md new file mode 100644 index 0000000..3500bad --- /dev/null +++ b/.changeset/modern-feet-move.md @@ -0,0 +1,7 @@ +--- +"@marko/compat-v4": patch +"@marko/compat-utils": patch +"marko-widgets": patch +--- + +Improve handling of non standard template literals. diff --git a/packages/compat-v4/src/migrate/non-standard-template-literals/index.ts b/packages/compat-v4/src/migrate/non-standard-template-literals/index.ts index 2ea7f84..51842bc 100644 --- a/packages/compat-v4/src/migrate/non-standard-template-literals/index.ts +++ b/packages/compat-v4/src/migrate/non-standard-template-literals/index.ts @@ -45,62 +45,94 @@ export function migrateNonStandardTemplateLiterals(path: t.NodePath) { } function StringLiteral(string: t.NodePath) { - const templateLiteral = parseNonStandardTemplateLiteral(string); - if (templateLiteral) { - if ( - templateLiteral.expressions.length === 1 && - templateLiteral.quasis.length === 2 && - templateLiteral.quasis[0].value.raw === "" && - templateLiteral.quasis[1].value.raw === "" - ) { - diagnosticDeprecate(string, { - label: "Non-standard template literals are deprecated.", - fix() { - string.replaceWith(templateLiteral.expressions[0]); - }, - }); - } else if (templateLiteral.expressions.every(isNotNullish)) { - diagnosticDeprecate(string, { - label: "Non-standard template literals are deprecated.", - fix() { - string.replaceWith(templateLiteral); - }, - }); + const { file } = string.hub; + const replacement = parseAllNonStandardTemplateLiterals(file, string.node); + if (replacement) { + if (t.isTemplateLiteral(replacement)) { + if (replacement.expressions.every(isNotNullish)) { + diagnosticDeprecate(string, { + label: "Non-standard template literals are deprecated.", + fix() { + string.replaceWith(replacement); + }, + }); + } else { + diagnosticDeprecate(string, { + label: "Non-standard template literals are deprecated.", + fix: { + type: "confirm", + message: + "Are the interpolated values guaranteed to not be null or undefined?", + apply(confirm) { + if (confirm) { + string.replaceWith(replacement); + } else { + string.replaceWith( + t.templateLiteral( + replacement.quasis, + replacement.expressions.map((expr) => { + return isNotNullish(expr) + ? expr + : castNullishToString(file, expr as t.Expression); + }), + ), + ); + } + }, + }, + }); + } } else { diagnosticDeprecate(string, { label: "Non-standard template literals are deprecated.", - fix: { - type: "confirm", - message: - "Are the interpolated values guaranteed to not be null or undefined?", - apply(confirm) { - if (confirm) { - string.replaceWith(templateLiteral); - } else { - string.replaceWith( - t.templateLiteral( - templateLiteral.quasis, - templateLiteral.expressions.map((expr) => { - return isNotNullish(expr) - ? expr - : castNullishToString(string, expr as t.Expression); - }), - ), - ); - } - }, + fix() { + string.replaceWith(replacement); }, }); } } } -function castNullishToString(string: t.NodePath, expression: t.Expression) { - let nullishHelper = nullishHelpers.get(string.hub); +function parseAllNonStandardTemplateLiterals( + file: t.BabelFile, + node: t.StringLiteral, +) { + const templateLiteral = parseNonStandardTemplateLiteral(file, node); + if (templateLiteral) { + for (let i = templateLiteral.expressions.length; i--; ) { + traverseWithParent( + file, + templateLiteral.expressions[i], + templateLiteral, + i, + replaceNestedNonStandardTemplateLiteral, + ); + } + + return isSingleExpressionTemplateLiteral(templateLiteral) + ? templateLiteral.expressions[0] + : templateLiteral; + } +} + +function replaceNestedNonStandardTemplateLiteral( + file: t.BabelFile, + node: t.Node, + parent: t.Node, + key: string | number, +) { + if (node.type === "StringLiteral") { + (parent as any)[key] = + parseAllNonStandardTemplateLiterals(file, node) || node; + } +} + +function castNullishToString(file: t.BabelFile, expression: t.Expression) { + let nullishHelper = nullishHelpers.get(file.hub); if (!nullishHelper) { - nullishHelper = string.scope.generateUidIdentifier("toString"); - nullishHelpers.set(string.hub, nullishHelper); - string.hub.file.path.unshiftContainer( + nullishHelper = file.path.scope.generateUidIdentifier("toString"); + nullishHelpers.set(file.hub, nullishHelper); + file.path.unshiftContainer( "body", t.markoScriptlet( [ @@ -129,6 +161,15 @@ function castNullishToString(string: t.NodePath, expression: t.Expression) { return t.callExpression(nullishHelper, [expression]); } +function isSingleExpressionTemplateLiteral(templateLiteral: t.TemplateLiteral) { + return ( + templateLiteral.expressions.length === 1 && + templateLiteral.quasis.length === 2 && + templateLiteral.quasis[0].value.raw === "" && + templateLiteral.quasis[1].value.raw === "" + ); +} + function isNotNullish(node: t.Node): boolean { switch (node.type) { case "ArrayExpression": @@ -162,3 +203,35 @@ function isNotNullish(node: t.Node): boolean { return false; } } + +function traverseWithParent( + file: t.BabelFile, + node: t.Node | null | undefined, + parent: t.Node, + key: string | number, + enter: ( + file: t.BabelFile, + node: t.Node, + parent: t.Node, + key: string | number, + ) => void, +): void { + if (!node) return; + + const keys = (t as any).VISITOR_KEYS[node.type]; + if (!keys) return; + + enter(file, node, parent, key); + + for (const key of keys) { + const value: t.Node | undefined | null = (node as any)[key]; + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverseWithParent(file, value[i], value, i, enter); + } + } else { + traverseWithParent(file, value, parent, key, enter); + } + } +} diff --git a/packages/compat-v4/src/migrate/non-standard-template-literals/parse.ts b/packages/compat-v4/src/migrate/non-standard-template-literals/parse.ts index 8d60f14..ab9a8cc 100644 --- a/packages/compat-v4/src/migrate/non-standard-template-literals/parse.ts +++ b/packages/compat-v4/src/migrate/non-standard-template-literals/parse.ts @@ -17,18 +17,19 @@ const enum CODE { OPEN_PAREN = 40, OPEN_SQUARE_BRACKET = 91, SINGLE_QUOTE = 39, + EXCLAMATION = 33, } export function parseNonStandardTemplateLiteral( - string: t.NodePath, + file: t.BabelFile, + string: t.StringLiteral, ) { - const { file } = string.hub; - const { extra } = string.node; + const { extra } = string; let value = extra?.raw as string | undefined; if (typeof value !== "string") return; value = value.slice(1, -1); const { length } = value; - const nodeStart = getStart(file, string.node); + const nodeStart = getStart(file, string); const valueStart = nodeStart == null ? null : nodeStart + 1; let elements: undefined | t.TemplateElement[]; let expressions: undefined | t.Expression[]; @@ -39,9 +40,16 @@ export function parseNonStandardTemplateLiteral( case CODE.BACK_SLASH: i++; break; - case CODE.DOLLAR_SIGN: - if (value.charCodeAt(i + 1) === CODE.OPEN_CURLY_BRACE) { - const bracketStart = i + 2; + case CODE.DOLLAR_SIGN: { + const bracketOffset = + value.charCodeAt(i + 1) === CODE.EXCLAMATION && + value.charCodeAt(i + 2) === CODE.OPEN_CURLY_BRACE + ? 3 + : value.charCodeAt(i + 1) === CODE.OPEN_CURLY_BRACE + ? 2 + : 0; + if (bracketOffset) { + const bracketStart = i + bracketOffset; const bracketEnd = skipBracketed( value, bracketStart, @@ -56,25 +64,31 @@ export function parseNonStandardTemplateLiteral( i = bracketEnd - 1; lastEndBracket = bracketEnd; - const expr = - valueStart != null - ? parseExpression( - file, - value.slice(bracketStart, i), - valueStart + bracketStart, - valueStart + i, - ) - : parseExpression(file, value.slice(bracketStart, i)); - - if (elements) { - elements.push(el); - expressions!.push(expr); - } else { - elements = [el]; - expressions = [expr]; + + try { + const expr = + valueStart != null + ? parseExpression( + file, + value.slice(bracketStart, i), + valueStart + bracketStart, + valueStart + i, + ) + : parseExpression(file, value.slice(bracketStart, i)); + + if (elements) { + elements.push(el); + expressions!.push(expr); + } else { + elements = [el]; + expressions = [expr]; + } + } catch { + return; // bail if we couldn't process the expression. } } break; + } } } diff --git a/tests/fixtures-class/non-standard-template-literals/__snapshots__/auto-migrate.expected/template.marko b/tests/fixtures-class/non-standard-template-literals/__snapshots__/auto-migrate.expected/template.marko index c2753b3..de879f3 100644 --- a/tests/fixtures-class/non-standard-template-literals/__snapshots__/auto-migrate.expected/template.marko +++ b/tests/fixtures-class/non-standard-template-literals/__snapshots__/auto-migrate.expected/template.marko @@ -40,7 +40,10 @@ $ const d = "d";
${'\${abc}'}
-
-
+
+
+
+ ${a} +
$ const handler = console.log;
+
+ 1 +
+
${STATIC}
${SCRIPLET}
1
abc}
abc}
abc}
abcd}ef
abc3
abcdef
${abc}
1
# Render ```html @@ -58,11 +58,16 @@ ${abc}
+
+ 1 +
"); out.w(`
`); out.w(`
`); + out.w("
"); + out.w(_marko_escapeXml(a)); + out.w("
"); const handler = console.log; out.w(""); }, { diff --git a/tests/fixtures-class/non-standard-template-literals/__snapshots__/hydrate.expected.md b/tests/fixtures-class/non-standard-template-literals/__snapshots__/hydrate.expected.md index d44a697..f315ecc 100644 --- a/tests/fixtures-class/non-standard-template-literals/__snapshots__/hydrate.expected.md +++ b/tests/fixtures-class/non-standard-template-literals/__snapshots__/hydrate.expected.md @@ -55,11 +55,16 @@ ${abc}
+
+ 1 +