@platformos/platformos-graph 0.0.19 → 0.1.0

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 (141) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/bin/platformos-graph +21 -81
  3. package/dist/cache-probes.spec.d.ts +1 -0
  4. package/dist/cache-probes.spec.js +62 -0
  5. package/dist/cache-probes.spec.js.map +1 -0
  6. package/dist/cli.d.ts +46 -0
  7. package/dist/cli.js +131 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/cli.spec.d.ts +1 -0
  10. package/dist/cli.spec.js +235 -0
  11. package/dist/cli.spec.js.map +1 -0
  12. package/dist/graph/app-index-resolution.spec.d.ts +1 -0
  13. package/dist/graph/app-index-resolution.spec.js +167 -0
  14. package/dist/graph/app-index-resolution.spec.js.map +1 -0
  15. package/dist/graph/asset-resolution.spec.d.ts +1 -0
  16. package/dist/graph/asset-resolution.spec.js +64 -0
  17. package/dist/graph/asset-resolution.spec.js.map +1 -0
  18. package/dist/graph/augment.js +3 -1
  19. package/dist/graph/augment.js.map +1 -1
  20. package/dist/graph/build.d.ts +15 -2
  21. package/dist/graph/build.js +54 -12
  22. package/dist/graph/build.js.map +1 -1
  23. package/dist/graph/build.spec.d.ts +1 -0
  24. package/dist/graph/build.spec.js +138 -0
  25. package/dist/graph/build.spec.js.map +1 -0
  26. package/dist/graph/edge-sources.d.ts +28 -0
  27. package/dist/graph/edge-sources.js +43 -0
  28. package/dist/graph/edge-sources.js.map +1 -0
  29. package/dist/graph/edge-sources.spec.d.ts +1 -0
  30. package/dist/graph/edge-sources.spec.js +125 -0
  31. package/dist/graph/edge-sources.spec.js.map +1 -0
  32. package/dist/graph/extract.spec.d.ts +1 -0
  33. package/dist/graph/extract.spec.js +163 -0
  34. package/dist/graph/extract.spec.js.map +1 -0
  35. package/dist/graph/module.d.ts +70 -2
  36. package/dist/graph/module.js +164 -29
  37. package/dist/graph/module.js.map +1 -1
  38. package/dist/graph/module.spec.d.ts +1 -0
  39. package/dist/graph/module.spec.js +74 -0
  40. package/dist/graph/module.spec.js.map +1 -0
  41. package/dist/graph/query.d.ts +72 -0
  42. package/dist/graph/query.js +140 -0
  43. package/dist/graph/query.js.map +1 -0
  44. package/dist/graph/query.spec.d.ts +1 -0
  45. package/dist/graph/query.spec.js +209 -0
  46. package/dist/graph/query.spec.js.map +1 -0
  47. package/dist/graph/serialize.spec.d.ts +1 -0
  48. package/dist/graph/serialize.spec.js +50 -0
  49. package/dist/graph/serialize.spec.js.map +1 -0
  50. package/dist/graph/structural.spec.d.ts +1 -0
  51. package/dist/graph/structural.spec.js +146 -0
  52. package/dist/graph/structural.spec.js.map +1 -0
  53. package/dist/graph/test-helpers.d.ts +2 -3
  54. package/dist/graph/test-helpers.js +5 -9
  55. package/dist/graph/test-helpers.js.map +1 -1
  56. package/dist/graph/traverse-edges.spec.d.ts +1 -0
  57. package/dist/graph/traverse-edges.spec.js +337 -0
  58. package/dist/graph/traverse-edges.spec.js.map +1 -0
  59. package/dist/graph/traverse.d.ts +63 -4
  60. package/dist/graph/traverse.js +364 -49
  61. package/dist/graph/traverse.js.map +1 -1
  62. package/dist/index.d.ts +5 -1
  63. package/dist/index.js +21 -4
  64. package/dist/index.js.map +1 -1
  65. package/dist/parsers.d.ts +35 -0
  66. package/dist/parsers.js +72 -0
  67. package/dist/parsers.js.map +1 -0
  68. package/dist/parsers.spec.d.ts +1 -0
  69. package/dist/parsers.spec.js +70 -0
  70. package/dist/parsers.spec.js.map +1 -0
  71. package/dist/toSourceCode.d.ts +14 -2
  72. package/dist/toSourceCode.js +23 -19
  73. package/dist/toSourceCode.js.map +1 -1
  74. package/dist/tsconfig.tsbuildinfo +1 -1
  75. package/dist/types.d.ts +114 -20
  76. package/dist/types.js.map +1 -1
  77. package/dist/utils/index.d.ts +1 -1
  78. package/fixtures/asset-edges/app/assets/logo.css +1 -0
  79. package/fixtures/asset-edges/app/assets/site/app.js +1 -0
  80. package/fixtures/asset-edges/app/views/pages/index.liquid +3 -0
  81. package/fixtures/background-edges/app/views/pages/broken.liquid +3 -0
  82. package/fixtures/background-edges/app/views/pages/index.liquid +3 -0
  83. package/fixtures/background-edges/app/views/partials/jobs/notify.liquid +3 -0
  84. package/fixtures/function-edges/app/lib/queries/list.liquid +3 -0
  85. package/fixtures/function-edges/app/views/pages/broken.liquid +3 -0
  86. package/fixtures/function-edges/app/views/pages/index.liquid +3 -0
  87. package/fixtures/graphql-edges/app/graphql/blog_posts/find.graphql +10 -0
  88. package/fixtures/graphql-edges/app/views/pages/broken.liquid +3 -0
  89. package/fixtures/graphql-edges/app/views/pages/index.liquid +3 -0
  90. package/fixtures/graphql-table/app/graphql/with_table.graphql +5 -0
  91. package/fixtures/graphql-table/app/graphql/without_table.graphql +3 -0
  92. package/fixtures/graphql-table/app/views/pages/index.liquid +4 -0
  93. package/fixtures/include-edges/app/views/pages/index.liquid +1 -0
  94. package/fixtures/include-edges/app/views/partials/shared/header.liquid +1 -0
  95. package/fixtures/layout-edges/app/views/layouts/theme.liquid +4 -0
  96. package/fixtures/layout-edges/app/views/pages/broken.liquid +5 -0
  97. package/fixtures/layout-edges/app/views/pages/index.liquid +5 -0
  98. package/fixtures/module-edges/app/views/pages/index.liquid +4 -0
  99. package/fixtures/module-edges/modules/my_module/public/lib/queries/get.liquid +3 -0
  100. package/fixtures/module-edges/modules/my_module/public/views/partials/card.liquid +1 -0
  101. package/fixtures/schema-nodes/app/schema/blog_post.yml +6 -0
  102. package/fixtures/schema-nodes/app/schema/no_name.yml +3 -0
  103. package/fixtures/schema-nodes/app/views/pages/index.liquid +1 -0
  104. package/fixtures/skeleton/app/assets/app.js +1 -0
  105. package/fixtures/skeleton/app/views/partials/child.liquid +2 -4
  106. package/fixtures/skeleton/app/views/partials/parent.liquid +2 -4
  107. package/fixtures/structural/app/views/pages/about.liquid +1 -0
  108. package/fixtures/structural/app/views/pages/blog/show.liquid +4 -0
  109. package/fixtures/structural/app/views/pages/index.liquid +6 -0
  110. package/fixtures/structural/app/views/pages/rich.liquid +8 -0
  111. package/fixtures/structural/app/views/partials/card.liquid +1 -0
  112. package/fixtures/structural/app/views/partials/documented.liquid +5 -0
  113. package/package.json +7 -5
  114. package/src/cli.spec.ts +271 -0
  115. package/src/cli.ts +167 -0
  116. package/src/graph/augment.ts +4 -2
  117. package/src/graph/build.spec.ts +888 -9
  118. package/src/graph/build.ts +68 -17
  119. package/src/graph/edge-sources.spec.ts +125 -0
  120. package/src/graph/edge-sources.ts +49 -0
  121. package/src/graph/module.spec.ts +168 -0
  122. package/src/graph/module.ts +167 -33
  123. package/src/graph/query.spec.ts +260 -0
  124. package/src/graph/query.ts +154 -0
  125. package/src/graph/serialize.spec.ts +4 -4
  126. package/src/graph/test-helpers.ts +5 -12
  127. package/src/graph/traverse.spec.ts +278 -0
  128. package/src/graph/traverse.ts +423 -54
  129. package/src/index.ts +16 -1
  130. package/src/parsers.spec.ts +85 -0
  131. package/src/parsers.ts +75 -0
  132. package/src/toSourceCode.ts +26 -26
  133. package/src/types.ts +120 -23
  134. package/src/utils/index.ts +1 -1
  135. package/bin/jsconfig.json +0 -18
  136. package/dist/getWebComponentMap.d.ts +0 -10
  137. package/dist/getWebComponentMap.js +0 -66
  138. package/dist/getWebComponentMap.js.map +0 -1
  139. package/fixtures/skeleton/assets/app.js +0 -7
  140. package/src/getWebComponentMap.ts +0 -81
  141. /package/fixtures/skeleton/{assets → app/assets}/app.css +0 -0
