Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/theme-check-common/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config';
import { configDefaults } from 'vitest/config';

export default defineConfig({
test: {
exclude: [...configDefaults.exclude],
pool: 'forks',
poolOptions: {
forks: {
singleFork: true,
isolate: true,
},
},
setupFiles: ['./src/test/test-setup.ts'],
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,232 @@ describe('Module: SectionRenameHandler', () => {
}
});

describe('config/settings_data.json handling', () => {
const settingsDataUri = 'mock-fs:/config/settings_data.json';

it('updates the type attribute in config/settings_data.json when a section is renamed', async () => {
fs = new MockFileSystem(
{
'sections/old-name.liquid': `
{% schema %}
{ "name": "Old name" }
{% endschema %}`,

'config/settings_data.json': JSON.stringify(
{
presets: {
Default: {
sections: {
'header-id': { type: 'old-name', settings: {} },
},
order: ['header-id'],
},
},
},
null,
2,
),
},
mockRoot,
);
documentManager = new DocumentManager(
fs,
undefined,
undefined,
async () => 'theme',
async () => true,
);
handler = new RenameHandler(connection, capabilities, documentManager, findThemeRootURI);

await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/sections/old-name.liquid',
newUri: 'mock-fs:/sections/new-name.liquid',
},
],
});

expect(connection.spies.sendRequest).toHaveBeenCalledWith(
'workspace/applyEdit',
expect.objectContaining({
edit: expect.objectContaining({
documentChanges: expect.arrayContaining([
{
textDocument: { uri: settingsDataUri, version: null },
edits: [
{
annotationId: 'renameSection',
newText: 'new-name',
range: { start: expect.any(Object), end: expect.any(Object) },
},
],
},
]),
}),
}),
);
});

it('replaces the correct text in config/settings_data.json', async () => {
const settingsDataContent = JSON.stringify(
{
presets: {
Default: {
sections: {
'header-id': { type: 'old-name', settings: {} },
},
order: ['header-id'],
},
},
},
null,
2,
);
fs = new MockFileSystem(
{
'sections/old-name.liquid': `
{% schema %}
{ "name": "Old name" }
{% endschema %}`,

'config/settings_data.json': settingsDataContent,
},
mockRoot,
);
documentManager = new DocumentManager(
fs,
undefined,
undefined,
async () => 'theme',
async () => true,
);
handler = new RenameHandler(connection, capabilities, documentManager, findThemeRootURI);

await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/sections/old-name.liquid',
newUri: 'mock-fs:/sections/new-name.liquid',
},
],
});

const params: ApplyWorkspaceEditParams = connection.spies.sendRequest.mock.calls[0][1];
const expectedFs = new MockFileSystem(
{
'config/settings_data.json': JSON.stringify(
{
presets: {
Default: {
sections: {
'header-id': { type: 'new-name', settings: {} },
},
order: ['header-id'],
},
},
},
null,
2,
),
},
mockRoot,
);

assert(params.edit);
assert(params.edit.documentChanges);

for (const docChange of params.edit.documentChanges) {
assert(TextDocumentEdit.is(docChange));
const uri = docChange.textDocument.uri;
const edits = docChange.edits;
const initialDoc = await fs.readFile(uri);
const expectedDoc = await expectedFs.readFile(uri);
expect(edits).to.applyEdits(initialDoc, expectedDoc);
}
});

it('completes without error when config/settings_data.json does not exist', async () => {
// The default beforeEach fs has no settings_data.json.
// Rename should still update templates, section groups, and liquid tags.
await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/sections/old-name.liquid',
newUri: 'mock-fs:/sections/new-name.liquid',
},
],
});

expect(connection.spies.sendRequest).toHaveBeenCalled();
const params: ApplyWorkspaceEditParams = connection.spies.sendRequest.mock.calls[0][1];
assert(params.edit.documentChanges);
const uris = (params.edit.documentChanges as TextDocumentEdit[]).map(
(dc) => dc.textDocument.uri,
);
expect(uris).not.toContain(settingsDataUri);
});

it('does not edit config/settings_data.json when section name is not present', async () => {
fs = new MockFileSystem(
{
'sections/old-name.liquid': `
{% schema %}
{ "name": "Old name" }
{% endschema %}`,

'templates/index.json': `
{
"sections": { "s": { "type": "old-name" } },
"order": ["s"]
}`,

'config/settings_data.json': JSON.stringify(
{
presets: {
Default: {
sections: {
'header-id': { type: 'some-other-section', settings: {} },
},
order: ['header-id'],
},
},
},
null,
2,
),
},
mockRoot,
);
documentManager = new DocumentManager(
fs,
undefined,
undefined,
async () => 'theme',
async () => true,
);
handler = new RenameHandler(connection, capabilities, documentManager, findThemeRootURI);

await handler.onDidRenameFiles({
files: [
{
oldUri: 'mock-fs:/sections/old-name.liquid',
newUri: 'mock-fs:/sections/new-name.liquid',
},
],
});

// sendRequest was called (for the template change), but not for settings_data.json
expect(connection.spies.sendRequest).toHaveBeenCalled();
const params: ApplyWorkspaceEditParams = connection.spies.sendRequest.mock.calls[0][1];
assert(params.edit.documentChanges);
const uris = (params.edit.documentChanges as TextDocumentEdit[]).map(
(dc) => dc.textDocument.uri,
);
expect(uris).not.toContain(settingsDataUri);
});
});

