From aed0967af18510601eafe7c326a3c52e8fc5112e Mon Sep 17 00:00:00 2001 From: Elia Schito Date: Wed, 18 Mar 2026 10:38:28 +0100 Subject: [PATCH] Read only theme directories when preloading files Restrict preload() to Shopify theme directories (assets, blocks, config, layout, locales, sections, snippets, templates). Previously all .json files under the root were loaded, including unrelated data files. A single 175 MB JSON file parsed into an AST can consume 1-2 GB of heap, easily exhausting even an 8 GB limit on large repos. Co-Authored-By: Claude --- .changeset/afraid-years-rule.md | 5 +++ .../src/documents/DocumentManager.spec.ts | 42 +++++++++---------- .../src/documents/DocumentManager.ts | 31 ++++++++++---- 3 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 .changeset/afraid-years-rule.md diff --git a/.changeset/afraid-years-rule.md b/.changeset/afraid-years-rule.md new file mode 100644 index 000000000..eeba9060a --- /dev/null +++ b/.changeset/afraid-years-rule.md @@ -0,0 +1,5 @@ +--- +"@shopify/theme-language-server-common": patch +--- + +Read only liquid and JSON files from theme directories (assets, blocks, config, layout, locales, sections, snippets, templates) when preloading files diff --git a/packages/theme-language-server-common/src/documents/DocumentManager.spec.ts b/packages/theme-language-server-common/src/documents/DocumentManager.spec.ts index 59a663ca7..3ff9b5f65 100644 --- a/packages/theme-language-server-common/src/documents/DocumentManager.spec.ts +++ b/packages/theme-language-server-common/src/documents/DocumentManager.spec.ts @@ -43,8 +43,8 @@ describe('Module: DocumentManager', () => { beforeEach(async () => { fs = new MockFileSystem( { - 'snippet/foo.liquid': `hello {% render 'bar' %}`, - 'snippet/bar.liquid': `world`, + 'snippets/foo.liquid': `hello {% render 'bar' %}`, + 'snippets/bar.liquid': `world`, }, 'mock-fs:', ); @@ -58,14 +58,14 @@ describe('Module: DocumentManager', () => { }); it('preloads source codes with a version of undefined', async () => { - const sc = documentManager.get('mock-fs:/snippet/foo.liquid'); + const sc = documentManager.get('mock-fs:/snippets/foo.liquid'); assert(sc); expect(sc.version).to.equal(undefined); }); it('returns defined versions of opened files', () => { - documentManager.open('mock-fs:/snippet/foo.liquid', 'hello {% render "bar" %}', 0); - const sc = documentManager.get('mock-fs:/snippet/foo.liquid'); + documentManager.open('mock-fs:/snippets/foo.liquid', 'hello {% render "bar" %}', 0); + const sc = documentManager.get('mock-fs:/snippets/foo.liquid'); assert(sc); expect(sc.version).to.equal(0); }); @@ -84,9 +84,9 @@ describe('Module: DocumentManager', () => { describe('Unit: close(uri)', () => { it('sets the source version to undefined (value is on disk)', () => { - documentManager.open('mock-fs:/snippet/foo.liquid', 'hello {% render "bar" %}', 10); - documentManager.close('mock-fs:/snippet/foo.liquid'); - const sc = documentManager.get('mock-fs:/snippet/foo.liquid'); + documentManager.open('mock-fs:/snippets/foo.liquid', 'hello {% render "bar" %}', 10); + documentManager.close('mock-fs:/snippets/foo.liquid'); + const sc = documentManager.get('mock-fs:/snippets/foo.liquid'); assert(sc); expect(sc.source).to.equal('hello {% render "bar" %}'); expect(sc.version).to.equal(undefined); @@ -96,9 +96,9 @@ describe('Module: DocumentManager', () => { describe('Unit: delete(uri)', () => { it('deletes the source code from the document manager', () => { // as though the file no longer exists - documentManager.open('mock-fs:/snippet/foo.liquid', 'hello {% render "bar" %}', 10); - documentManager.delete('mock-fs:/snippet/foo.liquid'); - const sc = documentManager.get('mock-fs:/snippet/foo.liquid'); + documentManager.open('mock-fs:/snippets/foo.liquid', 'hello {% render "bar" %}', 10); + documentManager.delete('mock-fs:/snippets/foo.liquid'); + const sc = documentManager.get('mock-fs:/snippets/foo.liquid'); assert(!sc); }); }); @@ -137,16 +137,16 @@ describe('Module: DocumentManager', () => { fs = new MockFileSystem( { - 'snippet/1.liquid': `hello {% render 'bar' %}`, - 'snippet/2.liquid': `hello {% render 'bar' %}`, - 'snippet/3.liquid': `hello {% render 'bar' %}`, - 'snippet/4.liquid': `hello {% render 'bar' %}`, - 'snippet/5.liquid': `hello {% render 'bar' %}`, - 'snippet/6.liquid': `hello {% render 'bar' %}`, - 'snippet/7.liquid': `hello {% render 'bar' %}`, - 'snippet/8.liquid': `hello {% render 'bar' %}`, - 'snippet/9.liquid': `hello {% render 'bar' %}`, - 'snippet/10.liquid': `hello {% render 'bar' %}`, + 'snippets/1.liquid': `hello {% render 'bar' %}`, + 'snippets/2.liquid': `hello {% render 'bar' %}`, + 'snippets/3.liquid': `hello {% render 'bar' %}`, + 'snippets/4.liquid': `hello {% render 'bar' %}`, + 'snippets/5.liquid': `hello {% render 'bar' %}`, + 'snippets/6.liquid': `hello {% render 'bar' %}`, + 'snippets/7.liquid': `hello {% render 'bar' %}`, + 'snippets/8.liquid': `hello {% render 'bar' %}`, + 'snippets/9.liquid': `hello {% render 'bar' %}`, + 'snippets/10.liquid': `hello {% render 'bar' %}`, }, mockRoot, ); diff --git a/packages/theme-language-server-common/src/documents/DocumentManager.ts b/packages/theme-language-server-common/src/documents/DocumentManager.ts index 6102b6e25..c3dd45759 100644 --- a/packages/theme-language-server-common/src/documents/DocumentManager.ts +++ b/packages/theme-language-server-common/src/documents/DocumentManager.ts @@ -3,7 +3,6 @@ import { assertNever, memoize, path, - recursiveReadDirectory, SourceCodeType, Theme, toSourceCode, @@ -13,6 +12,7 @@ import { memo, Mode, isError, + recursiveReadDirectory, } from '@shopify/theme-check-common'; import { Connection } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -131,13 +131,28 @@ export class DocumentManager { progress.start('Initializing Liquid LSP'); - // We'll only load the files that aren't already in the store. No need to - // parse a file we already parsed. - const filesToLoad = await recursiveReadDirectory( - this.fs, - rootUri, - ([uri]) => /\.(liquid|json)$/.test(uri) && !this.sourceCodes.has(uri), - ); + // NOTE: Only read known theme dirs, a stray 100MB JSON file can consume 1-2GB of heap. + // See https://shopify.dev/docs/storefronts/themes/architecture#directory-structure-and-component-types + const filesToLoad = ( + await Promise.all( + [ + 'assets', + 'blocks', + 'config', + 'layout', + 'locales', + 'sections', + 'snippets', + 'templates', + ].map((dir) => + recursiveReadDirectory( + fs, + path.join(rootUri, dir), + ([uri]) => /\.(liquid|json)$/.test(uri) && !this.sourceCodes.has(uri), + ).catch(() => []), + ), + ) + ).flat(); progress.report(10, 'Preloading files');