diff --git a/.eslintrc.js b/.eslintrc.js index 8eb68d7e..64db77d9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,6 +45,8 @@ module.exports = { '@typescript-eslint/no-dynamic-delete': 'error', '@typescript-eslint/no-extra-semi': 'error', '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-require-imports': 'error', '@typescript-eslint/no-unnecessary-condition': 'error', @@ -72,6 +74,7 @@ module.exports = { 'jsdoc/tag-lines': 'off', 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', + 'unicorn/consistent-destructuring': 'off', 'unicorn/consistent-function-scoping': [ 'error', { checkArrowFunctions: false }, diff --git a/src/main.ts b/src/main.ts index 7a0d6be5..0af8e15d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import type { Parser, Plugin, Printer, SupportLanguage } from 'prettier'; import { PARSER_NAME, PRINTER_NAME } from './config.js'; import { options } from './options.js'; -import { parser } from './parse.js'; +import { parser } from './parse/index.js'; import { printer } from './print/index.js'; const languages: SupportLanguage[] = [ diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index 9312115c..00000000 --- a/src/parse.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { traverse } from '@babel/core'; -import type { Node } from '@babel/types'; -import { type Comment } from '@babel/types'; -import { Preprocessor } from 'content-tag'; -import type { Parser } from 'prettier'; -import { parsers as babelParsers } from 'prettier/plugins/babel.js'; - -import { PRINTER_NAME } from './config'; -import type { Options } from './options.js'; -import { assert } from './utils'; - -const typescript = babelParsers['babel-ts'] as Parser; -const p = new Preprocessor(); - -interface Path { - node: Node; - parent: Node | null; - parentKey: string | null; - parentPath: Path | null; -} - -export interface TemplateNode { - type: 'FunctionDeclaration'; - leadingComments: Comment[]; - range: [number, number]; - start: number; - end: number; - extra: { - isGlimmerTemplate: boolean; - isDefaultTemplate: boolean; - isAssignment: boolean; - isAlreadyExportDefault: boolean; - template: string; - }; -} - -interface PreprocessedResult { - templateVisitorKeys: Record; - templateInfos: { - /** Range of the template including tags */ - templateRange: [number, number]; - /** Range of the template content, excluding tags */ - range: [number, number]; - ast: TemplateNode | undefined; - }[]; -} - -/** Traverses the AST and replaces the transformed template parts with other AST */ -function convertAst( - result: { ast: Node; code: string }, - preprocessedResult: PreprocessedResult, -): void { - const templateInfos = preprocessedResult.templateInfos; - let counter = 0; - - traverse(result.ast, { - enter(path: Path) { - const node = path.node; - if ( - node.type === 'ObjectExpression' || - node.type === 'BlockStatement' || - node.type === 'StaticBlock' - ) { - const range = node.range as [number, number]; - - const template = templateInfos.find( - (t) => - (t.templateRange[0] === range[0] && - t.templateRange[1] === range[1]) || - (t.templateRange[0] === range[0] - 1 && - t.templateRange[1] === range[1] + 1) || - (t.templateRange[0] === range[0] && - t.templateRange[1] === range[1] + 1), - ); - - if (!template) { - return null; - } - counter++; - const ast = template.ast as TemplateNode; - ast.extra.isAlreadyExportDefault = - path.parent?.type === 'ExportDefaultDeclaration' || - path.parentPath?.parent?.type === 'ExportDefaultDeclaration'; - ast.extra.isDefaultTemplate = - path.parent?.type === 'ExportDefaultDeclaration' || - path.parent?.type === 'Program' || - (path.parent?.type === 'ExpressionStatement' && - path.parentPath?.parent?.type === 'Program') || - (path.parent?.type === 'TSAsExpression' && - path.parentPath?.parentPath?.parent?.type === 'Program') || - path.parentPath?.parent?.type === 'ExportDefaultDeclaration'; - - ast.extra.isAssignment = - !ast.extra.isDefaultTemplate && node.type !== 'StaticBlock'; - - ast.leadingComments = node.leadingComments as Comment[]; - Object.assign(node, ast); - } - return null; - }, - }); - - if (counter !== templateInfos.length) { - throw new Error('failed to process all templates'); - } -} - -interface Info { - output: string; - templateInfos: { - type: 'expression' | 'class-member'; - tagName: 'template'; - contents: string; - range: { - start: number; - end: number; - }; - contentRange: { - start: number; - end: number; - }; - startRange: { - end: number; - start: number; - }; - endRange: { - start: number; - end: number; - }; - }[]; -} - -/** - * Preprocesses the template info, parsing the template content to Glimmer AST, - * fixing the offsets and locations of all nodes also calculates the block - * params locations & ranges and adding it to the info - */ -function preprocessGlimmerTemplates( - info: Info, - code: string, -): PreprocessedResult { - const templateInfos = info.templateInfos.map((r) => ({ - range: [r.contentRange.start, r.contentRange.end] as [number, number], - templateRange: [r.range.start, r.range.end] as [number, number], - ast: undefined as undefined | TemplateNode, - })); - const templateVisitorKeys = {}; - for (const tpl of templateInfos) { - const range = tpl.range; - const template = code.slice(...range); - const ast: TemplateNode = { - type: 'FunctionDeclaration', - leadingComments: [], - range: [tpl.templateRange[0], tpl.templateRange[1]], - start: tpl.templateRange[0], - end: tpl.templateRange[1], - extra: { - isGlimmerTemplate: true, - isDefaultTemplate: false, - isAssignment: false, - isAlreadyExportDefault: false, - template, - }, - }; - tpl.ast = ast; - } - return { - templateVisitorKeys, - templateInfos, - }; -} - -function replaceRange( - s: string, - start: number, - end: number, - substitute: string, -): string { - return s.slice(0, start) + substitute + s.slice(end); -} - -function transformForPrettier(code: string): Info { - let jsCode = code; - const result = p.parse(code) as Info['templateInfos']; - for (const tplInfo of result.reverse()) { - const lineBreaks = [...tplInfo.contents].reduce( - (previous, current) => previous + (current === '\n' ? 1 : 0), - 0, - ); - if (tplInfo.type === 'class-member') { - const tplLength = tplInfo.range.end - tplInfo.range.start; - const spaces = tplLength - 'static{`'.length - '`}'.length - lineBreaks; - const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks); - const replacementCode = `static{\`${total}\`}`; - jsCode = replaceRange( - jsCode, - tplInfo.range.start, - tplInfo.range.end, - replacementCode, - ); - } else { - const tplLength = tplInfo.range.end - tplInfo.range.start; - const nextWord = code.slice(tplInfo.range.end).match(/\S+/); - let prefix = '{'; - let suffix = '}'; - if (nextWord && nextWord[0] === 'as') { - prefix = '(' + prefix; - suffix = suffix + ')'; - } else if (!nextWord || ![',', ')'].includes(nextWord[0][0] || '')) { - suffix += ';'; - } - const spaces = tplLength - prefix.length - suffix.length - lineBreaks; - const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks); - const replacementCode = `${prefix}${total}${suffix}`; - jsCode = replaceRange( - jsCode, - tplInfo.range.start, - tplInfo.range.end, - replacementCode, - ); - } - } - return { - templateInfos: result, - output: jsCode, - }; -} - -export const parser: Parser = { - ...typescript, - astFormat: PRINTER_NAME, - - preprocess(text: string): string { - return text; - }, - - async parse(code: string, options: Options): Promise { - const info = transformForPrettier(code); - const ast = await typescript.parse(info.output, options); - const preprocessedResult = preprocessGlimmerTemplates(info, code); - assert('expected ast', ast); - convertAst({ ast, code }, preprocessedResult); - return ast; - }, -}; diff --git a/src/parse/index.ts b/src/parse/index.ts new file mode 100644 index 00000000..b81a0cb2 --- /dev/null +++ b/src/parse/index.ts @@ -0,0 +1,121 @@ +import type { NodePath } from '@babel/core'; +import { traverse } from '@babel/core'; +import type { + BlockStatement, + Node, + ObjectExpression, + StaticBlock, +} from '@babel/types'; +import { Preprocessor } from 'content-tag'; +import type { Parser } from 'prettier'; +import { parsers as babelParsers } from 'prettier/plugins/babel.js'; + +import { PRINTER_NAME } from '../config'; +import type { Options } from '../options.js'; +import type { GlimmerTemplateInfo, RawGlimmerTemplate } from '../types/glimmer'; +import { isDefaultTemplate } from '../types/glimmer'; +import { assert } from '../utils'; +import { normalizeWhitespace } from './whitespace'; + +const typescript = babelParsers['babel-ts'] as Parser; +const p = new Preprocessor(); + +/** Converts a node into a GlimmerTemplate node */ +function convertNode( + path: NodePath, + node: BlockStatement | ObjectExpression | StaticBlock, + templateInfo: GlimmerTemplateInfo, +): void { + Object.assign(node, templateInfo, { + type: 'FunctionDeclaration', + extra: Object.assign(node.extra ?? {}, templateInfo.extra, { + isGlimmerTemplate: true, + isDefaultTemplate: isDefaultTemplate(path), + }), + }); +} + +/** Traverses the AST and replaces the transformed template parts with other AST */ +function convertAst(ast: Node, templateInfos: GlimmerTemplateInfo[]): void { + let counter = 0; + + traverse(ast, { + enter(path) { + const { node } = path; + if ( + node.type === 'ObjectExpression' || + node.type === 'BlockStatement' || + node.type === 'StaticBlock' + ) { + const { range } = node; + assert('expected range', range); + + const templateInfo = templateInfos.find( + (p) => + (p.range[0] === range[0] && p.range[1] === range[1]) || + (p.range[0] === range[0] - 1 && p.range[1] === range[1] + 1) || + (p.range[0] === range[0] && p.range[1] === range[1] + 1), + ); + + if (!templateInfo) { + return null; + } + + convertNode(path, node, templateInfo); + + counter++; + } + return null; + }, + }); + + if (counter !== templateInfos.length) { + throw new Error('failed to process all templates'); + } +} + +/** + * Pre-processes the template info, parsing the template content to Glimmer AST, + * fixing the offsets and locations of all nodes also calculates the block + * params locations & ranges and adding it to the info + */ +function preprocess(code: string): { + code: string; + templateInfos: GlimmerTemplateInfo[]; +} { + const templateNodes = p.parse(code) as RawGlimmerTemplate[]; + const templateInfos: GlimmerTemplateInfo[] = []; + let output = code; + for (const templateNode of templateNodes) { + output = normalizeWhitespace(templateNode, code, output); + + const template = code.slice( + templateNode.contentRange.start, + templateNode.contentRange.end, + ); + const templateInfo: GlimmerTemplateInfo = { + range: [templateNode.range.start, templateNode.range.end], + start: templateNode.range.start, + end: templateNode.range.end, + extra: { + template, + }, + }; + templateInfos.push(templateInfo); + } + + return { templateInfos, code: output }; +} + +export const parser: Parser = { + ...typescript, + astFormat: PRINTER_NAME, + + async parse(code: string, options: Options): Promise { + const preprocessed = preprocess(code); + const ast = await typescript.parse(preprocessed.code, options); + assert('expected ast', ast); + convertAst(ast, preprocessed.templateInfos); + return ast; + }, +}; diff --git a/src/parse/whitespace.ts b/src/parse/whitespace.ts new file mode 100644 index 00000000..b0e16baf --- /dev/null +++ b/src/parse/whitespace.ts @@ -0,0 +1,54 @@ +import type { RawGlimmerTemplate } from '../types/glimmer'; + +const STATIC_OPEN = 'static{`'; +const STATIC_CLOSE = '`}'; +const NEWLINE = '\n'; + +function replaceRange( + original: string, + range: { start: number; end: number }, + substitute: string, +): string { + return ( + original.slice(0, range.start) + substitute + original.slice(range.end) + ); +} + +/** Hacks to normalize whitespace. */ +export function normalizeWhitespace( + templateNode: RawGlimmerTemplate, + originalCode: string, + currentCode: string, +): string { + let prefix: string; + let suffix: string; + + if (templateNode.type === 'class-member') { + prefix = STATIC_OPEN; + suffix = STATIC_CLOSE; + } else { + const nextWord = originalCode.slice(templateNode.range.end).match(/\S+/); + prefix = '{'; + suffix = '}'; + if (nextWord && nextWord[0] === 'as') { + prefix = '(' + prefix; + suffix = suffix + ')'; + } else if (!nextWord || ![',', ')'].includes(nextWord[0][0] || '')) { + suffix += ';'; + } + } + + const lineBreakCount = [...templateNode.contents].reduce( + (sum, currentContents) => sum + (currentContents === NEWLINE ? 1 : 0), + 0, + ); + const totalLength = templateNode.range.end - templateNode.range.start; + const spaces = totalLength - prefix.length - suffix.length - lineBreakCount; + const content = ' '.repeat(spaces) + NEWLINE.repeat(lineBreakCount); + + return replaceRange( + currentCode, + templateNode.range, + `${prefix}${content}${suffix}`, + ); +} diff --git a/src/print/ambiguity.ts b/src/print/ambiguity.ts new file mode 100644 index 00000000..5f5d71f4 --- /dev/null +++ b/src/print/ambiguity.ts @@ -0,0 +1,61 @@ +import type { Node } from '@babel/types'; +import type { AstPath, doc, Printer } from 'prettier'; +import { printers as estreePrinters } from 'prettier/plugins/estree.js'; + +import type { Options } from '../options.js'; + +const estreePrinter = estreePrinters['estree'] as Printer; + +/** NOTE: This is highly specialized for use in `fixPreviousPrint` */ +function flattenDoc(doc: doc.builders.Doc): string[] { + if (Array.isArray(doc)) { + return doc.flatMap(flattenDoc); + } else if (typeof doc === 'string') { + return [doc]; + } else if ('contents' in doc) { + return flattenDoc(doc.contents); + } else { + return []; + } +} + +/** + * Search next non EmptyStatement node and set current print, so we can fix it + * later if its ambiguous + */ +export function saveCurrentPrintOnSiblingNode( + path: AstPath, + printed: doc.builders.Doc[], +): void { + const { index, siblings } = path; + if (index !== null) { + const nextNode = siblings + ?.slice(index + 1) + .find((n) => n?.type !== 'EmptyStatement'); + if (nextNode) { + nextNode.extra = nextNode.extra || {}; + nextNode.extra['prevTemplatePrinted'] = printed; + } + } +} + +/** HACK to fix ASI semi-colons. */ +export function fixPreviousPrint( + previousTemplatePrinted: doc.builders.Doc[], + path: AstPath, + options: Options, + print: (path: AstPath) => doc.builders.Doc, + args: unknown, +): void { + const printedSemiFalse = estreePrinter.print( + path, + { ...options, semi: false }, + print, + args, + ); + const flat = flattenDoc(printedSemiFalse); + const previousFlat = flattenDoc(previousTemplatePrinted); + if (flat[0]?.startsWith(';') && previousFlat.at(-1) !== ';') { + previousTemplatePrinted.push(';'); + } +} diff --git a/src/print/index.ts b/src/print/index.ts index 46f42d76..40ce46f8 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -1,98 +1,25 @@ import type { Node } from '@babel/types'; -import type { doc, Options as PrettierOptions, Printer } from 'prettier'; -import type { AstPath } from 'prettier'; +import type { + AstPath, + doc, + Options as PrettierOptions, + Printer, +} from 'prettier'; import { printers as estreePrinters } from 'prettier/plugins/estree.js'; import type { Options } from '../options.js'; -import type { TemplateNode } from '../parse'; +import { getGlimmerTemplate, isGlimmerTemplate } from '../types/glimmer'; import { assert } from '../utils'; +import { fixPreviousPrint, saveCurrentPrintOnSiblingNode } from './ambiguity'; import { printTemplateContent, printTemplateTag } from './template'; const estreePrinter = estreePrinters['estree'] as Printer; -function getGlimmerExpression(node: Node | undefined): Node | null { - if (!node) return null; - if (node.extra?.['isGlimmerTemplate']) { - return node; - } - if ( - node.type === 'ExportDefaultDeclaration' && - node.declaration.extra?.['isGlimmerTemplate'] - ) { - return node.declaration; - } - if ( - node.type === 'ExportDefaultDeclaration' && - node.declaration.type === 'TSAsExpression' && - node.declaration.expression.extra?.['isGlimmerTemplate'] - ) { - return node.declaration.expression; - } - return null; -} - -function flattenDoc(doc: doc.builders.Doc): string[] { - const array = (doc as unknown as doc.builders.Group).contents || doc; - if (!Array.isArray(array)) return array as unknown as string[]; - return array.flatMap((x) => - (x as doc.builders.Group).contents - ? flattenDoc((x as doc.builders.Group).contents) - : Array.isArray(x) - ? flattenDoc(x) - : x, - ) as string[]; -} - -/** - * Search next non EmptyStatement node and set current print, so we can fix it - * later if its ambiguous - */ -function saveCurrentPrintOnSiblingNode( - path: AstPath, - printed: doc.builders.Doc, -): void { - const { index, siblings } = path; - if (index !== null) { - const nextNode = siblings - ?.slice(index + 1) - .find((n) => n?.type !== 'EmptyStatement'); - if (nextNode) { - nextNode.extra = nextNode.extra || {}; - nextNode.extra['prevTemplatePrinted'] = printed; - } - } -} - -function fixPreviousPrint( - path: AstPath, - options: Options, - print: (path: AstPath) => doc.builders.Doc, - args: unknown, -): void { - const printedSemiFalse = estreePrinter.print( - path, - { ...options, semi: false }, - print, - args, - ); - const flat = flattenDoc(printedSemiFalse); - const previousTemplatePrinted = path.node?.extra?.[ - 'prevTemplatePrinted' - ] as string[]; - const previousFlat = flattenDoc(previousTemplatePrinted); - if (flat[0]?.startsWith(';') && previousFlat.at(-1) !== ';') { - previousTemplatePrinted.push(';'); - } -} - export const printer: Printer = { ...estreePrinter, getVisitorKeys(node, nonTraversableKeys) { - if (node === undefined) { - return []; - } - if (node.extra?.['isGlimmerTemplate']) { + if (!node || isGlimmerTemplate(node)) { return []; } return estreePrinter.getVisitorKeys?.(node, nonTraversableKeys) || []; @@ -106,7 +33,7 @@ export const printer: Printer = { ) { const { node } = path; const hasPrettierIgnore = checkPrettierIgnore(path); - if (getGlimmerExpression(node)) { + if (getGlimmerTemplate(node)) { if (hasPrettierIgnore) { return printRawText(path, options); } else { @@ -114,21 +41,32 @@ export const printer: Printer = { assert('Expected Glimmer doc to be an array', Array.isArray(printed)); trimPrinted(printed); + // Always remove export default so we start with a blank slate if ( - !options.templateExportDefault && docMatchesString(printed[0], 'export') && docMatchesString(printed[1], 'default') ) { printed = printed.slice(2); trimPrinted(printed); } + + if (options.templateExportDefault) { + printed.unshift('export ', 'default '); + } + saveCurrentPrintOnSiblingNode(path, printed); return printed; } } if (options.semi && node?.extra?.['prevTemplatePrinted']) { - fixPreviousPrint(path, options, print, args); + fixPreviousPrint( + node.extra['prevTemplatePrinted'] as doc.builders.Doc[], + path, + options, + print, + args, + ); } return hasPrettierIgnore @@ -141,68 +79,40 @@ export const printer: Printer = { const { node } = path; const hasPrettierIgnore = checkPrettierIgnore(path); - const options = { ...embedOptions } as Options; if (hasPrettierIgnore) { return printRawText(path, embedOptions as Options); } return async (textToDoc) => { - try { - if (node?.extra?.['isGlimmerTemplate'] && node.extra['template']) { - let content = null; - let raw = false; - try { - content = await printTemplateContent( - node.extra['template'] as string, - textToDoc, - embedOptions as Options, - ); - } catch { - content = node.extra['template'] as string; - raw = true; - } - const extra = node.extra as TemplateNode['extra']; - const { isDefaultTemplate, isAssignment, isAlreadyExportDefault } = - extra; - const useHardline = !isAssignment || isDefaultTemplate || false; - const shouldExportDefault = - (!isAlreadyExportDefault && - isDefaultTemplate && - options.templateExportDefault) || - false; - const printed = printTemplateTag(content, { - exportDefault: shouldExportDefault, - useHardline, - raw, - }); + if (node && isGlimmerTemplate(node)) { + try { + const content = await printTemplateContent( + node.extra.template, + textToDoc, + embedOptions as Options, + ); + + const printed = printTemplateTag( + content, + node.extra.isDefaultTemplate, + ); + saveCurrentPrintOnSiblingNode(path, printed); + return printed; + } catch { + const printed = [printRawText(path, embedOptions as Options)]; saveCurrentPrintOnSiblingNode(path, printed); return printed; } - } catch (error) { - console.log(error); - const printed = [printRawText(path, embedOptions as Options)]; - saveCurrentPrintOnSiblingNode(path, printed); - return printed; } // Nothing to embed, so move on to the regular printer. return; }; }, - - /** - * Turn off any built-in prettier-ignore handling because it will skip - * embedding, which will print `[__GLIMMER_TEMPLATE(...)]` instead of - * ``. - */ - hasPrettierIgnore: undefined, }; -/** - * Remove the semicolons and empty strings that Prettier added so we can manage - * them. - */ +/** Remove the empty strings that Prettier added so we can manage them. */ function trimPrinted(printed: doc.builders.Doc[]): void { while (docMatchesString(printed[0], '')) { printed.shift(); diff --git a/src/print/template.ts b/src/print/template.ts index 5f3d54b9..ec7d7e7e 100644 --- a/src/print/template.ts +++ b/src/print/template.ts @@ -44,22 +44,15 @@ export async function printTemplateContent( */ export function printTemplateTag( content: doc.builders.Doc, - options: { - exportDefault: boolean; - useHardline: boolean; - raw: boolean; - }, -): doc.builders.Doc { - const line = options.raw ? '' : options.useHardline ? hardline : softline; + useHardline: boolean, +): doc.builders.Doc[] { + const line = useHardline ? hardline : softline; const doc = [ TEMPLATE_TAG_OPEN, indent([line, group(content)]), line, TEMPLATE_TAG_CLOSE, ]; - if (options.exportDefault) { - doc.splice(0, 0, 'export default '); - } return [group(doc)]; } diff --git a/src/types/glimmer.ts b/src/types/glimmer.ts new file mode 100644 index 00000000..a5ffabfd --- /dev/null +++ b/src/types/glimmer.ts @@ -0,0 +1,124 @@ +import type { NodePath } from '@babel/core'; +import type { Comment, Node } from '@babel/types'; + +/** The raw GlimmerTemplate node as parsed by the content-tag parser. */ +export interface RawGlimmerTemplate { + type: 'expression' | 'class-member'; + + tagName: 'template'; + + /** Raw template contents */ + contents: string; + + /** + * Range of the contents, inclusive of inclusive of the + * `` tags. + */ + range: { + start: number; + end: number; + }; + + /** + * Range of the template contents, not inclusive of the + * `` tags. + */ + contentRange: { + start: number; + end: number; + }; + + /** Range of the opening `` tag. */ + endRange: { + start: number; + end: number; + }; +} + +export interface GlimmerTemplateInfo { + /** + * Range of the contents, inclusive of inclusive of the + * `` tags. + */ + range: [start: number, end: number]; + + /** Beginning of the range, before the opening `` tag. */ + end: number; + + extra: { + template: string; + }; +} + +export interface GlimmerTemplate { + type: 'FunctionDeclaration'; + + leadingComments: Comment[]; + + /** + * Range of the contents, inclusive of inclusive of the + * `` tags. + */ + range: [start: number, end: number]; + + /** Beginning of the range, before the opening `` tag. */ + end: number; + + extra: { + isGlimmerTemplate: true; + isDefaultTemplate: boolean; + template: string; + }; +} + +/** Returns true if the node is a GlimmerTemplate. */ +export function isGlimmerTemplate(node: Node): node is Node & GlimmerTemplate { + return node.extra?.['isGlimmerTemplate'] === true; +} + +/** Returns true if the GlimmerTemplate path is already a default template. */ +export function isDefaultTemplate(path: NodePath): boolean { + return ( + // Top level `` + path.parent.type === 'Program' || + // Top level ` as TemplateOnlyComponent` + (path.parent.type === 'TSAsExpression' && + path.parentPath?.parentPath?.parent.type === 'Program') + ); +} + +/** Extracts GlimmerTemplate from node. */ +export function getGlimmerTemplate( + node: Node | undefined, +): GlimmerTemplate | null { + if (!node) return null; + if (isGlimmerTemplate(node)) { + return node; + } + if ( + node.type === 'ExportDefaultDeclaration' && + isGlimmerTemplate(node.declaration) + ) { + return node.declaration; + } + if ( + node.type === 'ExportDefaultDeclaration' && + node.declaration.type === 'TSAsExpression' && + isGlimmerTemplate(node.declaration.expression) + ) { + return node.declaration.expression; + } + return null; +} diff --git a/tests/helpers/ambiguous.ts b/tests/helpers/ambiguous.ts index 1a05cb97..6d6ac366 100644 --- a/tests/helpers/ambiguous.ts +++ b/tests/helpers/ambiguous.ts @@ -107,5 +107,6 @@ async function behavesLikeFormattedAmbiguousCase( throw error; } expect(isSyntaxError, 'Expected SyntaxError').toBeTruthy(); + expect('Syntax Error').toMatchSnapshot(); } } diff --git a/tests/unit-tests/ambiguous/__snapshots__/arrow-parens-avoid.test.ts.snap b/tests/unit-tests/ambiguous/__snapshots__/arrow-parens-avoid.test.ts.snap index 3b9549ac..77cd5593 100644 --- a/tests/unit-tests/ambiguous/__snapshots__/arrow-parens-avoid.test.ts.snap +++ b/tests/unit-tests/ambiguous/__snapshots__/arrow-parens-avoid.test.ts.snap @@ -1,5 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`ambiguous > config > arrowParens: "avoid" > (oh, no) => {} > with semi, with newline > it formats ../cases/gjs/component-class.gjs 1`] = `"Syntax Error"`; + +exports[`ambiguous > config > arrowParens: "avoid" > (oh, no) => {} > with semi, with newline > it formats ../cases/gjs/component-class-with-content-before-template.gjs 1`] = `"Syntax Error"`; + exports[`ambiguous > config > arrowParens: "avoid" > (oh, no) => {} > with semi, with newline > it formats ../cases/gjs/default-export.gjs 1`] = ` "