@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
@@ -0,0 +1,271 @@
1
+ import nodePath from 'node:path';
2
+ import { path } from '@platformos/platformos-check-common';
3
+ import { AbstractFileSystem, FileType, uriFromPath } from '@platformos/platformos-common';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { buildSerializedFileDependencies, buildSerializedGraph, nodeFileSystem } from './cli';
6
+ import { skeleton } from './graph/test-helpers';
7
+ import { LiquidModuleKind, ModuleType } from './types';
8
+
9
+ const skeletonPath = path.fsPath(skeleton);
10
+ const p = (rel: string) => path.join(skeleton, ...rel.split('/'));
11
+ const fsPathOf = (rel: string) => nodePath.join(skeletonPath, ...rel.split('/'));
12
+
13
+ describe('platformos-graph CLI: buildSerializedGraph', () => {
14
+ it('serializes the full node and edge set for a project path', async () => {
15
+ const graph = await buildSerializedGraph(skeletonPath);
16
+
17
+ // Root is the project path re-expressed as a file URI.
18
+ expect(graph.rootUri).toBe(uriFromPath(skeletonPath));
19
+
20
+ // Whole node set, sorted by URI so the assertion is order-independent.
21
+ // (`app/assets/*` sorts ahead of `app/views/*`.)
22
+ expect([...graph.nodes].sort((a, b) => a.uri.localeCompare(b.uri))).toEqual([
23
+ {
24
+ uri: p('app/assets/app.css'),
25
+ type: ModuleType.Asset,
26
+ kind: 'unused',
27
+ exists: true,
28
+ },
29
+ {
30
+ uri: p('app/assets/app.js'),
31
+ type: ModuleType.Asset,
32
+ kind: 'unused',
33
+ exists: true,
34
+ },
35
+ {
36
+ uri: p('app/views/layouts/application.liquid'),
37
+ type: ModuleType.Liquid,
38
+ kind: LiquidModuleKind.Layout,
39
+ exists: true,
40
+ },
41
+ {
42
+ uri: p('app/views/pages/index.liquid'),
43
+ type: ModuleType.Liquid,
44
+ kind: LiquidModuleKind.Page,
45
+ exists: true,
46
+ },
47
+ {
48
+ uri: p('app/views/partials/child.liquid'),
49
+ type: ModuleType.Liquid,
50
+ kind: LiquidModuleKind.Partial,
51
+ exists: true,
52
+ },
53
+ {
54
+ uri: p('app/views/partials/header.liquid'),
55
+ type: ModuleType.Liquid,
56
+ kind: LiquidModuleKind.Partial,
57
+ exists: true,
58
+ },
59
+ {
60
+ uri: p('app/views/partials/parent.liquid'),
61
+ type: ModuleType.Liquid,
62
+ kind: LiquidModuleKind.Partial,
63
+ exists: true,
64
+ },
65
+ ]);
66
+
67
+ // Whole edge set, reduced to (source, target, type, kind) and sorted, so an
68
+ // extra, missing, or mis-kinded edge fails the assertion.
69
+ const edges = graph.edges
70
+ .map((edge) => ({
71
+ source: edge.source.uri,
72
+ target: edge.target.uri,
73
+ type: edge.type,
74
+ kind: edge.kind,
75
+ }))
76
+ .sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
77
+
78
+ expect(edges).toEqual([
79
+ {
80
+ source: p('app/views/layouts/application.liquid'),
81
+ target: p('app/assets/app.css'),
82
+ type: 'direct',
83
+ kind: 'asset',
84
+ },
85
+ {
86
+ source: p('app/views/layouts/application.liquid'),
87
+ target: p('app/assets/app.js'),
88
+ type: 'direct',
89
+ kind: 'asset',
90
+ },
91
+ {
92
+ source: p('app/views/layouts/application.liquid'),
93
+ target: p('app/views/partials/header.liquid'),
94
+ type: 'direct',
95
+ kind: 'render',
96
+ },
97
+ {
98
+ source: p('app/views/pages/index.liquid'),
99
+ target: p('app/views/layouts/application.liquid'),
100
+ type: 'direct',
101
+ kind: 'layout',
102
+ },
103
+ {
104
+ source: p('app/views/pages/index.liquid'),
105
+ target: p('app/views/partials/parent.liquid'),
106
+ type: 'direct',
107
+ kind: 'render',
108
+ },
109
+ {
110
+ source: p('app/views/partials/header.liquid'),
111
+ target: p('app/views/partials/child.liquid'),
112
+ type: 'direct',
113
+ kind: 'render',
114
+ },
115
+ {
116
+ source: p('app/views/partials/parent.liquid'),
117
+ target: p('app/views/partials/child.liquid'),
118
+ type: 'direct',
119
+ kind: 'render',
120
+ },
121
+ ]);
122
+ });
123
+ });
124
+
125
+ describe('platformos-graph CLI: nodeFileSystem', () => {
126
+ it('reads a file by URI', async () => {
127
+ const uri = path.join(skeleton, 'app', 'assets', 'app.js');
128
+ expect(await nodeFileSystem.readFile(uri)).toBe(
129
+ '// Skeleton app bundle (asset reference target).\n',
130
+ );
131
+ });
132
+
133
+ it('stats a directory and a file', async () => {
134
+ expect(await nodeFileSystem.stat(path.join(skeleton, 'app', 'assets'))).toEqual({
135
+ type: FileType.Directory,
136
+ size: expect.any(Number),
137
+ });
138
+ expect(await nodeFileSystem.stat(path.join(skeleton, 'app', 'assets', 'app.css'))).toEqual({
139
+ type: FileType.File,
140
+ size: expect.any(Number),
141
+ });
142
+ });
143
+
144
+ it('lists a directory as [childUri, FileType] tuples', async () => {
145
+ const entries = await nodeFileSystem.readDirectory(path.join(skeleton, 'app', 'assets'));
146
+ expect([...entries].sort((a, b) => a[0].localeCompare(b[0]))).toEqual([
147
+ [path.join(skeleton, 'app', 'assets', 'app.css'), FileType.File],
148
+ [path.join(skeleton, 'app', 'assets', 'app.js'), FileType.File],
149
+ ]);
150
+ });
151
+ });
152
+
153
+ describe('platformos-graph CLI: buildSerializedFileDependencies', () => {
154
+ const edge = (ref: {
155
+ source: { uri: string };
156
+ target: { uri: string };
157
+ type: string;
158
+ kind?: string;
159
+ }) => ({
160
+ source: ref.source.uri,
161
+ target: ref.target.uri,
162
+ type: ref.type,
163
+ kind: ref.kind,
164
+ });
165
+
166
+ it('returns the outgoing and incoming edges of a single file', async () => {
167
+ const result = await buildSerializedFileDependencies(
168
+ skeletonPath,
169
+ fsPathOf('app/views/partials/parent.liquid'),
170
+ );
171
+
172
+ expect(result.uri).toBe(p('app/views/partials/parent.liquid'));
173
+ expect(result.dependencies.map(edge)).toEqual([
174
+ {
175
+ source: p('app/views/partials/parent.liquid'),
176
+ target: p('app/views/partials/child.liquid'),
177
+ type: 'direct',
178
+ kind: 'render',
179
+ },
180
+ ]);
181
+ expect(result.references.map(edge)).toEqual([
182
+ {
183
+ source: p('app/views/pages/index.liquid'),
184
+ target: p('app/views/partials/parent.liquid'),
185
+ type: 'direct',
186
+ kind: 'render',
187
+ },
188
+ ]);
189
+ });
190
+
191
+ it('returns no dependencies for a leaf partial and every incoming reference', async () => {
192
+ const result = await buildSerializedFileDependencies(
193
+ skeletonPath,
194
+ fsPathOf('app/views/partials/child.liquid'),
195
+ );
196
+
197
+ expect(result.uri).toBe(p('app/views/partials/child.liquid'));
198
+ expect(result.dependencies).toEqual([]);
199
+ expect(result.references.map(edge).sort((a, b) => a.source.localeCompare(b.source))).toEqual([
200
+ {
201
+ source: p('app/views/partials/header.liquid'),
202
+ target: p('app/views/partials/child.liquid'),
203
+ type: 'direct',
204
+ kind: 'render',
205
+ },
206
+ {
207
+ source: p('app/views/partials/parent.liquid'),
208
+ target: p('app/views/partials/child.liquid'),
209
+ type: 'direct',
210
+ kind: 'render',
211
+ },
212
+ ]);
213
+ });
214
+
215
+ it('resolves a relative file argument against the project root, not the cwd', async () => {
216
+ const result = await buildSerializedFileDependencies(
217
+ skeletonPath,
218
+ 'app/views/partials/parent.liquid',
219
+ );
220
+
221
+ expect(result.uri).toBe(p('app/views/partials/parent.liquid'));
222
+ expect(result.dependencies.map(edge)).toEqual([
223
+ {
224
+ source: p('app/views/partials/parent.liquid'),
225
+ target: p('app/views/partials/child.liquid'),
226
+ type: 'direct',
227
+ kind: 'render',
228
+ },
229
+ ]);
230
+ });
231
+
232
+ it('throws for a file that is not part of the app graph', async () => {
233
+ const missing = fsPathOf('app/views/partials/nonexistent.liquid');
234
+ const missingUri = uriFromPath(missing);
235
+ const rootUri = uriFromPath(skeletonPath);
236
+
237
+ const error = await buildSerializedFileDependencies(skeletonPath, missing).catch((e) => e);
238
+ expect(error).toBeInstanceOf(Error);
239
+ expect((error as Error).message).toBe(
240
+ `File is not part of the app graph: ${missingUri}\n` +
241
+ `It must exist and be reachable from a layout or page entry point. ` +
242
+ `Check the path is correct and inside the project root (${rootUri}).`,
243
+ );
244
+ });
245
+ });
246
+
247
+ describe('platformos-graph CLI: project-root validation', () => {
248
+ /**
249
+ * A filesystem where nothing exists. Injected so `findRoot` walks up from the
250
+ * given path entirely in memory — no real disk, no dependency on what sits
251
+ * above the OS temp dir — and deterministically finds no project marker.
252
+ */
253
+ const emptyFs: AbstractFileSystem = {
254
+ stat: () => Promise.reject(new Error('ENOENT')),
255
+ readFile: () => Promise.reject(new Error('ENOENT')),
256
+ readDirectory: () => Promise.resolve([]),
257
+ };
258
+
259
+ it('throws instead of emitting an empty graph for a non-platformOS directory', async () => {
260
+ const dir = nodePath.resolve('no-such-project', 'sub');
261
+ const startUri = uriFromPath(dir);
262
+
263
+ const error = await buildSerializedGraph(dir, emptyFs).catch((e) => e);
264
+ expect(error).toBeInstanceOf(Error);
265
+ expect((error as Error).message).toBe(
266
+ `Not a platformOS project: ${startUri}\n` +
267
+ `No app/, marketplace_builder/, modules/, .pos, or .platformos-check.yml found ` +
268
+ `at or above this path. Pass the path to a platformOS app directory.`,
269
+ );
270
+ });
271
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,167 @@
1
+ import nodePath from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import {
4
+ makeFileExists,
5
+ path,
6
+ PROJECT_ROOT_MARKERS,
7
+ resolveProjectRoot,
8
+ } from '@platformos/platformos-check-common';
9
+ import {
10
+ AbstractFileSystem,
11
+ FileStat,
12
+ FileTuple,
13
+ FileType,
14
+ uriFromPath,
15
+ } from '@platformos/platformos-common';
16
+ import { buildAppGraph } from './graph/build';
17
+ import { serializeAppGraph } from './graph/serialize';
18
+ import { Reference, SerializableGraph } from './types';
19
+
20
+ /**
21
+ * A self-contained {@link AbstractFileSystem} backed by `node:fs`.
22
+ *
23
+ * The graph driver speaks file URIs, so every method converts the incoming URI
24
+ * to a native path with `path.fsPath` and re-emits child URIs with `path.join`
25
+ * (which preserves the `file://` scheme). This mirrors `NodeFileSystem` from
26
+ * `@platformos/platformos-check-node` exactly, but is inlined here so the CLI
27
+ * runs with only this package's own dependencies — `platformos-check-node` is a
28
+ * dev-only dependency and must not be required at runtime.
29
+ */
30
+ export const nodeFileSystem: AbstractFileSystem = {
31
+ async readFile(uri: string): Promise<string> {
32
+ return fs.readFile(path.fsPath(uri), 'utf8');
33
+ },
34
+
35
+ async readDirectory(uri: string): Promise<FileTuple[]> {
36
+ const entries = await fs.readdir(path.fsPath(uri), { withFileTypes: true });
37
+ return entries.map((entry) => [
38
+ path.childUri(uri, entry.name),
39
+ entry.isDirectory() ? FileType.Directory : FileType.File,
40
+ ]);
41
+ },
42
+
43
+ async stat(uri: string): Promise<FileStat> {
44
+ try {
45
+ const stats = await fs.stat(path.fsPath(uri));
46
+ return {
47
+ type: stats.isDirectory() ? FileType.Directory : FileType.File,
48
+ size: stats.size,
49
+ };
50
+ } catch (e) {
51
+ throw new Error(`Failed to get file stat: ${e}`);
52
+ }
53
+ },
54
+ };
55
+
56
+ /**
57
+ * The dependency and reference edges of a single module, as produced for the
58
+ * CLI's single-file mode.
59
+ */
60
+ export interface SerializableFileDependencies {
61
+ /** The resolved file URI, as keyed in the graph. */
62
+ uri: string;
63
+ /** Outgoing edges — the modules this file renders/includes/runs/queries. */
64
+ dependencies: Reference[];
65
+ /** Incoming edges — the modules that render/include/run/query this file. */
66
+ references: Reference[];
67
+ }
68
+
69
+ /**
70
+ * Resolves the given root path (absolute, or relative to `process.cwd()`) to the
71
+ * forward-slash-normalized `file://` URI of the enclosing platformOS project,
72
+ * using the same `findRoot` heuristic as the language server (an `app/` — or its
73
+ * legacy `marketplace_builder/` sibling — `modules/`, `.pos`, or
74
+ * `.platformos-check.yml` marker at or above the path).
75
+ *
76
+ * Throws when no project is found, so a typo'd or non-platformOS directory fails
77
+ * loudly instead of silently producing an empty graph. Resolving via `findRoot`
78
+ * also lets the CLI be pointed anywhere inside a project, not just at its root.
79
+ *
80
+ * Normalization is load-bearing: the graph keys every module via `path.join` /
81
+ * `path.normalize` (forward slashes), so any lookup must key the same way or it
82
+ * silently misses on Windows, where raw URIs keep backslashes.
83
+ */
84
+ async function resolveGraphRoot(root: string, fs: AbstractFileSystem): Promise<string> {
85
+ // Only the cwd-relative step stays here: `process.cwd()` is node-specific, and the resolution
86
+ // itself lives in platformos-check-common so the linter shares it rather than owning a copy.
87
+ const absolute = nodePath.isAbsolute(root) ? root : nodePath.resolve(process.cwd(), root);
88
+
89
+ const { given, root: rootUri } = await resolveProjectRoot(absolute, makeFileExists(fs));
90
+ if (!rootUri) {
91
+ // The marker list is derived rather than restated, so it cannot drift from what findRoot
92
+ // actually looks for. The wording is unchanged from when it was hand-written — this is a
93
+ // refactor, and the existing message assertion is what proves it stayed one.
94
+ const markers = PROJECT_ROOT_MARKERS.join(', ').replace(/, ([^,]*)$/, ', or $1');
95
+ throw new Error(
96
+ `Not a platformOS project: ${given}\n` +
97
+ `No ${markers} found ` +
98
+ `at or above this path. Pass the path to a platformOS app directory.`,
99
+ );
100
+ }
101
+ return rootUri;
102
+ }
103
+
104
+ /**
105
+ * Resolves the single-file argument to a graph module key. An absolute path is
106
+ * taken as-is; a relative path is resolved against the project **root** (not
107
+ * `process.cwd()`), matching the natural "a file within this project" mental
108
+ * model — only files under the root can appear in the graph anyway.
109
+ */
110
+ function resolveFileUri(rootUri: string, file: string): string {
111
+ if (nodePath.isAbsolute(file)) {
112
+ return uriFromPath(file);
113
+ }
114
+ return path.join(rootUri, ...file.split(/[\\/]+/).filter(Boolean));
115
+ }
116
+
117
+ /**
118
+ * Builds the platformOS app graph for the project rooted at `root` (a native
119
+ * filesystem path, absolute or relative to `process.cwd()`) and returns it in
120
+ * serializable JSON form.
121
+ *
122
+ * `fs` is injectable for testing; it defaults to the node-backed filesystem.
123
+ */
124
+ export async function buildSerializedGraph(
125
+ root: string,
126
+ fs: AbstractFileSystem = nodeFileSystem,
127
+ ): Promise<SerializableGraph> {
128
+ const graph = await buildAppGraph(await resolveGraphRoot(root, fs), { fs });
129
+ return serializeAppGraph(graph);
130
+ }
131
+
132
+ /**
133
+ * Builds the full app graph for `root` and returns the dependency and reference
134
+ * edges of the single module at `file`.
135
+ *
136
+ * The graph is built from its real entry points (every layout and page), not
137
+ * seeded from `file`, so that incoming `references` are complete — the same
138
+ * approach the language server's `AppGraphManager` takes. A file that is not
139
+ * reachable from any entry point therefore has no node in the graph; rather
140
+ * than reporting a misleading empty edge set, this throws.
141
+ *
142
+ * `fs` is injectable for testing; it defaults to the node-backed filesystem.
143
+ */
144
+ export async function buildSerializedFileDependencies(
145
+ root: string,
146
+ file: string,
147
+ fs: AbstractFileSystem = nodeFileSystem,
148
+ ): Promise<SerializableFileDependencies> {
149
+ const rootUri = await resolveGraphRoot(root, fs);
150
+ const fileUri = resolveFileUri(rootUri, file);
151
+ const graph = await buildAppGraph(rootUri, { fs });
152
+
153
+ const module = graph.modules[fileUri];
154
+ if (!module) {
155
+ throw new Error(
156
+ `File is not part of the app graph: ${fileUri}\n` +
157
+ `It must exist and be reachable from a layout or page entry point. ` +
158
+ `Check the path is correct and inside the project root (${rootUri}).`,
159
+ );
160
+ }
161
+
162
+ return {
163
+ uri: module.uri,
164
+ dependencies: module.dependencies,
165
+ references: module.references,
166
+ };
167
+ }
@@ -7,6 +7,10 @@ export function augmentDependencies(rootUri: string, ideps: IDependencies): Augm
7
7
  return {
8
8
  fs: ideps.fs,
9
9
 
10
+ // Passed through as given. There is nothing to default: an absent app means the
11
+ // caller has none, and `DocumentsLocator` already owns what to do then.
12
+ app: ideps.app,
13
+
10
14
  // parse at most once
11
15
  getSourceCode: memoize(
12
16
  ideps.getSourceCode ??
@@ -16,7 +20,5 @@ export function augmentDependencies(rootUri: string, ideps: IDependencies): Augm
16
20
  },
17
21
  identity,
18
22
  ),
19
-
20
- getWebComponentDefinitionReference: ideps.getWebComponentDefinitionReference,
21
23
  };
22
24
  }