diff --git a/.changeset/lazy-papers-report.md b/.changeset/lazy-papers-report.md new file mode 100644 index 00000000..b0a76409 --- /dev/null +++ b/.changeset/lazy-papers-report.md @@ -0,0 +1,5 @@ +--- +"@codemod-utils/ast-template": minor +--- + +Replaced ember-template-recast with @glimmer/syntax diff --git a/packages/ast/template-tag/tests/helpers/update-templates.ts b/packages/ast/template-tag/tests/helpers/update-templates.ts index 0785abc1..5bc86b0a 100644 --- a/packages/ast/template-tag/tests/helpers/update-templates.ts +++ b/packages/ast/template-tag/tests/helpers/update-templates.ts @@ -4,6 +4,7 @@ export function removeClassAttribute(file: string): string { const traverse = AST.traverse(); const ast = traverse(file, { + // @ts-expect-error: Incorrect type AttrNode(node) { if (node.name !== 'class') { return; diff --git a/packages/ast/template/package.json b/packages/ast/template/package.json index 85543990..69d047ff 100644 --- a/packages/ast/template/package.json +++ b/packages/ast/template/package.json @@ -46,7 +46,7 @@ "test": "sh build.sh --test && mt dist-for-testing --quiet" }, "dependencies": { - "ember-template-recast": "^6.1.5" + "@glimmer/syntax": "^0.95.0" }, "devDependencies": { "@codemod-utils/tests": "workspace:*", diff --git a/packages/ast/template/src/-private/glimmer-syntax.ts b/packages/ast/template/src/-private/glimmer-syntax.ts new file mode 100644 index 00000000..03dc7ede --- /dev/null +++ b/packages/ast/template/src/-private/glimmer-syntax.ts @@ -0,0 +1,41 @@ +import { + type AST, + builders, + type NodeVisitor, + print as upstreamPrint, + traverse as upstreamTraverse, +} from '@glimmer/syntax'; + +import { type NodeInfo, Parser } from './glimmer-syntax/parser.js'; + +const NODE_INFO = new WeakMap(); + +export function parse(template: string): AST.Template { + return new Parser(template, NODE_INFO).ast; +} + +export function print(ast: AST.Node): string { + return upstreamPrint(ast, { + entityEncoding: 'raw', + // @ts-expect-error: Incorrect type + override: (ast) => { + const info = NODE_INFO.get(ast); + + if (info) { + return info.parse_result.print(ast); + } + }, + }); +} + +export function traverse() { + return function (file: string, visitMethods: NodeVisitor = {}): AST.Template { + const { ast } = new Parser(file, NODE_INFO); + + upstreamTraverse(ast, visitMethods); + + return ast; + }; +} + +export { builders }; diff --git a/packages/ast/template/src/-private/glimmer-syntax/parser.ts b/packages/ast/template/src/-private/glimmer-syntax/parser.ts new file mode 100644 index 00000000..bc4b0d4c --- /dev/null +++ b/packages/ast/template/src/-private/glimmer-syntax/parser.ts @@ -0,0 +1,1508 @@ +import { + type AST, + builders, + preprocess, + print as _print, + traverse, +} from '@glimmer/syntax'; + +import { getLines, sortByLoc, sourceForLoc } from './utils.js'; + +type QuoteType = '"' | "'"; + +interface AnnotatedAttrNode extends AST.AttrNode { + /** + * Supports cases like `` or `
` + */ + isValueless?: boolean; + + /** + * TextNode values can use single, double, or no quotes + * `type=input` vs `type='input'` vs `type="input"` + * ConcatStatement values can use single or double quotes + * `class='thing {{get this classNames}}'` vs `class="thing {{get this classNames}}"` + * MustacheStatements never use quotes + */ + quoteType?: QuoteType | null; +} + +interface AnnotatedStringLiteral extends AST.StringLiteral { + quoteType?: QuoteType; +} + +/** + * The glimmer printer doesn't have any formatting suppport. It always uses + * double quotes, and won't print attrs without a value. To choose quote types + * or omit the value, we have to do it ourselves. + */ +function useCustomPrinter(node: AST.BaseNode): boolean { + switch (node.type) { + case 'AttrNode': { + const n = node as AnnotatedAttrNode; + return Boolean(n.isValueless) || n.quoteType !== undefined; + } + + case 'StringLiteral': { + return Boolean((node as AnnotatedStringLiteral).quoteType); + } + + default: { + return false; + } + } +} + +const leadingWhitespace = /(^\s+)/; +const attrNodeParts = /(^[^=]+)(\s+)?(=)?(\s+)?(['"])?(\S+)?/; +const hashPairParts = /(^[^=]+)(\s+)?=(\s+)?(\S+)/; +const invalidUnquotedAttrValue = /[^-.a-zA-Z0-9]/; + +const voidTagNames = new Set([ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +]); + +/* + This is needed to address issues in the glimmer-vm AST _before_ any of the nodes and node + values are cached. The specific issues being worked around are: + + * https://github.com/glimmerjs/glimmer-vm/pull/953 + * https://github.com/glimmerjs/glimmer-vm/pull/954 +*/ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any +function fixASTIssues(sourceLines: any, ast: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + traverse(ast, { + AttrNode(attr: AST.AttrNode) { + const node = attr as AnnotatedAttrNode; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const source = sourceForLoc(sourceLines, node.loc); + const attrNodePartsResults = source.match(attrNodeParts); + if (attrNodePartsResults === null) { + throw new Error(`Could not match attr node parts for ${source}`); + } + const [, , , equals, , quote] = attrNodePartsResults; + const isValueless = !equals; + + // TODO: manually working around https://github.com/glimmerjs/glimmer-vm/pull/953 + if ( + isValueless && + node.value.type === 'TextNode' && + node.value.chars === '' + ) { + // \n is not valid within an attribute name (it would indicate two attributes) + // always assume the attribute ends on the starting line + const { + start: { line, column }, + } = node.loc; + node.loc = builders.loc(line, column, line, column + node.name.length); + } + + node.isValueless = isValueless; + node.quoteType = (quote as QuoteType) || null; + }, + + StringLiteral(lit) { + const quotes = /^['"]/; + const node = lit as AnnotatedStringLiteral; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const source = sourceForLoc(sourceLines, node.loc); + if (!source.match(quotes)) { + throw new Error('Invalid string literal found'); + } + node.quoteType = source[0] as QuoteType; + }, + + TextNode(node, path) { + if (path.parentNode === null) { + throw new Error( + 'ember-template-recast: Error while sanitizing input AST: found TextNode with no parentNode', + ); + } + + switch (path.parentNode.type) { + case 'AttrNode': { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const source = sourceForLoc(sourceLines, node.loc); + if ( + node.chars.length > 0 && + ((source.startsWith(`'`) && source.endsWith(`'`)) || + (source.startsWith(`"`) && source.endsWith(`"`))) + ) { + const { start, end } = node.loc; + node.loc = builders.loc( + start.line, + start.column + 1, + end.line, + end.column - 1, + ); + } + break; + } + case 'ConcatStatement': { + const parent = path.parentNode; + const isFirstPart = parent.parts.indexOf(node) === 0; + + const { start, end } = node.loc; + if ( + isFirstPart && + node.loc.start.column > path.parentNode.loc.start.column + 1 + ) { + // TODO: manually working around https://github.com/glimmerjs/glimmer-vm/pull/954 + node.loc = builders.loc( + start.line, + start.column - 1, + end.line, + end.column, + ); + } else if (isFirstPart && node.chars.charAt(0) === '\n') { + node.loc = builders.loc( + start.line, + start.column + 1, + end.line, + end.column, + ); + } + } + } + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return ast; +} + +export interface NodeInfo { + hadHash?: boolean; + hadParams?: boolean; + hashSource?: string; + node: AST.Node; + original: AST.Node; + paramsSource?: string; + parse_result: Parser; + postHashWhitespace?: string; + postParamsWhitespace?: string; + postPathWhitespace?: string; + source: string; +} + +export class Parser { + private _originalAst: AST.Template; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private ancestor = new Map(); + public ast: AST.Template; + private dirtyFields = new Map>(); + private nodeInfo: WeakMap; + private source: string[]; + + constructor( + template: string, + nodeInfo: WeakMap = new WeakMap(), + ) { + let ast = preprocess(template, { + mode: 'codemod', + }); + + const source = getLines(template); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ast = fixASTIssues(source, ast); + this.source = source; + this._originalAst = ast; + + this.nodeInfo = nodeInfo; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.ast = this.wrapNode(null, ast); + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + private _rebuildParamsHash( + ast: + | AST.MustacheStatement + | AST.SubExpression + | AST.ElementModifierStatement + | AST.BlockStatement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nodeInfo: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dirtyFields: any, + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { original } = nodeInfo; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (dirtyFields.has('hash')) { + if (ast.hash.pairs.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.hashSource = ''; + + if (ast.params.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postPathWhitespace = ''; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postParamsWhitespace = ''; + } + } else { + let joinWith; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (original.hash.pairs.length > 1) { + joinWith = this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + start: original.hash.pairs[0].loc.end, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.hash.pairs[1].loc.start, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (nodeInfo.hadParams) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + joinWith = nodeInfo.postPathWhitespace; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (nodeInfo.hadHash) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + joinWith = nodeInfo.postParamsWhitespace; + } else { + joinWith = ' '; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (joinWith.trim() !== '') { + // if the autodetection above resulted in some non whitespace + // values, reset to `' '` + joinWith = ' '; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.hashSource = ast.hash.pairs + .map((pair: AST.HashPair) => { + return this.print(pair); + }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .join(joinWith); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!nodeInfo.hadHash) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + nodeInfo.postParamsWhitespace = joinWith; + } + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + dirtyFields.delete('hash'); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (dirtyFields.has('params')) { + let joinWith; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (original.params.length > 1) { + joinWith = this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + start: original.params[0].loc.end, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.params[1].loc.start, + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (nodeInfo.hadParams) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + joinWith = nodeInfo.postPathWhitespace; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (nodeInfo.hadHash) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + joinWith = nodeInfo.postParamsWhitespace; + } else { + joinWith = ' '; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (joinWith.trim() !== '') { + // if the autodetection above resulted in some non whitespace + // values, reset to `' '` + joinWith = ' '; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.paramsSource = ast.params + .map((param) => this.print(param)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .join(joinWith); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (nodeInfo.hadParams && ast.params.length === 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postPathWhitespace = ''; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (!nodeInfo.hadParams && ast.params.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + nodeInfo.postPathWhitespace = joinWith; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + dirtyFields.delete('params'); + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any + private _updateNodeInfoForParamsHash(_ast: any, nodeInfo: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { original } = nodeInfo; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const hadParams = (nodeInfo.hadParams = original.params.length > 0); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const hadHash = (nodeInfo.hadHash = original.hash.pairs.length > 0); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postPathWhitespace = hadParams + ? this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + start: original.path.loc.end, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.params[0].loc.start, + }) + : ''; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.paramsSource = hadParams + ? this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + start: original.params[0].loc.start, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.params[original.params.length - 1].loc.end, + }) + : ''; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postParamsWhitespace = hadHash + ? this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + start: hadParams + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + original.params[original.params.length - 1].loc.end + : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + original.path.loc.end, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.hash.loc.start, + }) + : ''; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.hashSource = hadHash ? this.sourceForLoc(original.hash.loc) : ''; + + const postHashSource = this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + start: hadHash + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + original.hash.loc.end + : hadParams + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + original.params[original.params.length - 1].loc.end + : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + original.path.loc.end, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: original.loc.end, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postHashWhitespace = ''; + const postHashWhitespaceMatch = postHashSource.match(leadingWhitespace); + if (postHashWhitespaceMatch) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + nodeInfo.postHashWhitespace = postHashWhitespaceMatch[0]; + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any + private markAsDirty(node: any, property: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + let dirtyFields = this.dirtyFields.get(node); + if (dirtyFields === undefined) { + dirtyFields = new Set(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.dirtyFields.set(node, dirtyFields); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + dirtyFields.add(property); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const ancestor = this.ancestor.get(node); + if (ancestor !== null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this.markAsDirty(ancestor.node, ancestor.key); + } + } + + print(_ast: AST.Node = this._originalAst): string { + if (!_ast) { + return ''; + } + + const nodeInfo = this.nodeInfo.get(_ast); + + if (nodeInfo === undefined) { + return this.printUserSuppliedNode(_ast); + } + + // this ensures that we are operating on the actual node and not a + // proxy (we can get Proxies here when transforms splice body/children) + const ast = nodeInfo.node; + + // make a copy of the dirtyFields, so we can easily track + // unhandled dirtied fields + const dirtyFields = new Set(this.dirtyFields.get(ast)); + if (dirtyFields.size === 0 && nodeInfo !== undefined) { + return nodeInfo.source; + } + + // TODO: splice the original source **excluding** "children" + // based on dirtyFields + const output = []; + + const { original } = nodeInfo; + + switch (ast.type) { + // @ts-expect-error: Incorrect type + case 'Program': + case 'Block': + case 'Template': + { + let bodySource = nodeInfo.source; + + if (dirtyFields.has('body')) { + bodySource = ast.body.map((node) => this.print(node)).join(''); + + dirtyFields.delete('body'); + } + + output.push(bodySource); + } + break; + case 'ElementNode': + { + const element = original as AST.ElementNode; + const { selfClosing, children } = element; + const hadChildren = children.length > 0; + const hadBlockParams = element.blockParams.length > 0; + + let openSource = `<${element.tag}`; + + const originalOpenParts = [ + ...element.attributes, + ...element.modifiers, + ...element.comments, + ].sort(sortByLoc); + + let postTagWhitespace; + if (originalOpenParts.length > 0) { + postTagWhitespace = this.sourceForLoc({ + start: { + line: element.loc.start.line, + column: + element.loc.start.column + 1 /* < */ + element.tag.length, + }, + // @ts-expect-error: Incorrect type + end: originalOpenParts[0].loc.start, + }); + } else if (selfClosing) { + postTagWhitespace = nodeInfo.source.substring( + openSource.length, + nodeInfo.source.length - 2, + ); + } else { + postTagWhitespace = ''; + } + + let openPartsSource = originalOpenParts.reduce( + (acc, part, index, parts) => { + const partSource = this.sourceForLoc(part.loc); + + if (index === parts.length - 1) { + return acc + partSource; + } + + let joinPartWith = this.sourceForLoc({ + // @ts-expect-error: Incorrect type + start: parts[index].loc.end, + // @ts-expect-error: Incorrect type + end: parts[index + 1].loc.start, + }); + + if (joinPartWith.trim() !== '') { + // if the autodetection above resulted in some non whitespace + // values, reset to `' '` + joinPartWith = ' '; + } + + return acc + partSource + joinPartWith; + }, + '', + ); + + let postPartsWhitespace = ''; + if (originalOpenParts.length > 0) { + const postPartsSource = this.sourceForLoc({ + // @ts-expect-error: Incorrect type + start: originalOpenParts[originalOpenParts.length - 1].loc.end, + end: hadChildren + ? // @ts-expect-error: Incorrect type + element.children[0].loc.start + : element.loc.end, + }); + + const matchedWhitespace = postPartsSource.match(leadingWhitespace); + if (matchedWhitespace) { + postPartsWhitespace = matchedWhitespace[0]; + } + } else if (hadBlockParams) { + const postPartsSource = this.sourceForLoc({ + start: { + line: element.loc.start.line, + column: element.loc.start.column + 1 + element.tag.length, + }, + end: hadChildren + ? // @ts-expect-error: Incorrect type + element.children[0].loc.start + : element.loc.end, + }); + + const matchedWhitespace = postPartsSource.match(leadingWhitespace); + if (matchedWhitespace) { + postPartsWhitespace = matchedWhitespace[0]; + } + } + + let blockParamsSource = ''; + let postBlockParamsWhitespace = ''; + if (element.blockParams.length > 0) { + const blockParamStartIndex = nodeInfo.source.indexOf('as |'); + const blockParamsEndIndex = nodeInfo.source.indexOf( + '|', + blockParamStartIndex + 4, + ); + blockParamsSource = nodeInfo.source.substring( + blockParamStartIndex, + blockParamsEndIndex + 1, + ); + + // Match closing index after start of block params to avoid closing tag if /> or > encountered in string + const closeOpenIndex = + nodeInfo.source + .substring(blockParamStartIndex) + .indexOf(selfClosing ? '/>' : '>') + blockParamStartIndex; + postBlockParamsWhitespace = nodeInfo.source.substring( + blockParamsEndIndex + 1, + closeOpenIndex, + ); + } + + let closeOpen = selfClosing ? `/>` : `>`; + + let childrenSource = hadChildren + ? this.sourceForLoc({ + // @ts-expect-error: Incorrect type + start: element.children[0].loc.start, + // @ts-expect-error: Incorrect type + end: element.children[children.length - 1].loc.end, + }) + : ''; + + let closeSource = selfClosing + ? '' + : voidTagNames.has(element.tag) + ? '' + : ``; + + if (dirtyFields.has('tag')) { + openSource = `<${ast.tag}`; + + closeSource = selfClosing + ? '' + : voidTagNames.has(ast.tag) + ? '' + : ``; + + dirtyFields.delete('tag'); + } + + if (dirtyFields.has('children')) { + childrenSource = ast.children + .map((child) => this.print(child)) + .join(''); + + if (selfClosing) { + closeOpen = `>`; + closeSource = ``; + ast.selfClosing = false; + + if (originalOpenParts.length === 0 && postTagWhitespace === ' ') { + postTagWhitespace = ''; + } + + if (originalOpenParts.length > 0 && postPartsWhitespace === ' ') { + postPartsWhitespace = ''; + } + } + + dirtyFields.delete('children'); + } + + if ( + dirtyFields.has('attributes') || + dirtyFields.has('comments') || + dirtyFields.has('modifiers') + ) { + const openParts = [ + ...ast.attributes, + ...ast.modifiers, + ...ast.comments, + ].sort(sortByLoc); + + openPartsSource = openParts.reduce((acc, part, index, parts) => { + const partSource = this.print(part); + + if (index === parts.length - 1) { + return acc + partSource; + } + + let joinPartWith = this.sourceForLoc({ + // @ts-expect-error: Incorrect type + start: parts[index].loc.end, + // @ts-expect-error: Incorrect type + end: parts[index + 1].loc.start, + }); + + if (joinPartWith === '' || joinPartWith.trim() !== '') { + // if the autodetection above resulted in some non whitespace + // values, reset to `' '` + joinPartWith = ' '; + } + + return acc + partSource + joinPartWith; + }, ''); + + if (originalOpenParts.length === 0) { + postTagWhitespace = ' '; + } + + if (openParts.length === 0 && originalOpenParts.length > 0) { + postTagWhitespace = ''; + } + + if (openParts.length > 0 && ast.selfClosing) { + postPartsWhitespace = postPartsWhitespace || ' '; + } + + dirtyFields.delete('attributes'); + dirtyFields.delete('comments'); + dirtyFields.delete('modifiers'); + } + + if (dirtyFields.has('blockParams')) { + if (ast.blockParams.length === 0) { + blockParamsSource = ''; + postPartsWhitespace = ''; + } else { + blockParamsSource = `as |${ast.blockParams.join(' ')}|`; + + // ensure we have at least a space + postPartsWhitespace = postPartsWhitespace || ' '; + } + + dirtyFields.delete('blockParams'); + } + + output.push( + openSource, + postTagWhitespace, + openPartsSource, + postPartsWhitespace, + blockParamsSource, + postBlockParamsWhitespace, + closeOpen, + childrenSource, + closeSource, + ); + } + break; + case 'MustacheStatement': + case 'ElementModifierStatement': + case 'SubExpression': + { + this._updateNodeInfoForParamsHash(ast, nodeInfo); + + let openSource = this.sourceForLoc({ + start: original.loc.start, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: (original as any).path.loc.end, + }); + + let endSource = this.sourceForLoc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + start: nodeInfo.hadHash + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (original as any).hash.loc.end + : nodeInfo.hadParams + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (original as any).params[(original as any).params.length - 1] + .loc.end + : // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (original as any).path.loc.end, + end: original.loc.end, + }).trimLeft(); + + if (dirtyFields.has('path')) { + openSource = + this.sourceForLoc({ + start: original.loc.start, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + end: (original as any).path.loc.start, + }) + this.print(ast.path); + + dirtyFields.delete('path'); + } + + if (dirtyFields.has('type')) { + // we only support going from SubExpression -> MustacheStatement + if ( + original.type !== 'SubExpression' || + ast.type !== 'MustacheStatement' + ) { + throw new Error( + `ember-template-recast only supports updating the 'type' for SubExpression to MustacheStatement (you attempted to change ${original.type} to ${ast.type})`, + ); + } + + // TODO: this is a logic error, assumes ast.path is a PathExpression but it could be a number of other things + openSource = `{{${(ast.path as AST.PathExpression).original}`; + endSource = '}}'; + + dirtyFields.delete('type'); + } + + this._rebuildParamsHash(ast, nodeInfo, dirtyFields); + + output.push( + openSource, + nodeInfo.postPathWhitespace, + nodeInfo.paramsSource, + nodeInfo.postParamsWhitespace, + nodeInfo.hashSource, + nodeInfo.postHashWhitespace, + endSource, + ); + } + break; + case 'ConcatStatement': + { + let partsSource = this.sourceForLoc({ + start: { + line: original.loc.start.line, + column: original.loc.start.column + 1, + }, + + end: { + line: original.loc.end.line, + column: original.loc.end.column - 1, + }, + }); + + if (dirtyFields.has('parts')) { + partsSource = ast.parts.map((part) => this.print(part)).join(''); + + dirtyFields.delete('parts'); + } + + output.push(partsSource); + } + break; + case 'BlockStatement': + { + const block = original as AST.BlockStatement; + + this._updateNodeInfoForParamsHash(ast, nodeInfo); + + const hadProgram = block.program.body.length > 0; + const hadProgramBlockParams = block.program.blockParams.length > 0; + + let openSource = this.sourceForLoc({ + start: block.loc.start, + end: block.path.loc.end, + }); + + let blockParamsSource = ''; + let postBlockParamsWhitespace = ''; + if (hadProgramBlockParams) { + const blockParamsSourceScratch = this.sourceForLoc({ + start: nodeInfo.hadHash + ? block.hash.loc.end + : nodeInfo.hadParams + ? // @ts-expect-error: Incorrect type + block.params[block.params.length - 1].loc.end + : block.path.loc.end, + end: original.loc.end, + }); + + const indexOfAsPipe = blockParamsSourceScratch.indexOf('as |'); + const indexOfEndPipe = blockParamsSourceScratch.indexOf( + '|', + indexOfAsPipe + 4, + ); + + blockParamsSource = blockParamsSourceScratch.substring( + indexOfAsPipe, + indexOfEndPipe + 1, + ); + + const postBlockParamsWhitespaceMatch = blockParamsSourceScratch + .substring(indexOfEndPipe + 1) + .match(leadingWhitespace); + if (postBlockParamsWhitespaceMatch) { + postBlockParamsWhitespace = postBlockParamsWhitespaceMatch[0]; + } + } + + let openEndSource; + { + const openEndSourceScratch = this.sourceForLoc({ + start: nodeInfo.hadHash + ? block.hash.loc.end + : nodeInfo.hadParams + ? // @ts-expect-error: Incorrect type + block.params[block.params.length - 1].loc.end + : block.path.loc.end, + end: block.loc.end, + }); + + let startingOffset = 0; + if (hadProgramBlockParams) { + const indexOfAsPipe = openEndSourceScratch.indexOf('as |'); + const indexOfEndPipe = openEndSourceScratch.indexOf( + '|', + indexOfAsPipe + 4, + ); + + startingOffset = indexOfEndPipe + 1; + } + + const indexOfFirstCurly = openEndSourceScratch.indexOf('}'); + const indexOfSecondCurly = openEndSourceScratch.indexOf( + '}', + indexOfFirstCurly + 1, + ); + + openEndSource = openEndSourceScratch + .substring(startingOffset, indexOfSecondCurly + 1) + .trimLeft(); + } + + let programSource = hadProgram + ? this.sourceForLoc(block.program.loc) + : ''; + + let inversePreamble = ''; + if (block.inverse) { + if (hadProgram) { + inversePreamble = this.sourceForLoc({ + start: block.program.loc.end, + end: block.inverse.loc.start, + }); + } else { + const openEndSourceScratch = this.sourceForLoc({ + start: nodeInfo.hadHash + ? block.hash.loc.end + : nodeInfo.hadParams + ? // @ts-expect-error: Incorrect type + block.params[block.params.length - 1].loc.end + : block.path.loc.end, + end: block.loc.end, + }); + + const indexOfFirstCurly = openEndSourceScratch.indexOf('}'); + const indexOfSecondCurly = openEndSourceScratch.indexOf( + '}', + indexOfFirstCurly + 1, + ); + const indexOfThirdCurly = openEndSourceScratch.indexOf( + '}', + indexOfSecondCurly + 1, + ); + const indexOfFourthCurly = openEndSourceScratch.indexOf( + '}', + indexOfThirdCurly + 1, + ); + + inversePreamble = openEndSourceScratch.substring( + indexOfSecondCurly + 1, + indexOfFourthCurly + 1, + ); + } + } + + // GH #149 + // In the event we're dealing with a chain of if/else-if/else, the inverse + // should encompass the entirety of the chain. Sadly, the loc param of + // original.inverse in this case only captures the block of the first inverse + // not the entire chain. We instead look at the loc param of the nested body + // node, which does report the entire chain. + // In this case, because it also includes the preamble, we must also trim + // that from our final inverse source. + let inverseSource; + if (block.inverse && block.inverse.chained) { + // @ts-expect-error: Incorrect type + inverseSource = this.sourceForLoc(block.inverse.body[0].loc) || ''; + inverseSource = inverseSource.slice(inversePreamble.length); + } else { + inverseSource = block.inverse + ? this.sourceForLoc(block.inverse.loc) + : ''; + } + + let endSource = ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + if (!(ast as any).wasChained) { + const firstOpenCurlyFromEndIndex = nodeInfo.source.lastIndexOf('{'); + const secondOpenCurlyFromEndIndex = nodeInfo.source.lastIndexOf( + '{', + firstOpenCurlyFromEndIndex - 1, + ); + + endSource = nodeInfo.source.substring(secondOpenCurlyFromEndIndex); + } + + this._rebuildParamsHash(ast, nodeInfo, dirtyFields); + + if (dirtyFields.has('path')) { + openSource = + this.sourceForLoc({ + start: original.loc.start, + end: block.path.loc.start, + }) + _print(ast.path); + + // TODO: this is a logic error + const pathIndex = endSource.indexOf( + (block.path as AST.PathExpression).original, + ); + endSource = + endSource.slice(0, pathIndex) + + (ast.path as AST.PathExpression).original + + endSource.slice( + pathIndex + (block.path as AST.PathExpression).original.length, + ); + + dirtyFields.delete('path'); + } + + if (dirtyFields.has('program')) { + const programDirtyFields = new Set( + this.dirtyFields.get(ast.program), + ); + + if (programDirtyFields.has('blockParams')) { + if (ast.program.blockParams.length === 0) { + nodeInfo.postHashWhitespace = ''; + blockParamsSource = ''; + } else { + nodeInfo.postHashWhitespace = + nodeInfo.postHashWhitespace || ' '; + blockParamsSource = `as |${ast.program.blockParams.join(' ')}|`; + } + programDirtyFields.delete('blockParams'); + } + + if (programDirtyFields.has('body')) { + programSource = ast.program.body + .map((child) => this.print(child)) + .join(''); + + programDirtyFields.delete('body'); + } + + if (programDirtyFields.size > 0) { + throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Unhandled mutations for ${ast.program.type}: ${Array.from(programDirtyFields)}`, + ); + } + + dirtyFields.delete('program'); + } + + if (dirtyFields.has('inverse')) { + if (!ast.inverse) { + inverseSource = ''; + inversePreamble = ''; + } else { + if (ast.inverse.chained) { + inversePreamble = ''; + const inverseBody = ast.inverse.body[0]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (inverseBody as any).wasChained = true; + inverseSource = this.print(inverseBody); + } else { + inverseSource = ast.inverse.body + .map((child) => this.print(child)) + .join(''); + } + + if (!block.inverse) { + // TODO: detect {{else}} vs {{else if foo}} + inversePreamble = '{{else}}'; + } + } + + dirtyFields.delete('inverse'); + } + + output.push( + openSource, + nodeInfo.postPathWhitespace, + nodeInfo.paramsSource, + nodeInfo.postParamsWhitespace, + nodeInfo.hashSource, + nodeInfo.postHashWhitespace, + blockParamsSource, + postBlockParamsWhitespace, + openEndSource, + programSource, + inversePreamble, + inverseSource, + endSource, + ); + } + break; + case 'HashPair': + { + const hashPair = original as AST.HashPair; + const { source } = nodeInfo; + const hashPairPartsResult = source.match(hashPairParts); + if (hashPairPartsResult === null) { + throw new Error('Could not match hash pair parts'); + } + // eslint-disable-next-line prefer-const + let [, keySource, postKeyWhitespace, postEqualsWhitespace] = + hashPairPartsResult; + let valueSource = this.sourceForLoc(hashPair.value.loc); + + if (dirtyFields.has('key')) { + keySource = ast.key; + + dirtyFields.delete('key'); + } + + if (dirtyFields.has('value')) { + valueSource = this.print(ast.value); + + dirtyFields.delete('value'); + } + + output.push( + keySource, + postKeyWhitespace, + '=', + postEqualsWhitespace, + valueSource, + ); + } + break; + case 'AttrNode': + { + const attrNode = original as AST.AttrNode; + const { source } = nodeInfo; + const attrNodePartsResults = source.match(attrNodeParts); + if (attrNodePartsResults === null) { + throw new Error(`Could not match attr node parts for ${source}`); + } + + let [ + , + nameSource, + // eslint-disable-next-line prefer-const + postNameWhitespace, + equals, + // eslint-disable-next-line prefer-const + postEqualsWhitespace, + quote, + ] = attrNodePartsResults; + let valueSource = this.sourceForLoc(attrNode.value.loc); + // Source of ConcatStatements includes their quotes, + // but source of an AttrNode's TextNode value does not. + // Normalize on not including them, then always printing them ourselves: + if (attrNode.value.type === 'ConcatStatement') { + valueSource = valueSource.slice(1, -1); + } + + const node = ast as AnnotatedAttrNode; + + if (dirtyFields.has('name')) { + nameSource = node.name; + dirtyFields.delete('name'); + } + + if (dirtyFields.has('quoteType')) { + // Ensure the quote type they've specified is valid for the value + if (node.value.type === 'MustacheStatement' && node.quoteType) { + throw new Error( + 'Mustache statements should not be quoted as attribute values', + ); + } else if ( + node.value.type === 'ConcatStatement' && + !node.quoteType + ) { + throw new Error( + 'ConcatStatements must be quoted as attribute values', + ); + } else if ( + node.value.type == 'TextNode' && + !node.quoteType && + node.value.chars.match(invalidUnquotedAttrValue) + ) { + throw new Error( + `\`${node.value.chars}\` is invalid as an unquoted attribute value. Alphanumeric, hyphens, and periods only`, + ); + } + quote = node.quoteType || ''; // null => empty string + } else if (dirtyFields.has('value')) { + // They updated the value without choosing a quote type. We'll use the previous quote + // type or default to double quote if necessary + if (node.value.type === 'MustacheStatement') { + quote = ''; + } else if ( + node.value.type === 'TextNode' && + node.quoteType === null && + !node.value.chars.match(invalidUnquotedAttrValue) + ) { + // If old value was unquoted, and new value is also ok as unquoted, preserve that. + quote = ''; + } else { + quote = quote || '"'; + } + } + dirtyFields.delete('quoteType'); + + if (dirtyFields.has('isValueless')) { + if (node.isValueless) { + equals = ''; + quote = ''; + valueSource = ''; + dirtyFields.delete('isValueless'); + dirtyFields.delete('value'); + } else { + equals = '='; + if (node.value.type !== 'MustacheStatement' && !quote) { + quote = '"'; + } + dirtyFields.delete('isValueless'); + } + } + + if (dirtyFields.has('value')) { + equals = '='; + // If they created a ConcatStatement node, we need to print it ourselves here. + // Otherwise, since it has no nodeInfo, it will print using the glimmer printer + // which hardcodes double quotes. + if (node.value.type === 'ConcatStatement') { + valueSource = node.value.parts + .map((part) => this.print(part)) + .join(''); + } else { + valueSource = this.print(node.value); + } + } + dirtyFields.delete('value'); + + output.push( + nameSource, + postNameWhitespace, + equals, + postEqualsWhitespace, + quote, + valueSource, + quote, + ); + } + break; + case 'PathExpression': + { + let { source } = nodeInfo; + + if (dirtyFields.has('original')) { + source = ast.original; + dirtyFields.delete('original'); + } + + output.push(source); + } + break; + case 'MustacheCommentStatement': + case 'CommentStatement': + { + const commentStatement = original as AST.CommentStatement; + const indexOfValue = nodeInfo.source.indexOf(commentStatement.value); + const openSource = nodeInfo.source.substring(0, indexOfValue); + let valueSource = commentStatement.value; + const endSource = nodeInfo.source.substring( + indexOfValue + valueSource.length, + ); + + if (dirtyFields.has('value')) { + valueSource = ast.value; + + dirtyFields.delete('value'); + } + + output.push(openSource, valueSource, endSource); + } + break; + case 'TextNode': + { + let { source } = nodeInfo; + + if (dirtyFields.has('chars')) { + source = ast.chars; + dirtyFields.delete('chars'); + } + + output.push(source); + } + break; + case 'StringLiteral': + { + const node = ast as AnnotatedStringLiteral; + output.push(node.quoteType, node.value, node.quoteType); + } + break; + case 'BooleanLiteral': + case 'NumberLiteral': + case 'NullLiteral': + { + let { source } = nodeInfo; + + if (dirtyFields.has('value')) { + source = ast.value?.toString() || ''; + dirtyFields.delete('value'); + } + + output.push(source); + } + break; + default: + throw new Error( + `ember-template-recast does not have the ability to update ${original.type}. Please open an issue so we can add support.`, + ); + } + + for (const field of dirtyFields.values()) { + if (field in Object.keys(original)) { + throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `ember-template-recast could not handle the mutations of \`${Array.from( + dirtyFields, + )}\` on ${original.type}`, + ); + } + } + + return output.join(''); + } + + // User-created nodes will have no nodeInfo, but we support + // formatting properties that the glimmer printer does not. + // If the user-created node specifies no custom formatting, + // just use the glimmer printer. + // These overrides could go away if glimmer had a concrete + // syntax tree type and printer. + printUserSuppliedNode(_ast: AST.Node): string { + switch (_ast.type) { + case 'StringLiteral': + { + const quote = (_ast as AnnotatedStringLiteral).quoteType || '"'; + return quote + _ast.value + quote; + } + // @ts-expect-error: Incorrect type + break; + case 'AttrNode': + { + const node = _ast as AnnotatedAttrNode; + if (node.isValueless) { + if (node.value.type !== 'TextNode' || node.value.chars !== '') { + throw new Error( + 'The value property of a valueless attr must be an empty TextNode', + ); + } + return node.name; + } + if ( + node.isValueless === undefined && + node.value.type === 'TextNode' && + node.value.chars === '' + ) { + return node.name; + } + switch (node.value.type) { + case 'MustacheStatement': + return node.name + '=' + this.print(node.value); + // @ts-expect-error: Incorrect type + break; + case 'ConcatStatement': + { + const value = node.value.parts + .map((part) => this.print(part)) + .join(''); + const quote = node.quoteType || '"'; + return node.name + '=' + quote + value + quote; + } + // @ts-expect-error: Incorrect type + break; + case 'TextNode': + { + if ( + node.quoteType === null && + node.value.chars.match(invalidUnquotedAttrValue) + ) { + throw new Error( + `You specified a quoteless attribute \`${node.value.chars}\`, which is invalid without quotes`, + ); + } + let quote: string; + if (node.quoteType === null) { + quote = ''; + } else { + quote = node.quoteType || '"'; + } + return node.name + '=' + quote + node.value.chars + quote; + } + // @ts-expect-error: Incorrect type + break; + } + } + // @ts-expect-error: Incorrect type + break; + default: + return _print(_ast, { + entityEncoding: 'raw', + // @ts-expect-error: Incorrect type + override: (ast) => { + if (this.nodeInfo.has(ast) || useCustomPrinter(ast)) { + return this.print(ast); + } + }, + }); + } + } + + /* + Used to associate the original source with a given node (while wrapping AST nodes + in a proxy). + */ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any + private sourceForLoc(loc: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return sourceForLoc(this.source, loc); + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any + private wrapNode(ancestor: any, node: any) { + this.ancestor.set(node, ancestor); + + const nodeInfo = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + node, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + original: JSON.parse(JSON.stringify(node)), + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + source: this.sourceForLoc(node.loc), + parse_result: this, + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.nodeInfo.set(node, nodeInfo); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const hasLocInfo = !!node.loc; + const propertyProxyMap = new Map(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const proxy = new Proxy(node, { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + get: (target, property) => { + if (propertyProxyMap.has(property)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return propertyProxyMap.get(property); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Reflect.get(target, property); + }, + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + set: (target, property, value) => { + if (propertyProxyMap.has(property)) { + propertyProxyMap.set(property, value); + } + + Reflect.set(target, property, value); + + if (hasLocInfo) { + this.markAsDirty(node, property); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this.markAsDirty(ancestor.node, ancestor.key); + } + + return true; + }, + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + deleteProperty: (target, property) => { + if (propertyProxyMap.has(property)) { + propertyProxyMap.delete(property); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const result = Reflect.deleteProperty(target, property); + + if (hasLocInfo) { + this.markAsDirty(node, property); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this.markAsDirty(ancestor.node, ancestor.key); + } + + return result; + }, + }); + + // this is needed in order to handle splicing of Template.body (which + // happens when during replacement) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.nodeInfo.set(proxy, nodeInfo); + + for (const key in node) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const value = node[key]; + + if (key !== 'loc' && typeof value === 'object' && value !== null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const propertyProxy = this.wrapNode({ node, key }, value); + + propertyProxyMap.set(key, propertyProxy); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return proxy; + } +} diff --git a/packages/ast/template/src/-private/glimmer-syntax/utils.ts b/packages/ast/template/src/-private/glimmer-syntax/utils.ts new file mode 100644 index 00000000..0b1d88e5 --- /dev/null +++ b/packages/ast/template/src/-private/glimmer-syntax/utils.ts @@ -0,0 +1,94 @@ +import type { AST } from '@glimmer/syntax'; + +export function getLines(source: string): string[] { + const result = source.match(/(.*?(?:\r\n?|\n|$))/gm); + + if (!result) { + throw new Error('could not parse source'); + } + + return result.slice(0, -1); +} + +function isSyntheticWithNoLocation(node: AST.Node): boolean { + if (node && node.loc) { + const { start, end } = node.loc; + + return ( + node.loc.module === '(synthetic)' && + start.column === end.column && + start.line === end.line + ); + } + + return false; +} + +export function sortByLoc(a: AST.Node, b: AST.Node): -1 | 0 | 1 { + // be conservative about the location where a new node is inserted + if (isSyntheticWithNoLocation(a) || isSyntheticWithNoLocation(b)) { + return 0; + } + + if (a.loc.start.line < b.loc.start.line) { + return -1; + } + + if ( + a.loc.start.line === b.loc.start.line && + a.loc.start.column < b.loc.start.column + ) { + return -1; + } + + if ( + a.loc.start.line === b.loc.start.line && + a.loc.start.column === b.loc.start.column + ) { + return 0; + } + + return 1; +} + +export function sourceForLoc( + source: string | string[], + loc?: AST.SourceLocation, +): string { + if (!loc) { + return ''; + } + + const sourceLines = Array.isArray(source) ? source : getLines(source); + + const firstLine = loc.start.line - 1; + const lastLine = loc.end.line - 1; + const firstColumn = loc.start.column; + const lastColumn = loc.end.column; + + const string = []; + let currentLine = firstLine - 1; + let line; + + while (currentLine < lastLine) { + currentLine++; + // for templates that are completely empty the outer Template loc is line + // 0, column 0 for both start and end defaulting to empty string prevents + // more complicated logic below + line = sourceLines[currentLine] || ''; + + if (currentLine === firstLine) { + if (firstLine === lastLine) { + string.push(line.slice(firstColumn, lastColumn)); + } else { + string.push(line.slice(firstColumn)); + } + } else if (currentLine === lastLine) { + string.push(line.slice(0, lastColumn)); + } else { + string.push(line); + } + } + + return string.join(''); +} diff --git a/packages/ast/template/src/index.ts b/packages/ast/template/src/index.ts index 486d27fb..d71c8159 100644 --- a/packages/ast/template/src/index.ts +++ b/packages/ast/template/src/index.ts @@ -1,26 +1,10 @@ -import { - type AST as _AST, - builders, - type NodeVisitor, - print, - transform, -} from 'ember-template-recast'; +import { builders, print, traverse } from './-private/glimmer-syntax.js'; -function traverse() { - return function ( - file: string, - visitMethods: NodeVisitor = {}, - ): _AST.Template { - const { ast } = transform({ - plugin() { - return visitMethods; - }, - template: file, - }); - - return ast; - }; -} +type AST = { + builders: typeof builders; + print: typeof print; + traverse: typeof traverse; +}; /** * An object that provides `builders`, `print`, and `traverse`. @@ -44,7 +28,7 @@ function traverse() { * } * ``` */ -export const AST = { +export const AST: AST = { builders, print, traverse, diff --git a/packages/ast/template/tests/-private/glimmer-syntax/attr-node.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/attr-node.test.ts new file mode 100644 index 00000000..96347a8d --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/attr-node.test.ts @@ -0,0 +1,455 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; +import type { AST } from '@glimmer/syntax'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | AttrNode > updating value', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.path.original = 'bar'; + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > updating attribute to be valueless', function () { + const template = ''; + let ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].isValueless = true; + + assert.strictEqual(print(ast), ''); + + ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].isValueless = true; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.text('blah'); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > adding value to valueless attribute', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.mustache('true'); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > updating valueless attribute to a mustache statement does not add quotes', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].isValueless = false; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.mustache('true'); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > modifying valueless attribute to have empty value', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].isValueless = false; + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > updating concat statement value', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts.push(builders.text(' other-static')); + + assert.strictEqual( + print(ast), + '', + ); +}); + +test('-private | glimmer-syntax | AttrNode > updating value from non-quotable to TextNode (GH#111)', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.text('hello!'); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > updating value from non-quotable to ConcatStatement (GH#111)', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.concat([ + builders.mustache('foo'), + builders.text(' static '), + builders.mustache('bar'), + ]); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > can determine if an AttrNode was valueless (required by ember-template-lint)', function () { + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + false, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].isValueless, + true, + ); +}); + +test('-private | glimmer-syntax | AttrNode > can determine type of quotes used from AST (required by ember-template-lint)', function () { + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + null, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + `"`, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + `'`, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + `"`, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + `'`, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + null, + ); + + assert.strictEqual( + // @ts-expect-error: Incorrect type + parse(``).body[0].attributes[0].quoteType, + null, + ); +}); + +test('-private | glimmer-syntax | AttrNode > renaming valueless attribute', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].name = 'data-foo'; + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | AttrNode > mutations retain custom whitespace formatting', function () { + const template = normalizeFile([``]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.path.original = 'bar'; + + assert.strictEqual( + print(ast), + normalizeFile([``]), + ); +}); + +test('-private | glimmer-syntax | AttrNode > mutations retain textarea whitespace formatting', function () { + const template = normalizeFile([``]); + const ast = parse(template); + + const element = ast.body[0] as AST.ElementNode; + const attrNode = element.attributes[0] as AST.AttrNode; + const attrValue = attrNode.value as AST.TextNode; + attrValue.chars = 'bar'; + + assert.strictEqual( + print(ast), + normalizeFile([``]), + ); +}); + +test('-private | glimmer-syntax | AttrNode > mutations in MustacheStatements retain whitespace in AttrNode', function () { + const template = normalizeFile([ + ``, + ` hello`, + `
`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts[1].params[1].value = 'bar'; + + assert.strictEqual(print(ast), template); +}); + +test('-private | glimmer-syntax | AttrNode > quotes are preserved when updated a TextNode value (double quote)', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.chars = 'hahah'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > quotes are preserved when updated a TextNode value (single quote)', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.chars = 'hahah'; + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | AttrNode > can update a quoteless attribute value', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.chars = 'zomgyasss'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > quoteless attribute values can be updated to a must-quote attribute value', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.chars = 'foo bar baz'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > quotes are preserved when updating a ConcatStatement value', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts[0].chars = 'hahah '; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > quotes are preserved when updating an AttrNode name - issue #319', function () { + const template = '
'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].name = 'class'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > quotes are preserved when updating an AttrNode value - issue #588', function () { + const template = '
'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.concat([builders.text('foobar')]); + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > TextNode quote types can be changed', function () { + let template = '
'; + let ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = "'"; + + assert.strictEqual(print(ast), "
"); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = '"'; + + assert.strictEqual(print(ast), '
'); + + template = '
'; + ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = null; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > ConcatStatement quote types can be changed', function () { + let template = '
'; + let ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = "'"; + + assert.strictEqual(print(ast), "
"); + + template = "
"; + ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = '"'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > can create a single-quoted concat value', function () { + // We usually use the glimmer printer for any user-created nodes. + // But the glimmer printer hardcodes double-quotes for ConcatStatements. + // So, if the user specifies single quotes and creates a concat value, + // make sure it doesn't accidentally use multiple qutoes. + const template = '
'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = "'"; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.concat([ + builders.mustache('foo'), + builders.text(' static '), + builders.mustache('bar'), + ]); + + assert.strictEqual(print(ast), "
"); +}); + +test('-private | glimmer-syntax | AttrNode > can specify quote style on a new attribute', function () { + const template = '
'; + const ast = parse(template); + + const c = builders.attr('class', builders.text('foo')); + // @ts-expect-error: Incorrect type + c.quoteType = null; + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push(c); + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > can specify valueless on a new attribute', function () { + const template = '
'; + const ast = parse(template); + + const c = builders.attr('...attributes', builders.text('')); + // @ts-expect-error: Incorrect type + c.isValueless = true; + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push(c); + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | AttrNode > invalid quote types are rejected', function () { + const template = '
'; + let ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = '"'; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.mustache('foo'); + + assert.throws(() => { + print(ast); + }, 'should not be quoted'); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = "'"; + + assert.throws(() => { + print(ast); + }, 'should not be quoted'); + + ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = null; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.concat([ + builders.mustache('foo'), + builders.text(' static'), + ]); + + assert.throws(() => { + print(ast); + }, 'must be quoted'); + + ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].quoteType = null; + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.text('foo bar'); + + assert.throws(() => { + print(ast); + }, '`foo bar` is invalid as an unquoted attribute'); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/block-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/block-statement.test.ts new file mode 100644 index 00000000..c3f03ba4 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/block-statement.test.ts @@ -0,0 +1,431 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | BlockStatement > rename block component', function () { + const template = normalizeFile([ + `{{#foo-bar`, + ` baz="stuff"`, + `}}`, + `
`, + `
`, + `{{/foo-bar}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path = builders.path('baz-derp'); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{#baz-derp`, + ` baz="stuff"`, + `}}`, + `
`, + `
`, + `{{/baz-derp}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | BlockStatement > rename block component from longer to shorter name', function () { + const template = normalizeFile([ + `{{#this-is-a-long-name`, + ` hello="world"`, + `}}`, + `
`, + `
`, + `{{/this-is-a-long-name}}{{someInlineComponent hello="world"}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path = builders.path('baz-derp'); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{#baz-derp`, + ` hello="world"`, + `}}`, + `
`, + `
`, + `{{/baz-derp}}{{someInlineComponent hello="world"}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | BlockStatement > replacing a previously empty hash', function () { + const template = `{{#foo-bar}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash = builders.hash([ + builders.pair('hello', builders.string('world')), + ]); + + assert.strictEqual( + print(ast), + '{{#foo-bar hello="world"}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding multiple HashPair to previously empty hash', function () { + const template = '{{#foo-bar}}Hi there!{{/foo-bar}}{{baz}}'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('hello', builders.string('world'))); + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('foo', builders.string('bar'))); + + assert.strictEqual( + print(ast), + '{{#foo-bar hello="world" foo="bar"}}Hi there!{{/foo-bar}}{{baz}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > replacing empty hash w/ block params works', function () { + const template = `{{#foo-bar as |a b c|}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash = builders.hash([ + builders.pair('hello', builders.string('world')), + ]); + + assert.strictEqual( + print(ast), + '{{#foo-bar hello="world" as |a b c|}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding new HashPair to an empty hash w/ block params works', function () { + const template = `{{#foo-bar as |a b c|}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('hello', builders.string('world'))); + + assert.strictEqual( + print(ast), + '{{#foo-bar hello="world" as |a b c|}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > changing a HashPair key with a StringLiteral value (GH#112)', function () { + const template = `{{#foo-bar foo="some thing with a space"}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].key = 'bar'; + + assert.strictEqual( + print(ast), + '{{#foo-bar bar="some thing with a space"}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > changing a HashPair key with a SubExpression value (GH#112)', function () { + const template = `{{#foo-bar foo=(helper-here this.arg1 this.arg2)}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].key = 'bar'; + + assert.strictEqual( + print(ast), + '{{#foo-bar bar=(helper-here this.arg1 this.arg2)}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > changing a HashPair value from StringLiteral to SubExpression', function () { + const template = `{{#foo-bar foo="bar!"}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value = builders.sexpr('concat', [ + builders.string('hello'), + builders.string('world'), + ]); + + assert.strictEqual( + print(ast), + '{{#foo-bar foo=(concat "hello" "world")}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > changing a HashPair value from SubExpression to StringLiteral', function () { + const template = `{{#foo-bar foo=(concat "hello" "world")}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value = builders.string('hello world!'); + + assert.strictEqual( + print(ast), + '{{#foo-bar foo="hello world!"}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding param with no params or hash', function () { + const template = `{{#foo-bar}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.push(builders.path('this.foo')); + + assert.strictEqual(print(ast), '{{#foo-bar this.foo}}Hi there!{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > adding param with empty program', function () { + const template = `{{#foo-bar}}{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.push(builders.path('this.foo')); + + assert.strictEqual(print(ast), '{{#foo-bar this.foo}}{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > adding param with existing params', function () { + const template = `{{#foo-bar this.first}}Hi there!{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.push(builders.path('this.foo')); + + assert.strictEqual( + print(ast), + '{{#foo-bar this.first this.foo}}Hi there!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding param with existing params infers indentation from existing params', function () { + const template = normalizeFile([ + `{{#foo-bar `, + ` `, + `this.first}}Hi there!{{/foo-bar}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.push(builders.path('this.foo')); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{#foo-bar `, + ` `, + `this.first `, + ` `, + `this.foo}}Hi there!{{/foo-bar}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to end of program', function () { + const template = `{{#foo-bar}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].program.body.push(builders.text(' world!')); + + assert.strictEqual(print(ast), '{{#foo-bar}}Hello world!{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to beginning of program', function () { + const template = `{{#foo-bar}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].program.body.unshift(builders.text('ZOMG! ')); + + assert.strictEqual(print(ast), '{{#foo-bar}}ZOMG! Hello{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to end of inverse', function () { + const template = `{{#foo-bar}}{{else}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body.push(builders.text(' world!')); + + assert.strictEqual( + print(ast), + '{{#foo-bar}}{{else}}Hello world!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to beginning of inverse', function () { + const template = `{{#foo-bar}}{{else}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body.unshift(builders.text('ZOMG! ')); + + assert.strictEqual(print(ast), '{{#foo-bar}}{{else}}ZOMG! Hello{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to end of inverse preserves whitespace and whitespace control when program is also present', function () { + const template = normalizeFile([ + `{{#foo-bar}}Goodbye`, + ` {{~ else ~}} Hello{{/foo-bar}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body.push(builders.text(' world!')); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{#foo-bar}}Goodbye`, + ` {{~ else ~}} Hello world!{{/foo-bar}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding child to end of inverse preserves whitespace and whitespace control', function () { + const template = `{{#foo-bar}}{{~ else ~}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body.push(builders.text(' world!')); + + assert.strictEqual( + print(ast), + '{{#foo-bar}}{{~ else ~}}Hello world!{{/foo-bar}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > add child in an {{else if foo}} chain', function () { + const template = `{{#if foo}}{{else if baz}}Hello{{/if}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body[0].program.body.push(builders.text(' world!')); + + assert.strictEqual( + print(ast), + '{{#if foo}}{{else if baz}}Hello world!{{/if}}', + ); +}); + +test('-private | glimmer-syntax | BlockStatement > adding an inverse', function () { + const template = `{{#foo-bar}}{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse = builders.blockItself([builders.text('ZOMG!')]); + + assert.strictEqual(print(ast), '{{#foo-bar}}{{else}}ZOMG!{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > removing an inverse', function () { + const template = `{{#foo-bar}}Goodbye{{else}}Hello{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse = null; + + assert.strictEqual(print(ast), '{{#foo-bar}}Goodbye{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > annotating an "else if" node', function () { + const template = '{{#if foo}}{{else if bar}}{{else}}{{/if}}'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].inverse.body[0]._isElseIfBlock = true; + + assert.strictEqual(print(ast), '{{#if foo}}{{else if bar}}{{else}}{{/if}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > add block param (when none existed)', function () { + const template = `{{#foo-bar}}{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].program.blockParams; + blockParams.push('foo'); + // @ts-expect-error: Incorrect type + ast.body[0].program.blockParams = blockParams; + + assert.strictEqual(print(ast), '{{#foo-bar as |foo|}}{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > remove only block param', function () { + const template = `{{#foo-bar as |a|}}{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].program.blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].program.blockParams = blockParams; + + assert.strictEqual(print(ast), '{{#foo-bar}}{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > remove one block param of many', function () { + const template = `{{#foo-bar as |a b|}}{{/foo-bar}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].program.blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].program.blockParams = blockParams; + + assert.strictEqual(print(ast), '{{#foo-bar as |a|}}{{/foo-bar}}'); +}); + +test('-private | glimmer-syntax | BlockStatement > remove one block param of many preserves custom whitespace', function () { + const template = normalizeFile([ + `{{#foo-bar`, + ` as |a b|`, + `}}`, + `{{/foo-bar}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].program.blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].program.blockParams = blockParams; + + assert.strictEqual( + print(ast), + normalizeFile([`{{#foo-bar`, ` as |a|`, `}}`, `{{/foo-bar}}`]), + ); +}); + +test('-private | glimmer-syntax | BlockStatement > remove only block param preserves custom whitespace', function () { + const template = normalizeFile([ + `{{#foo-bar`, + ` some=thing`, + ` as |a|`, + `}}`, + `{{/foo-bar}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].program.blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].program.blockParams = blockParams; + + assert.strictEqual( + print(ast), + normalizeFile([`{{#foo-bar`, ` some=thing`, `}}`, `{{/foo-bar}}`]), + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/boolean-literal.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/boolean-literal.test.ts new file mode 100644 index 00000000..354da673 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/boolean-literal.test.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | BooleanLiteral > can be updated in MustacheStatement .path position', function () { + const template = `{{true}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path.value = false; + + assert.strictEqual(print(ast), `{{false}}`); +}); + +test('-private | glimmer-syntax | BooleanLiteral > can be updated in MustacheStatement .hash position', function () { + const template = `{{foo thing=true}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.value = false; + + assert.strictEqual(print(ast), `{{foo thing=false}}`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/comment-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/comment-statement.test.ts new file mode 100644 index 00000000..c8d1787c --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/comment-statement.test.ts @@ -0,0 +1,13 @@ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | CommentStatement > can be updated', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].value = ' otherthing '; + + assert.strictEqual(print(ast), ``); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/concat-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/concat-statement.test.ts new file mode 100644 index 00000000..686763ef --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/concat-statement.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | ConcatStatement > can add parts', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts.push(builders.text(' baz')); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ConcatStatement > preserves quote style', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts.push(builders.text(' baz')); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ConcatStatement > updating parts preserves custom whitespace', function () { + const template = normalizeFile([`
`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts.push(builders.text(' baz')); + + assert.strictEqual( + print(ast), + normalizeFile([`
`]), + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/element-modifier-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/element-modifier-statement.test.ts new file mode 100644 index 00000000..967d585f --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/element-modifier-statement.test.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | ElementModifierStatement > can be updated', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers[0].path.original = 'other'; + + assert.strictEqual(print(ast), `
`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/element-node.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/element-node.test.ts new file mode 100644 index 00000000..b6f898a9 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/element-node.test.ts @@ -0,0 +1,709 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { EOL } from 'node:os'; + +import { assert, normalizeFile, test } from '@codemod-utils/tests'; +import type { AST } from '@glimmer/syntax'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | ElementNode > creating void element', function () { + const template = ``; + const ast = parse(template); + + // in @glimmer/syntax v0.82.0, + // builders.element requires an empty object as a second arg + ast.body.push(builders.element('img', {})); + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > updating attributes on a non-self-closing void element', function () { + const template = ``; + const ast = parse(template); + + const element = ast.body[0] as AST.ElementNode; + const attribute = element.attributes[0] as AST.AttrNode; + const concat = attribute.value as AST.ConcatStatement; + (concat.parts[0] as AST.MustacheStatement).path = + builders.path('this.something'); + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > reusing another template part to build a new template', function () { + const template = `foo`; + const original = parse(template); + const text = original.body[0] as AST.TextNode; + const ast = builders.template([text]); + + assert.strictEqual(print(ast), `foo`); +}); + +test('-private | glimmer-syntax | ElementNode > wrapping a parsed node (which uses custom formatting) with a raw node', function () { + // Ensuring fix for GH#586 + // (infinite recursion when printing custom nodes containing parsed nodes) + // plays nicely with custom printing from GH#653 + // (specifying quoteType on custom nodes, adds a printing override) + const template = ``; + const original = parse(template); + + const raw_wrapping_ast = builders.template([ + builders.element('div', { + children: original.body, + }), + ]); + + assert.strictEqual( + print(raw_wrapping_ast), + `
`, + ); +}); + +test('-private | glimmer-syntax | ElementNode > changing an element to a void element does not print closing tag', function () { + const template = `
`; + const ast = parse(template); + + const element = ast.body[0] as AST.ElementNode; + element.tag = 'img'; + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > updating attributes on a self-closing void element', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.parts[0].path = + builders.path('this.something'); + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > changing an attribute value from mustache to text node (GH#111)', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.text('static thing 1'); + // @ts-expect-error: Incorrect type + ast.body[0].attributes[1].value = builders.text('static thing 2'); + + assert.strictEqual( + print(ast), + ``, + ); +}); + +test('-private | glimmer-syntax | ElementNode > changing an attribute value from text node to mustache (GH #139)', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.mustache('my-awesome-helper', [ + builders.string('hello'), + builders.string('world'), + ]); + + assert.strictEqual( + print(ast), + ``, + ); +}); + +test('-private | glimmer-syntax | ElementNode > changing an attribute value from text node to concat statement (GH #139)', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.concat([ + builders.text('Hello '), + builders.mustache('my-awesome-helper', [ + builders.string('hello'), + builders.string('world'), + ]), + builders.text(' world'), + ]); + + assert.strictEqual( + print(ast), + ``, + ); +}); + +test('-private | glimmer-syntax | ElementNode > changing an attribute value from mustache to mustache', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value = builders.mustache('my-awesome-helper', [ + builders.string('hello'), + builders.string('world'), + ]); + + assert.strictEqual( + print(ast), + ``, + ); +}); + +test('-private | glimmer-syntax | ElementNode > rename element tagname', function () { + const template = normalizeFile([ + `
`, + `
`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].tag = 'a'; + + assert.strictEqual( + print(ast), + normalizeFile([``, ` `]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > rename element tagname without children', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].tag = 'a'; + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > rename self-closing element tagname', function () { + const ast = parse(''); + + // @ts-expect-error: Incorrect type + ast.body[0].tag = 'Qux'; + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > rename self-closing element tagname with trailing whitespace', function () { + const ast = parse(''); + + // @ts-expect-error: Incorrect type + ast.body[0].tag = 'Qux'; + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > Rename tag and convert from self-closing with attributes to block element', function () { + const ast = parse(''); + + // @ts-expect-error: Incorrect type + ast.body[0].tag = 'Qux'; + // @ts-expect-error: Incorrect type + ast.body[0].children = [builders.text('bay')]; + + assert.strictEqual(print(ast), 'bay'); +}); + +test('-private | glimmer-syntax | ElementNode > convert from self-closing with attributes to block element', function () { + const ast = parse(''); + + // @ts-expect-error: Incorrect type + ast.body[0].children = [builders.text('bay')]; + + assert.strictEqual(print(ast), 'bay'); +}); + +test('-private | glimmer-syntax | ElementNode > convert from self-closing with specially spaced attributes to block element', function () { + const ast = parse(normalizeFile([``])); + + // @ts-expect-error: Incorrect type + ast.body[0].children = [builders.text('bay')]; + + assert.strictEqual( + print(ast), + normalizeFile([`bay`]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > Convert self-closing element with modifiers block element', function () { + const ast = parse(''); + + // @ts-expect-error: Incorrect type + ast.body[0].children = [builders.text('bay')]; + + assert.strictEqual( + print(ast), + 'bay', + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding attribute when none originally existed', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push( + builders.attr('data-test', builders.text('wheee')), + ); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ElementNode > adding attribute to ElementNode with block params', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push( + builders.attr('data-test', builders.text('wheee')), + ); + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > adding attribute to ElementNode with block params (extra whitespace)', function () { + const template = normalizeFile([``]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push( + builders.attr('data-test', builders.text('wheee')), + ); + + assert.strictEqual( + print(ast), + normalizeFile([``]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding boolean attribute to ElementNode', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push( + builders.attr('disabled', builders.mustache(builders.boolean(true))), + ); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > adding an attribute to existing list', function () { + const template = normalizeFile([ + `
`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push( + builders.attr('data-test', builders.text('wheee')), + ); + + assert.strictEqual( + print(ast), + normalizeFile([ + `
`, + ]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > creating an element with complex attributes', function () { + const template = ''; + const ast = parse(template); + + ast.body.push( + builders.element( + { name: 'FooBar', selfClosing: true }, + { + attrs: [ + builders.attr( + '@thing', + builders.mustache( + builders.path('hash'), + [], + builders.hash([builders.pair('something', builders.path('bar'))]), + ), + ), + ], + }, + ), + ); + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > modifying an attribute name (GH#112)', function () { + const template = normalizeFile([ + `
`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].name = 'data-test'; + + assert.strictEqual( + print(ast), + normalizeFile([ + `
`, + ]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > modifying attribute after valueless attribute', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[1].value.path = builders.path('this.hmmm'); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > modifying attribute after valueless attribute with special whitespace', function () { + const template = normalizeFile([ + ``, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[1].value.path = builders.path('this.hmmm'); + + assert.strictEqual( + print(ast), + normalizeFile([``]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding attribute after valueless attribute', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push(builders.attr('data-bar', builders.text('foo'))); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > adding valueless attribute when no open parts existed', function () { + const template = ''; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes.push(builders.attr('data-bar', builders.text(''))); + + assert.strictEqual(print(ast), ''); +}); + +test('-private | glimmer-syntax | ElementNode > adding modifier when no open parts originally existed', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers.push( + builders.elementModifier('on', [ + builders.string('click'), + builders.path('this.foo'), + ]), + ); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ElementNode > adding modifier with existing attributes', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers.push( + builders.elementModifier('on', [ + builders.string('click'), + builders.path('this.foo'), + ]), + ); + + assert.strictEqual( + print(ast), + `
`, + ); +}); + +// This is specifically testing the issue described in https://github.com/glimmerjs/glimmer-vm/pull/953 +test('-private | glimmer-syntax | ElementNode > adding modifier when ...attributes is present', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers.push( + builders.elementModifier('on', [ + builders.string('click'), + builders.path('this.foo'), + ]), + ); + + assert.strictEqual( + print(ast), + `
`, + ); +}); + +test('-private | glimmer-syntax | ElementNode > removing a modifier with other attributes', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers.shift(); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ElementNode > removing a modifier with no other attributes/comments/modifiers', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].modifiers.shift(); + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | ElementNode > adding comment when no open parts originally existed', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].comments.push( + builders.mustacheComment(' template-lint-disable '), + ); + + assert.strictEqual( + print(ast), + `
`, + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding comment with existing attributes', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].comments.push( + builders.mustacheComment(' template-lint-disable '), + ); + + assert.strictEqual( + print(ast), + `
`, + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding block param', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].blockParams; + blockParams.push('blah'); + // @ts-expect-error: Incorrect type + ast.body[0].blockParams = blockParams; + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > removing a block param', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].blockParams = blockParams; + + assert.strictEqual(print(ast), ``); +}); + +test('-private | glimmer-syntax | ElementNode > removing a block param preserves formatting of "open element closing"', function () { + const template = normalizeFile([ + ``, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const blockParams = ast.body[0].blockParams; + blockParams.pop(); + // @ts-expect-error: Incorrect type + ast.body[0].blockParams = blockParams; + + assert.strictEqual( + print(ast), + normalizeFile([``]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > interleaved attributes and modifiers are not modified when unchanged', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].comments.push( + builders.mustacheComment(' template-lint-disable '), + ); + + assert.strictEqual( + print(ast), + `
`, + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding children to element with children', function () { + const template = normalizeFile([`
    `, `
  • `, `
`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].children.splice( + 2, + 0, + builders.text(`${EOL} `), + builders.element('li', { + attrs: [builders.attr('data-foo', builders.text('bar'))], + }), + ); + + assert.strictEqual( + print(ast), + normalizeFile([ + `
    `, + `
  • `, + `
  • `, + `
`, + ]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding children to an empty element', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].children.push(builders.text('some text')); + + assert.strictEqual(print(ast), '
some text
'); +}); + +test('-private | glimmer-syntax | ElementNode > adding children to a self closing element', function () { + const template = ``; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].children.push(builders.text('some text')); + + assert.strictEqual(print(ast), 'some text'); +}); + +test('-private | glimmer-syntax | ElementNode > moving a child to another ElementNode', function () { + const template = normalizeFile([ + `{{`, + ` special-formatting-here`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const child = ast.body[0].children.pop(); + ast.body.unshift(builders.text(EOL)); + ast.body.unshift(child); + + assert.strictEqual( + print(ast), + normalizeFile([`{{`, ` special-formatting-here`, `}}`, ``]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > adding a new attribute to an ElementNode while preserving the existing whitespaces', function () { + const template = normalizeFile([ + `
`, + `
`, + ]); + const ast = parse(template); + + const element = ast.body[0] as AST.ElementNode; + element.attributes.push(builders.attr('foo-foo', builders.text('wheee'))); + + assert.strictEqual( + print(ast), + normalizeFile([ + `
`, + `
`, + ]), + ); +}); + +test('-private | glimmer-syntax | ElementNode > issue 706', function () { + const template = normalizeFile([ + ``, + ` {{#each this.data as |chunks|}}`, + ` {{#each chunks as |chunk|}}`, + ` {{#if (this.shouldShowImage chunk)}}`, + `

foo

`, + ` {{else}}`, + `

bar

`, + ` {{/if}}`, + ` {{/each}}`, + ` {{/each}}`, + `
`, + ]); + const ast = parse(template); + + const block1 = (ast.body[0] as AST.ElementNode) + .children[1] as AST.BlockStatement; + const block2 = block1.program.body[1] as AST.BlockStatement; + const block3 = block2.program.body[1] as AST.BlockStatement; + const element = block3.program.body[1] as AST.ElementNode; + const attribute = element.attributes[0] as AST.AttrNode; + (attribute.value as AST.TextNode).chars = 'foo'; + + assert.strictEqual( + print(ast), + normalizeFile([ + ``, + ` {{#each this.data as |chunks|}}`, + ` {{#each chunks as |chunk|}}`, + ` {{#if (this.shouldShowImage chunk)}}`, + `

foo

`, + ` {{else}}`, + `

bar

`, + ` {{/if}}`, + ` {{/each}}`, + ` {{/each}}`, + `
`, + ]), + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/hash-pair.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/hash-pair.test.ts new file mode 100644 index 00000000..23943daa --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/hash-pair.test.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | HashPair > mutations', function () { + const template = '{{foo-bar bar=foo}}'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.original = 'bar'; + + assert.strictEqual(print(ast), '{{foo-bar bar=bar}}'); +}); + +test('-private | glimmer-syntax | HashPair > mutations retain formatting', function () { + const template = '{{foo-bar bar= foo}}'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.original = 'bar'; + + assert.strictEqual(print(ast), '{{foo-bar bar= bar}}'); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/mustache-comment-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/mustache-comment-statement.test.ts new file mode 100644 index 00000000..9804634a --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/mustache-comment-statement.test.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | MustacheCommentStatement > can be updated', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].comments[0].value = ' otherthing '; + + assert.strictEqual(print(ast), `
`); +}); + +test('-private | glimmer-syntax | MustacheCommentStatement > comments without `--` are preserved', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].comments[0].value = ' otherthing '; + + assert.strictEqual(print(ast), `
`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/mustache-statement.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/mustache-statement.test.ts new file mode 100644 index 00000000..5d9b0570 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/mustache-statement.test.ts @@ -0,0 +1,231 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; +import type { AST } from '@glimmer/syntax'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | MustacheStatement > path mutations retain custom whitespace formatting', function () { + const template = `{{ foo }}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path.original = 'bar'; + + assert.strictEqual(print(ast), '{{ bar }}'); +}); + +test('-private | glimmer-syntax | MustacheStatement > updating from this.foo to @foo via path.original mutation', function () { + const template = `{{this.foo}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path.original = '@foo'; + + assert.strictEqual(print(ast), '{{@foo}}'); +}); + +test('-private | glimmer-syntax | MustacheStatement > updating from this.foo to @foo via path replacement', function () { + const template = `{{this.foo}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path = builders.path('@foo'); + + assert.strictEqual(print(ast), '{{@foo}}'); +}); + +test('-private | glimmer-syntax | MustacheStatement > updating path via path replacement retains custom whitespace', function () { + const template = normalizeFile([`{{`, `@foo`, `}}`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path = builders.path('this.foo'); + + assert.strictEqual(print(ast), normalizeFile([`{{`, `this.foo`, `}}`])); +}); + +test('-private | glimmer-syntax | MustacheStatement > rename non-block component', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz="stuff"`, + ` other='single quote'`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].path = builders.path('baz-derp'); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{baz-derp`, + ` baz="stuff"`, + ` other='single quote'`, + `}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > can add param', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz=(stuff`, + ` goes='here')`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.push(builders.path('zomg')); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{foo-bar`, + ` zomg`, + ` baz=(stuff`, + ` goes='here')`, + `}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > can remove param', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` hhaahahaha`, + ` baz=(stuff`, + ` goes='here')`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params.pop(); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` baz=(stuff`, ` goes='here')`, `}}`]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > replacing empty hash pair on MustacheStatement works', function () { + const template = '{{foo-bar}}'; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash = builders.hash([ + builders.pair('hello', builders.string('world')), + ]); + + assert.strictEqual(print(ast), `{{foo-bar hello="world"}}`); +}); + +test('-private | glimmer-syntax | MustacheStatement > infers indentation of hash when multiple HashPairs existed', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz="stuff"`, + ` other='single quote'`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push( + builders.pair('some', builders.string('other-thing')), + ); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{foo-bar`, + ` baz="stuff"`, + ` other='single quote'`, + ` some="other-thing"`, + `}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > infers indentation of hash when no existing hash existed but params do', function () { + const template = normalizeFile([`{{foo-bar`, ` someParam`, `}}`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push( + builders.pair('some', builders.string('other-thing')), + ); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` someParam`, ` some="other-thing"`, `}}`]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > infers indentation of new HashPairs when existing hash with single entry (but no params)', function () { + const template = normalizeFile([`{{foo-bar`, ` stuff=here`, `}}`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push( + builders.pair('some', builders.string('other-thing')), + ); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` stuff=here`, ` some="other-thing"`, `}}`]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > can add literal hash pair values', function () { + const template = normalizeFile([`{{foo-bar`, ` first=thing`, `}}`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('some', builders.null())); + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('other', builders.undefined())); + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('things', builders.boolean(true))); + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('go', builders.number(42))); + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs.push(builders.pair('here', builders.boolean(false))); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{foo-bar`, + ` first=thing`, + ` some=null`, + ` other=undefined`, + ` things=true`, + ` go=42`, + ` here=false`, + `}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | MustacheStatement > creating new MustacheStatement with single param has correct whitespace', function () { + const ast = parse(''); + + ast.body.push(builders.mustache('foo', [builders.string('hi')])); + + assert.strictEqual(print(ast), `{{foo "hi"}}`); +}); + +test('-private | glimmer-syntax | MustacheStatement > copying params and hash from a sub expression into a new MustacheStatement has correct whitespace', function () { + const ast = parse('{{some-helper (foo "hi")}}'); + + const mustache = ast.body[0] as AST.MustacheStatement; + const sexpr = mustache.params[0] as AST.SubExpression; + ast.body.push(builders.mustache(sexpr.path, sexpr.params, sexpr.hash)); + + assert.strictEqual(print(ast), `{{some-helper (foo "hi")}}{{foo "hi"}}`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/null-literal.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/null-literal.test.ts new file mode 100644 index 00000000..19bffcae --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/null-literal.test.ts @@ -0,0 +1,22 @@ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; +import type { AST } from '@glimmer/syntax'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | NullLiteral > it should print correctly', function () { + const template = normalizeFile([`{{contact-null`, ` null`, `}}`]); + const ast = parse(template); + + const mustache = ast.body[0] as AST.MustacheStatement; + const param = mustache.params[0] as AST.BaseNode; + + // Mark the param as dirty + const oldType = param.type; + param.type = 'ElementNode'; + param.type = oldType; + + assert.strictEqual( + print(ast), + normalizeFile([`{{contact-null`, ` null`, `}}`]), + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/number-literal.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/number-literal.test.ts new file mode 100644 index 00000000..f6bf5959 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/number-literal.test.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | NumberLiteral > can be updated', function () { + const template = `{{foo 42}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params[0].value = 0; + + assert.strictEqual(print(ast), `{{foo 0}}`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/string-literal.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/string-literal.test.ts new file mode 100644 index 00000000..6b457c58 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/string-literal.test.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | StringLiteral > can be updated', function () { + const template = `{{foo "blah"}}`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].params[0].value = 'derp'; + + assert.strictEqual(print(ast), `{{foo "derp"}}`); +}); + +test('-private | glimmer-syntax | StringLiteral > can determine type of quotes used from AST (required by ember-template-lint)', function () { + // @ts-expect-error: Incorrect type + assert.strictEqual(parse(`{{foo "blah"}}`).body[0].params[0].quoteType, '"'); + + // @ts-expect-error: Incorrect type + assert.strictEqual(parse(`{{foo 'blah'}}`).body[0].params[0].quoteType, "'"); +}); + +test('-private | glimmer-syntax | StringLiteral > can update quote style', function () { + let ast = parse(`{{foo "blah"}}`); + + // @ts-expect-error: Incorrect type + ast.body[0].params[0].quoteType = "'"; + + assert.strictEqual(print(ast), `{{foo 'blah'}}`); + + ast = parse(`{{foo 'blah'}}`); + + // @ts-expect-error: Incorrect type + ast.body[0].params[0].quoteType = '"'; + + assert.strictEqual(print(ast), `{{foo "blah"}}`); +}); + +test('-private | glimmer-syntax | StringLiteral > can specify quote style on a new string literal', function () { + const ast = parse(`{{foo}}`); + + const s = builders.string('blah'); + // @ts-expect-error: Incorrect type + s.quoteType = "'"; + // @ts-expect-error: Incorrect type + ast.body[0].params.push(s); + + assert.strictEqual(print(ast), `{{foo 'blah'}}`); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/sub-expression.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/sub-expression.test.ts new file mode 100644 index 00000000..3a6a50aa --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/sub-expression.test.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; + +import { + builders, + parse, + print, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | SubExpression > rename path', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz=(stuff`, + ` goes='here')`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.path = builders.path('zomg'); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` baz=(zomg`, ` goes='here')`, `}}`]), + ); +}); + +test('-private | glimmer-syntax | SubExpression > can add param', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz=(stuff`, + ` goes='here')`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.params.push(builders.path('zomg')); + + assert.strictEqual( + print(ast), + normalizeFile([ + `{{foo-bar`, + ` baz=(stuff`, + ` zomg`, + ` goes='here')`, + `}}`, + ]), + ); +}); + +test('-private | glimmer-syntax | SubExpression > can remove param', function () { + const template = normalizeFile([ + `{{foo-bar`, + ` baz=(stuff`, + ` hhaahahaha`, + ` goes='here')`, + `}}`, + ]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.params.pop(); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` baz=(stuff`, ` goes='here')`, `}}`]), + ); +}); + +test('-private | glimmer-syntax | SubExpression > replacing empty hash pair', function () { + const template = normalizeFile([`{{foo-bar`, ` baz=(stuff)`, `}}`]); + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].hash.pairs[0].value.hash = builders.hash([ + builders.pair('hello', builders.string('world')), + ]); + + assert.strictEqual( + print(ast), + normalizeFile([`{{foo-bar`, ` baz=(stuff hello="world")`, `}}`]), + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/text-node.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/text-node.test.ts new file mode 100644 index 00000000..7d6ec1a5 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/text-node.test.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ +import { assert, test } from '@codemod-utils/tests'; + +import { parse, print } from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | TextNode > can be updated', function () { + const template = `Foo`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].chars = 'Bar'; + + assert.strictEqual(print(ast), 'Bar'); +}); + +test('-private | glimmer-syntax | TextNode > can be updated as value of AttrNode', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + ast.body[0].attributes[0].value.chars = 'hahah'; + + assert.strictEqual(print(ast), '
'); +}); + +test('-private | glimmer-syntax | TextNode > an AttrNode values quotes are removed when inserted in alternate positions (e.g. content)', function () { + const template = `
`; + const ast = parse(template); + + // @ts-expect-error: Incorrect type + const text = ast.body[0].attributes[0].value; + // @ts-expect-error: Incorrect type + ast.body[0].children.push(text); + + assert.strictEqual(print(ast), '
lol
'); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/traverse.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/traverse.test.ts new file mode 100644 index 00000000..27e57a12 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/traverse.test.ts @@ -0,0 +1,73 @@ +import { assert, normalizeFile, test } from '@codemod-utils/tests'; +import type { NodeVisitor } from '@glimmer/syntax'; + +import { + builders, + print, + traverse, +} from '../../../src/-private/glimmer-syntax.js'; + +test('-private | glimmer-syntax | traverse > can remove during traversal by returning `null`', function () { + const template = normalizeFile([ + `

here is some multiline string

`, + `{{ other-stuff }}`, + ]); + + const ast = traverse()(template, { + ElementNode() { + return null; + }, + } as NodeVisitor); + + assert.strictEqual(print(ast), normalizeFile([``, `{{ other-stuff }}`])); +}); + +test('-private | glimmer-syntax | traverse > can replace with many items during traversal by returning an array', function () { + const template = normalizeFile([ + `

here is some multiline string

`, + `{{other-stuff}}`, + ]); + + const ast = traverse()(template, { + ElementNode() { + return [builders.text('hello '), builders.text('world')]; + }, + }); + + assert.strictEqual( + print(ast), + normalizeFile([`hello world`, `{{other-stuff}}`]), + ); +}); + +test('-private | glimmer-syntax | traverse > issue can handle angle brackets in modifier argument values', function () { + const template = normalizeFile([ + `> Some Text Here"}}`, + ` @options={{this.items}}`, + ` as |item|`, + `>`, + ` {{item.name}}`, + ``, + ]); + + const ast = traverse()(template, { + ElementNode(node) { + node.tag = `${node.tag}`; + }, + }); + + assert.strictEqual(print(ast), template); +}); + +test('-private | glimmer-syntax | traverse > MustacheStatements retain whitespace when multiline replacements occur', function () { + const template = normalizeFile([`

`, `{{ other-stuff }}`]); + + const ast = traverse()(template, { + ElementNode() { + return [builders.text('x'), builders.text('y')]; + }, + }); + + assert.strictEqual(print(ast), normalizeFile([`xy`, `{{ other-stuff }}`])); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-nodes-by-their-line-numbers.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-nodes-by-their-line-numbers.test.ts new file mode 100644 index 00000000..0032d306 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-nodes-by-their-line-numbers.test.ts @@ -0,0 +1,31 @@ +import { assert, test } from '@codemod-utils/tests'; +import { builders } from '@glimmer/syntax'; + +import { sortByLoc } from '../../../../../src/-private/glimmer-syntax/utils.js'; + +test('-private | glimmer-syntax | utils | sortByLoc > sorts nodes by their line numbers', function () { + const a = builders.pair( + 'a', + builders.path('foo'), + builders.loc(1, 1, 1, 5, 'foo.hbs'), + ); + + const b = builders.pair( + 'b', + builders.path('foo'), + builders.loc(3, 1, 1, 5, 'foo.hbs'), + ); + + const c = builders.pair( + 'c', + builders.path('foo'), + builders.loc(2, 1, 1, 5, 'foo.hbs'), + ); + + const nodes = [a, b, c].sort(sortByLoc); + + assert.deepStrictEqual( + nodes.map((node) => node.key), + ['a', 'c', 'b'], + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-synthetic-nodes-last.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-synthetic-nodes-last.test.ts new file mode 100644 index 00000000..630050c0 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/sorts-synthetic-nodes-last.test.ts @@ -0,0 +1,21 @@ +import { assert, test } from '@codemod-utils/tests'; +import { builders } from '@glimmer/syntax'; + +import { sortByLoc } from '../../../../../src/-private/glimmer-syntax/utils.js'; + +test('-private | glimmer-syntax | utils | sortByLoc > sorts synthetic nodes last', function () { + const a = builders.pair('a', builders.path('foo') /* no loc, "synthetic" */); + + const b = builders.pair( + 'b', + builders.path('foo'), + builders.loc(1, 1, 1, 5, 'foo.hbs'), + ); + + const nodes = [a, b].sort(sortByLoc); + + assert.deepStrictEqual( + nodes.map((node) => node.key), + ['a', 'b'], + ); +}); diff --git a/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/when-start-line-matches-sorts-by-starting-column.test.ts b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/when-start-line-matches-sorts-by-starting-column.test.ts new file mode 100644 index 00000000..5e9a9e78 --- /dev/null +++ b/packages/ast/template/tests/-private/glimmer-syntax/utils/sortByLoc/when-start-line-matches-sorts-by-starting-column.test.ts @@ -0,0 +1,31 @@ +import { assert, test } from '@codemod-utils/tests'; +import { builders } from '@glimmer/syntax'; + +import { sortByLoc } from '../../../../../src/-private/glimmer-syntax/utils.js'; + +test('-private | glimmer-syntax | utils | sortByLoc > when start line matches, sorts by starting column', function () { + const a = builders.pair( + 'a', + builders.path('foo'), + builders.loc(1, 1, 1, 5, 'foo.hbs'), + ); + + const b = builders.pair( + 'b', + builders.path('foo'), + builders.loc(2, 1, 1, 5, 'foo.hbs'), + ); + + const c = builders.pair( + 'c', + builders.path('foo'), + builders.loc(1, 6, 1, 9, 'foo.hbs'), + ); + + const nodes = [a, b, c].sort(sortByLoc); + + assert.deepStrictEqual( + nodes.map((node) => node.key), + ['a', 'c', 'b'], + ); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f690088..fa3d2911 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,9 +166,9 @@ importers: packages/ast/template: dependencies: - ember-template-recast: - specifier: ^6.1.5 - version: 6.1.5 + '@glimmer/syntax': + specifier: ^0.95.0 + version: 0.95.0 devDependencies: '@codemod-utils/tests': specifier: workspace:* @@ -762,29 +762,21 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@glimmer/env@0.1.7': - resolution: {integrity: sha512-JKF/a9I9jw6fGoz8kA7LEQslrwJ5jms5CXhu/aqkBWk+PmZ6pTl8mlb/eJ/5ujBGTiQzBhy5AIWF712iA+4/mw==} + '@glimmer/interfaces@0.94.6': + resolution: {integrity: sha512-sp/1WePvB/8O+jrcUHwjboNPTKrdGicuHKA9T/lh0vkYK2qM5Xz4i25lQMQ38tEMiw7KixrjHiTUiaXRld+IwA==} - '@glimmer/global-context@0.84.3': - resolution: {integrity: sha512-8Oy9Wg5IZxMEeAnVmzD2NkObf89BeHoFSzJgJROE/deutd3rxg83mvlOez4zBBGYwnTb+VGU2LYRpet92egJjA==} + '@glimmer/syntax@0.95.0': + resolution: {integrity: sha512-W/PHdODnpONsXjbbdY9nedgIHpglMfOzncf/moLVrKIcCfeQhw2vG07Rs/YW8KeJCgJRCLkQsi+Ix7XvrurGAg==} - '@glimmer/interfaces@0.84.3': - resolution: {integrity: sha512-dk32ykoNojt0mvEaIW6Vli5MGTbQo58uy3Epj7ahCgTHmWOKuw/0G83f2UmFprRwFx689YTXG38I/vbpltEjzg==} + '@glimmer/util@0.94.8': + resolution: {integrity: sha512-HfCKeZ74clF9BsPDBOqK/yRNa/ke6niXFPM6zRn9OVYw+ZAidLs7V8He/xljUHlLRL322kaZZY8XxRW7ALEwyg==} - '@glimmer/reference@0.84.3': - resolution: {integrity: sha512-lV+p/aWPVC8vUjmlvYVU7WQJsLh319SdXuAWoX/SE3pq340BJlAJiEcAc6q52y9JNhT57gMwtjMX96W5Xcx/qw==} + '@glimmer/wire-format@0.94.8': + resolution: {integrity: sha512-A+Cp5m6vZMAEu0Kg/YwU2dJZXyYxVJs2zI57d3CP6NctmX7FsT8WjViiRUmt5abVmMmRH5b8BUovqY6GSMAdrw==} - '@glimmer/syntax@0.84.3': - resolution: {integrity: sha512-ioVbTic6ZisLxqTgRBL2PCjYZTFIwobifCustrozRU2xGDiYvVIL0vt25h2c1ioDsX59UgVlDkIK4YTAQQSd2A==} - - '@glimmer/util@0.84.3': - resolution: {integrity: sha512-qFkh6s16ZSRuu2rfz3T4Wp0fylFj3HBsONGXQcrAdZjdUaIS6v3pNj6mecJ71qRgcym9Hbaq/7/fefIwECUiKw==} - - '@glimmer/validator@0.84.3': - resolution: {integrity: sha512-RTBV4TokUB0vI31UC7ikpV7lOYpWUlyqaKV//pRC4pexYMlmqnVhkFrdiimB/R1XyNdUOQUmnIAcdic39NkbhQ==} - - '@handlebars/parser@2.0.0': - resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} + '@handlebars/parser@2.2.2': + resolution: {integrity: sha512-n/SZW+12rwikx/f8YcSv9JCi5p9vn1Bnts9ZtVvfErG4h0gbjHI1H1ZMhVUnaOC7yzFc6PtsCKIK8XeTnL90Gw==} + engines: {node: ^18 || ^20 || ^22 || >=24} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -1331,6 +1323,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -1589,12 +1582,6 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - async-promise-queue@1.0.5: - resolution: {integrity: sha512-xi0aQ1rrjPWYmqbwr18rrSKbSaXIeIwSd1J4KAgVfkq8utNbdZoht7GfvfY6swFUAMJ9obkc4WPJmtGwl+B8dw==} - - async@2.6.4: - resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1602,9 +1589,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.19: resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} engines: {node: '>=6.0.0'} @@ -1617,9 +1601,6 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1636,9 +1617,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - cacheable@2.3.4: resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} @@ -1665,14 +1643,6 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1681,10 +1651,6 @@ packages: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1695,17 +1661,9 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - colors@1.4.0: - resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} - engines: {node: '>=0.1.90'} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - comment-parser@1.4.6: resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} engines: {node: '>= 12.0.0'} @@ -1756,14 +1714,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1776,9 +1726,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1809,11 +1756,6 @@ packages: electron-to-chromium@1.5.336: resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==} - ember-template-recast@6.1.5: - resolution: {integrity: sha512-VnRN8FzEHQnw/5rCv6Wnq8MVYXbGQbFY+rEufvWV+FO/IsxMahGEud4MYWtTA2q8iG+qJFrDQefNvQ//7MI7Qw==} - engines: {node: 12.* || 14.* || >= 16.*} - hasBin: true - emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2191,9 +2133,6 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2213,9 +2152,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2237,10 +2173,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2261,10 +2193,6 @@ packages: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -2428,10 +2356,6 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} @@ -2485,10 +2409,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -2507,9 +2427,6 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2545,10 +2462,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -2559,10 +2472,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -2712,10 +2621,6 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -2748,10 +2653,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2767,9 +2668,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2797,9 +2695,6 @@ packages: shiki@3.23.0: resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2860,9 +2755,6 @@ packages: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -2950,10 +2842,6 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tmp@0.2.3: - resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} - engines: {node: '>=14.14'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2995,6 +2883,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-fest@5.6.0: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} @@ -3137,9 +3029,6 @@ packages: resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} engines: {node: 10.* || >= 12.*} - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3159,9 +3048,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@6.5.1: - resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3582,43 +3468,28 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@glimmer/env@0.1.7': {} - - '@glimmer/global-context@0.84.3': - dependencies: - '@glimmer/env': 0.1.7 - - '@glimmer/interfaces@0.84.3': + '@glimmer/interfaces@0.94.6': dependencies: '@simple-dom/interface': 1.4.0 + type-fest: 4.41.0 - '@glimmer/reference@0.84.3': + '@glimmer/syntax@0.95.0': dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.84.3 - '@glimmer/interfaces': 0.84.3 - '@glimmer/util': 0.84.3 - '@glimmer/validator': 0.84.3 - - '@glimmer/syntax@0.84.3': - dependencies: - '@glimmer/interfaces': 0.84.3 - '@glimmer/util': 0.84.3 - '@handlebars/parser': 2.0.0 + '@glimmer/interfaces': 0.94.6 + '@glimmer/util': 0.94.8 + '@glimmer/wire-format': 0.94.8 + '@handlebars/parser': 2.2.2 simple-html-tokenizer: 0.5.11 - '@glimmer/util@0.84.3': + '@glimmer/util@0.94.8': dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/interfaces': 0.84.3 - '@simple-dom/interface': 1.4.0 + '@glimmer/interfaces': 0.94.6 - '@glimmer/validator@0.84.3': + '@glimmer/wire-format@0.94.8': dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.84.3 + '@glimmer/interfaces': 0.94.6 - '@handlebars/parser@2.0.0': {} + '@handlebars/parser@2.2.2': {} '@humanfs/core@0.19.1': {} @@ -4333,23 +4204,10 @@ snapshots: astral-regex@2.0.0: {} - async-promise-queue@1.0.5: - dependencies: - async: 2.6.4 - debug: 2.6.9 - transitivePeerDependencies: - - supports-color - - async@2.6.4: - dependencies: - lodash: 4.18.1 - balanced-match@1.0.2: {} balanced-match@4.0.4: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.10.19: {} better-path-resolve@1.0.0: @@ -4358,12 +4216,6 @@ snapshots: birpc@2.9.0: {} - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4385,11 +4237,6 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 @@ -4415,12 +4262,6 @@ snapshots: chardet@2.1.1: {} - cli-cursor@3.1.0: - dependencies: - restore-cursor: 3.1.0 - - cli-spinners@2.9.2: {} - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4433,8 +4274,6 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.0 - clone@1.0.4: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4443,12 +4282,8 @@ snapshots: colord@2.9.3: {} - colors@1.4.0: {} - comma-separated-tokens@2.0.3: {} - commander@8.3.0: {} - comment-parser@1.4.6: {} concat-map@0.0.1: {} @@ -4494,20 +4329,12 @@ snapshots: dataloader@1.4.0: {} - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@4.4.3: dependencies: ms: 2.1.3 deep-is@0.1.4: {} - defaults@1.0.4: - dependencies: - clone: 1.0.4 - dequal@2.0.3: {} detect-indent@6.1.0: {} @@ -4528,22 +4355,6 @@ snapshots: electron-to-chromium@1.5.336: {} - ember-template-recast@6.1.5: - dependencies: - '@glimmer/reference': 0.84.3 - '@glimmer/syntax': 0.84.3 - '@glimmer/validator': 0.84.3 - async-promise-queue: 1.0.5 - colors: 1.4.0 - commander: 8.3.0 - globby: 11.1.0 - ora: 5.4.1 - slash: 3.0.0 - tmp: 0.2.3 - workerpool: 6.5.1 - transitivePeerDependencies: - - supports-color - emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -4948,8 +4759,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -4963,8 +4772,6 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - ini@1.3.8: {} is-arrayish@0.2.1: {} @@ -4981,8 +4788,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-interactive@1.0.0: {} - is-number@7.0.0: {} is-path-inside@4.0.0: {} @@ -4995,8 +4800,6 @@ snapshots: dependencies: better-path-resolve: 1.0.0 - is-unicode-supported@0.1.0: {} - is-windows@1.0.2: {} isexe@2.0.0: {} @@ -5120,11 +4923,6 @@ snapshots: lodash@4.18.1: {} - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - lru-cache@11.2.6: {} lru-cache@5.1.1: @@ -5184,8 +4982,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mimic-fn@2.1.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -5200,8 +4996,6 @@ snapshots: mri@1.2.0: {} - ms@2.0.0: {} - ms@2.1.3: {} nanoid@3.3.12: {} @@ -5220,10 +5014,6 @@ snapshots: normalize-path@3.0.0: {} - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -5241,18 +5031,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@5.4.1: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - outdent@0.5.0: {} oxc-minify@0.128.0: @@ -5396,12 +5174,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -5430,11 +5202,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - restore-cursor@3.1.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - reusify@1.1.0: {} rolldown@1.0.0-rc.17: @@ -5466,8 +5233,6 @@ snapshots: dependencies: tslib: 2.8.1 - safe-buffer@5.2.1: {} - safer-buffer@2.1.2: {} semver@6.3.1: {} @@ -5493,8 +5258,6 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} simple-html-tokenizer@0.5.11: {} @@ -5553,10 +5316,6 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -5671,8 +5430,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmp@0.2.3: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5705,6 +5462,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.41.0: {} + type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 @@ -5883,10 +5642,6 @@ snapshots: matcher-collection: 2.0.1 minimatch: 3.1.5 - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 - webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -5904,8 +5659,6 @@ snapshots: word-wrap@1.2.5: {} - workerpool@6.5.1: {} - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0