From d4bf8cf2f08c945aaec62ecd4c1f9446c3782790 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:42:49 +0100 Subject: [PATCH 01/11] chore: Set severity level for jsdoc/require-jsdoc to warn --- eslint.config.mjs | 1 - src/utils/content-tag.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index b6c0625..b891487 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -28,7 +28,6 @@ const customRules = { rules: { 'jsdoc/check-param-names': 'off', 'jsdoc/newline-after-description': 'off', - 'jsdoc/require-jsdoc': ['error', { publicOnly: true }], 'jsdoc/require-param': 'off', 'jsdoc/require-param-type': 'off', 'jsdoc/require-returns': 'off', diff --git a/src/utils/content-tag.ts b/src/utils/content-tag.ts index 24bc586..fe4bffa 100644 --- a/src/utils/content-tag.ts +++ b/src/utils/content-tag.ts @@ -1,4 +1,4 @@ -/* eslint-disable jsdoc/require-jsdoc, unicorn/prefer-export-from */ +/* eslint-disable unicorn/prefer-export-from */ import { type Parsed as ContentTag, Preprocessor, From c796b875b13a9b93916444b74f7a880c670a9327 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:42:55 +0100 Subject: [PATCH 02/11] refactor: Created utils/index.ts --- src/config.ts | 6 ------ src/main.ts | 9 ++++----- src/parse/index.ts | 2 +- src/print/index.ts | 2 +- src/print/template.ts | 2 +- src/utils/index.ts | 9 +++++++++ tests/helpers/format.ts | 3 +-- 7 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 src/config.ts create mode 100644 src/utils/index.ts diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 5c90b66..0000000 --- a/src/config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const PARSER_NAME = 'ember-template-tag'; -export const PRINTER_NAME = 'ember-template-tag-estree'; -export const TEMPLATE_TAG_NAME = 'template'; - -export const TEMPLATE_TAG_OPEN = `<${TEMPLATE_TAG_NAME}>`; -export const TEMPLATE_TAG_CLOSE = ``; diff --git a/src/main.ts b/src/main.ts index 0af8e15..75ecc79 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,9 @@ -import type { Node } from '@babel/types'; 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/index.js'; import { printer } from './print/index.js'; +import { type NodeType, PARSER_NAME, PRINTER_NAME } from './utils/index.js'; const languages: SupportLanguage[] = [ { @@ -25,15 +24,15 @@ const languages: SupportLanguage[] = [ }, ]; -const parsers: Record> = { +const parsers: Record> = { [PARSER_NAME]: parser, }; -const printers: Record> = { +const printers: Record> = { [PRINTER_NAME]: printer, }; -const plugin: Plugin = { +const plugin: Plugin = { languages, parsers, printers, diff --git a/src/parse/index.ts b/src/parse/index.ts index 533f115..f1b77d7 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -9,9 +9,9 @@ import type { import type { Parser } from 'prettier'; import { parsers as babelParsers } from 'prettier/plugins/babel.js'; -import { PRINTER_NAME } from '../config.js'; import type { Options } from '../options.js'; import { assert } from '../utils/assert.js'; +import { PRINTER_NAME } from '../utils/index.js'; import { preprocess, type Template } from './preprocess.js'; const typescript = babelParsers['babel-ts'] as Parser; diff --git a/src/print/index.ts b/src/print/index.ts index f25a76f..4fcff2d 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -7,13 +7,13 @@ import type { } from 'prettier'; import { printers as estreePrinters } from 'prettier/plugins/estree.js'; -import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../config.js'; import type { Options } from '../options.js'; import { isGlimmerTemplate, isGlimmerTemplateParent, } from '../types/glimmer.js'; import { assert } from '../utils/assert.js'; +import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; import { fixPreviousPrint, saveCurrentPrintOnSiblingNode, diff --git a/src/print/template.ts b/src/print/template.ts index 39499d5..50d4976 100644 --- a/src/print/template.ts +++ b/src/print/template.ts @@ -1,10 +1,10 @@ import type { Options as PrettierOptions } from 'prettier'; import { doc } from 'prettier'; -import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../config.js'; import type { Options } from '../options.js'; import { getTemplateSingleQuote } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; +import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; const { builders: { group, hardline, indent, softline }, diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..bc5153f --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,9 @@ +import type { Node } from '@babel/types'; + +export type NodeType = Node | undefined; + +export const PARSER_NAME = 'ember-template-tag'; +export const PRINTER_NAME = 'ember-template-tag-estree'; + +export const TEMPLATE_TAG_OPEN = ''; diff --git a/tests/helpers/format.ts b/tests/helpers/format.ts index f15b230..90eeaf4 100644 --- a/tests/helpers/format.ts +++ b/tests/helpers/format.ts @@ -1,12 +1,11 @@ import type { Plugin } from 'prettier'; import { format as prettierFormat } from 'prettier'; -import { PARSER_NAME } from '../../src/config.js'; import plugin from '../../src/main.js'; import type { Options } from '../../src/options.js'; const DEFAULT_OPTIONS: Partial = { - parser: PARSER_NAME, + parser: 'ember-template-tag', plugins: [plugin as Plugin], }; From da8545c32b44fb89152b90ff68dec4c4df5a2ac8 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:42:59 +0100 Subject: [PATCH 03/11] refactor: Created languages.ts --- src/languages.ts | 22 ++++++++++++++++++++++ src/main.ts | 22 ++-------------------- 2 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 src/languages.ts diff --git a/src/languages.ts b/src/languages.ts new file mode 100644 index 0000000..536de8e --- /dev/null +++ b/src/languages.ts @@ -0,0 +1,22 @@ +import type { SupportLanguage } from 'prettier'; + +import { PARSER_NAME } from './utils/index.js'; + +export const languages: SupportLanguage[] = [ + { + aliases: ['gjs', 'glimmer-js'], + extensions: ['.gjs'], + group: 'JavaScript', + name: 'Ember Template Tag (gjs)', + parsers: [PARSER_NAME], + vscodeLanguageIds: ['glimmer-js'], + }, + { + aliases: ['gts', 'glimmer-ts'], + extensions: ['.gts'], + group: 'TypeScript', + name: 'Ember Template Tag (gts)', + parsers: [PARSER_NAME], + vscodeLanguageIds: ['glimmer-ts'], + }, +]; diff --git a/src/main.ts b/src/main.ts index 75ecc79..c5f3d91 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,29 +1,11 @@ -import type { Parser, Plugin, Printer, SupportLanguage } from 'prettier'; +import type { Parser, Plugin, Printer } from 'prettier'; +import { languages } from './languages.js'; import { options } from './options.js'; import { parser } from './parse/index.js'; import { printer } from './print/index.js'; import { type NodeType, PARSER_NAME, PRINTER_NAME } from './utils/index.js'; -const languages: SupportLanguage[] = [ - { - name: 'Ember Template Tag (gjs)', - aliases: ['gjs', 'glimmer-js'], - extensions: ['.gjs'], - vscodeLanguageIds: ['glimmer-js'], - parsers: [PARSER_NAME], - group: 'JavaScript', - }, - { - name: 'Ember Template Tag (gts)', - aliases: ['gts', 'glimmer-ts'], - extensions: ['.gts'], - vscodeLanguageIds: ['glimmer-ts'], - parsers: [PARSER_NAME], - group: 'TypeScript', - }, -]; - const parsers: Record> = { [PARSER_NAME]: parser, }; From 7921d2449a3b6d788a97db72dc65490f80307ffe Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:05 +0100 Subject: [PATCH 04/11] refactor: Standardized options.ts --- src/options.ts | 10 +++++----- src/parse/index.ts | 4 ++-- src/print/ambiguity.ts | 4 ++-- src/print/index.ts | 17 ++++++++++------- src/print/template.ts | 4 ++-- tests/helpers/ambiguous.ts | 4 ++-- tests/helpers/format.ts | 6 +++--- tests/helpers/make-suite.ts | 4 ++-- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/options.ts b/src/options.ts index 9a5b70d..e04e224 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,21 +1,21 @@ -import type { Node } from '@babel/types'; import type { BooleanSupportOption, ParserOptions, SupportOptions, } from 'prettier'; -export interface Options extends ParserOptions { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface PluginOptions extends ParserOptions { templateExportDefault?: boolean; templateSingleQuote?: boolean; } const templateExportDefault: BooleanSupportOption = { category: 'Format', - type: 'boolean', default: false, description: 'Prepend default export template tags with "export default". Since 0.1.0.', + type: 'boolean', }; /** @@ -23,7 +23,7 @@ const templateExportDefault: BooleanSupportOption = { * `templateSingleQuote` is defined, it will be used, otherwise the value for * `singleQuote` will be inherited. */ -export function getTemplateSingleQuote(options: Options): boolean { +export function getTemplateSingleQuote(options: PluginOptions): boolean { const { singleQuote, templateSingleQuote } = options; return typeof templateSingleQuote === 'boolean' ? templateSingleQuote @@ -32,9 +32,9 @@ export function getTemplateSingleQuote(options: Options): boolean { const templateSingleQuote: BooleanSupportOption = { category: 'Format', - type: 'boolean', description: 'Use single quotes instead of double quotes within template tags. Since 0.0.3.', + type: 'boolean', }; export const options: SupportOptions = { diff --git a/src/parse/index.ts b/src/parse/index.ts index f1b77d7..e05d9d8 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -9,7 +9,7 @@ import type { import type { Parser } from 'prettier'; import { parsers as babelParsers } from 'prettier/plugins/babel.js'; -import type { Options } from '../options.js'; +import type { PluginOptions } from '../options.js'; import { assert } from '../utils/assert.js'; import { PRINTER_NAME } from '../utils/index.js'; import { preprocess, type Template } from './preprocess.js'; @@ -124,7 +124,7 @@ export const parser: Parser = { ...typescript, astFormat: PRINTER_NAME, - async parse(code: string, options: Options): Promise { + async parse(code: string, options: PluginOptions): Promise { const preprocessed = preprocess(code, options.filepath); const ast = await typescript.parse(preprocessed.code, options); diff --git a/src/print/ambiguity.ts b/src/print/ambiguity.ts index 7ff879f..9ad9bf8 100644 --- a/src/print/ambiguity.ts +++ b/src/print/ambiguity.ts @@ -2,7 +2,7 @@ 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'; +import type { PluginOptions } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; const estreePrinter = estreePrinters['estree'] as Printer; @@ -31,7 +31,7 @@ export function saveCurrentPrintOnSiblingNode( export function fixPreviousPrint( previousTemplatePrinted: doc.builders.Doc[], path: AstPath, - options: Options, + options: PluginOptions, print: (path: AstPath) => doc.builders.Doc, args: unknown, ): void { diff --git a/src/print/index.ts b/src/print/index.ts index 4fcff2d..3fdfb99 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -7,7 +7,7 @@ import type { } from 'prettier'; import { printers as estreePrinters } from 'prettier/plugins/estree.js'; -import type { Options } from '../options.js'; +import type { PluginOptions } from '../options.js'; import { isGlimmerTemplate, isGlimmerTemplateParent, @@ -32,13 +32,16 @@ export const printer: Printer = { return estreePrinter.getVisitorKeys?.(node, nonTraversableKeys) || []; }, - printPrettierIgnored(path: AstPath, options: Options) { + printPrettierIgnored( + path: AstPath, + options: PluginOptions, + ) { return printRawText(path, options); }, print( path: AstPath, - options: Options, + options: PluginOptions, print: (path: AstPath) => doc.builders.Doc, args: unknown, ) { @@ -102,14 +105,14 @@ export const printer: Printer = { return async (textToDoc) => { if (node && isGlimmerTemplate(node)) { if (checkPrettierIgnore(path)) { - return printRawText(path, embedOptions as Options); + return printRawText(path, embedOptions as PluginOptions); } try { const content = await printTemplateContent( node.extra.template.contents, textToDoc, - embedOptions as Options, + embedOptions as PluginOptions, ); const printed = printTemplateTag(content); @@ -117,7 +120,7 @@ export const printer: Printer = { return printed; } catch (error) { console.error(error); - const printed = [printRawText(path, embedOptions as Options)]; + const printed = [printRawText(path, embedOptions as PluginOptions)]; saveCurrentPrintOnSiblingNode(path, printed); return printed; } @@ -142,7 +145,7 @@ function trimPrinted(printed: doc.builders.Doc[]): void { function printRawText( { node }: AstPath, - options: Options, + options: PluginOptions, ): string { if (!node) { return ''; diff --git a/src/print/template.ts b/src/print/template.ts index 50d4976..5461bba 100644 --- a/src/print/template.ts +++ b/src/print/template.ts @@ -1,7 +1,7 @@ import type { Options as PrettierOptions } from 'prettier'; import { doc } from 'prettier'; -import type { Options } from '../options.js'; +import type { PluginOptions } from '../options.js'; import { getTemplateSingleQuote } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; @@ -25,7 +25,7 @@ export async function printTemplateContent( // should normalize them into standard Prettier options at this point. options: PrettierOptions, ) => Promise, - options: Options, + options: PluginOptions, ): Promise { return await textToDoc(text.trim(), { ...options, diff --git a/tests/helpers/ambiguous.ts b/tests/helpers/ambiguous.ts index 84a4f21..b4a961d 100644 --- a/tests/helpers/ambiguous.ts +++ b/tests/helpers/ambiguous.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import type { Options } from '../../src/options.js'; +import type { PluginOptions } from '../../src/options.js'; import { parse } from '../../src/utils/content-tag.js'; import type { TestCase } from '../helpers/cases.js'; import { getAllCases } from '../helpers/cases.js'; @@ -89,7 +89,7 @@ export function makeAmbiguousExpressionTest( async function behavesLikeFormattedAmbiguousCase( code: string, - formatOptions: Partial = {}, + formatOptions: Partial = {}, ): Promise { try { const result = await format(code, formatOptions); diff --git a/tests/helpers/format.ts b/tests/helpers/format.ts index 90eeaf4..f597034 100644 --- a/tests/helpers/format.ts +++ b/tests/helpers/format.ts @@ -2,9 +2,9 @@ import type { Plugin } from 'prettier'; import { format as prettierFormat } from 'prettier'; import plugin from '../../src/main.js'; -import type { Options } from '../../src/options.js'; +import type { PluginOptions } from '../../src/options.js'; -const DEFAULT_OPTIONS: Partial = { +const DEFAULT_OPTIONS: Partial = { parser: 'ember-template-tag', plugins: [plugin as Plugin], }; @@ -17,7 +17,7 @@ const DEFAULT_OPTIONS: Partial = { */ export async function format( code: string, - overrides: Partial = {}, + overrides: Partial = {}, ): Promise { return await prettierFormat(code, { ...DEFAULT_OPTIONS, diff --git a/tests/helpers/make-suite.ts b/tests/helpers/make-suite.ts index 082a714..29a1d52 100644 --- a/tests/helpers/make-suite.ts +++ b/tests/helpers/make-suite.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import type { Options } from '../../src/options.js'; +import type { PluginOptions } from '../../src/options.js'; import { AMBIGUOUS_PLACEHOLDER, getAmbiguousCases, @@ -12,7 +12,7 @@ import { format } from './format.js'; export interface Config { name: string; - options?: Partial; + options?: Partial; } /** From 0cb2e897f9db273c6962e5143a2e7758c3edc833 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:10 +0100 Subject: [PATCH 05/11] refactor: Created parsers.ts (1) --- src/main.ts | 10 +++------- src/parsers.ts | 8 ++++++++ src/{parse => parsers}/index.ts | 0 src/{parse => parsers}/preprocess.ts | 0 tests/unit-tests/preprocess.test.ts | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 src/parsers.ts rename src/{parse => parsers}/index.ts (100%) rename src/{parse => parsers}/preprocess.ts (100%) diff --git a/src/main.ts b/src/main.ts index c5f3d91..c68c1a5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,10 @@ -import type { Parser, Plugin, Printer } from 'prettier'; +import type { Plugin, Printer } from 'prettier'; import { languages } from './languages.js'; import { options } from './options.js'; -import { parser } from './parse/index.js'; +import { parsers } from './parsers.js'; import { printer } from './print/index.js'; -import { type NodeType, PARSER_NAME, PRINTER_NAME } from './utils/index.js'; - -const parsers: Record> = { - [PARSER_NAME]: parser, -}; +import { type NodeType, PRINTER_NAME } from './utils/index.js'; const printers: Record> = { [PRINTER_NAME]: printer, diff --git a/src/parsers.ts b/src/parsers.ts new file mode 100644 index 0000000..f120066 --- /dev/null +++ b/src/parsers.ts @@ -0,0 +1,8 @@ +import type { Parser } from 'prettier'; + +import { parser } from './parsers/index.js'; +import { type NodeType, PARSER_NAME } from './utils/index.js'; + +export const parsers: Record> = { + [PARSER_NAME]: parser, +}; diff --git a/src/parse/index.ts b/src/parsers/index.ts similarity index 100% rename from src/parse/index.ts rename to src/parsers/index.ts diff --git a/src/parse/preprocess.ts b/src/parsers/preprocess.ts similarity index 100% rename from src/parse/preprocess.ts rename to src/parsers/preprocess.ts diff --git a/tests/unit-tests/preprocess.test.ts b/tests/unit-tests/preprocess.test.ts index 386ebfb..245e483 100644 --- a/tests/unit-tests/preprocess.test.ts +++ b/tests/unit-tests/preprocess.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'; import { codeToGlimmerAst, preprocessTemplateRange, -} from '../../src/parse/preprocess.js'; +} from '../../src/parsers/preprocess.js'; const TEST_CASES = [ { From f04e498a3eb6039d4a266d78dad7cd7377e1c786 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:17 +0100 Subject: [PATCH 06/11] refactor: Created parsers.ts (2) --- src/parsers.ts | 27 ++++++-- src/parsers/convert-ast.ts | 119 ++++++++++++++++++++++++++++++++ src/parsers/index.ts | 138 +------------------------------------ 3 files changed, 144 insertions(+), 140 deletions(-) create mode 100644 src/parsers/convert-ast.ts diff --git a/src/parsers.ts b/src/parsers.ts index f120066..204c4f6 100644 --- a/src/parsers.ts +++ b/src/parsers.ts @@ -1,8 +1,27 @@ -import type { Parser } from 'prettier'; +import type { File } from '@babel/types'; +import type { Parser, ParserOptions } from 'prettier'; +import { parsers as prettierParsers } from 'prettier/plugins/babel.js'; -import { parser } from './parsers/index.js'; -import { type NodeType, PARSER_NAME } from './utils/index.js'; +import { convertAst, preprocess } from './parsers/index.js'; +import { assert } from './utils/assert.js'; +import { type NodeType, PARSER_NAME, PRINTER_NAME } from './utils/index.js'; + +const parser = prettierParsers['babel-ts'] as Parser; + +async function parse(text: string, options: ParserOptions) { + const { code, templates } = preprocess(text, options.filepath); + + const ast = await parser.parse(code, options); + assert('expected ast', ast); + convertAst(ast as File, { parser, templates }); + + return ast; +} export const parsers: Record> = { - [PARSER_NAME]: parser, + [PARSER_NAME]: { + ...parser, + astFormat: PRINTER_NAME, + parse, + }, }; diff --git a/src/parsers/convert-ast.ts b/src/parsers/convert-ast.ts new file mode 100644 index 0000000..f41a3da --- /dev/null +++ b/src/parsers/convert-ast.ts @@ -0,0 +1,119 @@ +import traverse from '@babel/traverse'; +import type { + BlockStatement, + File, + ObjectExpression, + StaticBlock, +} from '@babel/types'; +import type { Parser } from 'prettier'; + +import type { NodeType } from '../utils/index.js'; +import type { Template } from './preprocess.js'; + +type Data = { + parser: Parser; + templates: Template[]; +}; + +/** Converts a node into a GlimmerTemplate node */ +function convertNode( + node: BlockStatement | ObjectExpression | StaticBlock, + rawTemplate: Template, +): void { + node.innerComments = []; + node.extra = Object.assign(node.extra ?? {}, { + isGlimmerTemplate: true as const, + template: rawTemplate, + }); +} + +function findCorrectCommentBlockIndex( + comments: File['comments'], + start: number, + end: number, +): number { + if (!comments) { + return -1; + } + + return comments.findIndex((comment) => { + const { start: commentStart, end: commentEnd } = comment; + + return ( + (commentStart === start && commentEnd === end) || + (commentStart === start + 1 && commentEnd === end - 1) || + (commentStart === start + 7 && commentEnd === end - 1) + ); + }); +} + +/** Traverses the AST and replaces the transformed template parts with other AST */ +export function convertAst(ast: File, data: Data): void { + const { parser, templates } = data; + + traverse(ast, { + enter(path) { + if (templates.length === 0) { + return null; + } + + const { node } = path; + + switch (node.type) { + case 'BlockStatement': + case 'ObjectExpression': + case 'StaticBlock': { + const [start, end] = [parser.locStart(node), parser.locEnd(node)]; + + const templateIndex = templates.findIndex((template) => { + const { utf16Range } = template; + + if (utf16Range.start === start && utf16Range.end === end) { + return true; + } + + return ( + node.type === 'ObjectExpression' && + utf16Range.start === start - 1 && + utf16Range.end === end + 1 + ); + }); + + if (templateIndex === -1) { + return null; + } + + const rawTemplate = templates.splice(templateIndex, 1)[0]; + + if (!rawTemplate) { + throw new Error( + 'expected raw template because splice index came from findIndex', + ); + } + + if (ast.comments && ast.comments.length > 0) { + const commentBlockIndex = findCorrectCommentBlockIndex( + ast.comments, + start, + end, + ); + + if (commentBlockIndex !== undefined && commentBlockIndex >= 0) { + ast.comments.splice(commentBlockIndex, 1); + } + } + + convertNode(node, rawTemplate); + } + } + + return null; + }, + }); + + if (templates.length > 0) { + throw new Error( + `failed to process all templates, ${templates.length} remaining`, + ); + } +} diff --git a/src/parsers/index.ts b/src/parsers/index.ts index e05d9d8..0ffcf91 100644 --- a/src/parsers/index.ts +++ b/src/parsers/index.ts @@ -1,136 +1,2 @@ -import traverse from '@babel/traverse'; -import type { - BlockStatement, - File, - Node, - ObjectExpression, - StaticBlock, -} from '@babel/types'; -import type { Parser } from 'prettier'; -import { parsers as babelParsers } from 'prettier/plugins/babel.js'; - -import type { PluginOptions } from '../options.js'; -import { assert } from '../utils/assert.js'; -import { PRINTER_NAME } from '../utils/index.js'; -import { preprocess, type Template } from './preprocess.js'; - -const typescript = babelParsers['babel-ts'] as Parser; - -/** Converts a node into a GlimmerTemplate node */ -function convertNode( - node: BlockStatement | ObjectExpression | StaticBlock, - rawTemplate: Template, -): void { - node.innerComments = []; - node.extra = Object.assign(node.extra ?? {}, { - isGlimmerTemplate: true as const, - template: rawTemplate, - }); -} - -function findCorrectCommentBlockIndex( - comments: File['comments'], - start: number, - end: number, -): number { - if (!comments) { - return -1; - } - - return comments.findIndex((comment) => { - const { start: commentStart, end: commentEnd } = comment; - - return ( - (commentStart === start && commentEnd === end) || - (commentStart === start + 1 && commentEnd === end - 1) || - (commentStart === start + 7 && commentEnd === end - 1) - ); - }); -} - -/** Traverses the AST and replaces the transformed template parts with other AST */ -function convertAst(ast: File, templates: Template[]): void { - traverse(ast, { - enter(path) { - if (templates.length === 0) { - return null; - } - - const { node } = path; - - switch (node.type) { - case 'BlockStatement': - case 'ObjectExpression': - case 'StaticBlock': { - const [start, end] = [ - typescript.locStart(node), - typescript.locEnd(node), - ]; - - const templateIndex = templates.findIndex((template) => { - const { utf16Range } = template; - - if (utf16Range.start === start && utf16Range.end === end) { - return true; - } - - return ( - node.type === 'ObjectExpression' && - utf16Range.start === start - 1 && - utf16Range.end === end + 1 - ); - }); - - if (templateIndex === -1) { - return null; - } - - const rawTemplate = templates.splice(templateIndex, 1)[0]; - - if (!rawTemplate) { - throw new Error( - 'expected raw template because splice index came from findIndex', - ); - } - - if (ast.comments && ast.comments.length > 0) { - const commentBlockIndex = findCorrectCommentBlockIndex( - ast.comments, - start, - end, - ); - - if (commentBlockIndex !== undefined && commentBlockIndex >= 0) { - ast.comments.splice(commentBlockIndex, 1); - } - } - - convertNode(node, rawTemplate); - } - } - - return null; - }, - }); - - if (templates.length > 0) { - throw new Error( - `failed to process all templates, ${templates.length} remaining`, - ); - } -} - -export const parser: Parser = { - ...typescript, - astFormat: PRINTER_NAME, - - async parse(code: string, options: PluginOptions): Promise { - const preprocessed = preprocess(code, options.filepath); - - const ast = await typescript.parse(preprocessed.code, options); - assert('expected ast', ast); - convertAst(ast as File, preprocessed.templates); - - return ast; - }, -}; +export * from './convert-ast.js'; +export * from './preprocess.js'; From 7d186bff2a5cd188ed1b85f3c28aa92e3d68b805 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:23 +0100 Subject: [PATCH 07/11] refactor: Created printers.ts (1) --- src/main.ts | 10 +++------- src/options.ts | 12 ------------ src/printers.ts | 8 ++++++++ src/{print => printers}/ambiguity.ts | 0 src/{print => printers}/index.ts | 0 src/{print => printers}/template.ts | 3 +-- 6 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 src/printers.ts rename src/{print => printers}/ambiguity.ts (100%) rename src/{print => printers}/index.ts (100%) rename src/{print => printers}/template.ts (94%) diff --git a/src/main.ts b/src/main.ts index c68c1a5..906c4b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,10 @@ -import type { Plugin, Printer } from 'prettier'; +import type { Plugin } from 'prettier'; import { languages } from './languages.js'; import { options } from './options.js'; import { parsers } from './parsers.js'; -import { printer } from './print/index.js'; -import { type NodeType, PRINTER_NAME } from './utils/index.js'; - -const printers: Record> = { - [PRINTER_NAME]: printer, -}; +import { printers } from './printers.js'; +import type { NodeType } from './utils/index.js'; const plugin: Plugin = { languages, diff --git a/src/options.ts b/src/options.ts index e04e224..db687a3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -18,18 +18,6 @@ const templateExportDefault: BooleanSupportOption = { type: 'boolean', }; -/** - * Extracts a valid `templateSingleQuote` option out of the provided options. If - * `templateSingleQuote` is defined, it will be used, otherwise the value for - * `singleQuote` will be inherited. - */ -export function getTemplateSingleQuote(options: PluginOptions): boolean { - const { singleQuote, templateSingleQuote } = options; - return typeof templateSingleQuote === 'boolean' - ? templateSingleQuote - : singleQuote; -} - const templateSingleQuote: BooleanSupportOption = { category: 'Format', description: diff --git a/src/printers.ts b/src/printers.ts new file mode 100644 index 0000000..15db47e --- /dev/null +++ b/src/printers.ts @@ -0,0 +1,8 @@ +import type { Printer } from 'prettier'; + +import { printer } from './printers/index.js'; +import { type NodeType, PRINTER_NAME } from './utils/index.js'; + +export const printers: Record> = { + [PRINTER_NAME]: printer, +}; diff --git a/src/print/ambiguity.ts b/src/printers/ambiguity.ts similarity index 100% rename from src/print/ambiguity.ts rename to src/printers/ambiguity.ts diff --git a/src/print/index.ts b/src/printers/index.ts similarity index 100% rename from src/print/index.ts rename to src/printers/index.ts diff --git a/src/print/template.ts b/src/printers/template.ts similarity index 94% rename from src/print/template.ts rename to src/printers/template.ts index 5461bba..8aca8f7 100644 --- a/src/print/template.ts +++ b/src/printers/template.ts @@ -2,7 +2,6 @@ import type { Options as PrettierOptions } from 'prettier'; import { doc } from 'prettier'; import type { PluginOptions } from '../options.js'; -import { getTemplateSingleQuote } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; @@ -30,7 +29,7 @@ export async function printTemplateContent( return await textToDoc(text.trim(), { ...options, parser: 'glimmer', - singleQuote: getTemplateSingleQuote(options), + singleQuote: options.templateSingleQuote ?? options.singleQuote, }); } From c62c62f621d6a758b42be80ee1186aad92a22ac1 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:29 +0100 Subject: [PATCH 08/11] refactor: Created printers.ts (2) --- src/printers.ts | 138 ++++++++++++++++++++++++++- src/printers/ambiguity.ts | 42 +++++---- src/printers/ignore.ts | 27 ++++++ src/printers/index.ts | 191 +------------------------------------- src/printers/print.ts | 103 ++++++++++++++++++++ src/printers/template.ts | 64 ------------- 6 files changed, 292 insertions(+), 273 deletions(-) create mode 100644 src/printers/ignore.ts create mode 100644 src/printers/print.ts delete mode 100644 src/printers/template.ts diff --git a/src/printers.ts b/src/printers.ts index 15db47e..0e522d6 100644 --- a/src/printers.ts +++ b/src/printers.ts @@ -1,8 +1,140 @@ -import type { Printer } from 'prettier'; +import { type AstPath, doc as AST, type Printer } from 'prettier'; +import { printers as prettierPrinters } from 'prettier/plugins/estree.js'; -import { printer } from './printers/index.js'; +import type { PluginOptions } from './options.js'; +import { + checkPrettierIgnore, + docMatchesString, + fixPreviousPrint, + printRawText, + printTemplateContent, + printTemplateTag, + saveCurrentPrintOnSiblingNode, + trimPrinted, +} from './printers/index.js'; +import { isGlimmerTemplate, isGlimmerTemplateParent } from './types/glimmer.js'; +import { assert } from './utils/assert.js'; import { type NodeType, PRINTER_NAME } from './utils/index.js'; +const printer = prettierPrinters['estree'] as Printer; + +function embed(path: AstPath, options: PluginOptions) { + const { node } = path; + + return async ( + textToDoc: ( + text: string, + options: PluginOptions, + ) => Promise, + ) => { + if (node && isGlimmerTemplate(node)) { + if (checkPrettierIgnore(path)) { + return printRawText(path, options); + } + + try { + const content = await printTemplateContent( + node.extra.template.contents, + textToDoc, + options, + ); + + const printed = printTemplateTag(content); + saveCurrentPrintOnSiblingNode(path, printed); + return printed; + } catch (error) { + console.error(error); + const printed = [printRawText(path, options)]; + saveCurrentPrintOnSiblingNode(path, printed); + return printed; + } + } + + // Nothing to embed, so move on to the regular printer. + return; + }; +} + +function getVisitorKeys(node: NodeType, nonTraversableKeys: Set) { + if (node && isGlimmerTemplate(node)) { + return []; + } + return printer.getVisitorKeys?.(node, nonTraversableKeys) || []; +} + +function print( + path: AstPath, + options: PluginOptions, + print: (path: AstPath) => AST.builders.Doc, + args?: unknown, +) { + const { node } = path; + + if (isGlimmerTemplateParent(node)) { + if (checkPrettierIgnore(path)) { + return printRawText(path, options); + } else { + let printed = printer.print(path, options, print, args); + + assert('Expected Glimmer doc to be an array', Array.isArray(printed)); + trimPrinted(printed); + + // Remove semicolons so we can manage them ourselves + if (docMatchesString(printed[0], ';')) { + printed.shift(); + } + if (docMatchesString(printed.at(-1), ';')) { + printed.pop(); + } + + trimPrinted(printed); + + // Always remove export default so we start with a blank slate + if ( + 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( + node.extra['prevTemplatePrinted'] as AST.builders.Doc[], + path, + options, + print, + args, + ); + } + + return printer.print(path, options, print, args); +} + +function printPrettierIgnored( + path: AstPath, + options: PluginOptions, +) { + return printRawText(path, options); +} + export const printers: Record> = { - [PRINTER_NAME]: printer, + [PRINTER_NAME]: { + ...printer, + // @ts-expect-error: Type <...> is not assignable to <...> + embed, + getVisitorKeys, + print, + printPrettierIgnored, + }, }; diff --git a/src/printers/ambiguity.ts b/src/printers/ambiguity.ts index 9ad9bf8..fbcf91a 100644 --- a/src/printers/ambiguity.ts +++ b/src/printers/ambiguity.ts @@ -1,48 +1,54 @@ -import type { Node } from '@babel/types'; -import type { AstPath, doc, Printer } from 'prettier'; -import { printers as estreePrinters } from 'prettier/plugins/estree.js'; +import type { AstPath, doc as AST, Printer } from 'prettier'; +import { printers as prettierPrinters } from 'prettier/plugins/estree.js'; import type { PluginOptions } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; +import type { NodeType } from '../utils/index.js'; -const estreePrinter = estreePrinters['estree'] as Printer; +const printer = prettierPrinters['estree'] as Printer; /** * 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[], + path: AstPath, + printed: AST.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; - } + + if (index === null) { + return; + } + + 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, + previousTemplatePrinted: AST.builders.Doc[], + path: AstPath, options: PluginOptions, - print: (path: AstPath) => doc.builders.Doc, + print: (path: AstPath) => AST.builders.Doc, args: unknown, ): void { - const printedSemiFalse = estreePrinter.print( + const printedSemiFalse = printer.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/printers/ignore.ts b/src/printers/ignore.ts new file mode 100644 index 0000000..8d4e2b3 --- /dev/null +++ b/src/printers/ignore.ts @@ -0,0 +1,27 @@ +import type { AstPath } from 'prettier'; + +import type { NodeType } from '../utils/index.js'; + +export function checkPrettierIgnore(path: AstPath): boolean { + if (hasPrettierIgnore(path)) { + return true; + } + + return ( + Boolean(path.getParentNode()) && + path.callParent((parent) => checkPrettierIgnore(parent)) + ); +} + +export function hasPrettierIgnore(path: AstPath): boolean { + let possibleComment = path.node?.leadingComments?.at(-1)?.value.trim(); + + // @ts-expect-error Comments exist on node sometimes + if (!path.node?.leadingComments && path.node?.comments) { + // @ts-expect-error Comments exist on node sometimes + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + possibleComment = path.node.comments?.at(-1)?.value.trim(); + } + + return possibleComment === 'prettier-ignore'; +} diff --git a/src/printers/index.ts b/src/printers/index.ts index 3fdfb99..141f0be 100644 --- a/src/printers/index.ts +++ b/src/printers/index.ts @@ -1,188 +1,3 @@ -import type { Node } from '@babel/types'; -import type { - AstPath, - doc, - Options as PrettierOptions, - Printer, -} from 'prettier'; -import { printers as estreePrinters } from 'prettier/plugins/estree.js'; - -import type { PluginOptions } from '../options.js'; -import { - isGlimmerTemplate, - isGlimmerTemplateParent, -} from '../types/glimmer.js'; -import { assert } from '../utils/assert.js'; -import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; -import { - fixPreviousPrint, - saveCurrentPrintOnSiblingNode, -} from './ambiguity.js'; -import { printTemplateContent, printTemplateTag } from './template.js'; - -const estreePrinter = estreePrinters['estree'] as Printer; - -export const printer: Printer = { - ...estreePrinter, - - getVisitorKeys(node, nonTraversableKeys) { - if (node && isGlimmerTemplate(node)) { - return []; - } - return estreePrinter.getVisitorKeys?.(node, nonTraversableKeys) || []; - }, - - printPrettierIgnored( - path: AstPath, - options: PluginOptions, - ) { - return printRawText(path, options); - }, - - print( - path: AstPath, - options: PluginOptions, - print: (path: AstPath) => doc.builders.Doc, - args: unknown, - ) { - const { node } = path; - - if (isGlimmerTemplateParent(node)) { - if (checkPrettierIgnore(path)) { - return printRawText(path, options); - } else { - let printed = estreePrinter.print(path, options, print, args); - - assert('Expected Glimmer doc to be an array', Array.isArray(printed)); - trimPrinted(printed); - - // Remove semicolons so we can manage them ourselves - if (docMatchesString(printed[0], ';')) { - printed.shift(); - } - if (docMatchesString(printed.at(-1), ';')) { - printed.pop(); - } - - trimPrinted(printed); - - // Always remove export default so we start with a blank slate - if ( - 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( - node.extra['prevTemplatePrinted'] as doc.builders.Doc[], - path, - options, - print, - args, - ); - } - - return estreePrinter.print(path, options, print, args); - }, - - /** Prints embedded GlimmerExpressions/GlimmerTemplates. */ - embed(path: AstPath, embedOptions: PrettierOptions) { - const { node } = path; - - return async (textToDoc) => { - if (node && isGlimmerTemplate(node)) { - if (checkPrettierIgnore(path)) { - return printRawText(path, embedOptions as PluginOptions); - } - - try { - const content = await printTemplateContent( - node.extra.template.contents, - textToDoc, - embedOptions as PluginOptions, - ); - - const printed = printTemplateTag(content); - saveCurrentPrintOnSiblingNode(path, printed); - return printed; - } catch (error) { - console.error(error); - const printed = [printRawText(path, embedOptions as PluginOptions)]; - saveCurrentPrintOnSiblingNode(path, printed); - return printed; - } - } - - // Nothing to embed, so move on to the regular printer. - return; - }; - }, -}; - -/** 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(); - } - - while (docMatchesString(printed.at(-1), '')) { - printed.pop(); - } -} - -function printRawText( - { node }: AstPath, - options: PluginOptions, -): string { - if (!node) { - return ''; - } - if (isGlimmerTemplate(node)) { - return ( - TEMPLATE_TAG_OPEN + node.extra.template.contents + TEMPLATE_TAG_CLOSE - ); - } - assert('expected start', typeof node.start == 'number'); - assert('expected end', typeof node.end == 'number'); - return options.originalText.slice(node.start, node.end); -} - -function hasPrettierIgnore(path: AstPath): boolean { - let possibleComment = path.node?.leadingComments?.at(-1)?.value.trim(); - - // @ts-expect-error Comments exist on node sometimes - if (!path.node?.leadingComments && path.node?.comments) { - // @ts-expect-error Comments exist on node sometimes - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - possibleComment = path.node.comments?.at(-1)?.value.trim(); - } - return possibleComment === 'prettier-ignore'; -} - -function checkPrettierIgnore(path: AstPath): boolean { - return ( - hasPrettierIgnore(path) || - (!!path.getParentNode() && - path.callParent((parent) => checkPrettierIgnore(parent))) - ); -} - -function docMatchesString( - doc: doc.builders.Doc | undefined, - string: string, -): doc is string { - return typeof doc === 'string' && doc.trim() === string; -} +export * from './ambiguity.js'; +export * from './ignore.js'; +export * from './print.js'; diff --git a/src/printers/print.ts b/src/printers/print.ts new file mode 100644 index 0000000..39bec54 --- /dev/null +++ b/src/printers/print.ts @@ -0,0 +1,103 @@ +import { type AstPath, doc as AST } from 'prettier'; + +import type { PluginOptions } from '../options.js'; +import { isGlimmerTemplate } from '../types/glimmer.js'; +import { assert } from '../utils/assert.js'; +import { flattenDoc } from '../utils/doc.js'; +import { + type NodeType, + TEMPLATE_TAG_CLOSE, + TEMPLATE_TAG_OPEN, +} from '../utils/index.js'; + +export function docMatchesString( + doc: AST.builders.Doc | undefined, + string: string, +): doc is string { + return typeof doc === 'string' && doc.trim() === string; +} + +export function printRawText( + { node }: AstPath, + options: PluginOptions, +): string { + if (!node) { + return ''; + } + + if (isGlimmerTemplate(node)) { + return ( + TEMPLATE_TAG_OPEN + node.extra.template.contents + TEMPLATE_TAG_CLOSE + ); + } + + assert('expected start', typeof node.start == 'number'); + assert('expected end', typeof node.end == 'number'); + + return options.originalText.slice(node.start, node.end); +} + +/** + * Returns a Prettier `Doc` for the given `TemplateLiteral` contents formatted + * using Prettier's built-in glimmer parser. + * + * NOTE: The contents are not surrounded with "`" + */ +export async function printTemplateContent( + text: string, + textToDoc: ( + text: string, + options: PluginOptions, + ) => Promise, + options: PluginOptions, +): Promise { + return await textToDoc(text.trim(), { + ...options, + parser: 'glimmer', + singleQuote: options.templateSingleQuote ?? options.singleQuote, + }); +} + +/** + * Prints the given template content as a template tag. + * + * If `useHardline` is `true`, will use Prettier's hardline builder to force + * each tag to print on a new line. + * + * If `useHardline` is `false`, will use Prettier's softline builder to allow + * the tags to print on the same line if they fit. + */ +export function printTemplateTag( + content: AST.builders.Doc, +): AST.builders.Doc[] { + const contents = flattenDoc(content); + + const useHardline = contents.some( + (c) => + // contains angle bracket tag + /<.+>/.test(c) || + // contains hbs block + /{{~?#.+}}/.test(c), + ); + const line = useHardline ? AST.builders.hardline : AST.builders.softline; + + const doc = [ + TEMPLATE_TAG_OPEN, + AST.builders.indent([line, AST.builders.group(content)]), + line, + TEMPLATE_TAG_CLOSE, + ]; + + return [AST.builders.group(doc)]; +} + +/** Remove the empty strings that Prettier added so we can manage them. */ +export function trimPrinted(printed: AST.builders.Doc[]): void { + while (docMatchesString(printed[0], '')) { + printed.shift(); + } + + while (docMatchesString(printed.at(-1), '')) { + printed.pop(); + } +} diff --git a/src/printers/template.ts b/src/printers/template.ts deleted file mode 100644 index 8aca8f7..0000000 --- a/src/printers/template.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Options as PrettierOptions } from 'prettier'; -import { doc } from 'prettier'; - -import type { PluginOptions } from '../options.js'; -import { flattenDoc } from '../utils/doc.js'; -import { TEMPLATE_TAG_CLOSE, TEMPLATE_TAG_OPEN } from '../utils/index.js'; - -const { - builders: { group, hardline, indent, softline }, -} = doc; - -/** - * Returns a Prettier `Doc` for the given `TemplateLiteral` contents formatted - * using Prettier's built-in glimmer parser. - * - * NOTE: The contents are not surrounded with "`" - */ -export async function printTemplateContent( - text: string, - textToDoc: ( - text: string, - // Don't use our `Options` here even though technically they are available - // because we don't want to accidentally pass them into `textToDoc`. We - // should normalize them into standard Prettier options at this point. - options: PrettierOptions, - ) => Promise, - options: PluginOptions, -): Promise { - return await textToDoc(text.trim(), { - ...options, - parser: 'glimmer', - singleQuote: options.templateSingleQuote ?? options.singleQuote, - }); -} - -/** - * Prints the given template content as a template tag. - * - * If `useHardline` is `true`, will use Prettier's hardline builder to force - * each tag to print on a new line. - * - * If `useHardline` is `false`, will use Prettier's softline builder to allow - * the tags to print on the same line if they fit. - */ -export function printTemplateTag( - content: doc.builders.Doc, -): doc.builders.Doc[] { - const contents = flattenDoc(content); - const useHardline = contents.some( - (c) => - // contains angle bracket tag - /<.+>/.test(c) || - // contains hbs block - /{{~?#.+}}/.test(c), - ); - const line = useHardline ? hardline : softline; - const doc = [ - TEMPLATE_TAG_OPEN, - indent([line, group(content)]), - line, - TEMPLATE_TAG_CLOSE, - ]; - return [group(doc)]; -} From 9e1d998d6070318a33c338270bfa49c67116eb08 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:37 +0100 Subject: [PATCH 09/11] refactor: Used named exports --- src/main.ts | 20 ++++---------------- tests/helpers/format.ts | 13 ++++++++----- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/main.ts b/src/main.ts index 906c4b7..b6599eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,4 @@ -import type { Plugin } from 'prettier'; - -import { languages } from './languages.js'; -import { options } from './options.js'; -import { parsers } from './parsers.js'; -import { printers } from './printers.js'; -import type { NodeType } from './utils/index.js'; - -const plugin: Plugin = { - languages, - parsers, - printers, - options, -}; - -export default plugin; +export { languages } from './languages.js'; +export { options } from './options.js'; +export { parsers } from './parsers.js'; +export { printers } from './printers.js'; diff --git a/tests/helpers/format.ts b/tests/helpers/format.ts index f597034..a45cb8f 100644 --- a/tests/helpers/format.ts +++ b/tests/helpers/format.ts @@ -1,12 +1,14 @@ import type { Plugin } from 'prettier'; import { format as prettierFormat } from 'prettier'; -import plugin from '../../src/main.js'; +import { languages, options, parsers, printers } from '../../src/main.js'; import type { PluginOptions } from '../../src/options.js'; -const DEFAULT_OPTIONS: Partial = { - parser: 'ember-template-tag', - plugins: [plugin as Plugin], +const plugin: Plugin = { + languages, + options, + parsers, + printers, }; /** @@ -20,7 +22,8 @@ export async function format( overrides: Partial = {}, ): Promise { return await prettierFormat(code, { - ...DEFAULT_OPTIONS, ...overrides, + parser: 'ember-template-tag', + plugins: [plugin], }); } From 8c4be33c1873ff0aa8828b7f96c12cf22e121b17 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:43:44 +0100 Subject: [PATCH 10/11] refactor: Simplified TS config --- @types/prettier/plugins/estree.d.ts | 5 ----- src/parsers.ts | 2 +- src/printers.ts | 2 +- src/printers/ambiguity.ts | 2 +- tsconfig.json | 8 ++------ tsconfig.lint.json | 2 +- types/prettier.d.ts | 7 +++++++ 7 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 @types/prettier/plugins/estree.d.ts create mode 100644 types/prettier.d.ts diff --git a/@types/prettier/plugins/estree.d.ts b/@types/prettier/plugins/estree.d.ts deleted file mode 100644 index 508c98d..0000000 --- a/@types/prettier/plugins/estree.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Printer } from 'prettier'; - -declare const printers: { - estree: Printer; -}; diff --git a/src/parsers.ts b/src/parsers.ts index 204c4f6..0cb8ebb 100644 --- a/src/parsers.ts +++ b/src/parsers.ts @@ -1,6 +1,6 @@ import type { File } from '@babel/types'; import type { Parser, ParserOptions } from 'prettier'; -import { parsers as prettierParsers } from 'prettier/plugins/babel.js'; +import { parsers as prettierParsers } from 'prettier/plugins/babel'; import { convertAst, preprocess } from './parsers/index.js'; import { assert } from './utils/assert.js'; diff --git a/src/printers.ts b/src/printers.ts index 0e522d6..78f75c3 100644 --- a/src/printers.ts +++ b/src/printers.ts @@ -1,5 +1,5 @@ import { type AstPath, doc as AST, type Printer } from 'prettier'; -import { printers as prettierPrinters } from 'prettier/plugins/estree.js'; +import { printers as prettierPrinters } from 'prettier/plugins/estree'; import type { PluginOptions } from './options.js'; import { diff --git a/src/printers/ambiguity.ts b/src/printers/ambiguity.ts index fbcf91a..9f45610 100644 --- a/src/printers/ambiguity.ts +++ b/src/printers/ambiguity.ts @@ -1,5 +1,5 @@ import type { AstPath, doc as AST, Printer } from 'prettier'; -import { printers as prettierPrinters } from 'prettier/plugins/estree.js'; +import { printers as prettierPrinters } from 'prettier/plugins/estree'; import type { PluginOptions } from '../options.js'; import { flattenDoc } from '../utils/doc.js'; diff --git a/tsconfig.json b/tsconfig.json index 8473b9e..38ec332 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,11 +9,7 @@ "resolveJsonModule": true, "isolatedModules": true, // "verbatimModuleSyntax": true, - "noEmit": true, - - "paths": { - "*": ["./@types/*"] - } + "noEmit": true }, - "include": ["src"] + "include": ["src", "types"] } diff --git a/tsconfig.lint.json b/tsconfig.lint.json index 2c2643d..4e0901f 100644 --- a/tsconfig.lint.json +++ b/tsconfig.lint.json @@ -4,5 +4,5 @@ "noEmit": true, "allowJs": true }, - "include": ["src", "tests", "vite.config.ts"] + "include": ["src", "tests", "types", "vite.config.ts"] } diff --git a/types/prettier.d.ts b/types/prettier.d.ts new file mode 100644 index 0000000..d9d99ab --- /dev/null +++ b/types/prettier.d.ts @@ -0,0 +1,7 @@ +import type { Printer } from 'prettier'; + +declare module 'prettier/plugins/estree' { + export const printers: { + estree: Printer; + }; +} From 67b9fb43c0611dae339c507b8220bea0bf058435 Mon Sep 17 00:00:00 2001 From: Isaac Lee <16869656+ijlee2@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:44:21 +0100 Subject: [PATCH 11/11] feedback: Removed `export *` --- src/parsers/index.ts | 4 ++-- src/printers/ignore.ts | 24 ++++++++++++------------ src/printers/index.ts | 15 ++++++++++++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/parsers/index.ts b/src/parsers/index.ts index 0ffcf91..9105d25 100644 --- a/src/parsers/index.ts +++ b/src/parsers/index.ts @@ -1,2 +1,2 @@ -export * from './convert-ast.js'; -export * from './preprocess.js'; +export { convertAst } from './convert-ast.js'; +export { preprocess } from './preprocess.js'; diff --git a/src/printers/ignore.ts b/src/printers/ignore.ts index 8d4e2b3..7934fbf 100644 --- a/src/printers/ignore.ts +++ b/src/printers/ignore.ts @@ -2,18 +2,7 @@ import type { AstPath } from 'prettier'; import type { NodeType } from '../utils/index.js'; -export function checkPrettierIgnore(path: AstPath): boolean { - if (hasPrettierIgnore(path)) { - return true; - } - - return ( - Boolean(path.getParentNode()) && - path.callParent((parent) => checkPrettierIgnore(parent)) - ); -} - -export function hasPrettierIgnore(path: AstPath): boolean { +function hasPrettierIgnore(path: AstPath): boolean { let possibleComment = path.node?.leadingComments?.at(-1)?.value.trim(); // @ts-expect-error Comments exist on node sometimes @@ -25,3 +14,14 @@ export function hasPrettierIgnore(path: AstPath): boolean { return possibleComment === 'prettier-ignore'; } + +export function checkPrettierIgnore(path: AstPath): boolean { + if (hasPrettierIgnore(path)) { + return true; + } + + return ( + Boolean(path.getParentNode()) && + path.callParent((parent) => checkPrettierIgnore(parent)) + ); +} diff --git a/src/printers/index.ts b/src/printers/index.ts index 141f0be..2fee7a9 100644 --- a/src/printers/index.ts +++ b/src/printers/index.ts @@ -1,3 +1,12 @@ -export * from './ambiguity.js'; -export * from './ignore.js'; -export * from './print.js'; +export { + fixPreviousPrint, + saveCurrentPrintOnSiblingNode, +} from './ambiguity.js'; +export { checkPrettierIgnore } from './ignore.js'; +export { + docMatchesString, + printRawText, + printTemplateContent, + printTemplateTag, + trimPrinted, +} from './print.js';