it('preserves local section definitions', async () => {
fs = new MockFileSystem(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import {
isLiquidSourceCode,
} from '../../documents';
import { FindThemeRootURI } from '../../internal-types';
import { isSection, isSectionGroup, isTemplate, sectionName } from '../../utils/uri';
import { isSection, isSectionGroup, isSettingsData, isTemplate, sectionName } from '../../utils/uri';
import { BaseRenameHandler } from '../BaseRenameHandler';
import { isValidSectionGroup, isValidTemplate } from './utils';
import { isValidSectionGroup, isValidSettingsData, isValidTemplate } from './utils';

type DocumentChange = TextDocumentEdit;

Expand Down Expand Up @@ -69,6 +69,7 @@ export class SectionRenameHandler implements BaseRenameHandler {
const liquidFiles = theme.filter(isLiquidSourceCode);
const templates = theme.filter(isJsonSourceCode).filter((file) => isTemplate(file.uri));
const sectionGroups = theme.filter(isJsonSourceCode).filter((file) => isSectionGroup(file.uri));
const settingsDataFiles = theme.filter(isJsonSourceCode).filter((file) => isSettingsData(file.uri));
const oldSectionName = sectionName(rename.oldUri);
const newSectionName = sectionName(rename.newUri);
const editLabel = `Rename section '${oldSectionName}' to '${newSectionName}'`;
Expand All @@ -84,13 +85,14 @@ export class SectionRenameHandler implements BaseRenameHandler {

// All the templates/*.json files need to be updated with the new block name
// when the old block name wasn't a local block.
const [templateChanges, sectionGroupChanges, sectionTagChanges] = await Promise.all([
const [templateChanges, sectionGroupChanges, sectionTagChanges, settingsDataChanges] = await Promise.all([
Promise.all(templates.map(this.getTemplateChanges(oldSectionName, newSectionName))),
Promise.all(sectionGroups.map(this.getSectionGroupChanges(oldSectionName, newSectionName))),
Promise.all(liquidFiles.map(this.getSectionTagChanges(oldSectionName, newSectionName))),
Promise.all(settingsDataFiles.map(this.getSettingsDataChanges(oldSectionName, newSectionName))),
]);

for (const docChange of [...templateChanges, ...sectionGroupChanges]) {
for (const docChange of [...templateChanges, ...sectionGroupChanges, ...settingsDataChanges]) {
if (docChange !== null) {
workspaceEdit.documentChanges!.push(docChange);
}
Expand Down Expand Up @@ -179,6 +181,42 @@ export class SectionRenameHandler implements BaseRenameHandler {
};
}

private getSettingsDataChanges(oldSectionName: string, newSectionName: string) {
return async (sourceCode: AugmentedJsonSourceCode) => {
const { textDocument, ast, source } = sourceCode;
const parsed = parseJSON(source);
if (!parsed || isError(parsed) || isError(ast)) return null;
if (!isValidSettingsData(parsed)) return null;

const edits: AnnotatedTextEdit[] = Object.entries(parsed.presets ?? {}).flatMap(
([presetName, preset]) =>
Object.entries(preset.sections ?? {})
.filter(([_key, section]) => section.type === oldSectionName)
.map(([sectionKey]) => {
const node = nodeAtPath(ast, [
'presets',
presetName,
'sections',
sectionKey,
'type',
]) as LiteralNode;
return {
annotationId,
newText: newSectionName,
range: Range.create(
textDocument.positionAt(node.loc.start.offset + 1),
textDocument.positionAt(node.loc.end.offset - 1),
),
} as AnnotatedTextEdit;
}),
);

if (edits.length === 0) return null;

return documentChanges(sourceCode, edits);
};
}

private getSectionTagChanges(oldSectionName: string, newSectionName: string) {
return async (sourceCode: AugmentedLiquidSourceCode) => {
const { textDocument, ast } = sourceCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { Template } from '@shopify/theme-check-common';

export interface SettingsDataPreset {
sections?: Record<string, { type: string }>;
}

export interface SettingsData {
presets?: Record<string, SettingsDataPreset>;
}

export function isValidSettingsData(parsed: unknown): parsed is SettingsData {
return (
typeof parsed === 'object' &&
parsed !== null &&
'presets' in parsed &&
typeof (parsed as SettingsData).presets === 'object'
);
}

// this is very very optimistic...
export function isValidTemplate(parsed: unknown): parsed is Template.Template {
return (
Expand Down
2 changes: 2 additions & 0 deletions packages/theme-language-server-common/src/utils/uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export const isSectionGroup = (uri: string) =>

export const templateName = (uri: string) => path.basename(uri, '.json');
export const isTemplate = (uri: string) => /\btemplates(\\|\/)[^\\\/]/.test(uri);

export const isSettingsData = (uri: string) => /\bconfig(\\|\/)settings_data\.json$/.test(uri);
19 changes: 19 additions & 0 deletions packages/theme-language-server-common/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
import { configDefaults } from 'vitest/config';

export default defineConfig({
test: {
exclude: [...configDefaults.exclude],
pool: 'forks',
poolOptions: {
forks: {
singleFork: true,
isolate: true,
},
},
setupFiles: [
'../theme-check-common/src/test/test-setup.ts',
'./src/test/test-setup.ts',
],
},
});
Loading