} }`).runtime).toBe(
runtime,
);
}
});
test("maps literal unions to their runtime primitive types", async () => {
const { runtimeTypeOf } = await import("../src/types.ts");
expect(runtimeTypeOf('"_blank" | "_self"')).toBe("string");
expect(runtimeTypeOf("1 | 2 | 3")).toBe("number");
expect(runtimeTypeOf("true | false")).toBe("boolean");
});
// An apostrophe in prose is not a string. The brace scanner used to treat one
// as an opening quote and swallow every brace until the next apostrophe, so a
// comment like "the Input's slot" broke the whole component with a baffling
// "Unbalanced braces" error pointing at the block's first line.
test("comments may contain apostrophes without unbalancing a block", () => {
const source = `component Demo {
style {
/* The panel's own color -- do not inherit it. */
.demo {
color: red;
}
// A trailing note about the card's border.
.demo-b {
color: blue;
}
}
view {
Docs at https://example.com/a//b are not comments.
}
}
`;
const ast = parse(source);
expect(ast.name).toBe("Demo");
expect(ast.styles.join(" ")).toContain(".demo-b");
// The URL in view text must survive: `//` is only a comment at line start.
expect(JSON.stringify(ast.view)).toContain("https://example.com/a//b");
});
// A regex literal is not a string and not a pair of braces. The brace scanner
// knew about quotes and comments but had no case for regexes, so a quote inside
// one opened a phantom string that swallowed every brace until the next quote,
// and a lone `{` or `}` inside one miscounted depth. Both failed the whole
// component -- with a green build in the reported case, because the damage
// landed in generated output rather than at parse time.
test("regex literals do not unbalance a block", () => {
const mk = (body: string) =>
`page P {
load server {
${body}
return { x };
}
view {
{x}
}
}
`;
// A quote inside a regex used to open a string that ran to the next quote.
expect(parse(mk(` const x = /it's/.test("its");`)).name).toBe("P");
expect(parse(mk(` const x = /"/.test("q");`)).name).toBe("P");
// A brace inside a regex used to be counted as block depth.
expect(parse(mk(` const x = /\\{/.test("{");`)).name).toBe("P");
expect(parse(mk(` const x = /}/.test("}");`)).name).toBe("P");
// A brace quantifier is balanced, but must not be counted either.
expect(parse(mk(` const x = /^a{2,3}$/.test("aa");`)).name).toBe("P");
// A `/` inside a character class does not close the regex.
expect(parse(mk(` const x = /[/'"{]/.test("/");`)).name).toBe("P");
// The case that already worked must keep working.
expect(parse(mk(` const x = "a-b".replace(/-/g, " ");`)).name).toBe("P");
});
// Division must not be mistaken for a regex, or the scanner would swallow code
// from the `/` to the next one and lose any braces in between.
test("division is not treated as a regex literal", () => {
const source = `page P {
load server {
const half = 10 / 2;
const ratio = (a + b) / 2;
const each = items[0] / total;
if (half > 1) {
return { half };
}
return { half: 0 };
}
view {
{half}
}
}
`;
expect(parse(source).name).toBe("P");
});
// `skipTrivia` skipped `// line comments` but not `/* block comments */`, so a
// block comment between page or component members failed with a bare
// "Unexpected character '/'". Block comments inside a braced body already
// worked, which made the failure look arbitrary: the same comment moved a few
// lines parsed or did not depending on whether it sat inside a block.
test("block comments are allowed between members", () => {
const page = `page P {
/* Explains the state below. */
state x = 1
/* Explains the view below. */
view {
{x}
}
}
`;
expect(parse(page).name).toBe("P");
const component = `component C {
/* A note about this component. */
state y = 2
view {
{y}
}
}
`;
expect(parse(component).name).toBe("C");
// A block comment before the opening brace, and a multi-line one.
const spaced = `/* Leading note. */
page Q {
/*
* A multi-line note about the view.
*/
view {
ok
}
}
`;
expect(parse(spaced).name).toBe("Q");
});
// Block comments inside props {} stay a deliberate, explained error -- the
// props parser cannot represent them, and silently skipping one would drop a
// declaration the author believed was there.
test("a block comment inside props is still refused with its own message", () => {
const source = `component C {
props {
/* not allowed here */
name: string
}
view {