diff --git a/.eslintrc.js b/.eslintrc.js index c51b5cfc4..d71c94f51 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -59,6 +59,7 @@ module.exports = { ], plugins: ['@liferay'], rules: { + '@liferay/import-extensions': 'off', 'notice/notice': [ 'error', { diff --git a/.github/workflows/prettier-plugin.yml b/.github/workflows/prettier-plugin.yml new file mode 100644 index 000000000..6407c7b11 --- /dev/null +++ b/.github/workflows/prettier-plugin.yml @@ -0,0 +1,67 @@ +# Based on: https://github.com/actions/starter-workflows/blob/master/ci/node.js.yml + +name: prettier-plugin + +on: + push: + branches: [master] + paths: + - 'projects/prettier-plugin/**' + pull_request: + branches: [master] + paths: + - 'projects/prettier-plugin/**' + +env: + CI: true + yarn-cache-name: yarn-cache-3 + yarn-cache-path: .yarn + +jobs: + check-lockfile: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Use or update Yarn cache + uses: actions/cache@v2 + with: + path: ${{ env.yarn-cache-path }} + key: ${{ runner.os }}-${{ env.yarn-cache-name }}-${{ hashFiles('**/yarn.lock') }} + - run: yarn --cache-folder=${{ env.yarn-cache-path }} + working-directory: projects/prettier-plugin + - run: git diff --quiet -- yarn.lock + working-directory: projects/prettier-plugin + + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Use or update Yarn cache + uses: actions/cache@v2 + with: + path: ${{ env.yarn-cache-path }} + key: ${{ runner.os }}-${{ env.yarn-cache-name }}-${{ hashFiles('**/yarn.lock') }}-prettier-plugin + - run: yarn --cache-folder=../../../${{ env.yarn-cache-path }} --frozen-lockfile + working-directory: projects/prettier-plugin + - run: yarn --cache-folder=../../../${{ env.yarn-cache-path }} build + working-directory: projects/prettier-plugin + - run: yarn --cache-folder=../../../${{ env.yarn-cache-path }} test + working-directory: projects/prettier-plugin diff --git a/projects/prettier-plugin/index.js b/projects/prettier-plugin/index.js new file mode 100644 index 000000000..0da87bf70 --- /dev/null +++ b/projects/prettier-plugin/index.js @@ -0,0 +1,7 @@ +/** + * SPDX-FileCopyrightText: © 2020 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +export {parsers} from './parsers.js'; +export {printers} from './printers.js'; diff --git a/projects/prettier-plugin/package.json b/projects/prettier-plugin/package.json new file mode 100644 index 000000000..112c9306c --- /dev/null +++ b/projects/prettier-plugin/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "prettier": "3.2.5" + }, + "exports": "./index.js", + "name": "@liferay/prettier-plugin", + "scripts": { + "build": "true", + "format": "liferay-workspace-scripts format", + "format:check": "liferay-workspace-scripts format:check", + "postversion": "liferay-workspace-scripts publish", + "preversion": "liferay-workspace-scripts ci", + "test": "node test" + }, + "type": "module", + "version": "1.0.0" +} diff --git a/projects/prettier-plugin/parsers.js b/projects/prettier-plugin/parsers.js new file mode 100644 index 000000000..1f298c3cc --- /dev/null +++ b/projects/prettier-plugin/parsers.js @@ -0,0 +1,44 @@ +/** + * SPDX-FileCopyrightText: © 2020 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +import {format} from 'prettier'; +import {parsers as babelParsers} from 'prettier/plugins/babel'; +import {parsers as typescriptParsers} from 'prettier/plugins/typescript'; + +import {linesAroundComments} from './rules/lines-around-comments.js'; + +function transformParser(parserName, defaultParser) { + return { + ...defaultParser, + astFormat: 'liferay-style-ast', + parse: async (text, options) => { + /* + * We need to filter out our own plugin before calling default prettier + */ + const plugins = options.plugins.filter( + (plugin) => !plugin.printers['liferay-style-ast'] + ); + + let formattedText = await format(text, { + ...options, + plugins, + }); + + const ast = defaultParser.parse(formattedText, options); + + formattedText = linesAroundComments(formattedText, ast, parserName); + + return { + body: formattedText, + type: 'FormattedText', + }; + }, + }; +} + +export const parsers = { + babel: transformParser('babel', babelParsers.babel), + typescript: transformParser('typescript', typescriptParsers.typescript), +}; diff --git a/projects/prettier-plugin/printers.js b/projects/prettier-plugin/printers.js new file mode 100644 index 000000000..f5d6c03d9 --- /dev/null +++ b/projects/prettier-plugin/printers.js @@ -0,0 +1,24 @@ +/** + * SPDX-FileCopyrightText: © 2020 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +function createPrinter() { + function main(path) { + const {node} = path; + + if (node.type === 'FormattedText') { + return node.body; + } + + throw new Error(`Unknown node type: ${node?.type}`); + } + + return { + print: main, + }; +} + +export const printers = { + 'liferay-style-ast': createPrinter(), +}; diff --git a/projects/prettier-plugin/rules/lines-around-comments.js b/projects/prettier-plugin/rules/lines-around-comments.js new file mode 100644 index 000000000..c95f37bc3 --- /dev/null +++ b/projects/prettier-plugin/rules/lines-around-comments.js @@ -0,0 +1,68 @@ +/** + * SPDX-FileCopyrightText: © 2020 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +export function linesAroundComments(formattedText, ast, parserName) { + let contentLengthIncrease = 0; + + ast.comments.forEach((commentNode) => { + if (isEndofLineComment(commentNode)) { + return; + } + + const nodeStart = + parserName === 'typescript' + ? commentNode.range[0] + : commentNode.start; + const nodeEnd = + parserName === 'typescript' + ? commentNode.range[1] + : commentNode.end; + + const commentStartIndex = nodeStart + contentLengthIncrease; + + if (!isNewLineBefore(formattedText, commentStartIndex)) { + const position = commentStartIndex - 1; + + formattedText = insertNewLine(formattedText, position); + + contentLengthIncrease += 1; + } + + const commentEndIndex = nodeEnd + contentLengthIncrease; + + if ( + !isBlockComment(commentNode) && + !isNewLineAfter(formattedText, commentEndIndex) + ) { + const position = commentEndIndex + 1; + + formattedText = insertNewLine(formattedText, position); + + contentLengthIncrease += 1; + } + }); + + return formattedText; +} + +function isBlockComment(node) { + return node.type === 'CommentBlock' || node.type === 'Block'; +} + +function isEndofLineComment(node) { + return node.loc.start.column !== 0; +} + +function isNewLineBefore(text, index) { + return text.charAt(index - 2) === '\n'; +} + +function isNewLineAfter(text, index) { + return text.charAt(index + 2) === '\n'; +} + +function insertNewLine(text, index) { + return [text.slice(0, index), '\n', text.slice(index)].join(''); +} diff --git a/projects/prettier-plugin/test/index.js b/projects/prettier-plugin/test/index.js new file mode 100644 index 000000000..bcdd6c62f --- /dev/null +++ b/projects/prettier-plugin/test/index.js @@ -0,0 +1,92 @@ +/** + * SPDX-FileCopyrightText: © 2020 Liferay, Inc. + * SPDX-License-Identifier: MIT + */ + +import assert from 'node:assert'; +import {describe, test} from 'node:test'; +import {format} from 'prettier'; + +import * as liferayPrettierPlugin from '../index.js'; + +const baseConfig = { + bracketSpacing: false, + plugins: [liferayPrettierPlugin], + singleQuote: true, + tabWidth: 4, + useTabs: true, +}; + +const babelConfig = { + ...baseConfig, + parser: 'babel', +}; + +const tsConfig = { + ...baseConfig, + parser: 'typescript', +}; + +const fixtures = [ + { + _name: 'inline comment', + code: `const foo = 'foo'; + // test + const bar = 'bar';`, + expected: `const foo = 'foo'; + +// test + +const bar = 'bar'; +`, + }, + { + _name: 'block comment', + code: `const foo = 'foo'; +/* + * blah + */ +const bar = 'bar';`, + expected: `const foo = 'foo'; + +/* + * blah + */ +const bar = 'bar'; +`, + }, +]; + +describe('babel', () => { + test('prettier runs', async () => { + const code = `if (foo) {}`; + + assert.ok(await format(code, babelConfig)); + }); + + fixtures.forEach((fixture) => { + test(fixture._name, async () => { + assert.equal( + await format(fixture.code, babelConfig), + fixture.expected + ); + }); + }); +}); + +describe('typescript', () => { + test('prettier runs', async () => { + const code = `if (foo) {}`; + + assert.ok(await format(code, tsConfig)); + }); + + fixtures.forEach((fixture) => { + test(fixture._name, async () => { + assert.equal( + await format(fixture.code, tsConfig), + fixture.expected + ); + }); + }); +});