fix(liquid): JSON under templates/ or sections/ is Liquid only inside a Shopify theme (#2393)

isShopifyLiquidJson matched every templates/**/*.json and sections/**/*.json
by path alone, so a .NET project template's template.json (jasontaylordev/
CleanArchitecture), schematics and CMS content were indexed as empty Liquid
files, and JSON listing `sections` could link to an unrelated Liquid file.

The rule now needs the directory holding templates/ or sections/ to also
hold layout/theme.liquid or config/settings_schema.json, looked up under the
project root. isSourceFile, detectLanguage and preloadLanguagesForFiles take
an optional rootDir, and every caller that decides what to index or how to
parse passes it: the scans, git change detection, scoped sync, the watcher,
indexAll, indexFileWithContent and CodeGraph.extractFromSource. Without a
root there is no theme to find. A sync removes JSON an older index stored
this way.

Shopify/dawn, expressjs/express and gofynd/Astra index byte-identically;
CleanArchitecture loses its 2 bogus Liquid files.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 23:18:41 +00:00
committed by GitHub
parent 3f5a9e2e1d
commit 0a670dc8cc
6 changed files with 164 additions and 33 deletions
+1
View File
@@ -62,6 +62,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Dart, an annotation is no longer lost when a comment sits between it and the declaration, like `@override` followed by `// ignore: must_call_super`, a `///` comment written below the annotations, or `@riverpod` followed by an `// expect_lint: …` line. Before, the comment hid the annotation, so a provider written that way was missing from the annotation's `callers` and impact. Re-index Dart projects after upgrading.
- In Dart, a call written through an import prefix, like `http.get(…)` after `import 'package:http/http.dart' as http;`, or `utils.Listener()` and `kit.Box.create()` through a prefix of your own, now links to what the library imported with that prefix exports, following its `export` chains, `show` and `hide`, and to nothing when that library is a package outside your project. Before, the prefix was taken for a variable, so `http.get(…)` could link to an unrelated `Http` class's `get` method, and calls made through a prefix into your own libraries were missing from `callers` and impact. Re-index Dart projects after upgrading.
- `codegraph sync` and the file watcher now link an import to the file it names when that file is added after the importing file was indexed, or comes back after being deleted: `import 'package:app/b.dart';` in Dart, `import './side'` in JavaScript or TypeScript, `#include "net/socket.h"` in C and C++, `require 'inc/db.php'` in PHP, `import pkg.mod` in Python, a Lua `require`, or a C# `using` of a namespace declared later. Before, the import stayed unlinked until the importing file itself changed or the project was re-indexed, so `codegraph affected` and the files listed as using the new file missed it. Re-index after upgrading.
- JSON files in a `templates` or `sections` folder are now read as Shopify Liquid only inside a Shopify theme, where the folder that contains `templates` or `sections` also has `layout/theme.liquid` or `config/settings_schema.json`. Before, all such JSON was, so a .NET project template's `template.json`, a schematic or a CMS's content showed up in the index as an empty Liquid file, and one that lists `sections` could be linked to an unrelated Liquid file of the same name. Shopify themes, including one kept in a subfolder of a repository, are indexed as before, and the next sync removes JSON that an earlier version indexed this way.
## [1.6.2] - 2026-10-03
+98
View File
@@ -7427,6 +7427,9 @@ describe('Liquid Shopify JSON template section resolution', () => {
// looked unused. The JSON is now indexed and its `type`s linked.
fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'templates/customers'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'layout'), { recursive: true });
// Every Shopify theme has layout/theme.liquid beside templates/ and sections/.
fs.writeFileSync(path.join(tempDir, 'layout/theme.liquid'), `{{ content_for_layout }}\n`);
fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
fs.writeFileSync(path.join(tempDir, 'sections/main-login.liquid'), `<form>{{ 'customer.login' | t }}</form>\n`);
fs.writeFileSync(path.join(tempDir, 'templates/product.json'), JSON.stringify({ sections: { main: { type: 'main-product' } }, order: ['main'] }));
@@ -7444,6 +7447,101 @@ describe('Liquid Shopify JSON template section resolution', () => {
expect(cg.getFileDependents(product!.filePath).some((p) => p.endsWith('templates/product.json')), 'top-level JSON template links its section').toBe(true);
expect(cg.getFileDependents(login!.filePath).some((p) => p.endsWith('customers/login.json')), 'nested JSON template links its section').toBe(true);
});
const jsonTemplate = (type: string): string => JSON.stringify({ sections: { main: { type } }, order: ['main'] });
const indexedJson = (): string[] =>
cg.getNodesByKind('file').map((n) => n.filePath).filter((p) => p.endsWith('.json')).sort();
it('leaves JSON under templates/ or sections/ out when no Shopify theme holds it', async () => {
// jasontaylordev/CleanArchitecture ships a .NET project template under
// templates/, and a CMS keeps its content under sections/: JSON in folders
// with those names, but nothing beside them makes a Shopify theme, so it is
// not Liquid — even when a Liquid file is named like one of its "types".
fs.mkdirSync(path.join(tempDir, 'templates/ca-use-case/.template.config'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'sections'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'templates/ca-use-case/.template.config/dotnetcli.host.json'), JSON.stringify({ symbolInfo: { UseCase: { longName: 'name' } } }));
fs.writeFileSync(path.join(tempDir, 'templates/ca-use-case/.template.config/template.json'), JSON.stringify({ identity: 'CleanArchitecture.UseCase', sections: { main: { type: 'hero' } } }));
fs.writeFileSync(path.join(tempDir, 'sections/home.json'), jsonTemplate('hero'));
fs.writeFileSync(path.join(tempDir, 'sections/hero.liquid'), `<h1>{{ page.title }}</h1>\n`);
cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.resolveReferences();
expect(indexedJson()).toEqual([]);
const hero = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('sections/hero.liquid'));
expect(hero, 'the Liquid file itself is still indexed').toBeDefined();
expect(cg.getFileDependents(hero!.filePath)).toEqual([]);
});
it('finds a Shopify theme in a subdirectory by its config/settings_schema.json', async () => {
// A theme under theme/ with no layout/theme.liquid committed: its settings
// schema marks it. The same JSON under tools/templates/ is in no theme.
for (const dir of ['theme/config', 'theme/sections', 'theme/templates', 'tools/templates']) {
fs.mkdirSync(path.join(tempDir, dir), { recursive: true });
}
fs.writeFileSync(path.join(tempDir, 'theme/config/settings_schema.json'), JSON.stringify([{ name: 'theme_info' }]));
fs.writeFileSync(path.join(tempDir, 'theme/sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
fs.writeFileSync(path.join(tempDir, 'theme/templates/product.json'), jsonTemplate('main-product'));
fs.writeFileSync(path.join(tempDir, 'tools/templates/product.json'), jsonTemplate('main-product'));
cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.resolveReferences();
expect(indexedJson()).toEqual(['theme/templates/product.json']);
expect(cg.getFileDependents('theme/sections/main-product.liquid')).toEqual(['theme/templates/product.json']);
// Extracting without storing tells them apart the same way.
const refsOf = (file: string) => cg.extractFromSource(file, jsonTemplate('main-product')).unresolvedReferences.map((r) => r.referenceName);
expect(refsOf('theme/templates/product.json')).toEqual(['sections/main-product.liquid']);
expect(refsOf('tools/templates/product.json')).toEqual([]);
});
it('drops JSON templates once their folder is no longer a theme, and takes them back when it is', async () => {
// The same path an index built before this check takes: JSON it stored as
// Liquid outside any theme leaves on the next sync, without a re-index.
for (const dir of ['layout', 'sections', 'templates']) fs.mkdirSync(path.join(tempDir, dir), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'layout/theme.liquid'), `{{ content_for_layout }}\n`);
fs.writeFileSync(path.join(tempDir, 'sections/main-product.liquid'), `<div>{{ product.title }}</div>\n`);
fs.writeFileSync(path.join(tempDir, 'templates/page.json'), jsonTemplate('main-product'));
fs.writeFileSync(path.join(tempDir, 'templates/product.json'), jsonTemplate('main-product'));
cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
expect(indexedJson()).toEqual(['templates/page.json', 'templates/product.json']);
fs.rmSync(path.join(tempDir, 'layout'), { recursive: true, force: true });
// A sync of only the reported paths (the watcher's) and a full sync agree.
await cg.sync({ paths: ['templates/product.json'] });
expect(indexedJson()).toEqual(['templates/page.json']);
await cg.sync();
expect(indexedJson()).toEqual([]);
fs.mkdirSync(path.join(tempDir, 'config'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'config/settings_schema.json'), '[]');
await cg.sync();
expect(indexedJson()).toEqual(['templates/page.json', 'templates/product.json']);
});
it('reads JSON under templates/ or sections/ as Liquid only inside a theme under the project root', () => {
fs.mkdirSync(path.join(tempDir, 'shop/layout'), { recursive: true });
fs.writeFileSync(path.join(tempDir, 'shop/layout/theme.liquid'), '');
for (const file of ['shop/templates/product.json', 'shop/templates/customers/login.json', 'shop/sections/header-group.json']) {
expect(isSourceFile(file, undefined, tempDir), file).toBe(true);
expect(detectLanguage(file, undefined, undefined, tempDir), file).toBe('liquid');
// Without the project root there is no theme to look for.
expect(isSourceFile(file), file).toBe(false);
expect(detectLanguage(file), file).toBe('unknown');
}
// Beside the theme, not in it; and the theme's other JSON was never a template.
for (const file of ['templates/product.json', 'other/sections/header-group.json', 'shop/config/settings_data.json']) {
expect(isSourceFile(file, undefined, tempDir), file).toBe(false);
expect(detectLanguage(file, undefined, undefined, tempDir), file).toBe('unknown');
}
// Outside a theme, JSON gets whatever the project maps `.json` to, as any other JSON does.
expect(detectLanguage('templates/product.json', undefined, { '.json': 'yaml' }, tempDir)).toBe('yaml');
expect(detectLanguage('shop/templates/product.json', undefined, { '.json': 'yaml' }, tempDir)).toBe('liquid');
});
});
describe('Lua/Luau require resolution', () => {
+36 -8
View File
@@ -7,6 +7,7 @@
*/
import * as path from 'path';
import * as fs from 'fs';
import * as fsp from 'fs/promises';
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
import { ExtractionError, Language } from '../types';
@@ -241,10 +242,14 @@ export function hasMpegTsExtension(filePath: string): boolean {
* `overrides` is the project's validated custom extension → language map (from
* `codegraph.json`); when present its extensions count as indexable in addition
* to the built-ins. Omitting it is byte-identical to the zero-config behavior.
*
* `rootDir` is the project root `filePath` is relative to. A Shopify theme's
* JSON templates count only when it is given, since telling a theme apart takes
* a look at the files beside them (see `isShopifyLiquidJson`).
*/
export function isSourceFile(filePath: string, overrides?: Record<string, Language>): boolean {
export function isSourceFile(filePath: string, overrides?: Record<string, Language>, rootDir?: string): boolean {
if (isPlayRoutesFile(filePath)) return true; // Play `conf/routes` is extensionless
if (isShopifyLiquidJson(filePath)) return true; // Shopify OS 2.0 JSON templates / section groups
if (isShopifyLiquidJson(filePath, rootDir)) return true; // Shopify OS 2.0 JSON templates / section groups
if (isErlangAppFile(filePath)) return true; // OTP `.app`/`.app.src` resource files
const dot = filePath.lastIndexOf('.');
if (dot < 0) return false;
@@ -252,15 +257,35 @@ export function isSourceFile(filePath: string, overrides?: Record<string, Langua
return ext in EXTENSION_MAP || (!!overrides && ext in overrides);
}
/**
* What marks a Shopify theme's root, beside its `templates/` and `sections/`:
* the `layout/theme.liquid` every theme must have, or its settings schema.
*/
const SHOPIFY_THEME_MARKERS = ['layout/theme.liquid', 'config/settings_schema.json'];
/**
* Shopify OS 2.0 JSON template (`templates/*.json`) or section group
* (`sections/*.json`) — these reference sections by `"type"`, so the Liquid
* extractor links them. (config/ + locales/ JSON have no section refs.)
*
* Only inside a Shopify theme: the directory holding that `templates/` or
* `sections/` must also hold one of `SHOPIFY_THEME_MARKERS`. Plenty of other
* projects keep JSON in folders with those names — a .NET project template's
* `templates/<name>/.template.config/template.json`, schematics, CMS content —
* and it is not Liquid. The theme is looked for under `rootDir`, the project
* root `filePath` is relative to; without one there is no theme to find.
*/
export function isShopifyLiquidJson(filePath: string): boolean {
export function isShopifyLiquidJson(filePath: string, rootDir?: string): boolean {
// Allow nested template dirs (`templates/customers/login.json`), not just
// top-level (`templates/product.json`).
return /(^|\/)(templates|sections)\/.+\.json$/i.test(filePath);
if (rootDir === undefined || !/(^|\/)(templates|sections)\/.+\.json$/i.test(filePath)) return false;
const segments = filePath.split('/');
for (let i = 0; i < segments.length - 1; i++) {
if (!/^(templates|sections)$/i.test(segments[i]!)) continue;
const themeDir = path.join(rootDir, ...segments.slice(0, i));
if (SHOPIFY_THEME_MARKERS.some((marker) => fs.existsSync(path.join(themeDir, marker)))) return true;
}
return false;
}
/**
@@ -536,15 +561,18 @@ export function getParser(language: Language): Parser | null {
* `overrides` is the project's validated custom extension → language map (from
* `codegraph.json`); when present its mappings take precedence over the built-in
* `EXTENSION_MAP`. Omitting it is byte-identical to the zero-config behavior.
*
* `rootDir` is the project root `filePath` is relative to, as for
* `isSourceFile`: a Shopify theme's JSON templates are Liquid only with it.
*/
export function detectLanguage(filePath: string, source?: string, overrides?: Record<string, Language>): Language {
export function detectLanguage(filePath: string, source?: string, overrides?: Record<string, Language>, rootDir?: string): Language {
// Play `conf/routes` has no grammar — route through the no-symbol path; the
// Play framework resolver extracts route nodes from it.
if (isPlayRoutesFile(filePath)) return 'yaml';
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
// Shopify OS 2.0 JSON templates / section groups → the Liquid extractor (it
// links each section `"type"` to its `sections/<type>.liquid`).
if (isShopifyLiquidJson(filePath)) return 'liquid';
// A Shopify theme's OS 2.0 JSON templates / section groups → the Liquid
// extractor (it links each section `"type"` to its `sections/<type>.liquid`).
if (isShopifyLiquidJson(filePath, rootDir)) return 'liquid';
// OTP `.app`/`.app.src` resource files — Erlang terms the grammar parses as
// top-level expressions (last-dot ext `.src` is too generic for the map).
if (isErlangAppFile(filePath)) return 'erlang';
+24 -22
View File
@@ -602,7 +602,7 @@ function collectIncludedFiles(
if (defaults.ignores(rel)) return;
if (!include.ignores(rel)) return;
if (exclude && exclude.ignores(rel)) return;
if (!isSourceFile(rel, overrides)) return;
if (!isSourceFile(rel, overrides, rootDir)) return;
out.add(rel);
}
};
@@ -811,9 +811,10 @@ function findNestedGitRepos(absDir: string, relPrefix: string): string[] {
*/
export function preloadLanguagesForFiles(
files: string[],
overrides?: Record<string, Language>
overrides?: Record<string, Language>,
rootDir?: string
): Language[] {
const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides)))];
const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides, rootDir)))];
// A Flow-typed `.js` is read with the TSX grammar (see detectLanguage).
if ((languages.includes('javascript') || languages.includes('jsx')) && !languages.includes('tsx')) languages.push('tsx');
if (languages.includes('c')) {
@@ -1358,7 +1359,8 @@ export function getGitChangedFiles(rootDir: string, sinceCommit?: string | null)
// Custom extension → language overrides from the project's codegraph.json,
// so change detection sees the same custom-extension files the full index does.
const overrides = loadExtensionOverrides(rootDir);
collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined);
const isSource = (filePath: string): boolean => isSourceFile(filePath, overrides, rootDir);
collectGitStatus(rootDir, '', changes, isSource, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined);
return changes;
} catch {
return null;
@@ -1430,7 +1432,7 @@ export function canTrustGitFastPath(rootDir: string, sinceCommit?: string | null
}
}
function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null, sinceCommit?: string): void {
function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, isSource: (filePath: string) => boolean, includeIgnored: Ignore | null = null, exclude: Ignore | null = null, sinceCommit?: string): void {
const output = execFileSync(
'git',
// `-uall` lists individual untracked files instead of collapsing an
@@ -1459,7 +1461,7 @@ function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, over
// filtered by exactly the rules a working-tree change is (#766, #999, #1829).
const classify = (statusCode: string, rel: string): void => {
const filePath = normalizePath(prefix + rel);
if (!isSourceFile(filePath, overrides)) return;
if (!isSource(filePath)) return;
if (statusCode.includes('D')) {
// Deletions stay unfiltered: getChangedFiles acts on one only when the
@@ -1523,11 +1525,11 @@ function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, over
// and they are left alone (#970, #976), mirroring the full-index scan.
for (const rel of untrackedDirs) {
for (const repoRel of findNestedGitRepos(path.join(repoDir, rel), rel)) {
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored, exclude);
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, isSource, includeIgnored, exclude);
}
}
for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored, exclude);
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, isSource, includeIgnored, exclude);
}
}
@@ -1551,7 +1553,7 @@ export function scanDirectory(
const files: string[] = [];
let count = 0;
for (const filePath of gitFiles) {
if (isSourceFile(filePath, overrides)) {
if (isSourceFile(filePath, overrides, rootDir)) {
files.push(filePath);
count++;
onProgress?.(count, filePath);
@@ -1602,7 +1604,7 @@ export async function scanDirectoryAsync(
const files: string[] = [];
let count = 0;
for (const filePath of gitFiles) {
if (isSourceFile(filePath, overrides)) {
if (isSourceFile(filePath, overrides, rootDir)) {
files.push(filePath);
count++;
onProgress?.(count, filePath);
@@ -1710,7 +1712,7 @@ function scanDirectoryWalk(
}
} else if (stat.isFile()) {
if (!isIgnored(fullPath, false, active)) {
if (isSourceFile(relativePath, overrides)) {
if (isSourceFile(relativePath, overrides, rootDir)) {
files.push(relativePath);
count++;
onProgress?.(count, relativePath);
@@ -1731,7 +1733,7 @@ function scanDirectoryWalk(
}
} else if (entry.isFile()) {
if (!isIgnored(fullPath, false, active)) {
if (isSourceFile(relativePath, overrides)) {
if (isSourceFile(relativePath, overrides, rootDir)) {
files.push(relativePath);
count++;
onProgress?.(count, relativePath);
@@ -2186,7 +2188,7 @@ export class ExtractionOrchestrator {
await new Promise(resolve => setImmediate(resolve));
// Detect needed languages and load grammars in the parse worker
const neededLanguages = preloadLanguagesForFiles(files, overrides);
const neededLanguages = preloadLanguagesForFiles(files, overrides, this.rootDir);
// Parse files on a pool of worker threads (keeps the main thread free for UI
// and uses every core). Falls back to in-process parsing when the compiled
@@ -2257,7 +2259,7 @@ export class ExtractionOrchestrator {
* here on the main thread, where the codegraph.json overrides are loaded.
*/
const parseFile = (filePath: string, content: string): Promise<ExtractionResult> => {
const language = detectLanguage(filePath, content, overrides);
const language = detectLanguage(filePath, content, overrides, this.rootDir);
const names = this.frameworksForFile(filePath, frameworkNames);
if (!pool) return Promise.resolve(extractFromSource(filePath, content, language, names));
return pool.requestParse({ filePath, content, language, frameworkNames: names });
@@ -2308,7 +2310,7 @@ export class ExtractionOrchestrator {
// Store: on the writer thread when active (fresh DB — bundles applied
// in the same file order this chain dispatches them), else on the main
// thread (SQLite connections are per-thread).
const language = detectLanguage(filePath, content, overrides);
const language = detectLanguage(filePath, content, overrides, this.rootDir);
if (grammarUnavailable) {
// Store nothing: a row from an earlier run keeps its data, and with no
// row (or an older hash) the next sync or index retries the file.
@@ -2352,7 +2354,7 @@ export class ExtractionOrchestrator {
// Files with no symbols but no errors (yaml, twig, properties) are
// tracked at the file level — count them as indexed so the CLI doesn't
// misleadingly report "No files found to index".
const lang = detectLanguage(filePath, content, overrides);
const lang = detectLanguage(filePath, content, overrides, this.rootDir);
if (isFileLevelOnlyLanguage(lang)) {
filesIndexed++;
} else {
@@ -2640,7 +2642,7 @@ export class ExtractionOrchestrator {
// so decode here — otherwise a kernel-language retry passes the gate
// below via `errors.length === 0`, stores nothing, and the file is
// permanently recorded as "(0 symbols)" with the error erased (#1541).
const language = detectLanguage(filePath, content, overrides);
const language = detectLanguage(filePath, content, overrides, this.rootDir);
result = materializeKernelResult(result, filePath, language);
if (result.nodes.length > 0 || result.errors.length === 0) {
@@ -2695,7 +2697,7 @@ export class ExtractionOrchestrator {
}
// Same undecoded-transport hazard as the first retry pass (#1541).
const language = detectLanguage(filePath, fullContent, overrides);
const language = detectLanguage(filePath, fullContent, overrides, this.rootDir);
result = materializeKernelResult(result, filePath, language);
if (result.nodes.length > 0 || result.errors.length === 0) {
@@ -2857,7 +2859,7 @@ export class ExtractionOrchestrator {
};
}
const language = detectLanguage(relativePath, content, loadExtensionOverrides(this.rootDir));
const language = detectLanguage(relativePath, content, loadExtensionOverrides(this.rootDir), this.rootDir);
// Check file size
if (stats.size > MAX_SOURCE_FILE_SIZE_BYTES) {
@@ -3350,7 +3352,7 @@ export class ExtractionOrchestrator {
const overrides = loadExtensionOverrides(this.rootDir);
currentFiles = unique.filter(
(p) =>
isSourceFile(p, overrides) &&
isSourceFile(p, overrides, this.rootDir) &&
!scope.ignores(p) &&
fs.existsSync(path.join(this.rootDir, p))
);
@@ -3500,7 +3502,7 @@ export class ExtractionOrchestrator {
// Load only grammars needed for changed files
if (filesToIndex.length > 0) {
const overrides = loadExtensionOverrides(this.rootDir);
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides));
await loadGrammarsForLanguages(preloadLanguagesForFiles(filesToIndex, overrides, this.rootDir));
}
// Index changed files
@@ -3627,7 +3629,7 @@ export class ExtractionOrchestrator {
for (const filePath of candidates) {
const tracked = this.queries.getFileByPath(filePath);
const fullPath = path.join(this.rootDir, filePath);
if (!isSourceFile(filePath, overrides) || scope.ignores(filePath) || !fs.existsSync(fullPath)) {
if (!isSourceFile(filePath, overrides, this.rootDir) || scope.ignores(filePath) || !fs.existsSync(fullPath)) {
if (tracked) removed.push(filePath);
continue;
}
+4 -2
View File
@@ -45,7 +45,7 @@ import {
extractFromSource,
initGrammars,
} from './extraction';
import { hasGrammarLoadFailure, isFileLevelOnlyLanguage } from './extraction/grammars';
import { detectLanguage, hasGrammarLoadFailure, isFileLevelOnlyLanguage } from './extraction/grammars';
import {
ReferenceResolver,
createResolver,
@@ -1415,7 +1415,9 @@ export class CodeGraph {
* Extract nodes and edges from source code (without storing)
*/
extractFromSource(filePath: string, source: string): ExtractionResult {
return extractFromSource(filePath, source);
// The project root is what tells a Shopify theme's JSON templates (Liquid)
// apart from other JSON.
return extractFromSource(filePath, source, detectLanguage(filePath, source, undefined, this.projectRoot));
}
// ===========================================================================
+1 -1
View File
@@ -650,7 +650,7 @@ export class FileWatcher {
this.refreshScope(rel);
return;
}
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) {
if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot), this.projectRoot)) {
this.maybeScheduleForRemovedDir(rel);
return;
}