@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +83 -0
  3. package/bin/jsconfig.json +18 -0
  4. package/bin/platformos-graph +107 -0
  5. package/dist/getWebComponentMap.d.ts +10 -0
  6. package/dist/getWebComponentMap.js +66 -0
  7. package/dist/getWebComponentMap.js.map +1 -0
  8. package/dist/graph/augment.d.ts +2 -0
  9. package/dist/graph/augment.js +22 -0
  10. package/dist/graph/augment.js.map +1 -0
  11. package/dist/graph/build.d.ts +3 -0
  12. package/dist/graph/build.js +31 -0
  13. package/dist/graph/build.js.map +1 -0
  14. package/dist/graph/module.d.ts +10 -0
  15. package/dist/graph/module.js +181 -0
  16. package/dist/graph/module.js.map +1 -0
  17. package/dist/graph/serialize.d.ts +2 -0
  18. package/dist/graph/serialize.js +18 -0
  19. package/dist/graph/serialize.js.map +1 -0
  20. package/dist/graph/test-helpers.d.ts +33 -0
  21. package/dist/graph/test-helpers.js +49 -0
  22. package/dist/graph/test-helpers.js.map +1 -0
  23. package/dist/graph/traverse.d.ts +14 -0
  24. package/dist/graph/traverse.js +458 -0
  25. package/dist/graph/traverse.js.map +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.js +31 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/toSourceCode.d.ts +8 -0
  30. package/dist/toSourceCode.js +76 -0
  31. package/dist/toSourceCode.js.map +1 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -0
  33. package/dist/types.d.ts +144 -0
  34. package/dist/types.js +13 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/utils/index.d.ts +11 -0
  37. package/dist/utils/index.js +47 -0
  38. package/dist/utils/index.js.map +1 -0
  39. package/docs/graph.png +0 -0
  40. package/docs/how-it-works.md +89 -0
  41. package/fixtures/skeleton/app/views/partials/child.liquid +9 -0
  42. package/fixtures/skeleton/app/views/partials/parent.liquid +9 -0
  43. package/fixtures/skeleton/assets/theme.css +0 -0
  44. package/fixtures/skeleton/assets/theme.js +7 -0
  45. package/fixtures/skeleton/blocks/_private.liquid +1 -0
  46. package/fixtures/skeleton/blocks/_static.liquid +10 -0
  47. package/fixtures/skeleton/blocks/group.liquid +27 -0
  48. package/fixtures/skeleton/blocks/render-static.liquid +22 -0
  49. package/fixtures/skeleton/blocks/text.liquid +14 -0
  50. package/fixtures/skeleton/jsconfig.json +9 -0
  51. package/fixtures/skeleton/layout/theme.liquid +14 -0
  52. package/fixtures/skeleton/sections/custom-section.liquid +6 -0
  53. package/fixtures/skeleton/sections/header-group.json +36 -0
  54. package/fixtures/skeleton/sections/header.liquid +1 -0
  55. package/fixtures/skeleton/templates/index.json +20 -0
  56. package/package.json +41 -0
  57. package/src/getWebComponentMap.ts +81 -0
  58. package/src/graph/augment.ts +34 -0
  59. package/src/graph/build.spec.ts +248 -0
  60. package/src/graph/build.ts +45 -0
  61. package/src/graph/module.ts +212 -0
  62. package/src/graph/serialize.spec.ts +62 -0
  63. package/src/graph/serialize.ts +20 -0
  64. package/src/graph/test-helpers.ts +57 -0
  65. package/src/graph/traverse.ts +639 -0
  66. package/src/index.ts +5 -0
  67. package/src/toSourceCode.ts +80 -0
  68. package/src/types.ts +213 -0
  69. package/src/utils/index.ts +51 -0
  70. package/tsconfig.build.json +20 -0
  71. 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
+ }