From d25a5a57c53149b6dd17d8ebffed2461d7c282bf Mon Sep 17 00:00:00 2001 From: dpiercey Date: Wed, 5 Nov 2025 09:41:57 -0700 Subject: [PATCH] fix: add validation for dynamic tag names --- .changeset/rude-berries-tan.md | 5 +++++ .../__snapshots__/.name-cache.json | 5 +++++ .../__snapshots__/csr-sanitized.expected.md | 1 + .../error-dynamic-tag-name/__snapshots__/csr.expected.md | 1 + .../__snapshots__/dom.expected/template.hydrate.js | 1 + .../__snapshots__/dom.expected/template.js | 9 +++++++++ .../__snapshots__/html.expected/template.js | 6 ++++++ .../__snapshots__/resume-sanitized.expected.md | 1 + .../__snapshots__/resume.expected.md | 1 + .../__snapshots__/ssr-sanitized.expected.md | 1 + .../error-dynamic-tag-name/__snapshots__/ssr.expected.md | 1 + .../fixtures/error-dynamic-tag-name/template.marko | 3 +++ .../__tests__/fixtures/error-dynamic-tag-name/test.ts | 1 + packages/runtime-tags/src/common/errors.ts | 8 ++++++++ packages/runtime-tags/src/dom/control-flow.ts | 5 +++++ packages/runtime-tags/src/html/dynamic-tag.ts | 5 +++++ 16 files changed, 54 insertions(+) create mode 100644 .changeset/rude-berries-tan.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/.name-cache.json create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr-sanitized.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.hydrate.js create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.js create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/html.expected/template.js create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume-sanitized.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr-sanitized.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr.expected.md create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/template.marko create mode 100644 packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/test.ts diff --git a/.changeset/rude-berries-tan.md b/.changeset/rude-berries-tan.md new file mode 100644 index 000000000..d558ba330 --- /dev/null +++ b/.changeset/rude-berries-tan.md @@ -0,0 +1,5 @@ +--- +"@marko/runtime-tags": patch +--- + +Add dev mode validation for dynamic tag names. diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/.name-cache.json new file mode 100644 index 000000000..db413faf9 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/.name-cache.json @@ -0,0 +1,5 @@ +{ + "vars": { + "props": {} + } +} diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr-sanitized.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr-sanitized.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/csr.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.hydrate.js new file mode 100644 index 000000000..3eb9cd79d --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.hydrate.js @@ -0,0 +1 @@ +// size: 0 diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.js new file mode 100644 index 000000000..f6dc99c5b --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/dom.expected/template.js @@ -0,0 +1,9 @@ +export const $template = ""; +export const $walks = /* over(1), replace, over(2) */"b%c"; +const tagName = "hello world"; +import * as _ from "@marko/runtime-tags/debug/dom"; +const $dynamicTag = /* @__PURE__ */_._dynamic_tag("#text/0"); +export function $setup($scope) { + $dynamicTag($scope, tagName); +} +export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/html.expected/template.js new file mode 100644 index 000000000..74359227b --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/html.expected/template.js @@ -0,0 +1,6 @@ +const tagName = "hello world"; +import * as _ from "@marko/runtime-tags/debug/html"; +export default _._template("__tests__/template.marko", input => { + const $scope0_id = _._scope_id(); + _._dynamic_tag($scope0_id, "#text/0", tagName, {}, 0, 0, 0); +}); \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/resume.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr.expected.md new file mode 100644 index 000000000..5394f1302 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/__snapshots__/ssr.expected.md @@ -0,0 +1 @@ +Invalid tag name: "hello world". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores. \ No newline at end of file diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/template.marko b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/template.marko new file mode 100644 index 000000000..5e2edb7f5 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/template.marko @@ -0,0 +1,3 @@ +static const tagName = "hello world"; +${tagName} + diff --git a/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/test.ts b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/test.ts new file mode 100644 index 000000000..86ac5a951 --- /dev/null +++ b/packages/runtime-tags/src/__tests__/fixtures/error-dynamic-tag-name/test.ts @@ -0,0 +1 @@ +export const error_runtime = true; diff --git a/packages/runtime-tags/src/common/errors.ts b/packages/runtime-tags/src/common/errors.ts index 71d3763c5..4cc23e94c 100644 --- a/packages/runtime-tags/src/common/errors.ts +++ b/packages/runtime-tags/src/common/errors.ts @@ -57,6 +57,14 @@ export function assertExclusiveAttrs( } } +export function assertValidTagName(tagName: string) { + if (!/^[a-z][a-z0-9._-]*$/i.test(tagName)) { + throw new Error( + `Invalid tag name: "${tagName}". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores.`, + ); + } +} + function throwErr(msg: string) { throw new Error(msg); } diff --git a/packages/runtime-tags/src/dom/control-flow.ts b/packages/runtime-tags/src/dom/control-flow.ts index 34a9d5e5d..d097ddc87 100644 --- a/packages/runtime-tags/src/dom/control-flow.ts +++ b/packages/runtime-tags/src/dom/control-flow.ts @@ -1,3 +1,4 @@ +import { assertValidTagName } from "../common/errors"; import { forIn, forOf, forTo, forUntil } from "../common/for"; import { normalizeDynamicRenderer } from "../common/helpers"; import { DYNAMIC_TAG_SCRIPT_REGISTER_ID } from "../common/meta"; @@ -522,6 +523,10 @@ function createBranchWithTagNameOrRenderer( parentScope: Scope, parentNode: ParentNode, ) { + if (MARKO_DEBUG && typeof tagNameOrRenderer === "string") { + assertValidTagName(tagNameOrRenderer); + } + const branch = createBranch( $global, tagNameOrRenderer, diff --git a/packages/runtime-tags/src/html/dynamic-tag.ts b/packages/runtime-tags/src/html/dynamic-tag.ts index d2b353572..d50ada022 100644 --- a/packages/runtime-tags/src/html/dynamic-tag.ts +++ b/packages/runtime-tags/src/html/dynamic-tag.ts @@ -1,3 +1,4 @@ +import { assertValidTagName } from "../common/errors"; import { normalizeDynamicRenderer } from "../common/helpers"; import { DYNAMIC_TAG_SCRIPT_REGISTER_ID } from "../common/meta"; import { type Accessor, AccessorPrefix, ResumeSymbol } from "../common/types"; @@ -53,6 +54,10 @@ export let _dynamic_tag = ( let result: unknown; if (typeof renderer === "string") { + if (MARKO_DEBUG) { + assertValidTagName(renderer); + } + const input = ((inputIsArgs ? (inputOrArgs as unknown[])[0] : inputOrArgs) || {}) as Record;