From 33271abbbb1eaa6c4602c17a06fe7edadb79be4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sun, 18 Oct 2026 11:54:24 +0300 Subject: [PATCH] fix(vscode): handle ESM Prettier config --- .changeset/tidy-config-loads.md | 5 ++ .../src/node/formatter.spec.ts | 46 +++++++++++++++++++ .../vscode-extension/src/node/formatter.ts | 38 ++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-config-loads.md diff --git a/.changeset/tidy-config-loads.md b/.changeset/tidy-config-loads.md new file mode 100644 index 000000000..45b6ca194 --- /dev/null +++ b/.changeset/tidy-config-loads.md @@ -0,0 +1,5 @@ +--- +'theme-check-vscode': patch +--- + +The VS Code formatter now supports ESM Prettier config exports and falls back with a warning when a config cannot be loaded. diff --git a/packages/vscode-extension/src/node/formatter.spec.ts b/packages/vscode-extension/src/node/formatter.spec.ts index a8282814e..dce154e3c 100644 --- a/packages/vscode-extension/src/node/formatter.spec.ts +++ b/packages/vscode-extension/src/node/formatter.spec.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { window } from 'vscode'; import type * as vscode from 'vscode'; import * as prettier from 'prettier'; import { vscodePrettierFormat, nodePrettierFormat } from './formatter'; @@ -15,6 +16,9 @@ vi.mock('vscode', () => ({ uri: { fsPath: '/workspace' }, })), }, + window: { + showWarningMessage: vi.fn(), + }, })); describe('formatter', () => { @@ -48,11 +52,17 @@ describe('formatter', () => { }); it('should format local files with config options', async () => { + vi.mocked(prettier.getFileInfo).mockResolvedValue({ + ignored: false, + } as prettier.FileInfoResult); + vi.mocked(prettier.resolveConfig).mockResolvedValue({ liquidSingleQuote: false }); + await vscodePrettierFormat(localFile); expect(prettier.getFileInfo).toHaveBeenCalled(); expect(prettier.resolveConfig).toHaveBeenCalled(); expect(prettier.format).toHaveBeenCalledWith('
{{ product.title }}
', { + liquidSingleQuote: false, parser: 'liquid-html', plugins: [expect.anything()], }); @@ -70,5 +80,41 @@ describe('formatter', () => { expect(prettier.format).not.toHaveBeenCalled(); expect(result).toBe('
{{ product.title }}
'); }); + + it('should unwrap ESM Prettier config exports', async () => { + vi.mocked(prettier.getFileInfo).mockResolvedValue({ + ignored: false, + } as prettier.FileInfoResult); + const esmConfig = { default: { liquidSingleQuote: false } } as unknown as prettier.Options; + Object.defineProperty(esmConfig, '__esModule', { value: true }); + vi.mocked(prettier.resolveConfig).mockResolvedValue(esmConfig); + + await nodePrettierFormat(localFile); + + expect(prettier.format).toHaveBeenCalledWith('
{{ product.title }}
', { + liquidSingleQuote: false, + parser: 'liquid-html', + plugins: [expect.anything()], + }); + expect(window.showWarningMessage).not.toHaveBeenCalled(); + }); + + it('should fall back to default options when the Prettier config cannot be loaded', async () => { + vi.mocked(prettier.getFileInfo).mockResolvedValue({ + ignored: false, + } as prettier.FileInfoResult); + vi.mocked(prettier.resolveConfig).mockRejectedValue(new Error('Unsupported ESM config')); + vi.mocked(prettier.format).mockReturnValue('formatted'); + + const result = await nodePrettierFormat(localFile); + await nodePrettierFormat(localFile); + + expect(result).toBe('formatted'); + expect(prettier.format).toHaveBeenCalledWith('
{{ product.title }}
', { + parser: 'liquid-html', + plugins: [expect.anything()], + }); + expect(window.showWarningMessage).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/vscode-extension/src/node/formatter.ts b/packages/vscode-extension/src/node/formatter.ts index b1685e011..e06ec7904 100644 --- a/packages/vscode-extension/src/node/formatter.ts +++ b/packages/vscode-extension/src/node/formatter.ts @@ -1,9 +1,11 @@ import path from 'node:path'; -import { workspace } from 'vscode'; +import { window, workspace } from 'vscode'; import LiquidPrettierPlugin from '@shopify/prettier-plugin-liquid'; import * as prettier from 'prettier'; import { Format } from '../common/formatter'; +const warnedConfigFolders = new Set(); + export const vscodePrettierFormat: Format = async (textDocument) => { if (textDocument.uri.scheme === 'file') { return nodePrettierFormat(textDocument); @@ -32,10 +34,42 @@ export const nodePrettierFormat: Format = async (textDocument) => { return text; } - const options = await prettier.resolveConfig(textDocument.uri.fsPath, { useCache: false }); + const warningScope = workspaceFolder?.uri.fsPath ?? path.dirname(textDocument.uri.fsPath); + let options: prettier.Options | null; + try { + options = await prettier.resolveConfig(textDocument.uri.fsPath, { useCache: false }); + warnedConfigFolders.delete(warningScope); + } catch (error) { + console.error('Could not load the Prettier configuration; using default options.', error); + options = null; + + if (!warnedConfigFolders.has(warningScope)) { + warnedConfigFolders.add(warningScope); + void window.showWarningMessage( + 'Could not load the Prettier configuration. Formatting with default options.', + ); + } + } + + options = unwrapPrettierConfig(options); return prettier.format(text, { ...options, parser: 'liquid-html', plugins: [LiquidPrettierPlugin as any], }); }; + +function unwrapPrettierConfig(options: prettier.Options | null): prettier.Options | null { + const moduleConfig = options as (prettier.Options & Record) | null; + const defaultConfig = moduleConfig?.default; + + if ( + moduleConfig?.['__esModule'] === true && + defaultConfig !== null && + typeof defaultConfig === 'object' + ) { + return defaultConfig as prettier.Options; + } + + return options; +}