Skip to content

Commit 6496f59

Browse files
author
Matthew Edwards
committed
Added prettier-plugin for inline hbs
1 parent bbe4b18 commit 6496f59

File tree

20 files changed

+773
-3
lines changed

20 files changed

+773
-3
lines changed

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
packages/@glimmerx/babel-plugin-component-templates/test/
1+
packages/@glimmerx/babel-plugin-component-templates/test/
2+
packages/@glimmerx/@glimmerx/prettier-plugin-component-templates/test/fixtures/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"eslint-plugin-prettier": "^3.1.2",
5555
"fs-extra": "^9.0.0",
5656
"lerna": "^3.20.2",
57-
"prettier": "^2.0.4",
57+
"prettier": "^2.3.0",
5858
"qunit": "^2.9.3",
5959
"release-it": "^13.5.1",
6060
"release-it-lerna-changelog": "^2.1.2",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test/__fixtures__/**/*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# prettier-plugin-glimmer-experimental
2+
3+
## Background
4+
5+
[Prettier](https://prettier.io/docs/en/index.html) is an opinionated code formatter. In Prettier `>2.0` there is support for `*.hbs` glimmer files, but does not support the experimental syntaxes that `glimmerx` components are authored in.
6+
7+
## Introduction
8+
9+
This plugin extends the internal printers to add an embedded syntax for glimmer template in an `hbs` TaggedTemplateExpressions.
10+
11+
## Installation and Usage
12+
13+
```bash
14+
yarn add -D @glimmerx/prettier-plugin-component-templates
15+
```
16+
17+
Once added prettier will discover and use the plugin to format any `hbs` tagged template expression.
18+
19+
## Development
20+
21+
Generate a test case, add it to the tests file,
22+
23+
- Add `PRETTIER_DEBUG=true` to the environment when running the plugin in order to get complete stack traces on errors.
24+
- To generate a new output file you can:
25+
```
26+
yarn prettier --plugin ./index.js ./test/fixtures/extension-gjs/code.gjs > ./test/fixtures/extension-gjs/output.gjs
27+
```
28+
29+
### Testing
30+
31+
Run all tests:
32+
33+
```
34+
yarn test
35+
```
36+
37+
Using the plugin on a single fixture file:
38+
39+
```
40+
PRETTIER_DEBUG=true --inspect-brk node node_modules/.bin/prettier --plugin ./index.js ./test/fixtures/extension-gjs/code.gjs
41+
```
42+
43+
Some caveats, the `.prettierignore` file will be respected, so please ensure that is commented out in repo root.
44+
45+
## TODO
46+
47+
- [ ] Add support for `<template>` tag semantics.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const babelParsers = require('prettier/parser-babel').parsers;
2+
const typescriptParsers = require('prettier/parser-typescript').parsers;
3+
4+
const { esTree } = require('./lib/util');
5+
6+
const {
7+
builders: { group, indent, softline, concat, hardline },
8+
utils: { mapDoc, stripTrailingHardline },
9+
} = require('prettier').doc;
10+
11+
const comments = require('./lib/comments');
12+
13+
function formatHbs(path, print, textToDoc, options) {
14+
const node = path.getValue();
15+
16+
const placeholderPattern = 'PRETTIER_HTML_PLACEHOLDER_(\\d+)_IN_JS';
17+
const placeholders = node.expressions.map((_, i) => `PRETTIER_HTML_PLACEHOLDER_${i}_IN_JS`);
18+
19+
const text = node.quasis
20+
.map((quasi, index, quasis) =>
21+
index === quasis.length - 1 ? quasi.value.raw : quasi.value.raw + placeholders[index]
22+
)
23+
.join('');
24+
25+
const expressionDocs = path.map(print, 'expressions');
26+
27+
if (expressionDocs.length === 0 && text.trim().length === 0) {
28+
return '``';
29+
}
30+
31+
const contentDoc = mapDoc(
32+
stripTrailingHardline(textToDoc(text, { parser: 'glimmer' })),
33+
(doc) => {
34+
const placeholderRegex = new RegExp(placeholderPattern, 'g');
35+
const hasPlaceholder = typeof doc === 'string' && placeholderRegex.test(doc);
36+
37+
if (!hasPlaceholder) {
38+
return doc;
39+
}
40+
41+
let parts = [];
42+
43+
const components = doc.split(placeholderRegex);
44+
for (let i = 0; i < components.length; i++) {
45+
const component = components[i];
46+
47+
if (i % 2 === 0) {
48+
if (component) {
49+
parts.push(component);
50+
}
51+
continue;
52+
}
53+
54+
const placeholderIndex = +component;
55+
56+
parts.push(
57+
concat([
58+
'${',
59+
group(concat([indent(concat([softline, expressionDocs[placeholderIndex]])), softline])),
60+
'}',
61+
])
62+
);
63+
}
64+
65+
return concat(parts);
66+
}
67+
);
68+
69+
return group(concat(['`', indent(concat([hardline, group(contentDoc)])), softline, '`']));
70+
}
71+
72+
function isHbs(path) {
73+
return path.match(
74+
(node) => {
75+
return node.type === 'TemplateLiteral';
76+
},
77+
(node, name) => {
78+
return (
79+
node.type === 'TaggedTemplateExpression' &&
80+
node.tag.type === 'Identifier' &&
81+
node.tag.name === 'hbs' &&
82+
name === 'quasi'
83+
);
84+
}
85+
);
86+
}
87+
88+
function embed(path, print, textToDoc, options) {
89+
if (isHbs(path)) {
90+
return formatHbs(path, print, textToDoc, options);
91+
}
92+
93+
return esTree(options).embed(path, print, textToDoc, options);
94+
}
95+
96+
function print(path, options, print) {
97+
return esTree(options).print(path, options, print);
98+
}
99+
100+
const languages = [
101+
{
102+
name: 'glimmer-experimental',
103+
group: 'JavaScript',
104+
parsers: ['babel', 'babel-ts', 'typescript'], // Which parsers do we want to support?
105+
extensions: ['.gjs', '.js', '.ts'],
106+
vscodeLanguageIds: ['javascript'],
107+
},
108+
];
109+
110+
const parsers = {
111+
babel: {
112+
...babelParsers.babel,
113+
astFormat: 'esTree',
114+
parse(text, parsers, options) {
115+
const ast = babelParsers.babel.parse(text, parsers, options);
116+
return ast;
117+
},
118+
},
119+
// babel-ts?
120+
typescript: {
121+
...typescriptParsers.typescript,
122+
astFormat: 'esTree',
123+
parse(text, parsers, options) {
124+
const ast = typescriptParsers.typescript.parse(text, parsers, options);
125+
return ast;
126+
},
127+
},
128+
};
129+
130+
const printers = {
131+
esTree: {
132+
embed,
133+
print,
134+
...comments,
135+
},
136+
};
137+
138+
module.exports = {
139+
languages,
140+
parsers,
141+
printers,
142+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const { esTree } = require('./util');
2+
3+
function canAttachComment(node) {
4+
return (
5+
node.type &&
6+
!isBlockComment(node) &&
7+
!isLineComment(node) &&
8+
node.type !== 'EmptyStatement' &&
9+
node.type !== 'TemplateElement' &&
10+
node.type !== 'Import' &&
11+
// `babel-ts` don't have similar node for `class Foo { bar() /* bat */; }`
12+
node.type !== 'TSEmptyBodyFunctionExpression'
13+
);
14+
}
15+
16+
function isBlockComment(comment) {
17+
return (
18+
comment.type === 'Block' ||
19+
comment.type === 'CommentBlock' ||
20+
// `meriyah`
21+
comment.type === 'MultiLine'
22+
);
23+
}
24+
25+
function isLineComment(comment) {
26+
return (
27+
comment.type === 'Line' ||
28+
comment.type === 'CommentLine' ||
29+
// `meriyah` has `SingleLine`, `HashbangComment`, `HTMLOpen`, and `HTMLClose`
30+
comment.type === 'SingleLine' ||
31+
comment.type === 'HashbangComment' ||
32+
comment.type === 'HTMLOpen' ||
33+
comment.type === 'HTMLClose'
34+
);
35+
}
36+
37+
function printComment(path, options) {
38+
return esTree(options).printComment(path, options);
39+
}
40+
41+
const handleComments = {
42+
avoidAstMutation: true,
43+
ownLine: function (context) {
44+
const options = context.options;
45+
return esTree(options).handleComments.ownLine(context);
46+
},
47+
endOfLine: function (context) {
48+
const options = context.options;
49+
return esTree(options).handleComments.endOfLine(context);
50+
},
51+
remaining: function (context) {
52+
const options = context.options;
53+
return esTree(options).handleComments.remaining(context);
54+
},
55+
};
56+
57+
function getCommentChildNodes(node, options) {
58+
return esTree(options).getCommentChildNodes(node, options);
59+
}
60+
61+
function massageAstNode(node, options) {
62+
return esTree(options).massageAstNode(node, options);
63+
}
64+
65+
function willPrintOwnComments(path, options) {
66+
return esTree(options).willPrintOwnComments(path, options);
67+
}
68+
69+
module.exports = {
70+
canAttachComment,
71+
handleComments,
72+
isBlockComment,
73+
getCommentChildNodes,
74+
massageAstNode,
75+
willPrintOwnComments,
76+
printComment,
77+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function esTree(options) {
2+
return options.plugins[0].printers.estree;
3+
}
4+
5+
module.exports = {
6+
esTree,
7+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@glimmerx/prettier-plugin-component-templates",
3+
"version": "0.0.1",
4+
"description": "A prettier formatter for glimmer component templates",
5+
"main": "index.js",
6+
"repository": "https://github.com/glimmerjs/glimmer-experimental",
7+
"author": "Matt Edwards <[email protected]>",
8+
"license": "MIT",
9+
"private": false,
10+
"files": [
11+
"index.js",
12+
"lib/**/*"
13+
],
14+
"scripts": {
15+
"test": "mocha"
16+
},
17+
"devDependencies": {
18+
"chai": "^4.3.4",
19+
"esm": "^3.2.25",
20+
"mocha": "^7.1.1",
21+
"prettier": "^2.3.0"
22+
},
23+
"volta": {
24+
"node": "12.10.0",
25+
"yarn": "1.22.4"
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Component, { hbs } from '@glimmerx/component';
2+
3+
export default class Page extends Component {
4+
static template = hbs`
5+
<html>
6+
<head>
7+
<title>Hello World</title>
8+
</head>
9+
<body>
10+
<h1>Goodbye Moon</h1>
11+
</body>
12+
</html>
13+
`;
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Component, { hbs } from '@glimmerx/component';
2+
3+
export default class Page extends Component {
4+
static template = hbs`
5+
<html>
6+
<head>
7+
<title>Hello World</title>
8+
</head>
9+
<body>
10+
<h1>Goodbye Moon</h1>
11+
</body>
12+
</html>
13+
`;
14+
}

0 commit comments

Comments
 (0)