@shopify/theme-language-server-common
Version:
<h1 align="center" style="position: relative;" > <br> <img src="https://github.com/Shopify/theme-check-vscode/blob/main/images/shopify_glyph.png?raw=true" alt="logo" width="141" height="160"> <br> Theme Language Server </h1>
140 lines (126 loc) • 4.84 kB
text/typescript
import {
check,
findRoot,
makeFileExists,
Offense,
path,
SectionSchema,
Severity,
SourceCodeType,
ThemeBlockSchema,
} from '@shopify/theme-check-common';
import { CSSLanguageService } from '../css/CSSLanguageService';
import { AugmentedSourceCode, DocumentManager } from '../documents';
import { Dependencies } from '../types';
import { DiagnosticsManager } from './DiagnosticsManager';
import { offenseSeverity } from './offenseToDiagnostic';
export function makeRunChecks(
documentManager: DocumentManager,
diagnosticsManager: DiagnosticsManager,
{
fs,
loadConfig,
themeDocset,
jsonValidationSet,
getMetafieldDefinitions,
cssLanguageService,
}: Pick<
Dependencies,
'fs' | 'loadConfig' | 'themeDocset' | 'jsonValidationSet' | 'getMetafieldDefinitions'
> & { cssLanguageService?: CSSLanguageService },
) {
return async function runChecks(triggerURIs: string[]): Promise<void> {
// This function takes an array of triggerURIs so that we can correctly
// recheck on file renames that came from out of bounds in a
// workspaces.
//
// e.g. if a user renames
// theme1/snippets/a.liquid to
// theme1/snippets/b.liquid
//
// then we recheck theme1
const fileExists = makeFileExists(fs);
const rootURIs = await Promise.all(triggerURIs.map((uri) => findRoot(uri, fileExists)));
const deduplicatedRootURIs = new Set(rootURIs);
await Promise.all([...deduplicatedRootURIs].map(runChecksForRoot));
return;
async function runChecksForRoot(configFileRootUri: string) {
const config = await loadConfig(configFileRootUri, fs);
const theme = documentManager.theme(config.rootUri);
const cssOffenses = cssLanguageService
? await Promise.all(
theme.map((sourceCode) => getCSSDiagnostics(cssLanguageService, sourceCode)),
).then((offenses) => offenses.flat())
: [];
const themeOffenses = await check(theme, config, {
fs,
themeDocset,
jsonValidationSet,
getMetafieldDefinitions,
// TODO should do something for app blocks?
async getBlockSchema(name) {
// We won't preload here. If it's available, we'll give it. Otherwise expect nothing.
const uri = path.join(config.rootUri, 'blocks', `${name}.liquid`);
const doc = documentManager.get(uri);
if (doc?.type !== SourceCodeType.LiquidHtml) return undefined;
const schema = await doc.getSchema();
return schema as ThemeBlockSchema | undefined;
},
async getSectionSchema(name) {
// We won't preload here. If it's available, we'll give it. Otherwise expect nothing.
const uri = path.join(config.rootUri, 'sections', `${name}.liquid`);
const doc = documentManager.get(uri);
if (doc?.type !== SourceCodeType.LiquidHtml) return undefined;
const schema = await doc.getSchema();
return schema as SectionSchema | undefined;
},
async getDocDefinition(relativePath) {
const uri = path.join(config.rootUri, relativePath);
const doc = documentManager.get(uri);
if (doc?.type !== SourceCodeType.LiquidHtml) return undefined;
return doc.getLiquidDoc();
},
});
const offenses = [...themeOffenses, ...cssOffenses];
// We iterate over the theme files (as opposed to offenses) because if
// there were offenses before, we need to send an empty array to clear
// them.
for (const sourceCode of theme) {
const sourceCodeOffenses = offenses.filter((offense) => offense.uri === sourceCode.uri);
diagnosticsManager.set(sourceCode.uri, sourceCode.version, sourceCodeOffenses);
}
}
};
}
async function getCSSDiagnostics(
cssLanguageService: CSSLanguageService,
sourceCode: AugmentedSourceCode,
): Promise<Offense[]> {
if (sourceCode.type !== SourceCodeType.LiquidHtml) {
return [];
}
const diagnostics = await cssLanguageService.diagnostics({
textDocument: { uri: sourceCode.uri },
});
return diagnostics
.map(
(diagnostic): Offense => ({
check: 'css',
message: diagnostic.message,
end: {
index: sourceCode.textDocument.offsetAt(diagnostic.range.end),
line: diagnostic.range.end.line,
character: diagnostic.range.end.character,
},
start: {
index: sourceCode.textDocument.offsetAt(diagnostic.range.start),
line: diagnostic.range.start.line,
character: diagnostic.range.start.character,
},
severity: offenseSeverity(diagnostic),
uri: sourceCode.uri,
type: SourceCodeType.LiquidHtml,
}),
)
.filter((offense) => offense.severity !== Severity.INFO);
}