astro/prettier-plugin-astro/index.js
Nate Moore 54409a0702
Prettier support for .astro files (#106)
* docs: fix readme

* chore: scaffold prettier plugin

* chore(prettier): switch to cjs

* test(prettier): scaffold prettier tests

* test(prettier): add simple prettier tests

* feat(prettier): first pass

* refactor: expose parser as CJS export

* test(prettier): add long expression

* refactor(prettier): use Astro parser + built-in prettier doc for prettier plugin

* chore: remove parser from git

* chore: add prettier-plugin-astro `build` to workflow

* chore: update package-lock

* chore: do not build prettier-plugin-astro

* fix: update engines

* chore: remove NPM restriction

* chore: fix workflow paths

* chore: update build script

* test: fix prettier expr test

* chore: fix parser build on windows

* refactor: add parser tsconfig, extending base config

* chore: relax ban-ts-comment

* chore: fix lint issue

Co-authored-by: Nate Moore <nate@skypack.dev>
2021-04-21 11:14:44 -05:00

154 lines
4.6 KiB
JavaScript

const {
doc: {
builders: { concat, hardline },
},
} = require('prettier');
const { parse } = require('astro/parser');
/** @type {Partial<import('prettier').SupportLanguage>[]} */
module.exports.languages = [
{
name: 'astro',
parsers: ['astro'],
extensions: ['.astro'],
vscodeLanguageIds: ['astro'],
},
];
/** @type {Record<string, import('prettier').Parser>} */
module.exports.parsers = {
astro: {
parse: (text) => {
let { html, css, module: frontmatter } = parse(text);
html = html ? { ...html, text: text.slice(html.start, html.end), isRoot: true } : null;
return [frontmatter, html, css].filter((v) => v);
},
locStart(node) {
return node.start;
},
locEnd(node) {
return node.end;
},
astFormat: 'astro-ast',
},
'astro-expression': {
parse: (text, parsers) => {
return { text };
},
locStart(node) {
return node.start;
},
locEnd(node) {
return node.end;
},
astFormat: 'astro-expression',
}
};
const findExpressionsInAST = (node, collect = []) => {
if (node.type === 'MustacheTag') {
return collect.concat(node);
}
if (node.children) {
collect.push(...[].concat(...node.children.map(child => findExpressionsInAST(child))));
}
return collect;
}
const formatExpression = ({ expression: { codeStart, codeEnd, children }}, text, options) => {
if (children.length === 0) {
if ([`'`, `"`].includes(codeStart[0])) {
return `<script $ lang="ts">${codeStart}${codeEnd}</script>`
}
return `{${codeStart}${codeEnd}}`;
}
return `<script $ lang="ts">${text}</script>`;
}
const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find(v => v === '$');
const walkDoc = (doc) => {
let inAstroScript = false;
const recurse = (node, { parent }) => {
if (node.type === 'concat') {
if (isAstroScript(node)) {
inAstroScript = true;
parent.contents = { type: 'concat', parts: ['{'] };
}
return node.parts.map(part => recurse(part, { parent: node }));
}
if (inAstroScript) {
if (node.type === 'break-parent') {
parent.parts = parent.parts.filter(part => !['break-parent', 'line'].includes(part.type));
}
if (node.type === 'indent') {
parent.parts = parent.parts.map(part => {
if (part.type !== 'indent') return part;
return {
type: 'concat',
parts: [part.contents]
}
})
}
if (typeof node === 'string' && node.endsWith(';')) {
parent.parts = parent.parts.map(part => {
if (typeof part === 'string' && part.endsWith(';')) return part.slice(0, -1);
return part;
});
}
if (node === '</script>') {
parent.parts = parent.parts.map(part => part === '</script>' ? '}' : part);
inAstroScript = false;
}
}
if (['group', 'indent'].includes(node.type)) {
return recurse(node.contents, { parent: node });
}
}
recurse(doc, { parent: null });
}
/** @type {Record<string, import('prettier').Printer>} */
module.exports.printers = {
'astro-ast': {
print(path, opts, print) {
const node = path.getValue();
if (Array.isArray(node)) return concat(path.map(print));
if (node.type === 'Fragment') return concat(path.map(print, 'children'));
return node;
},
embed(path, print, textToDoc, options) {
const node = path.getValue();
if (node.type === 'Script' && node.context === 'setup') {
return concat(['---', hardline, textToDoc(node.content, { ...options, parser: 'typescript' }), '---', hardline, hardline]);
}
if (node.type === 'Fragment' && node.isRoot) {
const expressions = findExpressionsInAST(node);
if (expressions.length > 0) {
const parts = [].concat(...expressions.map((expr, i, all) => {
const prev = all[i - 1];
const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start);
const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options);
if (i === all.length - 1) {
const end = node.text.slice(expr.end - node.start);
return [start, exprText, end]
}
return [start, exprText]
}));
const html = parts.join('\n');
const doc = textToDoc(html, { parser: 'html' });
walkDoc(doc);
return doc;
}
return textToDoc(node.text, { parser: 'html' });
}
return null;
},
},
};