Skip to content

Commit ce7c01e

Browse files
authored
Support for SVGs and images in theme-graph (#988)
1 parent 5ca2fca commit ce7c01e

File tree

6 files changed

+126
-50
lines changed

6 files changed

+126
-50
lines changed

.changeset/dirty-flies-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-graph': patch
3+
---
4+
5+
Allow SVGs and images to appear on theme-graph dependencies

packages/theme-graph/src/graph/module.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { path, UriString } from '@shopify/theme-check-common';
22
import {
33
CssModule,
4+
ImageModule,
45
JavaScriptModule,
56
JsonModule,
67
JsonModuleKind,
78
LiquidModule,
89
LiquidModuleKind,
910
ModuleType,
11+
SUPPORTED_ASSET_IMAGE_EXTENSIONS,
12+
SvgModule,
1013
ThemeGraph,
1114
ThemeModule,
1215
} from '../types';
@@ -134,36 +137,32 @@ export function getSectionGroupModule(
134137
export function getAssetModule(
135138
themeGraph: ThemeGraph,
136139
asset: string,
137-
): JavaScriptModule | CssModule | undefined {
138-
// return undefined;
140+
): JavaScriptModule | CssModule | SvgModule | ImageModule | undefined {
139141
const extension = extname(asset);
140-
switch (extension) {
141-
case 'js': {
142-
const uri = path.join(themeGraph.rootUri, 'assets', asset);
143-
return module(themeGraph, {
144-
type: ModuleType.JavaScript,
145-
kind: 'unused',
146-
dependencies: [],
147-
references: [],
148-
uri: uri,
149-
});
150-
}
151142

152-
case 'css': {
153-
const uri = path.join(themeGraph.rootUri, 'assets', asset);
154-
return module(themeGraph, {
155-
type: ModuleType.Css,
156-
kind: 'unused',
157-
dependencies: [],
158-
references: [],
159-
uri: uri,
160-
});
161-
}
143+
let type: ModuleType | undefined = undefined;
162144

163-
default: {
164-
return undefined;
165-
}
145+
if (SUPPORTED_ASSET_IMAGE_EXTENSIONS.includes(extension)) {
146+
type = ModuleType.Image;
147+
} else if (extension === 'js') {
148+
type = ModuleType.JavaScript;
149+
} else if (extension === 'css') {
150+
type = ModuleType.Css;
151+
} else if (extension === 'svg') {
152+
type = ModuleType.Svg;
153+
}
154+
155+
if (!type) {
156+
return undefined;
166157
}
158+
159+
return module(themeGraph, {
160+
type,
161+
kind: 'unused',
162+
dependencies: [],
163+
references: [],
164+
uri: path.join(themeGraph.rootUri, 'assets', asset),
165+
});
167166
}
168167

169168
export function getSnippetModule(themeGraph: ThemeGraph, snippet: string): LiquidModule {

packages/theme-graph/src/graph/traverse.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export async function traverseModule(
7373
return; // TODO graph import/exports ?
7474
}
7575

76-
case ModuleType.Css: {
76+
case ModuleType.Css:
77+
case ModuleType.Svg:
78+
case ModuleType.Image: {
7779
return; // Nothing to do??
7880
}
7981

@@ -97,9 +99,10 @@ async function traverseLiquidModule(
9799
{ target: ThemeModule; sourceRange: Range; targetRange?: Range }
98100
> = {
99101
// {{ 'theme.js' | asset_url }}
100-
// {{ 'theme.css' | asset_url }}
102+
// {{ 'image.png' | asset_img_url }}
103+
// {{ 'icon.svg' | inline_asset_content }}
101104
LiquidFilter: (node, ancestors) => {
102-
if (node.name === 'asset_url') {
105+
if (['asset_url', 'asset_img_url', 'inline_asset_content'].includes(node.name)) {
103106
const parentNode = ancestors[ancestors.length - 1]!;
104107
if (parentNode.type !== NodeTypes.LiquidVariable) return;
105108
if (parentNode.expression.type !== NodeTypes.String) return;

packages/theme-graph/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { buildThemeGraph } from './graph/build';
22
export { serializeThemeGraph } from './graph/serialize';
33
export { getWebComponentMap, findWebComponentReferences } from './getWebComponentMap';
4-
export { toCssSourceCode, toJsSourceCode, toSourceCode } from './toSourceCode';
4+
export { toCssSourceCode, toJsSourceCode, toSourceCode, toSvgSourceCode } from './toSourceCode';
55
export * from './types';

packages/theme-graph/src/toSourceCode.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1-
import {
2-
asError,
3-
JSONSourceCode,
4-
LiquidSourceCode,
5-
toSourceCode as tcToSourceCode,
6-
UriString,
7-
} from '@shopify/theme-check-common';
1+
import { asError, toSourceCode as tcToSourceCode, UriString } from '@shopify/theme-check-common';
82
import { parse as acornParse, Program } from 'acorn';
9-
import { CssSourceCode, JsSourceCode } from './types';
3+
import {
4+
CssSourceCode,
5+
FileSourceCode,
6+
ImageSourceCode,
7+
JsSourceCode,
8+
SUPPORTED_ASSET_IMAGE_EXTENSIONS,
9+
SvgSourceCode,
10+
} from './types';
11+
import { extname } from './utils';
1012

1113
export async function toCssSourceCode(uri: UriString, source: string): Promise<CssSourceCode> {
1214
return {
1315
type: 'css',
1416
uri,
1517
source,
16-
ast: new Error('CSS parsing not implemented yet'), // Placeholder for CSS parsing
18+
ast: new Error('File parsing not implemented yet'), // Placeholder for CSS parsing
19+
};
20+
}
21+
22+
export async function toSvgSourceCode(uri: UriString, source: string): Promise<SvgSourceCode> {
23+
return {
24+
type: 'svg',
25+
uri,
26+
source,
27+
ast: new Error('File parsing not implemented yet'), // Placeholder for SVG parsing
28+
};
29+
}
30+
31+
async function toImageSourceCode(uri: UriString, source: string): Promise<ImageSourceCode> {
32+
return {
33+
type: 'image',
34+
uri,
35+
source,
36+
ast: new Error('Image files are not parsed'),
1737
};
1838
}
1939

@@ -37,16 +57,19 @@ export function parseJs(source: string): Program | Error {
3757
}
3858
}
3959

40-
export async function toSourceCode(
41-
uri: UriString,
42-
source: string,
43-
): Promise<JSONSourceCode | LiquidSourceCode | JsSourceCode | CssSourceCode> {
44-
if (uri.endsWith('.json') || uri.endsWith('.liquid')) {
60+
export async function toSourceCode(uri: UriString, source: string): Promise<FileSourceCode> {
61+
const extension = extname(uri);
62+
63+
if (extension === 'json' || extension === 'liquid') {
4564
return tcToSourceCode(uri, source);
46-
} else if (uri.endsWith('.js')) {
65+
} else if (extension === 'js') {
4766
return toJsSourceCode(uri, source);
48-
} else if (uri.endsWith('.css')) {
67+
} else if (extension === 'css') {
68+
return toCssSourceCode(uri, source);
69+
} else if (extension === 'svg') {
4970
return toCssSourceCode(uri, source);
71+
} else if (SUPPORTED_ASSET_IMAGE_EXTENSIONS.includes(extension)) {
72+
return toImageSourceCode(uri, source);
5073
} else {
5174
throw new Error(`Unknown source code type for ${uri}`);
5275
}

packages/theme-graph/src/types.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ export interface IDependencies {
2222
getSectionSchema: NonNullable<ThemeCheckDependencies['getSectionSchema']>;
2323

2424
/** Optional perf improvement if you somehow have access to pre-computed source code info */
25-
getSourceCode?: (
26-
uri: UriString,
27-
) => Promise<JSONSourceCode | LiquidSourceCode | JsSourceCode | CssSourceCode>;
25+
getSourceCode?: (uri: UriString) => Promise<FileSourceCode>;
2826

2927
/** A way to link <custom-element> to its window.customElements.define statement */
3028
getWebComponentDefinitionReference: (
@@ -44,7 +42,21 @@ export interface ThemeGraph {
4442
modules: Record<UriString, ThemeModule>;
4543
}
4644

47-
export type ThemeModule = LiquidModule | JsonModule | JavaScriptModule | CssModule;
45+
export type ThemeModule =
46+
| LiquidModule
47+
| JsonModule
48+
| JavaScriptModule
49+
| CssModule
50+
| SvgModule
51+
| ImageModule;
52+
53+
export type FileSourceCode =
54+
| LiquidSourceCode
55+
| JSONSourceCode
56+
| JsSourceCode
57+
| CssSourceCode
58+
| SvgSourceCode
59+
| ImageSourceCode;
4860

4961
export interface SerializableGraph {
5062
rootUri: UriString;
@@ -75,6 +87,14 @@ export interface CssModule extends IThemeModule<ModuleType.Css> {
7587
kind: 'unused';
7688
}
7789

90+
export interface SvgModule extends IThemeModule<ModuleType.Svg> {
91+
kind: 'unused';
92+
}
93+
94+
export interface ImageModule extends IThemeModule<ModuleType.Image> {
95+
kind: 'unused';
96+
}
97+
7898
export interface IThemeModule<T extends ModuleType> {
7999
/** Used as a discriminant in the ThemeNode union */
80100
type: T;
@@ -109,6 +129,8 @@ export const enum ModuleType {
109129
JavaScript = 'JavaScript',
110130
Json = 'JSON',
111131
Css = 'CSS',
132+
Svg = 'SVG',
133+
Image = 'Image',
112134
}
113135

114136
export const enum JsonModuleKind {
@@ -136,13 +158,37 @@ export const enum LiquidModuleKind {
136158
Template = 'template',
137159
}
138160

161+
export const SUPPORTED_ASSET_IMAGE_EXTENSIONS = [
162+
'jpg',
163+
'jpeg',
164+
'png',
165+
'gif',
166+
'webp',
167+
'heic',
168+
'ico',
169+
];
170+
139171
export interface CssSourceCode {
140172
type: 'css';
141173
uri: UriString;
142174
source: string;
143175
ast: any | Error;
144176
}
145177

178+
export interface SvgSourceCode {
179+
type: 'svg';
180+
uri: UriString;
181+
source: string;
182+
ast: any | Error;
183+
}
184+
185+
export interface ImageSourceCode {
186+
type: 'image';
187+
uri: UriString;
188+
source: string;
189+
ast: any | Error;
190+
}
191+
146192
export interface JsSourceCode {
147193
type: 'javascript';
148194
uri: UriString;

0 commit comments

Comments
 (0)