@platformos/platformos-graph 0.0.20 → 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 (120) hide show
  1. package/CHANGELOG.md +511 -8
  2. package/dist/cache-probes.spec.d.ts +1 -0
  3. package/dist/cache-probes.spec.js +62 -0
  4. package/dist/cache-probes.spec.js.map +1 -0
  5. package/dist/cli.js +18 -13
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cli.spec.d.ts +1 -0
  8. package/dist/cli.spec.js +235 -0
  9. package/dist/cli.spec.js.map +1 -0
  10. package/dist/graph/app-index-resolution.spec.d.ts +1 -0
  11. package/dist/graph/app-index-resolution.spec.js +167 -0
  12. package/dist/graph/app-index-resolution.spec.js.map +1 -0
  13. package/dist/graph/asset-resolution.spec.d.ts +1 -0
  14. package/dist/graph/asset-resolution.spec.js +64 -0
  15. package/dist/graph/asset-resolution.spec.js.map +1 -0
  16. package/dist/graph/augment.js +3 -0
  17. package/dist/graph/augment.js.map +1 -1
  18. package/dist/graph/build.d.ts +15 -2
  19. package/dist/graph/build.js +54 -12
  20. package/dist/graph/build.js.map +1 -1
  21. package/dist/graph/build.spec.d.ts +1 -0
  22. package/dist/graph/build.spec.js +138 -0
  23. package/dist/graph/build.spec.js.map +1 -0
  24. package/dist/graph/edge-sources.d.ts +28 -0
  25. package/dist/graph/edge-sources.js +43 -0
  26. package/dist/graph/edge-sources.js.map +1 -0
  27. package/dist/graph/edge-sources.spec.d.ts +1 -0
  28. package/dist/graph/edge-sources.spec.js +125 -0
  29. package/dist/graph/edge-sources.spec.js.map +1 -0
  30. package/dist/graph/extract.spec.d.ts +1 -0
  31. package/dist/graph/extract.spec.js +163 -0
  32. package/dist/graph/extract.spec.js.map +1 -0
  33. package/dist/graph/module.d.ts +60 -7
  34. package/dist/graph/module.js +137 -41
  35. package/dist/graph/module.js.map +1 -1
  36. package/dist/graph/module.spec.d.ts +1 -0
  37. package/dist/graph/module.spec.js +74 -0
  38. package/dist/graph/module.spec.js.map +1 -0
  39. package/dist/graph/query.d.ts +72 -0
  40. package/dist/graph/query.js +140 -0
  41. package/dist/graph/query.js.map +1 -0
  42. package/dist/graph/query.spec.d.ts +1 -0
  43. package/dist/graph/query.spec.js +209 -0
  44. package/dist/graph/query.spec.js.map +1 -0
  45. package/dist/graph/serialize.spec.d.ts +1 -0
  46. package/dist/graph/serialize.spec.js +50 -0
  47. package/dist/graph/serialize.spec.js.map +1 -0
  48. package/dist/graph/structural.spec.d.ts +1 -0
  49. package/dist/graph/structural.spec.js +146 -0
  50. package/dist/graph/structural.spec.js.map +1 -0
  51. package/dist/graph/test-helpers.js +3 -3
  52. package/dist/graph/test-helpers.js.map +1 -1
  53. package/dist/graph/traverse-edges.spec.d.ts +1 -0
  54. package/dist/graph/traverse-edges.spec.js +337 -0
  55. package/dist/graph/traverse-edges.spec.js.map +1 -0
  56. package/dist/graph/traverse.d.ts +51 -20
  57. package/dist/graph/traverse.js +239 -29
  58. package/dist/graph/traverse.js.map +1 -1
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +19 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/parsers.d.ts +35 -0
  63. package/dist/parsers.js +72 -0
  64. package/dist/parsers.js.map +1 -0
  65. package/dist/parsers.spec.d.ts +1 -0
  66. package/dist/parsers.spec.js +70 -0
  67. package/dist/parsers.spec.js.map +1 -0
  68. package/dist/toSourceCode.d.ts +14 -2
  69. package/dist/toSourceCode.js +23 -19
  70. package/dist/toSourceCode.js.map +1 -1
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/dist/types.d.ts +102 -3
  73. package/dist/types.js.map +1 -1
  74. package/dist/utils/index.d.ts +1 -1
  75. package/fixtures/asset-edges/app/assets/logo.css +1 -0
  76. package/fixtures/asset-edges/app/assets/site/app.js +1 -0
  77. package/fixtures/asset-edges/app/views/pages/index.liquid +3 -0
  78. package/fixtures/graphql-table/app/graphql/with_table.graphql +5 -0
  79. package/fixtures/graphql-table/app/graphql/without_table.graphql +3 -0
  80. package/fixtures/graphql-table/app/views/pages/index.liquid +4 -0
  81. package/fixtures/layout-edges/app/views/layouts/theme.liquid +4 -0
  82. package/fixtures/layout-edges/app/views/pages/broken.liquid +5 -0
  83. package/fixtures/layout-edges/app/views/pages/index.liquid +5 -0
  84. package/fixtures/schema-nodes/app/schema/blog_post.yml +6 -0
  85. package/fixtures/schema-nodes/app/schema/no_name.yml +3 -0
  86. package/fixtures/schema-nodes/app/views/pages/index.liquid +1 -0
  87. package/fixtures/structural/app/views/pages/about.liquid +1 -0
  88. package/fixtures/structural/app/views/pages/blog/show.liquid +4 -0
  89. package/fixtures/structural/app/views/pages/index.liquid +6 -0
  90. package/fixtures/structural/app/views/pages/rich.liquid +8 -0
  91. package/fixtures/structural/app/views/partials/card.liquid +1 -0
  92. package/fixtures/structural/app/views/partials/documented.liquid +5 -0
  93. package/package.json +7 -5
  94. package/src/cli.spec.ts +37 -31
  95. package/src/cli.ts +31 -15
  96. package/src/graph/augment.ts +4 -0
  97. package/src/graph/build.spec.ts +871 -6
  98. package/src/graph/build.ts +68 -17
  99. package/src/graph/edge-sources.spec.ts +125 -0
  100. package/src/graph/edge-sources.ts +49 -0
  101. package/src/graph/module.spec.ts +168 -0
  102. package/src/graph/module.ts +139 -45
  103. package/src/graph/query.spec.ts +260 -0
  104. package/src/graph/query.ts +154 -0
  105. package/src/graph/serialize.spec.ts +4 -4
  106. package/src/graph/test-helpers.ts +3 -3
  107. package/src/graph/{extract.spec.ts → traverse.spec.ts} +125 -2
  108. package/src/graph/traverse.ts +278 -37
  109. package/src/index.ts +16 -1
  110. package/src/parsers.spec.ts +85 -0
  111. package/src/parsers.ts +75 -0
  112. package/src/toSourceCode.ts +26 -26
  113. package/src/types.ts +106 -7
  114. package/src/utils/index.ts +1 -1
  115. package/dist/getWebComponentMap.d.ts +0 -10
  116. package/dist/getWebComponentMap.js +0 -66
  117. package/dist/getWebComponentMap.js.map +0 -1
  118. package/src/graph/traverse-edges.spec.ts +0 -278
  119. /package/fixtures/skeleton/{assets → app/assets}/app.css +0 -0
  120. /package/fixtures/skeleton/{assets → app/assets}/app.js +0 -0
