Skip to content

Commit

Permalink
feat: support prettier v3 (#2100)
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed Jul 19, 2023
1 parent 2f2972e commit f8d7c20
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 38 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"typescript": "^5.1.3"
},
"devDependencies": {
"prettier": "2.8.6",
"cross-env": "^7.0.2",
"prettier": "~2.8.8",
"ts-node": "^10.0.0"
},
"packageManager": "pnpm@8.4.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"estree-walker": "^2.0.1",
"fast-glob": "^3.2.7",
"lodash": "^4.17.21",
"prettier": "2.8.6",
"prettier": "~2.8.8",
"prettier-plugin-svelte": "~2.10.1",
"svelte": "^3.57.0",
"svelte-preprocess": "~5.0.4",
Expand Down
80 changes: 64 additions & 16 deletions packages/language-server/src/plugins/svelte/SveltePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,46 @@ export class SveltePlugin
}

const filePath = document.getFilePath()!;
const prettier = importPrettier(filePath);
// Try resolving the config through prettier and fall back to possible editor config
const config = this.configManager.getMergedPrettierConfig(
await prettier.resolveConfig(filePath, { editorconfig: true }),
// Be defensive here because IDEs other than VSCode might not have these settings
options && {
tabWidth: options.tabSize,
useTabs: !options.insertSpaces

/**
* Prettier v2 can't use v3 plugins and vice versa. Therefore, we need to check
* which version of prettier is used in the workspace and import the correct
* version of the Svelte plugin. If user uses Prettier >= 3 and has no Svelte plugin
* then fall back to our built-in versions which are both v2 and compatible with
* each other.
* TODO switch this around at some point to load Prettier v3 by default because it's
* more likely that users have that installed.
*/
const importFittingPrettier = async () => {
const getConfig = async (p: any) => {
// Try resolving the config through prettier and fall back to possible editor config
return this.configManager.getMergedPrettierConfig(
await p.resolveConfig(filePath, { editorconfig: true }),
// Be defensive here because IDEs other than VSCode might not have these settings
options && {
tabWidth: options.tabSize,
useTabs: !options.insertSpaces
}
);
};

const prettier1 = importPrettier(filePath);
const config1 = await getConfig(prettier1);
const pluginLoaded = await hasSveltePluginLoaded(prettier1, config1.plugins);
if (Number(prettier1.version[0]) < 3 || pluginLoaded) {
// plugin loaded, or referenced in user config as a plugin, or same version as our fallback version -> ok
return { prettier: prettier1, config: config1, isFallback: false };
}
);

// User either only has Plugin or incompatible Prettier major version installed or none
// -> load our fallback version
const prettier2 = importPrettier(__dirname);
const config2 = await getConfig(prettier2);
return { prettier: prettier2, config: config2, isFallback: true };
};

const { prettier, config, isFallback } = await importFittingPrettier();

// If user has prettier-plugin-svelte 1.x, then remove `options` from the sort
// order or else it will throw a config error (`options` was not present back then).
if (
Expand All @@ -95,6 +125,16 @@ export class SveltePlugin
.replace('-options', '')
.replace('options-', '');
}
// If user has prettier-plugin-svelte 3.x, then add `options` from the sort
// order or else it will throw a config error (now required).
if (
config?.svelteSortOrder &&
!config.svelteSortOrder.includes('options') &&
config.svelteSortOrder !== 'none' &&
getPackageInfo('prettier-plugin-svelte', filePath)?.version.major >= 3
) {
config.svelteSortOrder = 'options-' + config.svelteSortOrder;
}
// Take .prettierignore into account
const fileInfo = await prettier.getFileInfo(filePath, {
ignorePath: this.configManager.getPrettierConfig()?.ignorePath ?? '.prettierignore',
Expand All @@ -106,14 +146,15 @@ export class SveltePlugin
return [];
}

const formattedCode = prettier.format(document.getText(), {
// Prettier v3 format is async, v2 is not
const formattedCode = await prettier.format(document.getText(), {
...config,
plugins: Array.from(
new Set([
...((config.plugins as string[]) ?? [])
.map(resolvePlugin)
.filter(isNotNullOrUndefined),
...getSveltePlugin()
...(await getSveltePlugin(config.plugins))
])
),
parser: 'svelte' as any
Expand All @@ -131,15 +172,22 @@ export class SveltePlugin
)
];

function getSveltePlugin() {
async function getSveltePlugin(plugins: string[] = []) {
// Only provide our version of the svelte plugin if the user doesn't have one in
// the workspace already. If we did it, Prettier would - for some reason - use
// the workspace version for parsing and the extension version for printing,
// which could crash if the contract of the parser output changed.
const hasPluginLoadedAlready = prettier
.getSupportInfo()
.languages.some((l) => l.name === 'svelte');
return hasPluginLoadedAlready ? [] : [require.resolve('prettier-plugin-svelte')];
return !isFallback && (await hasSveltePluginLoaded(prettier, plugins))
? []
: [require.resolve('prettier-plugin-svelte')];
}

async function hasSveltePluginLoaded(p: typeof prettier, plugins: string[] = []) {
if (plugins.some((plugin) => plugin.includes('prettier-plugin-svelte'))) return true;
if (Number(p.version[0]) >= 3) return false; // Prettier version 3 has removed the "search plugins" feature
// Prettier v3 getSupportInfo is async, v2 is not
const info = await p.getSupportInfo();
return info.languages.some((l) => l.name === 'svelte');
}

function resolvePlugin(plugin: string) {
Expand Down
93 changes: 90 additions & 3 deletions packages/language-server/test/plugins/svelte/SveltePlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ describe('Svelte Plugin', () => {
assert.deepStrictEqual(diagnostics, []);
});

describe('#formatDocument', () => {
function stubPrettier(config: any) {
describe.only('#formatDocument', () => {
function stubPrettierV2(config: any) {
const formatStub = sinon.stub().returns('formatted');

sinon.stub(importPackage, 'importPrettier').returns(<any>{
version: '2.8.0',
resolveConfig: () => Promise.resolve(config),
getFileInfo: () => ({ ignored: false }),
format: formatStub,
Expand All @@ -84,7 +85,8 @@ describe('Svelte Plugin', () => {
async function testFormat(
config: any,
fallbackPrettierConfig: any,
options?: Parameters<typeof setup>[2]
options?: Parameters<typeof setup>[2],
stubPrettier = stubPrettierV2
) {
const { plugin, document } = setup('unformatted', fallbackPrettierConfig, options);
const formatStub = stubPrettier(config);
Expand Down Expand Up @@ -182,6 +184,91 @@ describe('Svelte Plugin', () => {
...defaultSettings
});
});

it('should load the user prettier version (version 2)', async () => {
function stubPrettier(config: any) {
const formatStub = sinon.stub().returns('formatted');

sinon
.stub(importPackage, 'importPrettier')
.onFirstCall()
.returns(<any>{
version: '2.8.0',
resolveConfig: () => Promise.resolve(config),
getFileInfo: () => ({ ignored: false }),
format: formatStub,
getSupportInfo: () => ({ languages: [{ name: 'svelte' }] })
})
.onSecondCall()
.throws(new Error('should not be called'));

return formatStub;
}

await testFormat({}, {}, undefined, stubPrettier);
});

it('should load the user prettier version (version 3)', async () => {
function stubPrettier(config: any) {
const formatStub = sinon.stub().returns(Promise.resolve('formatted'));

sinon
.stub(importPackage, 'importPrettier')
.onFirstCall()
.returns(<any>{
version: '3.0.0',
resolveConfig: () => Promise.resolve(config),
getFileInfo: () => ({ ignored: false }),
format: formatStub,
getSupportInfo: () => Promise.resolve({ languages: [] })
})
.onSecondCall()
.throws(new Error('should not be called'));

return formatStub;
}

await testFormat(
// written like this to not trigger require.resolve which fails here
{ plugins: ['./node_modules/prettier-plugin-svelte'] },
{},
undefined,
stubPrettier
);
});

it('should fall back to built-in prettier version', async () => {
function stubPrettier(config: any) {
const formatStub = sinon.stub().returns('formatted');

sinon
.stub(importPackage, 'importPrettier')
.onFirstCall()
.returns(<any>{
version: '3.0.0',
resolveConfig: () => Promise.resolve(config),
getFileInfo: () => ({ ignored: false }),
format: () => {
throw new Error('should not be called');
},
getSupportInfo: () => Promise.resolve({ languages: [] })
})
.onSecondCall()
.returns(<any>{
version: '2.8.0',
resolveConfig: () => Promise.resolve(config),
getFileInfo: () => ({ ignored: false }),
format: formatStub,
getSupportInfo: () => ({ languages: [] })
})
.onThirdCall()
.throws(new Error('should not be called'));

return formatStub;
}

await testFormat({}, {}, undefined, stubPrettier);
});
});

it('can cancel completion before promise resolved', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async function executeTest(
: defaultExpectedFile;
const snapshotFormatter = await createJsonSnapshotFormatter(dir);

updateSnapshotIfFailedOrEmpty({
await updateSnapshotIfFailedOrEmpty({
assertion() {
assert.deepStrictEqual(diagnostics, JSON.parse(readFileSync(expectedFile, 'utf-8')));
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function executeTest(

const snapshotFormatter = await createJsonSnapshotFormatter(dir);

updateSnapshotIfFailedOrEmpty({
await updateSnapshotIfFailedOrEmpty({
assertion() {
assert.deepStrictEqual(
JSON.parse(JSON.stringify(inlayHints)),
Expand Down
12 changes: 6 additions & 6 deletions packages/language-server/test/plugins/typescript/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export function createSnapshotTester<
}
}

export function updateSnapshotIfFailedOrEmpty({
export async function updateSnapshotIfFailedOrEmpty({
assertion,
expectedFile,
rootDir,
Expand All @@ -216,25 +216,25 @@ export function updateSnapshotIfFailedOrEmpty({
assertion: () => void;
expectedFile: string;
rootDir: string;
getFileContent: () => string;
getFileContent: () => string | Promise<string>;
}) {
if (existsSync(expectedFile)) {
try {
assertion();
} catch (e) {
if (process.argv.includes('--auto')) {
writeFile(`Updated ${expectedFile} for`);
await writeFile(`Updated ${expectedFile} for`);
} else {
throw e;
}
}
} else {
writeFile(`Created ${expectedFile} for`);
await writeFile(`Created ${expectedFile} for`);
}

function writeFile(msg: string) {
async function writeFile(msg: string) {
console.info(msg, dirname(expectedFile).substring(rootDir.length));
writeFileSync(expectedFile, getFileContent(), 'utf-8');
writeFileSync(expectedFile, await getFileContent(), 'utf-8');
}
}

Expand Down
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f8d7c20

Please sign in to comment.