@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,639 @@
1
+ import { NamedTags, NodeTypes } from '@platformos/liquid-html-parser';
2
+ import {
3
+ JSONNode,
4
+ nodeAtPath,
5
+ ObjectNode,
6
+ parseJSON,
7
+ path,
8
+ Preset,
9
+ Section,
10
+ SectionSchema,
11
+ SourceCodeType,
12
+ Template,
13
+ ThemeBlock,
14
+ ThemeBlockSchema,
15
+ visit,
16
+ Visitor,
17
+ } from '@platformos/platformos-check-common';
18
+ import {
19
+ AugmentedDependencies,
20
+ JsonModule,
21
+ JsonModuleKind,
22
+ LiquidModule,
23
+ LiquidModuleKind,
24
+ ModuleType,
25
+ Range,
26
+ Reference,
27
+ ThemeGraph,
28
+ ThemeModule,
29
+ Void,
30
+ } from '../types';
31
+ import { acceptsLocalBlocks, assertNever, exists, isString, unexpected, unique } from '../utils';
32
+ import {
33
+ getAssetModule,
34
+ getLayoutModule,
35
+ getSectionGroupModule,
36
+ getSectionModule,
37
+ getPartialModule,
38
+ getThemeBlockModule,
39
+ } from './module';
40
+
41
+ export async function traverseModule(
42
+ module: ThemeModule,
43
+ themeGraph: ThemeGraph,
44
+ deps: AugmentedDependencies,
45
+ ): Promise<Void> {
46
+ // If the module is already traversed, skip it
47
+ if (themeGraph.modules[module.uri]) {
48
+ return;
49
+ }
50
+
51
+ // Signal to all users that the file is being traversed
52
+ // This will prevent multiple traversals of the same file
53
+ themeGraph.modules[module.uri] = module;
54
+
55
+ // Check if the module exists on disk
56
+ module.exists = await exists(deps.fs, module.uri);
57
+
58
+ // If the module doesn't exist, we can't traverse it
59
+ if (!module.exists) {
60
+ return;
61
+ }
62
+
63
+ switch (module.type) {
64
+ case ModuleType.Liquid: {
65
+ return traverseLiquidModule(module, themeGraph, deps);
66
+ }
67
+
68
+ case ModuleType.Json: {
69
+ return traverseJsonModule(module, themeGraph, deps);
70
+ }
71
+
72
+ case ModuleType.JavaScript: {
73
+ return; // TODO graph import/exports ?
74
+ }
75
+
76
+ case ModuleType.Css:
77
+ case ModuleType.Svg:
78
+ case ModuleType.Image: {
79
+ return; // Nothing to do??
80
+ }
81
+
82
+ default: {
83
+ return assertNever(module);
84
+ }
85
+ }
86
+ }
87
+
88
+ async function traverseLiquidModule(
89
+ module: LiquidModule,
90
+ themeGraph: ThemeGraph,
91
+ deps: AugmentedDependencies,
92
+ ) {
93
+ const sourceCode = await deps.getSourceCode(module.uri);
94
+
95
+ if (sourceCode.ast instanceof Error) return; // can't visit what you can't parse
96
+
97
+ const visitor: Visitor<
98
+ SourceCodeType.LiquidHtml,
99
+ { target: ThemeModule; sourceRange: Range; targetRange?: Range }
100
+ > = {
101
+ // {{ 'theme.js' | asset_url }}
102
+ // {{ 'image.png' | asset_img_url }}
103
+ // {{ 'icon.svg' | inline_asset_content }}
104
+ LiquidFilter: async (node, ancestors) => {
105
+ if (['asset_url', 'asset_img_url', 'inline_asset_content'].includes(node.name)) {
106
+ const parentNode = ancestors[ancestors.length - 1]!;
107
+ if (parentNode.type !== NodeTypes.LiquidVariable) return;
108
+ if (parentNode.expression.type !== NodeTypes.String) return;
109
+ if (parentNode.filters[0] !== node) return;
110
+ const asset = parentNode.expression.value;
111
+ const module = getAssetModule(themeGraph, asset);
112
+ if (!module) return;
113
+ return {
114
+ target: module,
115
+ sourceRange: [parentNode.position.start, parentNode.position.end],
116
+ };
117
+ }
118
+ },
119
+
120
+ // {% content_for 'block', type: 'staticBlockName', id: 'id' %}
121
+ ContentForMarkup: async (node, ancestors) => {
122
+ const parentNode = ancestors.at(-1)!;
123
+ if (node.contentForType.value !== 'block') return;
124
+ const blockTypeArg = node.args.find((arg) => arg.name === 'type');
125
+ if (!blockTypeArg) return;
126
+
127
+ const blockTypeValue = blockTypeArg.value;
128
+ if (blockTypeValue.type !== NodeTypes.String) return;
129
+
130
+ const blockType = blockTypeValue.value;
131
+
132
+ return {
133
+ target: getThemeBlockModule(themeGraph, blockType),
134
+ sourceRange: [parentNode.position.start, node.position.end],
135
+ };
136
+ },
137
+
138
+ // <custom-element></custom-element>
139
+ HtmlElement: async (node) => {
140
+ if (node.name.length !== 1) return;
141
+ if (node.name[0].type !== NodeTypes.TextNode) return;
142
+ const nodeNameNode = node.name[0];
143
+ const nodeName = nodeNameNode.value;
144
+ if (!nodeName.includes('-')) return; // skip non-custom-elements
145
+
146
+ const result = deps.getWebComponentDefinitionReference(nodeName);
147
+ if (!result) return;
148
+ const { assetName, range } = result;
149
+ const module = getAssetModule(themeGraph, assetName);
150
+ if (!module) return;
151
+
152
+ return {
153
+ target: module,
154
+ sourceRange: [node.blockStartPosition.start, nodeNameNode.position.end],
155
+ targetRange: range,
156
+ };
157
+ },
158
+
159
+ // {% render 'partial' %}
160
+ RenderMarkup: async (node, ancestors) => {
161
+ const snippet = node.snippet;
162
+ const tag = ancestors.at(-1)!;
163
+ if (!isString(snippet) && snippet.type === NodeTypes.String) {
164
+ return {
165
+ target: getPartialModule(themeGraph, snippet.value),
166
+ sourceRange: [tag.position.start, tag.position.end],
167
+ };
168
+ }
169
+ },
170
+
171
+ LiquidTag: async (node) => {
172
+ switch (node.name) {
173
+ // {% sections 'section-group' %}
174
+ case NamedTags.sections: {
175
+ if (!isString(node.markup)) {
176
+ const sectionGroupType = node.markup.value;
177
+ return {
178
+ target: getSectionGroupModule(themeGraph, sectionGroupType),
179
+ sourceRange: [node.position.start, node.position.end],
180
+ };
181
+ }
182
+ }
183
+
184
+ // {% section 'section' %}
185
+ case NamedTags.section: {
186
+ if (!isString(node.markup)) {
187
+ const sectionType = node.markup.value;
188
+ return {
189
+ target: getSectionModule(themeGraph, sectionType),
190
+ sourceRange: [node.position.start, node.position.end],
191
+ };
192
+ }
193
+ }
194
+ }
195
+ },
196
+ };
197
+
198
+ const references = await visit(sourceCode.ast, visitor);
199
+
200
+ for (const reference of references) {
201
+ bind(module, reference.target, {
202
+ sourceRange: reference.sourceRange,
203
+ targetRange: reference.targetRange,
204
+ });
205
+ }
206
+
207
+ const modules = unique(references.map((ref) => ref.target));
208
+ const promises = modules.map((mod) => traverseModule(mod, themeGraph, deps));
209
+
210
+ // Look at schema references if any
211
+ if (module.kind === LiquidModuleKind.Section) {
212
+ const sectionName = path.basename(module.uri, '.liquid');
213
+ const sectionSchema = await deps.getSectionSchema(sectionName);
214
+ promises.push(traverseLiquidSchema(sectionSchema, module, themeGraph, deps));
215
+ } else if (module.kind === LiquidModuleKind.Block) {
216
+ const blockName = path.basename(module.uri, '.liquid');
217
+ const blockSchema = await deps.getBlockSchema(blockName);
218
+ promises.push(traverseLiquidSchema(blockSchema, module, themeGraph, deps));
219
+ }
220
+
221
+ return Promise.all(promises);
222
+ }
223
+
224
+ async function traverseLiquidSchema(
225
+ schema: SectionSchema | ThemeBlockSchema | undefined,
226
+ module: LiquidModule,
227
+ themeGraph: ThemeGraph,
228
+ deps: AugmentedDependencies,
229
+ ): Promise<Void> {
230
+ if (!schema) return;
231
+
232
+ const isSection = module.kind === LiquidModuleKind.Section;
233
+ const hasLocalBlocks =
234
+ isSection && (await acceptsLocalBlocks(path.basename(module.uri, '.liquid'), deps));
235
+ if (hasLocalBlocks) return;
236
+
237
+ const { ast, validSchema } = schema;
238
+ if (validSchema instanceof Error || ast instanceof Error) return;
239
+
240
+ const promises: Promise<Void>[] = [];
241
+
242
+ // Traverse the blocks
243
+ if (validSchema.blocks) {
244
+ promises.push(traverseSchemaBlocks(schema, module, ast, validSchema.blocks, themeGraph, deps));
245
+ }
246
+
247
+ // Traverse the presets
248
+ if (validSchema.presets) {
249
+ promises.push(
250
+ traverseSchemaPresets(schema, module, ast, validSchema.presets, themeGraph, deps),
251
+ );
252
+ }
253
+
254
+ // Traverse section.default if it exists
255
+ if ('default' in validSchema && validSchema.default) {
256
+ promises.push(
257
+ traverseSchemaDefault(
258
+ schema as SectionSchema,
259
+ module,
260
+ ast,
261
+ validSchema.default,
262
+ themeGraph,
263
+ deps,
264
+ ),
265
+ );
266
+ }
267
+
268
+ return Promise.all(promises);
269
+ }
270
+
271
+ async function traverseSchemaBlocks(
272
+ schema: SectionSchema | ThemeBlockSchema,
273
+ module: LiquidModule,
274
+ ast: JSONNode,
275
+ blocks: Section.Block[] | ThemeBlock.Block[],
276
+ themeGraph: ThemeGraph,
277
+ deps: AugmentedDependencies,
278
+ ) {
279
+ const promises: Promise<Void>[] = [];
280
+
281
+ for (const [i, blockDef] of Object.entries(blocks)) {
282
+ const nodePath = ['blocks', i];
283
+ const node = nodeAtPath(ast, nodePath)! as ObjectNode;
284
+ const typeProperty = node.children.find((child) => child.key.value === 'type');
285
+ if (!typeProperty) continue;
286
+
287
+ const sourceRange: Range = [
288
+ schema.offset + typeProperty.loc.start.offset,
289
+ schema.offset + typeProperty.loc.end.offset,
290
+ ];
291
+
292
+ // blocks: [{ "type": "@theme" }, { "type": "custom-block" }]
293
+ switch (blockDef.type) {
294
+ case '@theme': {
295
+ const publicBlocks = await deps
296
+ .getThemeBlockNames()
297
+ .then((blocks) => blocks.filter((name) => !name.startsWith('_')));
298
+ for (const publicBlock of publicBlocks) {
299
+ const blockModule = getThemeBlockModule(
300
+ themeGraph,
301
+ path.basename(publicBlock, '.liquid'),
302
+ );
303
+ bind(module, blockModule, { sourceRange, type: 'indirect' });
304
+ promises.push(traverseModule(blockModule, themeGraph, deps));
305
+ }
306
+
307
+ break;
308
+ }
309
+
310
+ case '@app': {
311
+ break;
312
+ }
313
+
314
+ default: {
315
+ const blockModule = getThemeBlockModule(themeGraph, blockDef.type);
316
+ bind(module, blockModule, { sourceRange });
317
+ promises.push(traverseModule(blockModule, themeGraph, deps));
318
+ }
319
+ }
320
+ }
321
+
322
+ return Promise.all(promises);
323
+ }
324
+
325
+ async function traverseSchemaPresets(
326
+ schema: SectionSchema | ThemeBlockSchema,
327
+ module: LiquidModule,
328
+ ast: JSONNode,
329
+ presets: Preset.Preset[],
330
+ themeGraph: ThemeGraph,
331
+ deps: AugmentedDependencies,
332
+ ) {
333
+ const promises: Promise<Void>[] = [];
334
+
335
+ for (const [i, preset] of presets.entries()) {
336
+ if (!('blocks' in preset)) continue;
337
+
338
+ // Iterate over array entries or object entries depending on how the blocks are defined
339
+ const iterator = Array.isArray(preset.blocks)
340
+ ? preset.blocks.entries()
341
+ : Object.entries(preset.blocks!);
342
+
343
+ for (const [keyOrIndex, block] of iterator) {
344
+ const nodePath = ['presets', i, 'blocks', keyOrIndex];
345
+ const node = nodeAtPath(ast, nodePath)! as ObjectNode;
346
+
347
+ const blockModule = getThemeBlockModule(themeGraph, block.type);
348
+ if (!blockModule) continue;
349
+
350
+ const typeProperty = node.children.find((child) => child.key.value === 'type');
351
+ if (!typeProperty) continue;
352
+
353
+ const sourceRange: Range = [
354
+ schema.offset + typeProperty.loc.start.offset,
355
+ schema.offset + typeProperty.loc.end.offset,
356
+ ];
357
+
358
+ bind(module, blockModule, { sourceRange, type: 'preset' });
359
+ promises.push(traverseModule(blockModule, themeGraph, deps));
360
+ if (block.blocks) {
361
+ promises.push(
362
+ traverseSchemaPresetBlock(schema, module, ast, block.blocks, nodePath, themeGraph, deps),
363
+ );
364
+ }
365
+ }
366
+ }
367
+
368
+ return Promise.all(promises);
369
+ }
370
+
371
+ async function traverseSchemaPresetBlock(
372
+ schema: SectionSchema | ThemeBlockSchema,
373
+ module: LiquidModule,
374
+ ast: JSONNode,
375
+ blocks: Preset.PresetBlockHash | Preset.PresetBlockForArray[],
376
+ parentPath: (string | number)[],
377
+ themeGraph: ThemeGraph,
378
+ deps: AugmentedDependencies,
379
+ ) {
380
+ const promises: Promise<Void>[] = [];
381
+
382
+ // Iterate over array entries or object entries depending on how the blocks are defined
383
+ const iterator = Array.isArray(blocks) ? blocks.entries() : Object.entries(blocks);
384
+
385
+ for (const [keyOrIndex, block] of iterator) {
386
+ const nodePath = [...parentPath, 'blocks', keyOrIndex];
387
+ const node = nodeAtPath(ast, nodePath)! as ObjectNode;
388
+
389
+ const blockModule = getThemeBlockModule(themeGraph, block.type);
390
+ if (!blockModule) continue;
391
+
392
+ const typeProperty = node.children.find((child) => child.key.value === 'type');
393
+ if (!typeProperty) continue;
394
+
395
+ const sourceRange: Range = [
396
+ schema.offset + typeProperty.loc.start.offset,
397
+ schema.offset + typeProperty.loc.end.offset,
398
+ ];
399
+
400
+ bind(module, blockModule, { sourceRange, type: 'preset' });
401
+ promises.push(traverseModule(blockModule, themeGraph, deps));
402
+ if (block.blocks) {
403
+ promises.push(
404
+ traverseSchemaPresetBlock(schema, module, ast, block.blocks, nodePath, themeGraph, deps),
405
+ );
406
+ }
407
+ }
408
+
409
+ return Promise.all(promises);
410
+ }
411
+
412
+ async function traverseSchemaDefault(
413
+ schema: SectionSchema,
414
+ module: LiquidModule,
415
+ ast: JSONNode,
416
+ preset: Section.Default,
417
+ themeGraph: ThemeGraph,
418
+ deps: AugmentedDependencies,
419
+ ) {
420
+ const promises: Promise<Void>[] = [];
421
+
422
+ if (!('blocks' in preset)) return;
423
+
424
+ // Iterate over array entries or object entries depending on how the blocks are defined
425
+ const iterator = Array.isArray(preset.blocks)
426
+ ? preset.blocks.entries()
427
+ : Object.entries(preset.blocks!);
428
+
429
+ for (const [keyOrIndex, block] of iterator) {
430
+ const nodePath = ['default', 'blocks', keyOrIndex];
431
+ const node = nodeAtPath(ast, nodePath)! as ObjectNode;
432
+
433
+ const blockModule = getThemeBlockModule(themeGraph, block.type);
434
+ if (!blockModule) continue;
435
+
436
+ const typeProperty = node.children.find((child) => child.key.value === 'type');
437
+ if (!typeProperty) continue;
438
+
439
+ const sourceRange: Range = [
440
+ schema.offset + typeProperty.loc.start.offset,
441
+ schema.offset + typeProperty.loc.end.offset,
442
+ ];
443
+
444
+ bind(module, blockModule, { sourceRange, type: 'preset' });
445
+ promises.push(traverseModule(blockModule, themeGraph, deps));
446
+ }
447
+
448
+ return Promise.all(promises);
449
+ }
450
+
451
+ async function traverseJsonModule(
452
+ module: JsonModule,
453
+ themeGraph: ThemeGraph,
454
+ deps: AugmentedDependencies,
455
+ ): Promise<Void> {
456
+ const sourceCode = await deps.getSourceCode(module.uri);
457
+ if (sourceCode.type !== SourceCodeType.JSON) throw unexpected();
458
+ const ast = sourceCode.ast;
459
+ if (ast instanceof Error) return; // can't visit what you can't parse
460
+
461
+ switch (module.kind) {
462
+ case JsonModuleKind.Template: {
463
+ // Should only happen once per template
464
+ const template = parseJSON(sourceCode.source) as Template.Template;
465
+ const promises: Promise<Void>[] = [];
466
+ for (const [key, section] of Object.entries(template.sections)) {
467
+ const sectionType = section.type;
468
+ const path = ['sections', key];
469
+ const node = nodeAtPath(ast, path)! as ObjectNode;
470
+ const sectionModule = getSectionModule(themeGraph, sectionType);
471
+ const typeProperty = node.children.find((child) => child.key.value === 'type')!;
472
+ const start = typeProperty.loc.start.offset;
473
+ const end = typeProperty.loc.end.offset;
474
+ const sourceRange: Range = [start, end];
475
+ // Link the template to the section
476
+ bind(module, sectionModule, { sourceRange });
477
+ promises.push(
478
+ // Traverse the section themeselves
479
+ traverseModule(sectionModule, themeGraph, deps),
480
+ // Link the blocks used in the section to the template
481
+ traverseSectionReferences(module, ast, path, section, themeGraph, deps),
482
+ );
483
+ }
484
+
485
+ // Link the template to the layout
486
+ const layout = template.layout;
487
+ const layoutModule = getLayoutModule(themeGraph, template.layout);
488
+ if (layoutModule) {
489
+ let sourceRange: Range | undefined = undefined;
490
+ let indirect = true;
491
+ if (layout !== false && layout !== undefined) {
492
+ const layoutPath = ['layout'];
493
+ const node = nodeAtPath(ast, layoutPath)!;
494
+ sourceRange = [node.loc.start.offset, node.loc.end.offset];
495
+ indirect = false; // this is a direct reference to the layout
496
+ }
497
+ bind(module, layoutModule, { sourceRange, type: 'indirect' });
498
+ promises.push(traverseModule(layoutModule, themeGraph, deps));
499
+ }
500
+
501
+ return Promise.all(promises);
502
+ }
503
+
504
+ case JsonModuleKind.SectionGroup: {
505
+ const sectionGroup = parseJSON(sourceCode.source) as Template.SectionGroup;
506
+ const promises: Promise<Void>[] = [];
507
+ for (const [key, section] of Object.entries(sectionGroup.sections)) {
508
+ const sectionType = section.type;
509
+ const path = ['sections', key];
510
+ const node = nodeAtPath(ast, path)! as ObjectNode;
511
+ const sectionModule = getSectionModule(themeGraph, sectionType);
512
+
513
+ const typeProperty = node.children.find((child) => child.key.value === 'type')!;
514
+ const start = typeProperty.loc.start.offset;
515
+ const end = typeProperty.loc.end.offset;
516
+ const sourceRange: Range = [start, end];
517
+
518
+ // Link the template to the section
519
+ bind(module, sectionModule, { sourceRange });
520
+ promises.push(
521
+ // Traverse the section themeselves
522
+ traverseModule(sectionModule, themeGraph, deps),
523
+ // Link the blocks used in the section to the template
524
+ traverseSectionReferences(module, ast, path, section, themeGraph, deps),
525
+ );
526
+ }
527
+
528
+ return Promise.all(promises);
529
+ }
530
+
531
+ default: {
532
+ return assertNever(module.kind);
533
+ }
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Traverses the actual references contained inside Template.Template['sections'] and Template.SectionGroup['sections'].
539
+ *
540
+ * Does nothing if the mode is not `GraphMode.Production`.
541
+ */
542
+ async function traverseSectionReferences(
543
+ source: ThemeModule, // template or section group
544
+ sourceAst: JSONNode,
545
+ nodePath: string[] = [],
546
+ section: Template.Section,
547
+ themeGraph: ThemeGraph,
548
+ deps: AugmentedDependencies,
549
+ ): Promise<Void> {
550
+ if (!section.blocks) return;
551
+ const sectionHasLocalBlocks = await acceptsLocalBlocks(section.type, deps);
552
+ if (sectionHasLocalBlocks) return;
553
+
554
+ const promises: Promise<Void>[] = [];
555
+
556
+ for (const [key, block] of Object.entries(section.blocks)) {
557
+ const blockType = block.type;
558
+ const blockModule = getThemeBlockModule(themeGraph, blockType);
559
+ const path = [...nodePath, 'blocks', key];
560
+ const node = nodeAtPath(sourceAst, path)! as ObjectNode;
561
+ const typeProperty = node.children.find((child) => child.key.value === 'type')!;
562
+ const start = typeProperty.loc.start.offset;
563
+ const end = typeProperty.loc.end.offset;
564
+ const sourceRange: Range = [start, end];
565
+ // Link the template to the block
566
+ bind(source, blockModule, { sourceRange });
567
+ promises.push(
568
+ // Traverse the block themselves
569
+ traverseModule(blockModule, themeGraph, deps),
570
+ // Traverse the block references
571
+ traverseBlockReferences(source, sourceAst, path, block, themeGraph, deps),
572
+ );
573
+ }
574
+
575
+ return Promise.all(promises);
576
+ }
577
+
578
+ async function traverseBlockReferences(
579
+ source: ThemeModule, // template or section group
580
+ sourceAst: JSONNode,
581
+ nodePath: string[] = [],
582
+ block: Template.Block,
583
+ themeGraph: ThemeGraph,
584
+ deps: AugmentedDependencies,
585
+ ): Promise<Void> {
586
+ if (!block.blocks) return;
587
+
588
+ const promises: Promise<Void>[] = [];
589
+ for (const [key, childBlock] of Object.entries(block.blocks)) {
590
+ const childBlockType = childBlock.type;
591
+ const childBlockModule = getThemeBlockModule(themeGraph, childBlockType);
592
+ const path = [...nodePath, 'blocks', key];
593
+ const node = nodeAtPath(sourceAst, path)! as ObjectNode;
594
+ const typeProperty = node.children.find((child) => child.key.value === 'type')!;
595
+ const start = typeProperty.loc.start.offset;
596
+ const end = typeProperty.loc.end.offset;
597
+ const sourceRange: Range = [start, end];
598
+ // Link the template/section group to the block
599
+ bind(source, childBlockModule, { sourceRange });
600
+ promises.push(
601
+ // Traverse the child block themselves
602
+ traverseModule(childBlockModule, themeGraph, deps),
603
+ // Traverse the child block references
604
+ traverseBlockReferences(source, sourceAst, path, childBlock, themeGraph, deps),
605
+ );
606
+ }
607
+
608
+ return Promise.all(promises);
609
+ }
610
+
611
+ /**
612
+ * The bind method is the method that links two modules together.
613
+ *
614
+ * It adds the dependency to the source module's dependencies and the target module's references.
615
+ *
616
+ * This function mutates the source and target modules.
617
+ */
618
+ export function bind(
619
+ source: ThemeModule,
620
+ target: ThemeModule,
621
+ {
622
+ sourceRange,
623
+ targetRange,
624
+ type = 'direct', // the type of dependency, can be 'direct', 'indirect' or 'preset'
625
+ }: {
626
+ sourceRange?: Range; // a range in the source module that references the child
627
+ targetRange?: Range; // a range in the child module that is being referenced
628
+ type?: Reference['type']; // the type of dependency
629
+ } = {},
630
+ ): void {
631
+ const dependency: Reference = {
632
+ source: { uri: source.uri, range: sourceRange },
633
+ target: { uri: target.uri, range: targetRange },
634
+ type: type,
635
+ };
636
+
637
+ source.dependencies.push(dependency);
638
+ target.references.push(dependency);
639
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { buildThemeGraph } from './graph/build';
2
+ export { serializeThemeGraph } from './graph/serialize';
3
+ export { getWebComponentMap, findWebComponentReferences } from './getWebComponentMap';
4
+ export { toCssSourceCode, toJsSourceCode, toSourceCode, toSvgSourceCode } from './toSourceCode';
5
+ export * from './types';