package/dist/types.d.ts CHANGED
@@ -1,17 +1,49 @@
1
1
  import { JSONSourceCode, LiquidSourceCode, Dependencies as CheckDependencies, UriString, Reference, ReferenceKind, Location, Range, GraphQLSourceCode, YAMLSourceCode } from '@platformos/platformos-check-common';
2
+ import type { App, AppResolver } from '@platformos/platformos-common';
2
3
  export interface IDependencies {
3
4
  fs: CheckDependencies['fs'];
4
5
  /** Optional perf improvement if you somehow have access to pre-computed source code info */
5
6
  getSourceCode?: (uri: UriString) => Promise<FileSourceCode>;
7
+ /**
8
+ * The {@link App} whose per-type name index answers `{% render 'ui/card' %}`,
9
+ * `{% function … = 'commands/create' %}`, `{% graphql … = 'user/find' %}` and
10
+ * `layout: application` — the same object {@link IDependencies.getSourceCode} reads through
11
+ * when it is `appBackedGetSourceCode`.
12
+ *
13
+ * The pairing is the point: a caller that holds an App should hand it over for BOTH, so the
14
+ * graph reuses the lint's parses AND its index. Without it, `DocumentsLocator` falls back to a
15
+ * walk-only stand-in and every reference costs a `readDirectory` per candidate directory.
16
+ *
17
+ * It cannot change the ANSWER, only what it costs: `App.findOrLocate` resolves index-first and
18
+ * falls through to the very walk the stand-in performs. Omit it where there is genuinely no
19
+ * App: a build in a worker thread, a CLI, a fixture.
20
+ */
21
+ app?: App | AppResolver;
6
22
  }
