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;
+}