@platformos/platformos-graph 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +83 -0
- package/bin/jsconfig.json +18 -0
- package/bin/platformos-graph +107 -0
- package/dist/getWebComponentMap.d.ts +10 -0
- package/dist/getWebComponentMap.js +66 -0
- package/dist/getWebComponentMap.js.map +1 -0
- package/dist/graph/augment.d.ts +2 -0
- package/dist/graph/augment.js +22 -0
- package/dist/graph/augment.js.map +1 -0
- package/dist/graph/build.d.ts +3 -0
- package/dist/graph/build.js +31 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/module.d.ts +10 -0
- package/dist/graph/module.js +181 -0
- package/dist/graph/module.js.map +1 -0
- package/dist/graph/serialize.d.ts +2 -0
- package/dist/graph/serialize.js +18 -0
- package/dist/graph/serialize.js.map +1 -0
- package/dist/graph/test-helpers.d.ts +33 -0
- package/dist/graph/test-helpers.js +49 -0
- package/dist/graph/test-helpers.js.map +1 -0
- package/dist/graph/traverse.d.ts +14 -0
- package/dist/graph/traverse.js +458 -0
- package/dist/graph/traverse.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/toSourceCode.d.ts +8 -0
- package/dist/toSourceCode.js +76 -0
- package/dist/toSourceCode.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.js +47 -0
- package/dist/utils/index.js.map +1 -0
- package/docs/graph.png +0 -0
- package/docs/how-it-works.md +89 -0
- package/fixtures/skeleton/app/views/partials/child.liquid +9 -0
- package/fixtures/skeleton/app/views/partials/parent.liquid +9 -0
- package/fixtures/skeleton/assets/theme.css +0 -0
- package/fixtures/skeleton/assets/theme.js +7 -0
- package/fixtures/skeleton/blocks/_private.liquid +1 -0
- package/fixtures/skeleton/blocks/_static.liquid +10 -0
- package/fixtures/skeleton/blocks/group.liquid +27 -0
- package/fixtures/skeleton/blocks/render-static.liquid +22 -0
- package/fixtures/skeleton/blocks/text.liquid +14 -0
- package/fixtures/skeleton/jsconfig.json +9 -0
- package/fixtures/skeleton/layout/theme.liquid +14 -0
- package/fixtures/skeleton/sections/custom-section.liquid +6 -0
- package/fixtures/skeleton/sections/header-group.json +36 -0
- package/fixtures/skeleton/sections/header.liquid +1 -0
- package/fixtures/skeleton/templates/index.json +20 -0
- package/package.json +41 -0
- package/src/getWebComponentMap.ts +81 -0
- package/src/graph/augment.ts +34 -0
- package/src/graph/build.spec.ts +248 -0
- package/src/graph/build.ts +45 -0
- package/src/graph/module.ts +212 -0
- package/src/graph/serialize.spec.ts +62 -0
- package/src/graph/serialize.ts +20 -0
- package/src/graph/test-helpers.ts +57 -0
- package/src/graph/traverse.ts +639 -0
- package/src/index.ts +5 -0
- package/src/toSourceCode.ts +80 -0
- package/src/types.ts +213 -0
- package/src/utils/index.ts +51 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { path as pathUtils, SourceCodeType } from '@platformos/platformos-check-common';
|
|
2
|
+
import { assert, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildThemeGraph } from '../index';
|
|
4
|
+
import { Dependencies, JsonModuleKind, LiquidModuleKind, ModuleType, ThemeGraph } from '../types';
|
|
5
|
+
import { getDependencies, skeleton } from './test-helpers';
|
|
6
|
+
|
|
7
|
+
describe('Module: index', () => {
|
|
8
|
+
const rootUri = skeleton;
|
|
9
|
+
const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
|
|
10
|
+
const loc = (part: string) => expect.objectContaining({ uri: p(part) });
|
|
11
|
+
let dependencies: Dependencies;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
dependencies = await getDependencies(rootUri);
|
|
15
|
+
}, 15000);
|
|
16
|
+
|
|
17
|
+
describe('Unit: buildThemeGraph', { timeout: 10000 }, () => {
|
|
18
|
+
it('build a graph of the theme', { timeout: 10000 }, async () => {
|
|
19
|
+
const graph = await buildThemeGraph(rootUri, dependencies);
|
|
20
|
+
expect(graph).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('with a valid theme graph', () => {
|
|
24
|
+
let graph: ThemeGraph;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
graph = await buildThemeGraph(rootUri, dependencies);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('have a root URI', () => {
|
|
31
|
+
expect(graph.rootUri).toBeDefined();
|
|
32
|
+
expect(graph.rootUri).toBe(rootUri);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// We're using sections as entry points because the section rendering API can render
|
|
36
|
+
// any section without it needing a preset or default value in its schema.
|
|
37
|
+
it('infers entry points from the templates folder and section files', () => {
|
|
38
|
+
expect(graph.entryPoints).toHaveLength(3);
|
|
39
|
+
expect(graph.entryPoints.map((x) => x.uri)).toEqual(
|
|
40
|
+
expect.arrayContaining([
|
|
41
|
+
p('templates/index.json'),
|
|
42
|
+
p('sections/custom-section.liquid'),
|
|
43
|
+
p('sections/header.liquid'),
|
|
44
|
+
]),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("finds layout/theme.liquid's dependencies and references", () => {
|
|
49
|
+
const themeLayout = graph.modules[p('layout/theme.liquid')];
|
|
50
|
+
assert(themeLayout);
|
|
51
|
+
|
|
52
|
+
// outgoing links
|
|
53
|
+
const deps = themeLayout.dependencies;
|
|
54
|
+
assert(deps.map((x) => x.source.uri).every((x) => x === p('layout/theme.liquid')));
|
|
55
|
+
expect(deps.map((x) => x.target.uri)).toEqual(
|
|
56
|
+
expect.arrayContaining([
|
|
57
|
+
p('sections/header-group.json'),
|
|
58
|
+
p('assets/theme.js'),
|
|
59
|
+
p('assets/theme.css'),
|
|
60
|
+
]),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ingoing links
|
|
64
|
+
const refs = themeLayout.references;
|
|
65
|
+
expect(refs).toHaveLength(1);
|
|
66
|
+
assert(refs.map((x) => x.target.uri).every((x) => x === p('layout/theme.liquid')));
|
|
67
|
+
expect(refs.map((x) => x.source.uri)).toEqual(
|
|
68
|
+
expect.arrayContaining([p('templates/index.json')]),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("finds templates/index.json's dependencies and references", () => {
|
|
73
|
+
const indexTemplate = graph.modules[p('templates/index.json')];
|
|
74
|
+
assert(indexTemplate);
|
|
75
|
+
assert(indexTemplate.type === ModuleType.Json);
|
|
76
|
+
assert(indexTemplate.kind === JsonModuleKind.Template);
|
|
77
|
+
|
|
78
|
+
// outgoing links
|
|
79
|
+
const deps = indexTemplate.dependencies;
|
|
80
|
+
assert(deps.map((x) => x.source.uri).every((x) => x === p('templates/index.json')));
|
|
81
|
+
expect(deps.map((x) => x.target.uri)).toEqual(
|
|
82
|
+
expect.arrayContaining([
|
|
83
|
+
p('layout/theme.liquid'),
|
|
84
|
+
p('sections/custom-section.liquid'),
|
|
85
|
+
p('blocks/group.liquid'),
|
|
86
|
+
p('blocks/text.liquid'),
|
|
87
|
+
]),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// ingoing links
|
|
91
|
+
const refs = indexTemplate.references;
|
|
92
|
+
expect(refs).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("finds sections/custom-section's dependencies and references", () => {
|
|
96
|
+
const customSection = graph.modules[p('sections/custom-section.liquid')];
|
|
97
|
+
assert(customSection);
|
|
98
|
+
assert(customSection.type === ModuleType.Liquid);
|
|
99
|
+
assert(customSection.kind === LiquidModuleKind.Section);
|
|
100
|
+
|
|
101
|
+
// outgoing links
|
|
102
|
+
const deps = customSection.dependencies;
|
|
103
|
+
assert(deps.map((x) => x.source.uri).every((x) => x === customSection.uri));
|
|
104
|
+
expect(deps.map((x) => x.target.uri)).toEqual(
|
|
105
|
+
expect.arrayContaining([
|
|
106
|
+
p('blocks/group.liquid'),
|
|
107
|
+
p('blocks/text.liquid'),
|
|
108
|
+
p('blocks/_private.liquid'),
|
|
109
|
+
]),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ingoing links
|
|
113
|
+
const refs = customSection.references;
|
|
114
|
+
assert(refs.map((x) => x.target.uri).every((x) => x === customSection.uri));
|
|
115
|
+
expect(refs.map((x) => x.source.uri)).toEqual(
|
|
116
|
+
expect.arrayContaining([p('templates/index.json'), p('sections/header-group.json')]),
|
|
117
|
+
);
|
|
118
|
+
expect(refs).toHaveLength(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("finds blocks/group's dependencies and references", () => {
|
|
122
|
+
const groupBlock = graph.modules[p('blocks/group.liquid')];
|
|
123
|
+
assert(groupBlock);
|
|
124
|
+
assert(groupBlock.type === ModuleType.Liquid);
|
|
125
|
+
assert(groupBlock.kind === LiquidModuleKind.Block);
|
|
126
|
+
|
|
127
|
+
const deps = groupBlock.dependencies;
|
|
128
|
+
assert(deps.map((x) => x.source.uri).every((x) => x === groupBlock.uri));
|
|
129
|
+
expect(deps).toEqual(
|
|
130
|
+
expect.arrayContaining([
|
|
131
|
+
{
|
|
132
|
+
source: loc('blocks/group.liquid'),
|
|
133
|
+
target: loc('app/views/partials/parent.liquid'),
|
|
134
|
+
type: 'direct',
|
|
135
|
+
}, // direct dep in partial
|
|
136
|
+
{
|
|
137
|
+
source: loc('blocks/group.liquid'),
|
|
138
|
+
target: loc('blocks/text.liquid'),
|
|
139
|
+
type: 'indirect',
|
|
140
|
+
}, // indirect because of @theme
|
|
141
|
+
{
|
|
142
|
+
source: loc('blocks/group.liquid'),
|
|
143
|
+
target: loc('blocks/text.liquid'),
|
|
144
|
+
type: 'preset',
|
|
145
|
+
}, // direct dep in preset
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const refs = groupBlock.references;
|
|
150
|
+
assert(refs.map((x) => x.target.uri).every((x) => x === groupBlock.uri));
|
|
151
|
+
expect(refs.map((x) => x.source.uri)).toEqual(
|
|
152
|
+
expect.arrayContaining([
|
|
153
|
+
p('templates/index.json'),
|
|
154
|
+
p('sections/custom-section.liquid'), // @theme ref
|
|
155
|
+
p('sections/header-group.json'), // custom-section > group
|
|
156
|
+
p('blocks/group.liquid'), // @theme ref
|
|
157
|
+
]),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(refs).toContainEqual(
|
|
161
|
+
// Expecting the `@theme` reference in the custom-section schema to be indirect
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
type: 'indirect',
|
|
164
|
+
source: {
|
|
165
|
+
uri: p('sections/custom-section.liquid'),
|
|
166
|
+
range: [expect.any(Number), expect.any(Number)],
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("finds the app/views/partials/parent's dependencies and references", async () => {
|
|
173
|
+
const parentPartial = graph.modules[p('app/views/partials/parent.liquid')];
|
|
174
|
+
assert(parentPartial);
|
|
175
|
+
assert(parentPartial.type === ModuleType.Liquid);
|
|
176
|
+
assert(parentPartial.kind === LiquidModuleKind.Partial);
|
|
177
|
+
|
|
178
|
+
// outgoing links
|
|
179
|
+
const deps = parentPartial.dependencies;
|
|
180
|
+
assert(deps.map((x) => x.source.uri).every((x) => x === parentPartial.uri));
|
|
181
|
+
expect(deps.map((x) => x.target.uri)).toEqual(
|
|
182
|
+
expect.arrayContaining([
|
|
183
|
+
p('app/views/partials/child.liquid'), // {% render 'child' %}
|
|
184
|
+
p('assets/theme.js'), // <parent-element>
|
|
185
|
+
]),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ingoing links
|
|
189
|
+
const refs = parentPartial.references;
|
|
190
|
+
assert(refs.map((x) => x.target.uri).every((x) => x === parentPartial.uri));
|
|
191
|
+
expect(refs.map((x) => x.source.uri)).toEqual(
|
|
192
|
+
expect.arrayContaining([
|
|
193
|
+
p('blocks/group.liquid'), // {% render 'parent' %}
|
|
194
|
+
]),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// {% render 'child', children: children %} dependency
|
|
198
|
+
const parentSource = await dependencies.getSourceCode(
|
|
199
|
+
p('app/views/partials/parent.liquid'),
|
|
200
|
+
);
|
|
201
|
+
assert(parentSource);
|
|
202
|
+
assert(parentSource.type === SourceCodeType.LiquidHtml);
|
|
203
|
+
expect(parentPartial.dependencies.map((x) => x.source)).toContainEqual(
|
|
204
|
+
expect.objectContaining({
|
|
205
|
+
uri: p('app/views/partials/parent.liquid'),
|
|
206
|
+
range: [
|
|
207
|
+
parentSource.source.indexOf('{% render "child"'),
|
|
208
|
+
parentSource.source.indexOf('{% render "child"') +
|
|
209
|
+
'{% render "child", children: children %}'.length,
|
|
210
|
+
],
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// <parent-element> dependency
|
|
215
|
+
expect(parentPartial.dependencies.map((x) => x.source)).toContainEqual(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
uri: p('app/views/partials/parent.liquid'),
|
|
218
|
+
range: [
|
|
219
|
+
parentSource.source.indexOf('<parent-element'),
|
|
220
|
+
parentSource.source.indexOf('<parent-element') + '<parent-element'.length,
|
|
221
|
+
],
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("finds the blocks/_static's dependencies and references", () => {
|
|
227
|
+
const staticBlock = graph.modules[p('blocks/_static.liquid')];
|
|
228
|
+
assert(staticBlock);
|
|
229
|
+
assert(staticBlock.type === ModuleType.Liquid);
|
|
230
|
+
assert(staticBlock.kind === LiquidModuleKind.Block);
|
|
231
|
+
|
|
232
|
+
// outgoing links
|
|
233
|
+
const deps = staticBlock.dependencies;
|
|
234
|
+
expect(deps).toEqual([]);
|
|
235
|
+
|
|
236
|
+
// ingoing links
|
|
237
|
+
const refs = staticBlock.references;
|
|
238
|
+
assert(refs.map((x) => x.target.uri).every((x) => x === staticBlock.uri));
|
|
239
|
+
expect(refs.map((x) => x.source.uri)).toEqual(
|
|
240
|
+
expect.arrayContaining([
|
|
241
|
+
p('sections/header-group.json'),
|
|
242
|
+
p('blocks/render-static.liquid'),
|
|
243
|
+
]),
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recursiveReadDirectory as findAllFiles,
|
|
3
|
+
path,
|
|
4
|
+
UriString,
|
|
5
|
+
} from '@platformos/platformos-check-common';
|
|
6
|
+
import { IDependencies, ThemeGraph, ThemeModule } from '../types';
|
|
7
|
+
import { augmentDependencies } from './augment';
|
|
8
|
+
import { getModule } from './module';
|
|
9
|
+
import { traverseModule } from './traverse';
|
|
10
|
+
|
|
11
|
+
export async function buildThemeGraph(
|
|
12
|
+
rootUri: UriString,
|
|
13
|
+
ideps: IDependencies,
|
|
14
|
+
entryPoints?: UriString[],
|
|
15
|
+
): Promise<ThemeGraph> {
|
|
16
|
+
const deps = augmentDependencies(rootUri, ideps);
|
|
17
|
+
|
|
18
|
+
entryPoints =
|
|
19
|
+
entryPoints ??
|
|
20
|
+
(await findAllFiles(deps.fs, rootUri, ([uri]) => {
|
|
21
|
+
// Templates are entry points in the theme graph.
|
|
22
|
+
const isTemplateFile = uri.startsWith(path.join(rootUri, 'templates'));
|
|
23
|
+
|
|
24
|
+
// Since any section file can be rendered directly by the Section Rendering API,
|
|
25
|
+
// we consider all section files as entry points.
|
|
26
|
+
const isSectionFile =
|
|
27
|
+
uri.startsWith(path.join(rootUri, 'sections')) && uri.endsWith('.liquid');
|
|
28
|
+
|
|
29
|
+
return isTemplateFile || isSectionFile;
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const graph: ThemeGraph = {
|
|
33
|
+
entryPoints: [],
|
|
34
|
+
modules: {},
|
|
35
|
+
rootUri,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
graph.entryPoints = entryPoints
|
|
39
|
+
.map((uri) => getModule(graph, uri))
|
|
40
|
+
.filter((x): x is ThemeModule => x !== undefined);
|
|
41
|
+
|
|
42
|
+
await Promise.all(graph.entryPoints.map((entry) => traverseModule(entry, graph, deps)));
|
|
43
|
+
|
|
44
|
+
return graph;
|
|
45
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { path, UriString } from '@platformos/platformos-check-common';
|
|
2
|
+
import {
|
|
3
|
+
CssModule,
|
|
4
|
+
ImageModule,
|
|
5
|
+
JavaScriptModule,
|
|
6
|
+
JsonModule,
|
|
7
|
+
JsonModuleKind,
|
|
8
|
+
LiquidModule,
|
|
9
|
+
LiquidModuleKind,
|
|
10
|
+
ModuleType,
|
|
11
|
+
SUPPORTED_ASSET_IMAGE_EXTENSIONS,
|
|
12
|
+
SvgModule,
|
|
13
|
+
ThemeGraph,
|
|
14
|
+
ThemeModule,
|
|
15
|
+
} from '../types';
|
|
16
|
+
import { extname } from '../utils';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* We're using a ModuleCache to prevent race conditions with traverse.
|
|
20
|
+
*
|
|
21
|
+
* e.g. if we have two modules that depend on the same 'assets/foo.js' file and
|
|
22
|
+
* that they somehow depend on it before it gets traversed (and thus added to the
|
|
23
|
+
* graphs' modules record), we want to avoid creating two different module objects
|
|
24
|
+
* that represent the same file.
|
|
25
|
+
*
|
|
26
|
+
* We're using a WeakMap<ThemeGraph> to cache modules so that if the theme graph
|
|
27
|
+
* gets garbage collected, the module cache will also be garbage collected.
|
|
28
|
+
*
|
|
29
|
+
* This allows us to have a module cache without changing the API of the
|
|
30
|
+
* ThemeGraph (no need for a `visited` property on modules, etc.)
|
|
31
|
+
*/
|
|
32
|
+
const ModuleCache: WeakMap<ThemeGraph, Map<string, ThemeModule>> = new WeakMap();
|
|
33
|
+
|
|
34
|
+
export function getModule(themeGraph: ThemeGraph, uri: UriString): ThemeModule | undefined {
|
|
35
|
+
const cache = getCache(themeGraph);
|
|
36
|
+
if (cache.has(uri)) {
|
|
37
|
+
return cache.get(uri)!;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const relativePath = path.relative(uri, themeGraph.rootUri);
|
|
41
|
+
|
|
42
|
+
switch (true) {
|
|
43
|
+
case relativePath.startsWith('assets'): {
|
|
44
|
+
return getAssetModule(themeGraph, path.basename(uri));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case relativePath.startsWith('blocks'): {
|
|
48
|
+
return getThemeBlockModule(themeGraph, path.basename(uri, '.liquid'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case relativePath.startsWith('layout'): {
|
|
52
|
+
return getLayoutModule(themeGraph, path.basename(uri, '.liquid'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case relativePath.startsWith('sections'): {
|
|
56
|
+
if (relativePath.endsWith('.json')) {
|
|
57
|
+
return getSectionGroupModule(themeGraph, path.basename(uri, '.json'));
|
|
58
|
+
}
|
|
59
|
+
return getSectionModule(themeGraph, path.basename(uri, '.liquid'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case relativePath.includes('/views/partials') || relativePath.includes('/lib/'): {
|
|
63
|
+
return getPartialModule(themeGraph, path.basename(uri, '.liquid'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case relativePath.startsWith('snippets'): {
|
|
67
|
+
return getPartialModule(themeGraph, path.basename(uri, '.liquid'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case relativePath.startsWith('templates'): {
|
|
71
|
+
return getTemplateModule(themeGraph, uri);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getTemplateModule(themeGraph: ThemeGraph, uri: UriString): ThemeModule {
|
|
77
|
+
const extension = extname(uri);
|
|
78
|
+
switch (extension) {
|
|
79
|
+
case 'json': {
|
|
80
|
+
return module(themeGraph, {
|
|
81
|
+
type: ModuleType.Json,
|
|
82
|
+
kind: JsonModuleKind.Template,
|
|
83
|
+
dependencies: [],
|
|
84
|
+
references: [],
|
|
85
|
+
uri: uri,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'liquid': {
|
|
90
|
+
return module(themeGraph, {
|
|
91
|
+
type: ModuleType.Liquid,
|
|
92
|
+
kind: LiquidModuleKind.Template,
|
|
93
|
+
dependencies: [],
|
|
94
|
+
references: [],
|
|
95
|
+
uri: uri,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
default: {
|
|
100
|
+
throw new Error(`Unknown template type for ${uri}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getThemeBlockModule(themeGraph: ThemeGraph, blockType: string): LiquidModule {
|
|
106
|
+
const uri = path.join(themeGraph.rootUri, 'blocks', `${blockType}.liquid`);
|
|
107
|
+
return module(themeGraph, {
|
|
108
|
+
type: ModuleType.Liquid,
|
|
109
|
+
kind: LiquidModuleKind.Block,
|
|
110
|
+
dependencies: [],
|
|
111
|
+
references: [],
|
|
112
|
+
uri,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getSectionModule(themeGraph: ThemeGraph, sectionType: string): LiquidModule {
|
|
117
|
+
const uri = path.join(themeGraph.rootUri, 'sections', `${sectionType}.liquid`);
|
|
118
|
+
return module(themeGraph, {
|
|
119
|
+
type: ModuleType.Liquid,
|
|
120
|
+
kind: LiquidModuleKind.Section,
|
|
121
|
+
dependencies: [],
|
|
122
|
+
references: [],
|
|
123
|
+
uri,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getSectionGroupModule(
|
|
128
|
+
themeGraph: ThemeGraph,
|
|
129
|
+
sectionGroupType: string,
|
|
130
|
+
): JsonModule {
|
|
131
|
+
const uri = path.join(themeGraph.rootUri, 'sections', `${sectionGroupType}.json`);
|
|
132
|
+
return module(themeGraph, {
|
|
133
|
+
type: ModuleType.Json,
|
|
134
|
+
kind: JsonModuleKind.SectionGroup,
|
|
135
|
+
dependencies: [],
|
|
136
|
+
references: [],
|
|
137
|
+
uri,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getAssetModule(
|
|
142
|
+
themeGraph: ThemeGraph,
|
|
143
|
+
asset: string,
|
|
144
|
+
): JavaScriptModule | CssModule | SvgModule | ImageModule | undefined {
|
|
145
|
+
const extension = extname(asset);
|
|
146
|
+
|
|
147
|
+
let type: ModuleType | undefined = undefined;
|
|
148
|
+
|
|
149
|
+
if (SUPPORTED_ASSET_IMAGE_EXTENSIONS.includes(extension)) {
|
|
150
|
+
type = ModuleType.Image;
|
|
151
|
+
} else if (extension === 'js') {
|
|
152
|
+
type = ModuleType.JavaScript;
|
|
153
|
+
} else if (extension === 'css') {
|
|
154
|
+
type = ModuleType.Css;
|
|
155
|
+
} else if (extension === 'svg') {
|
|
156
|
+
type = ModuleType.Svg;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!type) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return module(themeGraph, {
|
|
164
|
+
type,
|
|
165
|
+
kind: 'unused',
|
|
166
|
+
dependencies: [],
|
|
167
|
+
references: [],
|
|
168
|
+
uri: path.join(themeGraph.rootUri, 'assets', asset),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getPartialModule(themeGraph: ThemeGraph, partial: string): LiquidModule {
|
|
173
|
+
const uri = path.join(themeGraph.rootUri, 'app/views/partials', `${partial}.liquid`);
|
|
174
|
+
return module(themeGraph, {
|
|
175
|
+
type: ModuleType.Liquid,
|
|
176
|
+
kind: LiquidModuleKind.Partial,
|
|
177
|
+
uri: uri,
|
|
178
|
+
dependencies: [],
|
|
179
|
+
references: [],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getLayoutModule(
|
|
184
|
+
themeGraph: ThemeGraph,
|
|
185
|
+
layoutName: string | false | undefined = 'theme',
|
|
186
|
+
): LiquidModule | undefined {
|
|
187
|
+
if (layoutName === false) return undefined;
|
|
188
|
+
if (layoutName === undefined) layoutName = 'theme';
|
|
189
|
+
const uri = path.join(themeGraph.rootUri, 'layout', `${layoutName}.liquid`);
|
|
190
|
+
return module(themeGraph, {
|
|
191
|
+
type: ModuleType.Liquid,
|
|
192
|
+
kind: LiquidModuleKind.Layout,
|
|
193
|
+
uri: uri,
|
|
194
|
+
dependencies: [],
|
|
195
|
+
references: [],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getCache(themeGraph: ThemeGraph): Map<string, ThemeModule> {
|
|
200
|
+
if (!ModuleCache.has(themeGraph)) {
|
|
201
|
+
ModuleCache.set(themeGraph, new Map());
|
|
202
|
+
}
|
|
203
|
+
return ModuleCache.get(themeGraph)!;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function module<T extends ThemeModule>(themeGraph: ThemeGraph, mod: T): T {
|
|
207
|
+
const cache = getCache(themeGraph);
|
|
208
|
+
if (!cache.has(mod.uri)) {
|
|
209
|
+
cache.set(mod.uri, mod);
|
|
210
|
+
}
|
|
211
|
+
return cache.get(mod.uri)! as T;
|
|
212
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { path as pathUtils } from '@platformos/platformos-check-common';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ThemeGraph } from '../types';
|
|
4
|
+
import { getSectionModule, getPartialModule, getTemplateModule } from './module';
|
|
5
|
+
import { serializeThemeGraph } from './serialize';
|
|
6
|
+
import { bind } from './traverse';
|
|
7
|
+
|
|
8
|
+
describe('Unit: serializeThemeGraph', () => {
|
|
9
|
+
it('serialize the graph', () => {
|
|
10
|
+
const rootUri = 'file:///theme';
|
|
11
|
+
const p = (part: string) => pathUtils.join(rootUri, part);
|
|
12
|
+
const graph: ThemeGraph = {
|
|
13
|
+
entryPoints: [],
|
|
14
|
+
modules: {},
|
|
15
|
+
rootUri,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const template = getTemplateModule(graph, p('templates/index.json'));
|
|
19
|
+
const customSection = getSectionModule(graph, 'custom-section');
|
|
20
|
+
const parentPartial = getPartialModule(graph, 'parent');
|
|
21
|
+
const childPartial = getPartialModule(graph, 'child');
|
|
22
|
+
bind(template, customSection, { sourceRange: [0, 5] });
|
|
23
|
+
bind(customSection, parentPartial, { sourceRange: [10, 15] });
|
|
24
|
+
bind(parentPartial, childPartial, { sourceRange: [20, 25] });
|
|
25
|
+
|
|
26
|
+
const section2 = getSectionModule(graph, 'section2');
|
|
27
|
+
bind(template, section2, { sourceRange: [20, 25] });
|
|
28
|
+
|
|
29
|
+
graph.entryPoints = [template];
|
|
30
|
+
[template, customSection, section2, parentPartial, childPartial].forEach((module) => {
|
|
31
|
+
graph.modules[module.uri] = module;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { nodes, edges } = serializeThemeGraph(graph);
|
|
35
|
+
expect(nodes).toHaveLength(5);
|
|
36
|
+
expect(edges).toHaveLength(4);
|
|
37
|
+
expect(edges).toEqual(
|
|
38
|
+
expect.arrayContaining([
|
|
39
|
+
{
|
|
40
|
+
source: { uri: 'file:///theme/templates/index.json', range: [0, 5] },
|
|
41
|
+
target: { uri: 'file:///theme/sections/custom-section.liquid' },
|
|
42
|
+
type: 'direct',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
source: { uri: 'file:///theme/sections/custom-section.liquid', range: [10, 15] },
|
|
46
|
+
target: { uri: 'file:///theme/app/views/partials/parent.liquid' },
|
|
47
|
+
type: 'direct',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
source: { uri: 'file:///theme/app/views/partials/parent.liquid', range: [20, 25] },
|
|
51
|
+
target: { uri: 'file:///theme/app/views/partials/child.liquid' },
|
|
52
|
+
type: 'direct',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
source: { uri: 'file:///theme/templates/index.json', range: [20, 25] },
|
|
56
|
+
target: { uri: 'file:///theme/sections/section2.liquid' },
|
|
57
|
+
type: 'direct',
|
|
58
|
+
},
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SerializableEdge, SerializableGraph, SerializableNode, ThemeGraph } from '../types';
|
|
2
|
+
|
|
3
|
+
export function serializeThemeGraph(graph: ThemeGraph): SerializableGraph {
|
|
4
|
+
const nodes: SerializableNode[] = Object.values(graph.modules).map((module) => ({
|
|
5
|
+
uri: module.uri,
|
|
6
|
+
type: module.type,
|
|
7
|
+
kind: module.kind,
|
|
8
|
+
...('exists' in module ? { exists: module.exists } : {}),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const edges: SerializableEdge[] = Object.values(graph.modules).flatMap(
|
|
12
|
+
(module) => module.dependencies,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
rootUri: graph.rootUri,
|
|
17
|
+
nodes,
|
|
18
|
+
edges,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LiquidSourceCode,
|
|
3
|
+
memoize,
|
|
4
|
+
path as pathUtils,
|
|
5
|
+
SectionSchema,
|
|
6
|
+
ThemeBlockSchema,
|
|
7
|
+
toSchema,
|
|
8
|
+
} from '@platformos/platformos-check-common';
|
|
9
|
+
import { AbstractFileSystem } from '@platformos/platformos-common';
|
|
10
|
+
import { NodeFileSystem } from '@platformos/platformos-check-node';
|
|
11
|
+
import { vi } from 'vitest';
|
|
12
|
+
import { URI } from 'vscode-uri';
|
|
13
|
+
import { getWebComponentMap } from '../getWebComponentMap';
|
|
14
|
+
import { toSourceCode } from '../toSourceCode';
|
|
15
|
+
import { identity } from '../utils';
|
|
16
|
+
|
|
17
|
+
export function makeGetSourceCode(fs: AbstractFileSystem) {
|
|
18
|
+
return memoize(async function getSourceCode(uri: string) {
|
|
19
|
+
const source = await fs.readFile(uri);
|
|
20
|
+
return toSourceCode(URI.file(uri).toString(), source);
|
|
21
|
+
}, identity);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const fixturesRoot = pathUtils.join(URI.file(__dirname), ...'../../fixtures'.split('/'));
|
|
25
|
+
export const skeleton = pathUtils.join(fixturesRoot, 'skeleton');
|
|
26
|
+
|
|
27
|
+
export async function getDependencies(rootUri: string, fs: AbstractFileSystem = NodeFileSystem) {
|
|
28
|
+
const getSourceCode = makeGetSourceCode(fs);
|
|
29
|
+
const deps = {
|
|
30
|
+
fs,
|
|
31
|
+
getSectionSchema: memoize(async (name: string) => {
|
|
32
|
+
const uri = pathUtils.join(skeleton, 'sections', `${name}.liquid`);
|
|
33
|
+
const sourceCode = (await getSourceCode(uri)) as LiquidSourceCode;
|
|
34
|
+
return (await toSchema('theme', uri, sourceCode, async () => true)) as SectionSchema;
|
|
35
|
+
}, identity),
|
|
36
|
+
getBlockSchema: memoize(async (name: string) => {
|
|
37
|
+
const uri = pathUtils.join(skeleton, 'blocks', `${name}.liquid`);
|
|
38
|
+
const sourceCode = (await getSourceCode(uri)) as LiquidSourceCode;
|
|
39
|
+
return (await toSchema('theme', uri, sourceCode, async () => true)) as ThemeBlockSchema;
|
|
40
|
+
}, identity),
|
|
41
|
+
getSourceCode,
|
|
42
|
+
getWebComponentDefinitionReference: (customElementName: string) =>
|
|
43
|
+
webComponentDefs.get(customElementName),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const webComponentDefs = await getWebComponentMap(rootUri, deps);
|
|
47
|
+
|
|
48
|
+
return deps;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// This thing is way too hard to type.
|
|
52
|
+
export function mockImpl(obj: any, method: any, callback: any) {
|
|
53
|
+
const original = obj[method].bind(obj);
|
|
54
|
+
return vi.spyOn(obj, method).mockImplementation(function () {
|
|
55
|
+
return callback(original, ...arguments);
|
|
56
|
+
});
|
|
57
|
+
}
|