7
- export type Dependencies = Required<IDependencies>;
23
+ /**
24
+ * `IDependencies` with the optional PERFORMANCE seams resolved by
25
+ * `augmentDependencies` — `getSourceCode` always present, `app` still optional
26
+ * because "no app here" is a legitimate final state, not an unaugmented one.
27
+ */
28
+ export type Dependencies = Required<Omit<IDependencies, 'app'>> & Pick<IDependencies, 'app'>;
8
29
  export type AugmentedDependencies = Dependencies;
30
+ /** Options controlling what a build/traversal computes beyond the edge graph. */
31
+ export interface GraphBuildOptions {
32
+ /**
33
+ * Populate each {@link LiquidModule.structural} (self-structural facts) during
34
+ * the build, as a by-product of the parse. OFF by default: a consumer that
35
+ * needs one buffer's structural facts should call `extractStructural` directly
36
+ * (the per-file primitive), so a full graph build — e.g. the LSP's, which never
37
+ * reads `structural` — does not compute a fact nothing consumes.
38
+ */
39
+ includeStructural?: boolean;
40
+ }
9
41
  export interface AppGraph {
10
42
  rootUri: UriString;
11
43
  entryPoints: AppModule[];
12
44
  modules: Record<UriString, AppModule>;
13
45
  }
14
- export type AppModule = LiquidModule | AssetModule | GraphQLModule;
46
+ export type AppModule = LiquidModule | AssetModule | GraphQLModule | SchemaModule;
15
47
  export type FileSourceCode = LiquidSourceCode | JSONSourceCode | GraphQLSourceCode | YAMLSourceCode | AssetSourceCode;