@@ -1,34 +1,27 @@
1
1
  import { memoize, path as pathUtils } from '@platformos/platformos-check-common';
2
- import { AbstractFileSystem } from '@platformos/platformos-common';
2
+ import { AbstractFileSystem, uriFromPath } from '@platformos/platformos-common';
3
3
  import { NodeFileSystem } from '@platformos/platformos-check-node';
4
4
  import { vi } from 'vitest';
5
5
  import { URI } from 'vscode-uri';
6
- import { getWebComponentMap } from '../getWebComponentMap';
7
6
  import { toSourceCode } from '../toSourceCode';
8
7
  import { identity } from '../utils';
9
8
 
10
9
  export function makeGetSourceCode(fs: AbstractFileSystem) {
11
10
  return memoize(async function getSourceCode(uri: string) {
12
11
  const source = await fs.readFile(uri);
13
- return toSourceCode(URI.file(uri).toString(), source);
12
+ return toSourceCode(pathUtils.toUri(uri), source);
14
13
  }, identity);
15
14
  }
16
15
 
17
- export const fixturesRoot = pathUtils.join(URI.file(__dirname), ...'../../fixtures'.split('/'));
16
+ export const fixturesRoot = pathUtils.join(uriFromPath(__dirname), ...'../../fixtures'.split('/'));
18
17
  export const skeleton = pathUtils.join(fixturesRoot, 'skeleton');
