Skip to content

Commit fc4d756

Browse files
authored
Merge pull request #27 from gitKrystan/template-export-default
Remove `export default` by default. Add `templateExportDefault` option to add it back.
2 parents 4dca5ab + 4163276 commit fc4d756

12 files changed

Lines changed: 1270 additions & 1685 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ With that said, I did have to make some decisions about when and where to includ
5050

5151
These configuration options are available in addition to [Prettier's standard config for JavaScript and Handlebars files](https://prettier.io/docs/en/options.html).
5252

53-
| Name | Default | Description |
54-
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------- |
55-
| `templateSingleQuote` | `false` | [Same as in Prettier](https://prettier.io/docs/en/options.html#quotes) but affecting only template tags |
53+
| Name | Default | Description |
54+
| ----------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55+
| `templateExportDefault` | `false` | If `true`, default export template tags will be prepended with `export default`. |
56+
| `templateSingleQuote` | undefined | [Same as in Prettier](https://prettier.io/docs/en/options.html#quotes) but affecting only template tags. If `undefined`, will fall back to whatever is set for the global `singleQuote` config. |
5657

5758
<!-- TODO: | `templatePrintWidth` | `80` | [Same as in Prettier](https://prettier.io/docs/en/options.html#print-width) but affecting only template tags | -->
5859

src/options.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@ import type {
77
import type { BaseNode } from './types/ast';
88

99
export interface Options extends ParserOptions<BaseNode> {
10+
templateExportDefault?: boolean;
1011
templateSingleQuote?: boolean;
1112
}
1213

14+
const templateExportDefault: BooleanSupportOption = {
15+
since: '0.1.0',
16+
category: 'Format',
17+
type: 'boolean',
18+
default: false,
19+
description: 'Prepend default export template tags with "export default".',
20+
};
21+
1322
/**
1423
* Extracts a valid `templateSingleQuote` option out of the provided options. If
1524
* `templateSingleQuote` is defined, it will be used, otherwise the value for
@@ -31,5 +40,6 @@ const templateSingleQuote: BooleanSupportOption = {
3140
};
3241

3342
export const options: SupportOptions = {
43+
templateExportDefault,
3444
templateSingleQuote,
3545
};

src/parse.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { traverse } from '@babel/core';
22
import type { Node } from '@babel/types';
3+
import {
4+
isBinaryExpression,
5+
isMemberExpression,
6+
isTaggedTemplateExpression,
7+
} from '@babel/types';
38
// @ts-expect-error FIXME: TS7016 Is this a hack? IDK!
49
import { defineAliasedType } from '@babel/types/lib/definitions/utils';
10+
import { getTemplateLocals } from '@glimmer/syntax';
511
import { preprocessEmbeddedTemplates } from 'ember-template-imports/lib/preprocess-embedded-templates';
612
import type { Parser } from 'prettier';
713
import { parsers as babelParsers } from 'prettier/parser-babel';
8-
import { getTemplateLocals } from '@glimmer/syntax';
914

1015
import {
1116
GLIMMER_EXPRESSION_TYPE,
@@ -18,6 +23,7 @@ import { definePrinter } from './print/index';
1823
import type { BaseNode } from './types/ast';
1924
import { extractGlimmerExpression } from './types/glimmer';
2025
import {
26+
hasGlimmerArrayExpression,
2127
isRawGlimmerArrayExpression,
2228
isRawGlimmerCallExpression,
2329
isRawGlimmerClassProperty,
@@ -35,7 +41,7 @@ const preprocess: Required<Parser<BaseNode>>['preprocess'] = (
3541
text,
3642
options
3743
) => {
38-
return preprocessEmbeddedTemplates(text, {
44+
const preprocessed = preprocessEmbeddedTemplates(text, {
3945
getTemplateLocals,
4046

4147
templateTag: TEMPLATE_TAG_NAME,
@@ -46,6 +52,25 @@ const preprocess: Required<Parser<BaseNode>>['preprocess'] = (
4652

4753
relativePath: options.filepath,
4854
}).output;
55+
56+
const placeholderOpen = `[${TEMPLATE_TAG_PLACEHOLDER}`; // intentionally missing ]
57+
const sugaredDefaultExport = new RegExp(`^\\s*\\(?\\s*\\${placeholderOpen}`);
58+
const desugaredDefaultExport = `export default ${placeholderOpen}`;
59+
return preprocessed
60+
.split(/\r?\n/)
61+
.map((line, index, array) => {
62+
const previousLine = findPreviousLine(index, array);
63+
if (
64+
previousLine &&
65+
(previousLine.includes('prettier-ignore') ||
66+
previousLine.trim().endsWith('{'))
67+
) {
68+
return line;
69+
} else {
70+
return line.replace(sugaredDefaultExport, desugaredDefaultExport);
71+
}
72+
})
73+
.join('\r\n');
4974
};
5075

5176
export const parser: Parser<BaseNode> = {
@@ -72,6 +97,42 @@ export const parser: Parser<BaseNode> = {
7297
aliases: ['Expression'],
7398
});
7499
traverse(ast as Node, {
100+
enter(path) {
101+
const node = path.node;
102+
const parentNode = path.parentPath?.node;
103+
104+
if (
105+
parentNode &&
106+
'property' in parentNode &&
107+
isRawGlimmerCallExpression(parentNode.property)
108+
) {
109+
throw new SyntaxError(
110+
'Ember <template> tag used as an object property.'
111+
);
112+
} else if (
113+
isBinaryExpression(node) &&
114+
(hasGlimmerArrayExpression(node.left) ||
115+
hasGlimmerArrayExpression(node.right))
116+
) {
117+
throw new SyntaxError(
118+
'Ember <template> tag used in binary expression.'
119+
);
120+
} else if (
121+
isTaggedTemplateExpression(node) &&
122+
hasGlimmerArrayExpression(node.tag)
123+
) {
124+
throw new SyntaxError(
125+
'Ember <template> tag used as tagged template expression.'
126+
);
127+
} else if (
128+
isMemberExpression(node) &&
129+
hasGlimmerArrayExpression(node.object)
130+
) {
131+
throw new SyntaxError(
132+
'Ember <template> tag used as member expression.'
133+
);
134+
}
135+
},
75136
ArrayExpression(path) {
76137
const node = path.node;
77138
if (isRawGlimmerArrayExpression(node)) {
@@ -102,3 +163,16 @@ export const parser: Parser<BaseNode> = {
102163
return ast;
103164
},
104165
};
166+
167+
function findPreviousLine(index: number, array: string[]): string | undefined {
168+
const previousIndex = index - 1;
169+
const previousLine = array[previousIndex];
170+
171+
if (previousLine === undefined) {
172+
return undefined;
173+
} else if (previousLine.length) {
174+
return previousLine;
175+
} else {
176+
return findPreviousLine(previousIndex, array);
177+
}
178+
}

src/print/index.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AstPath, Plugin, Printer } from 'prettier';
1+
import type { AstPath, doc, Plugin, Printer } from 'prettier';
22

33
import {
44
TEMPLATE_TAG_CLOSE,
@@ -25,16 +25,13 @@ import { printTemplateTag } from './template';
2525
// @ts-expect-error FIXME: HACK because estree printer isn't exported. See below.
2626
export const printer: Printer<BaseNode> = {};
2727

28-
let originalOptions: Options;
29-
3028
/**
3129
* FIXME: HACK because estree printer isn't exported.
3230
*
3331
* @see https://github.com/prettier/prettier/issues/10259
3432
* @see https://github.com/prettier/prettier/issues/4424
3533
*/
3634
export function definePrinter(options: Options): void {
37-
originalOptions = { ...options };
3835
const estreePlugin = assertExists(options.plugins.find(isEstreePlugin));
3936
const estreePrinter = estreePlugin.printers.estree;
4037

@@ -51,7 +48,12 @@ export function definePrinter(options: Options): void {
5148
Object.create(estreePrinter) as Printer<BaseNode>
5249
);
5350

54-
printer.print = (path, options, print, args) => {
51+
printer.print = (
52+
path: AstPath<BaseNode>,
53+
options: Options,
54+
print: (path: AstPath<BaseNode>) => doc.builders.Doc,
55+
args: unknown
56+
) => {
5557
const hasPrettierIgnore = checkPrettierIgnore(
5658
path,
5759
defaultHasPrettierIgnore
@@ -66,38 +68,41 @@ export function definePrinter(options: Options): void {
6668
if (hasPrettierIgnore) {
6769
return printRawText(path, options);
6870
} else {
69-
options.semi = false;
7071
const printed = defaultPrint(path, options, print, args);
7172
const glimmerExpression = getGlimmerExpression(path);
7273

73-
if (Array.isArray(printed)) {
74-
const { semi } = options;
75-
const { forceSemi } = glimmerExpression.extra;
76-
const hasSemi = printed[printed.length - 1] === ';';
77-
// I think prettier is mutating the options somewhere, making the semi
78-
// check necessary
79-
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
80-
if (semi && forceSemi && !hasSemi) {
81-
// We only need to push the semi-colon in semi: true mode because
82-
// in semi: false mode, the ambiguous statement will get a prefixing
83-
// semicolon
84-
printed.push(';');
85-
} else if ((!semi || !forceSemi) && hasSemi) {
86-
// HACK: Prettier hardcodes a semicolon for GlimmerExportDefaultDeclarationTSPath
87-
printed.pop();
74+
assert(
75+
'Expected GlimmerExpression doc to be an array',
76+
Array.isArray(printed)
77+
);
78+
79+
// Remove the semicolons that Prettier added so we can manage them
80+
let adjusted = printed.filter(
81+
(doc) => typeof doc !== 'string' || doc !== ';'
82+
);
83+
84+
// FIXME: Make configurable
85+
if (
86+
!options.templateExportDefault &&
87+
docMatchesString(adjusted[0], 'export') &&
88+
docMatchesString(adjusted[1], 'default')
89+
) {
90+
adjusted = adjusted.slice(2);
91+
if (docMatchesString(adjusted[0], '')) {
92+
adjusted = adjusted.slice(1);
8893
}
89-
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
90-
} else {
91-
// FIXME: Should we throw in DEBUG mode?
92-
console.error(
93-
'Expected GlimmerExpression to be printed within an array',
94-
path.getValue()
95-
);
9694
}
97-
return printed;
95+
96+
if (options.semi && glimmerExpression.extra.forceSemi) {
97+
// We only need to push the semi-colon in semi: true mode because
98+
// in semi: false mode, the ambiguous statement will get a prefixing
99+
// semicolon
100+
adjusted.push(';');
101+
}
102+
103+
return adjusted;
98104
}
99105
} else {
100-
options.semi = originalOptions.semi;
101106
if (hasPrettierIgnore) {
102107
return printRawText(path, options);
103108
} else {
@@ -187,3 +192,10 @@ function checkPrettierIgnore(
187192
))
188193
);
189194
}
195+
196+
function docMatchesString(
197+
doc: doc.builders.Doc | undefined,
198+
string: string
199+
): doc is string {
200+
return typeof doc === 'string' && doc.trim() === string;
201+
}

src/types/raw.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import type {
33
CallExpression,
44
ClassProperty,
55
Identifier,
6+
Node,
67
TemplateLiteral,
78
} from '@babel/types';
89
import {
910
isArrayExpression,
1011
isCallExpression,
1112
isClassProperty,
1213
isIdentifier,
14+
isNode,
1315
isTemplateLiteral,
1416
} from '@babel/types';
1517
import type { AstPath } from 'prettier';
@@ -39,7 +41,7 @@ export function isRawGlimmerArrayExpressionPath(
3941

4042
/** Type predicate */
4143
export function isRawGlimmerArrayExpression(
42-
value: BaseNode | null | undefined
44+
value: Node | BaseNode | null | undefined
4345
): value is RawGlimmerArrayExpression {
4446
return (
4547
isArrayExpression(value) && isRawGlimmerCallExpression(value.elements[0])
@@ -139,3 +141,13 @@ export function isRawGlimmerCallExpression(
139141
export interface RawGlimmerIdentifier extends Identifier {
140142
name: typeof TEMPLATE_TAG_PLACEHOLDER; // This is just `string` so not SUPER useful, just documentation
141143
}
144+
145+
/** Recursively checks if the node has a Glimmer Array Expression. */
146+
export function hasGlimmerArrayExpression(node: Node): boolean {
147+
return (
148+
isRawGlimmerArrayExpression(node) ||
149+
('expression' in node &&
150+
isNode(node.expression) &&
151+
hasGlimmerArrayExpression(node.expression))
152+
);
153+
}

tests/unit-tests/__snapshots__/format.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class MyComponent extends Component {
3333
`;
3434
3535
exports[`format > config > default > it formats ../cases/gjs/default-export.gjs 1`] = `
36-
"export default <template>
36+
"<template>
3737
Explicit default export module top level component. Explicit default export
3838
module top level component. Explicit default export module top level
3939
component. Explicit default export module top level component. Explicit
@@ -242,7 +242,7 @@ export interface Signature {
242242
Yields: [];
243243
}
244244
245-
export default <template>
245+
<template>
246246
Explicit default export module top level component. Explicit default export
247247
module top level component. Explicit default export module top level
248248
component. Explicit default export module top level component. Explicit

0 commit comments

Comments
 (0)