16
48
  export interface SerializableGraph {
17
49
  rootUri: UriString;
@@ -20,8 +52,46 @@ export interface SerializableGraph {
20
52
  }
21
53
  export type SerializableEdge = Reference;
22
54
  export type SerializableNode = Pick<AppModule, 'uri' | 'type' | 'kind' | 'exists'>;
55
+ /**
56
+ * A Liquid file's own structural declarations — a by-product of the parse the
57
+ * graph already does, so consumers need not re-parse the file.
58
+ *
59
+ * The usage arrays are ALWAYS present (sorted, de-duplicated): an empty array
60
+ * means "the file uses none", since the whole AST is analyzed — never "not
61
+ * extracted". The routing facts (`slug`/`layout`/`method`) are optional: an
62
+ * absent one means "not applicable / not declared" (e.g. a partial has no slug).
63
+ * `doc_params` lands in a later phase and will appear here when it does.
64
+ */
65
+ export interface ModuleStructural {
66
+ /** Partial names rendered via `{% render %}` / `{% include %}` (the literal target names). */
67
+ renders_used: string[];
68
+ /** Operation names invoked via `{% graphql %}` (the literal target names). */
69
+ graphql_queries_used: string[];
70
+ /** Liquid filter names used anywhere in the file. */
71
+ filters_used: string[];
72
+ /** Liquid tag names used anywhere in the file. */
73
+ tags_used: string[];
74
+ /** Translation keys referenced via the `t` / `translate` filter. */
75
+ translation_keys: string[];
76
+ /** `@param` names declared in the file's `{% doc %}` block. */
77
+ doc_params: string[];
78
+ /**
79
+ * Effective URL slug: the frontmatter `slug` override if declared, else
80
+ * derived from the page's path. Present for page files only.
81
+ */
82
+ slug?: string;
83
+ /** Declared wrapper layout (frontmatter `layout`), when declared. */
84
+ layout?: string;
85
+ /** Declared HTTP method (frontmatter `method`), when declared. */
86
+ method?: string;
87
+ }
23
88
  export interface LiquidModule extends IAppModule<ModuleType.Liquid> {
24
89
  kind: LiquidModuleKind;
90
+ /**
91
+ * The file's own structural declarations, populated during a full
92
+ * `buildAppGraph`. Absent when the file declares none of the surfaced facts.
93
+ */
94
+ structural?: ModuleStructural;
25
95
  }
26
96
  export interface AssetModule extends IAppModule<ModuleType.Asset> {
27
97
  kind: 'unused';
@@ -33,6 +103,34 @@ export interface AssetModule extends IAppModule<ModuleType.Asset> {
33
103
  */
34
104
  export interface GraphQLModule extends IAppModule<ModuleType.GraphQL> {
35
105
  kind: 'graphql';
106
+ /**
107
+ * The platformOS model tables this operation targets (from its `table`
108
+ * filters / `record_create` inputs) — every distinct table in document order.
109
+ * Populated during `buildAppGraph` traversal; always present (empty = the
110
+ * operation declares no table, or was never resolved on disk).
111
+ */
112
+ tables: string[];
113
+ }
114
+ /**
115
+ * A custom model type / schema file (`custom_model_types`/`model_schemas`/
116
+ * `schema` dirs — `PlatformOSFileType.Table`). A platform primitive,
117
+ * modeled as a neutral graph node. A leaf node — schema files have no outgoing
118
+ * platformOS dependencies — and not render-reachable, so it is discovered and
119
+ * added explicitly during a full `buildAppGraph` (never an entry point).
120
+ *
121
+ * NOTE (ADR 004): this is a neutral platform fact. The commands/queries
122
+ * convention and resource/CRUD completeness that build ON schemas are NOT
123
+ * modeled here — they live in the convention overlay.
124
+ */
125
+ export interface SchemaModule extends IAppModule<ModuleType.Schema> {
126
+ kind: 'schema';
127
+ /**
128
+ * The model table name (the schema's top-level `name:`), when declared. A
129
+ * schema declares exactly one, so this is a single value (unlike a GraphQL
130
+ * op's {@link GraphQLModule.tables}) that a consumer can join against. Absent
131
+ * when the file declares no `name:` or could not be parsed.
132
+ */
133
+ table?: string;
36
134
  }
37
135
  export interface IAppModule<T extends ModuleType> {
38
136
  /** Used as a discriminant in the AppModule union */
@@ -61,7 +159,8 @@ export interface IAppModule<T extends ModuleType> {
61
159
  export declare const enum ModuleType {
62
160
  Liquid = "Liquid",
63
161
  Asset = "Asset",
64
- GraphQL = "GraphQL"
162
+ GraphQL = "GraphQL",
163
+ Schema = "Schema"
65
164
  }
66
165
  export declare const enum LiquidModuleKind {
67
166
  /** app/views/layouts/*.liquid files */
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAkHa,QAAA,gCAAgC,GAAG;IAC9C,KAAK;IACL,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,KAAK;CACN,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAqNa,QAAA,gCAAgC,GAAG;IAC9C,KAAK;IACL,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,KAAK;CACN,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { UriString } from '@platformos/platformos-check-common';
2
2
  import { AbstractFileSystem } from '@platformos/platformos-common';
3
3
  export declare function unique<T>(array: T[]): T[];
4
- export declare function assertNever(module: never): void;
4
+ export declare function assertNever(module: never): never;
5
5
  export declare const identity: <T>(x: T) => T;
6
6
  export declare function isString(x: unknown): x is string;
7
7
  export declare function extname(uri: UriString): string;
@@ -0,0 +1 @@
1
+ body { color: red; }
@@ -0,0 +1 @@
1
+ console.log("app");
@@ -0,0 +1,3 @@
1
+ <script src="{{ 'site/app.js' | asset_url }}" defer></script>
2
+ <link rel="stylesheet" href="{{ 'logo.css' | asset_url }}">
3
+ <link rel="icon" href="{{ 'images/missing.png' | asset_url }}">
@@ -0,0 +1,5 @@
1
+ query {
2
+ records(per_page: 1, filter: { table: { value: "blog_post" } }) {
3
+ results { id }
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ query {
2
+ current_user { id email }
3
+ }
@@ -0,0 +1,4 @@
1
+ {% liquid
2
+ graphql posts = 'with_table'
3
+ graphql me = 'without_table'
4
+ %}
@@ -0,0 +1,4 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>{{ content_for_layout }}</body>
4
+ </html>
@@ -0,0 +1,5 @@
1
+ ---
2
+ slug: broken
3
+ layout: ghost
4
+ ---
5
+ <h1>broken</h1>
@@ -0,0 +1,5 @@
1
+ ---
2
+ slug: index
3
+ layout: theme
4
+ ---
5
+ <h1>home</h1>
@@ -0,0 +1,6 @@
1
+ name: blog_post
2
+ properties:
3
+ - name: title
4
+ type: string
5
+ - name: body
6
+ type: text
@@ -0,0 +1,3 @@
1
+ properties:
2
+ - name: anon
3
+ type: string
@@ -0,0 +1 @@
1
+ <h1>home</h1>
@@ -0,0 +1 @@
1
+ <h1>about</h1>
@@ -0,0 +1,4 @@
1
+ ---
2
+ slug: blog/custom
3
+ ---
4
+ <h1>show</h1>
@@ -0,0 +1,6 @@
1
+ ---
2
+ layout: application
3
+ method: get
4
+ ---
5
+ {% render 'card' %}
6
+ {% render 'documented' %}
@@ -0,0 +1,8 @@
1
+ ---
2
+ layout: application
3
+ ---
4
+ {% assign x = 'a' %}
5
+ {% if x %}{{ x | upcase }}{% endif %}
6
+ {% render 'card' %}
7
+ {% graphql res = 'blog/find' %}
8
+ {{ 'greeting.hello' | t }}
@@ -0,0 +1 @@
1
+ <div class="card">{{ title }}</div>
@@ -0,0 +1,5 @@
1
+ {% doc %}
2
+ @param title [String] The card title.
3
+ @param count [Number] How many.
4
+ {% enddoc %}
5
+ <div>{{ title }}</div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformos/platformos-graph",
3
- "version": "0.0.20",
3
+ "version": "0.1.0",
4
4
  "description": "platformOS App Graph as a data structure",
5
5
  "author": "platformOS",
6
6
  "homepage": "https://github.com/Platform-OS/platformos-tools/tree/master/packages/platformos-graph#readme",
@@ -29,13 +29,15 @@
29
29
  "type-check": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@platformos/liquid-html-parser": "^0.0.18",
33
- "@platformos/platformos-check-common": "0.0.20",
34
- "@platformos/platformos-common": "0.0.18",
32
+ "@platformos/liquid-html-parser": "^0.1.0",
33
+ "@platformos/platformos-check-common": "1.0.0",
34
+ "@platformos/platformos-common": "0.1.0",
35
35
  "acorn": "^8.16.0",
36
+ "js-yaml": "^4.1.1",
36
37
  "vscode-uri": "^3.1.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@platformos/platformos-check-node": "0.0.20"
40
+ "@platformos/platformos-check-node": "1.0.0",
41
+ "@types/js-yaml": "^4.0.9"
40
42
  }
41
43
  }
package/src/cli.spec.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import nodePath from 'node:path';
2
2
  import { path } from '@platformos/platformos-check-common';
3
- import { AbstractFileSystem, FileType } from '@platformos/platformos-common';
3
+ import { AbstractFileSystem, FileType, uriFromPath } from '@platformos/platformos-common';
4
4
  import { describe, expect, it } from 'vitest';
5
- import { URI } from 'vscode-uri';
6
5
  import { buildSerializedFileDependencies, buildSerializedGraph, nodeFileSystem } from './cli';
7
6
  import { skeleton } from './graph/test-helpers';
8
7
  import { LiquidModuleKind, ModuleType } from './types';
@@ -16,10 +15,23 @@ describe('platformos-graph CLI: buildSerializedGraph', () => {
16
15
  const graph = await buildSerializedGraph(skeletonPath);
17
16
 
18
17
  // Root is the project path re-expressed as a file URI.
19
- expect(graph.rootUri).toBe(URI.file(skeletonPath).toString(true));
18
+ expect(graph.rootUri).toBe(uriFromPath(skeletonPath));
20
19
 
21
20
  // Whole node set, sorted by URI so the assertion is order-independent.
21
+ // (`app/assets/*` sorts ahead of `app/views/*`.)
22
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
+ },
23
35
  {
24
36
  uri: p('app/views/layouts/application.liquid'),
25
37
  type: ModuleType.Liquid,
@@ -50,18 +62,6 @@ describe('platformos-graph CLI: buildSerializedGraph', () => {
50
62
  kind: LiquidModuleKind.Partial,
51
63
  exists: true,
52
64
  },
53
- {
54
- uri: p('assets/app.css'),
55
- type: ModuleType.Asset,
56
- kind: 'unused',
57
- exists: true,
58
- },
59
- {
60
- uri: p('assets/app.js'),
61
- type: ModuleType.Asset,
62
- kind: 'unused',
63
- exists: true,
64
- },
65
65
  ]);
66
66
 
67
67
  // Whole edge set, reduced to (source, target, type, kind) and sorted, so an
@@ -78,21 +78,27 @@ describe('platformos-graph CLI: buildSerializedGraph', () => {
78
78
  expect(edges).toEqual([
79
79
  {
80
80
  source: p('app/views/layouts/application.liquid'),
81
- target: p('app/views/partials/header.liquid'),
81
+ target: p('app/assets/app.css'),
82
82
  type: 'direct',
83
- kind: 'render',
83
+ kind: 'asset',
84
84
  },
85
85
  {
86
86
  source: p('app/views/layouts/application.liquid'),
87
- target: p('assets/app.css'),
87
+ target: p('app/assets/app.js'),
88
88
  type: 'direct',
89
89
  kind: 'asset',
90
90
  },
91
91
  {
92
92
  source: p('app/views/layouts/application.liquid'),
93
- target: p('assets/app.js'),
93
+ target: p('app/views/partials/header.liquid'),
94
94
  type: 'direct',
95
- kind: 'asset',
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',
96
102
  },
97
103
  {
98
104
  source: p('app/views/pages/index.liquid'),
@@ -118,28 +124,28 @@ describe('platformos-graph CLI: buildSerializedGraph', () => {
118
124
 
119
125
  describe('platformos-graph CLI: nodeFileSystem', () => {
120
126
  it('reads a file by URI', async () => {
121
- const uri = path.join(skeleton, 'assets', 'app.js');
127
+ const uri = path.join(skeleton, 'app', 'assets', 'app.js');
122
128
  expect(await nodeFileSystem.readFile(uri)).toBe(
123
129
  '// Skeleton app bundle (asset reference target).\n',
124
130
  );
125
131
  });
126
132
 
127
133
  it('stats a directory and a file', async () => {
128
- expect(await nodeFileSystem.stat(path.join(skeleton, 'assets'))).toEqual({
134
+ expect(await nodeFileSystem.stat(path.join(skeleton, 'app', 'assets'))).toEqual({
129
135
  type: FileType.Directory,
130
136
  size: expect.any(Number),
131
137
  });
132
- expect(await nodeFileSystem.stat(path.join(skeleton, 'assets', 'app.css'))).toEqual({
138
+ expect(await nodeFileSystem.stat(path.join(skeleton, 'app', 'assets', 'app.css'))).toEqual({
133
139
  type: FileType.File,
134
140
  size: expect.any(Number),
135
141
  });
136
142
  });
137
143
 
138
144
  it('lists a directory as [childUri, FileType] tuples', async () => {
139
- const entries = await nodeFileSystem.readDirectory(path.join(skeleton, 'assets'));
145
+ const entries = await nodeFileSystem.readDirectory(path.join(skeleton, 'app', 'assets'));
140
146
  expect([...entries].sort((a, b) => a[0].localeCompare(b[0]))).toEqual([
141
- [path.join(skeleton, 'assets', 'app.css'), FileType.File],
142
- [path.join(skeleton, 'assets', 'app.js'), FileType.File],
147
+ [path.join(skeleton, 'app', 'assets', 'app.css'), FileType.File],
148
+ [path.join(skeleton, 'app', 'assets', 'app.js'), FileType.File],
143
149
  ]);
144
150
  });
145
151
  });
@@ -225,8 +231,8 @@ describe('platformos-graph CLI: buildSerializedFileDependencies', () => {
225
231
 
226
232
  it('throws for a file that is not part of the app graph', async () => {
227
233
  const missing = fsPathOf('app/views/partials/nonexistent.liquid');
228
- const missingUri = path.normalize(URI.file(missing));
229
- const rootUri = path.normalize(URI.file(skeletonPath));
234
+ const missingUri = uriFromPath(missing);
235
+ const rootUri = uriFromPath(skeletonPath);
230
236
 
231
237
  const error = await buildSerializedFileDependencies(skeletonPath, missing).catch((e) => e);
232
238
  expect(error).toBeInstanceOf(Error);
@@ -252,14 +258,14 @@ describe('platformos-graph CLI: project-root validation', () => {
252
258
 
253
259
  it('throws instead of emitting an empty graph for a non-platformOS directory', async () => {
254
260
  const dir = nodePath.resolve('no-such-project', 'sub');
255
- const startUri = path.normalize(URI.file(dir));
261
+ const startUri = uriFromPath(dir);
256
262
 
257
263
  const error = await buildSerializedGraph(dir, emptyFs).catch((e) => e);
258
264
  expect(error).toBeInstanceOf(Error);
259
265
  expect((error as Error).message).toBe(
260
266
  `Not a platformOS project: ${startUri}\n` +
261
- `No app/, modules/, .pos, or .platformos-check.yml found at or above this path. ` +
262
- `Pass the path to a platformOS app directory.`,
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.`,
263
269
  );
264
270
  });
265
271
  });
package/src/cli.ts CHANGED
@@ -1,8 +1,18 @@
1
1
  import nodePath from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
- import { findRoot, makeFileExists, path } from '@platformos/platformos-check-common';
4
- import { AbstractFileSystem, FileStat, FileTuple, FileType } from '@platformos/platformos-common';
5
- import { URI } from 'vscode-uri';
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';
6
16
  import { buildAppGraph } from './graph/build';
7
17
  import { serializeAppGraph } from './graph/serialize';
8
18
  import { Reference, SerializableGraph } from './types';
@@ -25,7 +35,7 @@ export const nodeFileSystem: AbstractFileSystem = {
25
35
  async readDirectory(uri: string): Promise<FileTuple[]> {
26
36
  const entries = await fs.readdir(path.fsPath(uri), { withFileTypes: true });
27
37
  return entries.map((entry) => [
28
- path.join(uri, entry.name),
38
+ path.childUri(uri, entry.name),
29
39
  entry.isDirectory() ? FileType.Directory : FileType.File,
30
40
  ]);
31
41
  },
@@ -59,8 +69,9 @@ export interface SerializableFileDependencies {
59
69
  /**
60
70
  * Resolves the given root path (absolute, or relative to `process.cwd()`) to the
61
71
  * forward-slash-normalized `file://` URI of the enclosing platformOS project,
62
- * using the same `findRoot` heuristic as the language server (an `app/`,
63
- * `modules/`, `.pos`, or `.platformos-check.yml` marker at or above the path).
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).
64
75
  *
65
76
  * Throws when no project is found, so a typo'd or non-platformOS directory fails
66
77
  * loudly instead of silently producing an empty graph. Resolving via `findRoot`
@@ -70,16 +81,21 @@ export interface SerializableFileDependencies {
70
81
  * `path.normalize` (forward slashes), so any lookup must key the same way or it
71
82
  * silently misses on Windows, where raw URIs keep backslashes.
72
83
  */
73
- async function resolveProjectRoot(root: string, fs: AbstractFileSystem): Promise<string> {
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.
74
87
  const absolute = nodePath.isAbsolute(root) ? root : nodePath.resolve(process.cwd(), root);
75
- const startUri = path.normalize(URI.file(absolute));
76
88
 
77
- const rootUri = await findRoot(startUri, makeFileExists(fs));
89
+ const { given, root: rootUri } = await resolveProjectRoot(absolute, makeFileExists(fs));
78
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');
79
95
  throw new Error(
80
- `Not a platformOS project: ${startUri}\n` +
81
- `No app/, modules/, .pos, or .platformos-check.yml found at or above this path. ` +
82
- `Pass the path to a platformOS app directory.`,
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.`,
83
99
  );
84
100
  }
85
101
  return rootUri;
@@ -93,7 +109,7 @@ async function resolveProjectRoot(root: string, fs: AbstractFileSystem): Promise
93
109
  */
94
110
  function resolveFileUri(rootUri: string, file: string): string {
95
111
  if (nodePath.isAbsolute(file)) {
96
- return path.normalize(URI.file(file));
112
+ return uriFromPath(file);
97
113
  }
98
114
  return path.join(rootUri, ...file.split(/[\\/]+/).filter(Boolean));
99
115
  }
@@ -109,7 +125,7 @@ export async function buildSerializedGraph(
109
125
  root: string,
110
126
  fs: AbstractFileSystem = nodeFileSystem,
111
127
  ): Promise<SerializableGraph> {
112
- const graph = await buildAppGraph(await resolveProjectRoot(root, fs), { fs });
128
+ const graph = await buildAppGraph(await resolveGraphRoot(root, fs), { fs });
113
129
  return serializeAppGraph(graph);
114
130
  }
115
131
 
@@ -130,7 +146,7 @@ export async function buildSerializedFileDependencies(
130
146
  file: string,
131
147
  fs: AbstractFileSystem = nodeFileSystem,
132
148
  ): Promise<SerializableFileDependencies> {
133
- const rootUri = await resolveProjectRoot(root, fs);
149
+ const rootUri = await resolveGraphRoot(root, fs);
134
150
  const fileUri = resolveFileUri(rootUri, file);
135
151
  const graph = await buildAppGraph(rootUri, { fs });
136
152
 
@@ -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 ??