19
18
 
20
- export async function getDependencies(rootUri: string, fs: AbstractFileSystem = NodeFileSystem) {
19
+ export function getDependencies(fs: AbstractFileSystem = NodeFileSystem) {
21
20
  const getSourceCode = makeGetSourceCode(fs);
22
- const deps = {
21
+ return {
23
22
  fs,
24
23
  getSourceCode,
25
- getWebComponentDefinitionReference: (customElementName: string) =>
26
- webComponentDefs.get(customElementName),
27
24
  };
28
-
29
- const webComponentDefs = await getWebComponentMap(rootUri, deps);
30
-
31
- return deps;
32
25
  }
33
26
 
34
27
  // This thing is way too hard to type.
@@ -0,0 +1,278 @@
1
+ import { path as pathUtils } from '@platformos/platformos-check-common';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { extractFileReferences, extractStructural } from '../index';
4
+ import { toSourceCode } from '../toSourceCode';
5
+ import { ModuleStructural, Reference } from '../types';
6
+ import { fixturesRoot, getDependencies } from './test-helpers';
7
+
8
+ /**
9
+ * `extractFileReferences` is the per-file primitive a `validate_code`-style
10
+ * consumer uses: parse an in-flight buffer with `toSourceCode`, then resolve its
11
+ * outgoing dependency edges against the on-disk project — WITHOUT building the
12
+ * whole app graph. The buffer is never read from disk, so these specs pass the
13
+ * content inline (and even use a `sourceUri` for a file that does not exist).
14
+ */
15
+ const { fs } = getDependencies();
16
+
17
+ /** The exact source range of `snippet` within `source`. */
18
+ function rangeOf(source: string, snippet: string): [number, number] {
19
+ const start = source.indexOf(snippet);
20
+ if (start < 0) throw new Error(`snippet not found: ${snippet}`);
21
+ return [start, start + snippet.length];
22
+ }
23
+
24
+ function directRef(
25
+ sourceUri: string,
26
+ sourceRange: [number, number],
27
+ targetUri: string,
28
+ kind: Reference['kind'],
29
+ args?: string[],
30
+ ): Reference {
31
+ return {
32
+ source: { uri: sourceUri, range: sourceRange },
33
+ target: { uri: targetUri },
34
+ type: 'direct',
35
+ kind,
36
+ ...(args ? { args } : {}),
37
+ };
38
+ }
39
+
40
+ async function extract(rootUri: string, sourceUri: string, content: string) {
41
+ return extractFileReferences(rootUri, sourceUri, await toSourceCode(sourceUri, content), { fs });
42
+ }
43
+
44
+ describe('extractFileReferences: resolves a single buffer against the project', () => {
45
+ const rootUri = pathUtils.join(fixturesRoot, 'function-edges');
46
+ const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
47
+
48
+ it('resolves a {% function %} edge to the canonical lib URI', async () => {
49
+ const sourceUri = p('app/views/pages/draft.liquid'); // not on disk — in-flight buffer
50
+ const content = `{% liquid
51
+ function items = 'queries/list'
52
+ %}`;
53
+
54
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
55
+ directRef(
56
+ sourceUri,
57
+ rangeOf(content, "function items = 'queries/list'"),
58
+ p('app/lib/queries/list.liquid'),
59
+ 'function',
60
+ ),
61
+ ]);
62
+ });
63
+
64
+ it('resolves a target that does not exist on disk (path-based resolution)', async () => {
65
+ const sourceUri = p('app/views/pages/draft.liquid');
66
+ const content = `{% liquid
67
+ function ghost = 'queries/missing'
68
+ %}`;
69
+
70
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
71
+ directRef(
72
+ sourceUri,
73
+ rangeOf(content, "function ghost = 'queries/missing'"),
74
+ p('app/lib/queries/missing.liquid'),
75
+ 'function',
76
+ ),
77
+ ]);
78
+ });
79
+
80
+ it('captures the named-argument names at a render call site', async () => {
81
+ const sourceUri = p('app/views/pages/draft.liquid');
82
+ const content = "{% render 'card', title: page.title, count: 3 %}";
83
+
84
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
85
+ directRef(
86
+ sourceUri,
87
+ rangeOf(content, "{% render 'card', title: page.title, count: 3 %}"),
88
+ p('app/views/partials/card.liquid'),
89
+ 'render',
90
+ ['title', 'count'],
91
+ ),
92
+ ]);
93
+ });
94
+ });
95
+
96
+ describe('extractFileReferences: kinds and resolution match the full graph build', () => {
97
+ it('tags include edges with kind "include"', async () => {
98
+ const rootUri = pathUtils.join(fixturesRoot, 'include-edges');
99
+ const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
100
+ const sourceUri = p('app/views/pages/draft.liquid');
101
+ const content = "{% include 'shared/header' %}";
102
+
103
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
104
+ directRef(
105
+ sourceUri,
106
+ rangeOf(content, "{% include 'shared/header' %}"),
107
+ p('app/views/partials/shared/header.liquid'),
108
+ 'include',
109
+ ),
110
+ ]);
111
+ });
112
+
113
+ it('resolves a {% graphql %} edge to the .graphql operation file', async () => {
114
+ const rootUri = pathUtils.join(fixturesRoot, 'graphql-edges');
115
+ const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
116
+ const sourceUri = p('app/views/pages/draft.liquid');
117
+ const content = `{% liquid
118
+ graphql posts = 'blog_posts/find', id: '1'
119
+ %}`;
120
+
121
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
122
+ directRef(
123
+ sourceUri,
124
+ rangeOf(content, "graphql posts = 'blog_posts/find', id: '1'"),
125
+ p('app/graphql/blog_posts/find.graphql'),
126
+ 'graphql',
127
+ ['id'],
128
+ ),
129
+ ]);
130
+ });
131
+
132
+ it('resolves module-namespaced render + function targets in declaration order', async () => {
133
+ const rootUri = pathUtils.join(fixturesRoot, 'module-edges');
134
+ const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
135
+ const sourceUri = p('app/views/pages/draft.liquid');
136
+ const content = `{% liquid
137
+ function items = 'modules/my_module/queries/get'
138
+ %}
139
+ {% render 'modules/my_module/card' %}`;
140
+
141
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
142
+ directRef(
143
+ sourceUri,
144
+ rangeOf(content, "function items = 'modules/my_module/queries/get'"),
145
+ p('modules/my_module/public/lib/queries/get.liquid'),
146
+ 'function',
147
+ ),
148
+ directRef(
149
+ sourceUri,
150
+ rangeOf(content, "{% render 'modules/my_module/card' %}"),
151
+ p('modules/my_module/public/views/partials/card.liquid'),
152
+ 'render',
153
+ ),
154
+ ]);
155
+ });
156
+ });
157
+
158
+ describe('extractFileReferences: only statically resolvable edges', () => {
159
+ const rootUri = pathUtils.join(fixturesRoot, 'function-edges');
160
+ const sourceUri = pathUtils.join(rootUri, 'app/views/pages/draft.liquid');
161
+
162
+ it('skips dynamic render targets', async () => {
163
+ expect(await extract(rootUri, sourceUri, '{% render partial_name %}')).toEqual([]);
164
+ });
165
+
166
+ it('returns nothing for an unparseable buffer instead of throwing', async () => {
167
+ expect(await extract(rootUri, sourceUri, '{% render %}{% endif %}')).toEqual([]);
168
+ });
169
+
170
+ it('returns nothing for a buffer with no references', async () => {
171
+ expect(await extract(rootUri, sourceUri, '<h1>{{ page.title }}</h1>')).toEqual([]);
172
+ });
173
+ });
174
+
175
+ describe('extractFileReferences: layout-association edges (frontmatter `layout:`)', () => {
176
+ const rootUri = pathUtils.join(fixturesRoot, 'function-edges'); // no layouts here → default path
177
+ const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
178
+ const sourceUri = p('app/views/pages/draft.liquid');
179
+
180
+ // The whole frontmatter block: start through the closing `---` line + newline.
181
+ const fmRange = (content: string): [number, number] => [
182
+ 0,
183
+ content.indexOf('\n', content.indexOf('---', 3)) + 1,
184
+ ];
185
+
186
+ it('emits a layout edge for an explicit frontmatter layout', async () => {
187
+ const content = `---
188
+ slug: draft
189
+ layout: theme
190
+ ---
191
+ <h1>x</h1>`;
192
+
193
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
194
+ directRef(sourceUri, fmRange(content), p('app/views/layouts/theme.liquid'), 'layout'),
195
+ ]);
196
+ });
197
+
198
+ it('resolves a module-prefixed layout', async () => {
199
+ const content = `---
200
+ layout: modules/core/admin
201
+ ---
202
+ <h1>x</h1>`;
203
+
204
+ expect(await extract(rootUri, sourceUri, content)).toEqual([
205
+ directRef(
206
+ sourceUri,
207
+ fmRange(content),
208
+ p('modules/core/public/views/layouts/admin.liquid'),
209
+ 'layout',
210
+ ),
211
+ ]);
212
+ });
213
+
214
+ it('emits no edge for an explicitly empty layout', async () => {
215
+ const content = `---
216
+ slug: draft
217
+ layout: ''
218
+ ---
219
+ <h1>x</h1>`;
220
+
221
+ expect(await extract(rootUri, sourceUri, content)).toEqual([]);
222
+ });
223
+
224
+ it('emits no edge when layout is omitted (no implicit default)', async () => {
225
+ const content = `---
226
+ slug: draft
227
+ ---
228
+ <h1>x</h1>`;
229
+
230
+ expect(await extract(rootUri, sourceUri, content)).toEqual([]);
231
+ });
232
+
233
+ it('emits no edge for a dynamic layout value', async () => {
234
+ const content = `---
235
+ layout: "{{ context.constants.LAYOUT }}"
236
+ ---
237
+ <h1>x</h1>`;
238
+
239
+ expect(await extract(rootUri, sourceUri, content)).toEqual([]);
240
+ });
241
+ });
242
+
243
+ describe('extractStructural: the per-file primitive', () => {
244
+ const rootUri = pathUtils.join(fixturesRoot, 'structural');
245
+ // `rootUri` is already a URI, so joining is the whole conversion — `URI.file` on one
246
+ // yields `file:///file%3A///…`, a plausible-looking URI for nowhere.
247
+ const uriOf = (part: string) => pathUtils.join(rootUri, ...part.split('/'));
248
+
249
+ const run = async (part: string, content: string): Promise<ModuleStructural | undefined> => {
250
+ const uri = uriOf(part);
251
+ return extractStructural(await toSourceCode(uri, content), uri);
252
+ };
253
+
254
+ it('extracts usage + routing facts from an in-flight buffer (no graph build)', async () => {
255
+ const content = `---
256
+ layout: theme
257
+ method: post
258
+ ---
259
+ {% render 'card' %}
260
+ {{ 'greeting.hi' | t }}
261
+ {{ title | upcase }}`;
262
+ expect(await run('app/views/pages/contact.liquid', content)).toEqual({
263
+ renders_used: ['card'],
264
+ graphql_queries_used: [],
265
+ filters_used: ['t', 'upcase'],
266
+ tags_used: ['render'],
267
+ translation_keys: ['greeting.hi'],
268
+ doc_params: [],
269
+ slug: 'contact',
270
+ layout: 'theme',
271
+ method: 'post',
272
+ });
273
+ });
274
+
275
+ it('returns undefined for a non-Liquid (.graphql) buffer', async () => {
276
+ expect(await run('app/graphql/find.graphql', 'query find { records { id } }')).toBeUndefined();
277
+ });
278
+ });