@sightmap/sightmap 0.1.0 → 0.2.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.
- package/README.md +30 -1
- package/dist/browser-ChD_xQt8.d.ts +253 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +353 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli/index.js +336 -79
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +10 -252
- package/dist/index.js +74 -29
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# @sightmap/sightmap
|
|
2
2
|
|
|
3
|
-
Library and CLI for the
|
|
3
|
+
Library and CLI for the Sightmap spec.
|
|
4
|
+
|
|
5
|
+
- **Spec & docs:** [sightmap.org](https://sightmap.org) · [github.com/sightmap/spec](https://github.com/sightmap/spec)
|
|
6
|
+
- **Package:** [`@sightmap/sightmap`](https://www.npmjs.com/package/@sightmap/sightmap) on npm
|
|
7
|
+
- **Source:** [github.com/sightmap/sightmap-js](https://github.com/sightmap/sightmap-js)
|
|
4
8
|
|
|
5
9
|
A Sightmap is a YAML description of a web app's views, components, and API requests — the runtime semantics agents need to navigate and interact with the app. This package gives a coding agent a reliable, machine-readable toolkit for curating the `.sightmap/` directory in a customer's repo.
|
|
6
10
|
|
|
11
|
+
```bash
|
|
12
|
+
npm install -D @sightmap/sightmap
|
|
13
|
+
```
|
|
14
|
+
|
|
7
15
|
## CLI
|
|
8
16
|
|
|
9
17
|
```bash
|
|
10
18
|
sightmap validate [path] # default path: .sightmap
|
|
11
19
|
sightmap lint [path] [--strict] [--rules <list>]
|
|
20
|
+
sightmap check [path] [--stdin] [--level schema|quality] # combined validate + lint
|
|
12
21
|
sightmap match <url> [path] [--method <m>] [--require-view]
|
|
13
22
|
sightmap explain <query> [path] [--by name|path] [--type view|component|request] [--require-hit]
|
|
23
|
+
sightmap check-conventions [path] # default path: .
|
|
14
24
|
```
|
|
15
25
|
|
|
26
|
+
`check` powers the WI-2 advisory hooks: `--stdin` reads YAML from stdin and validates it as a virtual `<stdin>` source. `--level schema` skips lint rules; the default `quality` runs both.
|
|
27
|
+
|
|
16
28
|
Every command supports `--json` for machine-readable output and `--cwd <dir>` for invocation from outside the project.
|
|
17
29
|
|
|
18
30
|
### Examples
|
|
@@ -52,6 +64,17 @@ $ sightmap explain 'app/(tabs)/index.tsx'
|
|
|
52
64
|
component ArcCard matchedAs=path src=app/(tabs)/index.tsx
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
### `sightmap check-conventions [path]`
|
|
68
|
+
|
|
69
|
+
Validate that the repo at `<path>` (default `.`) follows the [filename conventions](https://github.com/sightmap/spec/blob/main/docs/repo-conventions.md) for SEPs (`seps/NNNN-{slug}.md`) and conformance fixtures (`conformance/NNN-{slug}.fixture/`). Exit code `0` if clean, `1` if any violations. Pass `--json` for machine-readable output.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
sightmap check-conventions .
|
|
73
|
+
sightmap check-conventions /path/to/repo --json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Unlike `validate`/`lint`/`match`/`explain`, this command takes a *repo root*, not a `.sightmap/` directory. It only inspects `seps/` and `conformance/`; everything else (including `spec/`, `docs/`, and top-level project files) is ignored.
|
|
77
|
+
|
|
55
78
|
### Exit codes
|
|
56
79
|
|
|
57
80
|
- `0` — success
|
|
@@ -103,6 +126,8 @@ const issues = await lint(sightmap, { root: process.cwd() });
|
|
|
103
126
|
|
|
104
127
|
`parse()` throws on invalid input; `validate()` returns a non-throwing `Result`. **Downstream packages should consume `validate()`,** not `parse()`. `parse()` is sugar for end-user code that knows the input is valid.
|
|
105
128
|
|
|
129
|
+
For the canonical spec semantics — schema fields, route glob syntax, scope rules, and the design rationale — see [sightmap.org](https://sightmap.org) and [`spec/v1/schema.md`](https://github.com/sightmap/spec/blob/main/spec/v1/schema.md) in the spec repo.
|
|
130
|
+
|
|
106
131
|
### Diagnostics
|
|
107
132
|
|
|
108
133
|
Lint and validate share a `Diagnostic` shape:
|
|
@@ -133,6 +158,10 @@ Codes are stable, kebab-case, and exported as constants:
|
|
|
133
158
|
| `route-shadowing` | warning | `lint` |
|
|
134
159
|
| `unknown-source` | warning | `lint` |
|
|
135
160
|
| `selector-syntax` | warning | `lint` |
|
|
161
|
+
| `convention.sep-filename` | error | `check-conventions` |
|
|
162
|
+
| `convention.fixture-dirname` | error | `check-conventions` |
|
|
163
|
+
| `convention.invalid-slug` | error | `check-conventions` |
|
|
164
|
+
| `convention.unexpected-file` | error | `check-conventions` |
|
|
136
165
|
|
|
137
166
|
Renames require an SEP. Future SDK ports (Python, Go) MUST emit identical codes for the same conditions.
|
|
138
167
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-level memory entries — surfaced whenever any definition from this file is active.
|
|
3
|
+
*/
|
|
4
|
+
type Memory = string[];
|
|
5
|
+
/**
|
|
6
|
+
* Freeform notes surfaced to agents at runtime in the [Guide] section.
|
|
7
|
+
*/
|
|
8
|
+
type Memory1 = string[];
|
|
9
|
+
/**
|
|
10
|
+
* A CSS selector, or a list of alternative selectors tried in order.
|
|
11
|
+
*/
|
|
12
|
+
type Selector = string | [string, ...string[]];
|
|
13
|
+
/**
|
|
14
|
+
* A sightmap file: one YAML document under .sightmap/ describing views, components, requests, and memory entries. See https://github.com/sightmap/spec/blob/main/spec/v1/schema.md for the human-readable reference.
|
|
15
|
+
*/
|
|
16
|
+
interface SightmapV1 {
|
|
17
|
+
/**
|
|
18
|
+
* Must be 1 for files that conform to this version of the spec.
|
|
19
|
+
*/
|
|
20
|
+
version: 1;
|
|
21
|
+
memory?: Memory;
|
|
22
|
+
views?: View[];
|
|
23
|
+
/**
|
|
24
|
+
* Global components — matched against every view.
|
|
25
|
+
*/
|
|
26
|
+
components?: Component[];
|
|
27
|
+
/**
|
|
28
|
+
* Global requests — matched against every view.
|
|
29
|
+
*/
|
|
30
|
+
requests?: Request[];
|
|
31
|
+
}
|
|
32
|
+
interface View {
|
|
33
|
+
name: string;
|
|
34
|
+
/**
|
|
35
|
+
* Glob pattern matched against the URL pathname. * matches one segment, ** matches any depth.
|
|
36
|
+
*/
|
|
37
|
+
route: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
source?: string;
|
|
40
|
+
memory?: Memory1;
|
|
41
|
+
components?: Component[];
|
|
42
|
+
requests?: Request[];
|
|
43
|
+
}
|
|
44
|
+
interface Component {
|
|
45
|
+
name: string;
|
|
46
|
+
selector: Selector;
|
|
47
|
+
source?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
memory?: Memory1;
|
|
50
|
+
/**
|
|
51
|
+
* Nested components. Selectors are scoped to the parent's matched subtree.
|
|
52
|
+
*/
|
|
53
|
+
children?: Component[];
|
|
54
|
+
}
|
|
55
|
+
interface Request {
|
|
56
|
+
name: string;
|
|
57
|
+
/**
|
|
58
|
+
* Glob pattern. Express-style :param segments normalize to *.
|
|
59
|
+
*/
|
|
60
|
+
route: string;
|
|
61
|
+
/**
|
|
62
|
+
* HTTP method filter. Case-insensitive; commonly GET, POST, PUT, PATCH, DELETE.
|
|
63
|
+
*/
|
|
64
|
+
method?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
source?: string;
|
|
67
|
+
request?: Payload;
|
|
68
|
+
response?: Payload;
|
|
69
|
+
headers?: string[];
|
|
70
|
+
memory?: Memory1;
|
|
71
|
+
}
|
|
72
|
+
interface Payload {
|
|
73
|
+
fields?: Field[];
|
|
74
|
+
}
|
|
75
|
+
interface Field {
|
|
76
|
+
name: string;
|
|
77
|
+
type?: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Severity = "error" | "warning" | "info";
|
|
82
|
+
interface Diagnostic {
|
|
83
|
+
severity: Severity;
|
|
84
|
+
code: string;
|
|
85
|
+
message: string;
|
|
86
|
+
file?: string;
|
|
87
|
+
path?: string;
|
|
88
|
+
loc?: {
|
|
89
|
+
line: number;
|
|
90
|
+
column: number;
|
|
91
|
+
};
|
|
92
|
+
source?: string;
|
|
93
|
+
}
|
|
94
|
+
declare const PARSE_ERROR = "parse-error";
|
|
95
|
+
declare const SCHEMA_VALIDATION_FAILED = "schema-validation-failed";
|
|
96
|
+
declare const UNKNOWN_VERSION = "unknown-version";
|
|
97
|
+
declare const MERGE_COLLISION_VIEW = "merge-collision-view";
|
|
98
|
+
declare const MERGE_COLLISION_COMPONENT = "merge-collision-component";
|
|
99
|
+
declare const DUPLICATE_VIEW_NAME = "duplicate-view-name";
|
|
100
|
+
declare const DUPLICATE_ROUTE = "duplicate-route";
|
|
101
|
+
declare const ROUTE_SHADOWING = "route-shadowing";
|
|
102
|
+
declare const UNKNOWN_SOURCE = "unknown-source";
|
|
103
|
+
declare const SELECTOR_SYNTAX = "selector-syntax";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A single-file fragment after parsing and selector normalization.
|
|
107
|
+
* Branded to prevent querying an unmerged fragment.
|
|
108
|
+
*/
|
|
109
|
+
interface SightmapFragment extends SightmapV1 {
|
|
110
|
+
readonly __brand: "SightmapFragment";
|
|
111
|
+
readonly __sourceFile?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A merged, queryable sightmap produced by `merge()` or `loadDirectory()`.
|
|
115
|
+
*/
|
|
116
|
+
interface Sightmap {
|
|
117
|
+
readonly version: 1;
|
|
118
|
+
readonly views: readonly View[];
|
|
119
|
+
readonly globalComponents: readonly Component[];
|
|
120
|
+
readonly globalRequests: readonly Request[];
|
|
121
|
+
readonly fileMemory: readonly {
|
|
122
|
+
memory: string[];
|
|
123
|
+
sourceFile: string;
|
|
124
|
+
}[];
|
|
125
|
+
/**
|
|
126
|
+
* Diagnostics produced during merge (e.g. collisions, duplicate names across files).
|
|
127
|
+
* Lint diagnostics are returned separately by `lint()`.
|
|
128
|
+
*/
|
|
129
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
130
|
+
readonly __brand: "Sightmap";
|
|
131
|
+
}
|
|
132
|
+
interface ResolvedView {
|
|
133
|
+
name: string;
|
|
134
|
+
route: string;
|
|
135
|
+
source?: string;
|
|
136
|
+
description?: string;
|
|
137
|
+
memory: string[];
|
|
138
|
+
definedIn: {
|
|
139
|
+
file: string;
|
|
140
|
+
line?: number;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
interface ResolvedComponent {
|
|
144
|
+
name: string;
|
|
145
|
+
selector: string[];
|
|
146
|
+
source?: string;
|
|
147
|
+
description?: string;
|
|
148
|
+
memory: string[];
|
|
149
|
+
parentChain: string[];
|
|
150
|
+
scope: "global" | "view-scoped";
|
|
151
|
+
/** Present iff `scope === "view-scoped"`. */
|
|
152
|
+
scopedToView?: string;
|
|
153
|
+
definedIn: {
|
|
154
|
+
file: string;
|
|
155
|
+
line?: number;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
interface ResolvedRequest {
|
|
159
|
+
name: string;
|
|
160
|
+
route: string;
|
|
161
|
+
method?: string;
|
|
162
|
+
source?: string;
|
|
163
|
+
description?: string;
|
|
164
|
+
request?: {
|
|
165
|
+
fields: readonly Field[];
|
|
166
|
+
};
|
|
167
|
+
response?: {
|
|
168
|
+
fields: readonly Field[];
|
|
169
|
+
};
|
|
170
|
+
headers?: readonly string[];
|
|
171
|
+
memory: string[];
|
|
172
|
+
definedIn: {
|
|
173
|
+
file: string;
|
|
174
|
+
line?: number;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
interface MatchResult {
|
|
178
|
+
view: ResolvedView | null;
|
|
179
|
+
components: ResolvedComponent[];
|
|
180
|
+
requests: ResolvedRequest[];
|
|
181
|
+
memory: string[];
|
|
182
|
+
}
|
|
183
|
+
type ExplainMatchedAs = "name" | "path" | "name-and-path";
|
|
184
|
+
/**
|
|
185
|
+
* Discriminated union: narrowing on `type` automatically narrows `entry`.
|
|
186
|
+
* if (hit.type === "view") { hit.entry.route; } // no cast needed
|
|
187
|
+
*/
|
|
188
|
+
type ExplainHit = {
|
|
189
|
+
type: "view";
|
|
190
|
+
matchedAs: ExplainMatchedAs;
|
|
191
|
+
entry: ResolvedView;
|
|
192
|
+
} | {
|
|
193
|
+
type: "component";
|
|
194
|
+
matchedAs: ExplainMatchedAs;
|
|
195
|
+
entry: ResolvedComponent;
|
|
196
|
+
} | {
|
|
197
|
+
type: "request";
|
|
198
|
+
matchedAs: ExplainMatchedAs;
|
|
199
|
+
entry: ResolvedRequest;
|
|
200
|
+
};
|
|
201
|
+
interface ExplainResult {
|
|
202
|
+
query: string;
|
|
203
|
+
hits: ExplainHit[];
|
|
204
|
+
}
|
|
205
|
+
/** Result returned by validate() — never throws. */
|
|
206
|
+
type ValidateResult = {
|
|
207
|
+
ok: true;
|
|
208
|
+
value: SightmapFragment;
|
|
209
|
+
} | {
|
|
210
|
+
ok: false;
|
|
211
|
+
diagnostics: readonly Diagnostic[];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
interface ParseOptions {
|
|
215
|
+
/** Optional source file path; recorded on the fragment for canonical-order merging. */
|
|
216
|
+
sourceFile?: string;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Parse a single sightmap file (YAML string or pre-parsed object).
|
|
220
|
+
* Throws on parse error, missing version, or version mismatch.
|
|
221
|
+
* Normalizes `selector: string` to `selector: [string]` everywhere.
|
|
222
|
+
*/
|
|
223
|
+
declare function parse(input: string | object, opts?: ParseOptions): SightmapFragment;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Merge fragments into a queryable Sightmap.
|
|
227
|
+
*
|
|
228
|
+
* Canonical order: fragments are sorted by `__sourceFile` (code-point order; locale-independent
|
|
229
|
+
* for cross-environment determinism). Fragments without a `__sourceFile` sort first, and ES2019
|
|
230
|
+
* sort stability preserves their input order. Within each fragment, declaration order is
|
|
231
|
+
* preserved. The merged collection's order = (sourceFile order, then declaration order).
|
|
232
|
+
*
|
|
233
|
+
* Duplicate view names produce `merge-collision-view` warnings; duplicate global component
|
|
234
|
+
* names produce `merge-collision-component`. The first occurrence wins.
|
|
235
|
+
*
|
|
236
|
+
* Returned arrays are fresh, but element objects (View/Component/Request) are shared with
|
|
237
|
+
* the input fragments — callers must not mutate them.
|
|
238
|
+
*/
|
|
239
|
+
declare function merge(fragments: SightmapFragment[]): Sightmap;
|
|
240
|
+
|
|
241
|
+
interface MatchOptions {
|
|
242
|
+
url: string;
|
|
243
|
+
method?: string;
|
|
244
|
+
}
|
|
245
|
+
declare function match(sightmap: Sightmap, opts: MatchOptions): MatchResult;
|
|
246
|
+
|
|
247
|
+
interface ExplainOptions {
|
|
248
|
+
by?: "name" | "path";
|
|
249
|
+
type?: "view" | "component" | "request";
|
|
250
|
+
}
|
|
251
|
+
declare function explain(sightmap: Sightmap, query: string, opts?: ExplainOptions): ExplainResult;
|
|
252
|
+
|
|
253
|
+
export { type Component as C, type Diagnostic as D, type ExplainHit as E, type Field as F, MERGE_COLLISION_COMPONENT as M, PARSE_ERROR as P, ROUTE_SHADOWING as R, type Sightmap as S, UNKNOWN_SOURCE as U, type ValidateResult as V, type SightmapV1 as a, DUPLICATE_ROUTE as b, DUPLICATE_VIEW_NAME as c, type ExplainMatchedAs as d, type ExplainResult as e, MERGE_COLLISION_VIEW as f, type MatchResult as g, type Request as h, type ResolvedComponent as i, type ResolvedRequest as j, type ResolvedView as k, SCHEMA_VALIDATION_FAILED as l, SELECTOR_SYNTAX as m, type Severity as n, type SightmapFragment as o, UNKNOWN_VERSION as p, type View as q, explain as r, match as s, merge as t, parse as u };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { E as ExplainHit, d as ExplainMatchedAs, e as ExplainResult, g as MatchResult, i as ResolvedComponent, j as ResolvedRequest, k as ResolvedView, S as Sightmap, o as SightmapFragment, r as explain, s as match, t as merge, u as parse } from './browser-ChD_xQt8.js';
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// src/parse.ts
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
function parse(input, opts = {}) {
|
|
4
|
+
let doc;
|
|
5
|
+
if (typeof input === "string") {
|
|
6
|
+
try {
|
|
7
|
+
doc = yaml.load(input);
|
|
8
|
+
} catch (err) {
|
|
9
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10
|
+
throw new Error(`YAML parse error: ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
} else {
|
|
13
|
+
doc = { ...input };
|
|
14
|
+
}
|
|
15
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
16
|
+
throw new Error("Expected sightmap document to be an object at the root");
|
|
17
|
+
}
|
|
18
|
+
const obj = doc;
|
|
19
|
+
if (obj["version"] === void 0) {
|
|
20
|
+
throw new Error("Missing required `version` field");
|
|
21
|
+
}
|
|
22
|
+
if (obj["version"] !== 1) {
|
|
23
|
+
throw new Error(`Unsupported version: ${String(obj["version"])} (expected 1)`);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(obj["components"])) {
|
|
26
|
+
obj["components"] = obj["components"].map(normalizeComponent);
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(obj["views"])) {
|
|
29
|
+
obj["views"] = obj["views"].map((v) => {
|
|
30
|
+
const out = { ...v };
|
|
31
|
+
if (Array.isArray(out["components"])) {
|
|
32
|
+
out["components"] = out["components"].map(normalizeComponent);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const fragment = {
|
|
38
|
+
...obj,
|
|
39
|
+
__brand: "SightmapFragment",
|
|
40
|
+
...opts.sourceFile !== void 0 ? { __sourceFile: opts.sourceFile } : {}
|
|
41
|
+
};
|
|
42
|
+
return fragment;
|
|
43
|
+
}
|
|
44
|
+
function normalizeComponent(c) {
|
|
45
|
+
if (c["selectors"] !== void 0 && c.selector === void 0) {
|
|
46
|
+
const name = c.name ?? "(unnamed)";
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Component "${name}": use \`selector\` (singular), not \`selectors\` (plural). \`selector\` accepts either a single string or an array of strings.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const sel = c.selector;
|
|
52
|
+
const normalized = {
|
|
53
|
+
...c,
|
|
54
|
+
selector: typeof sel === "string" ? [sel] : sel
|
|
55
|
+
};
|
|
56
|
+
if (Array.isArray(c.children)) {
|
|
57
|
+
normalized.children = c.children.map(normalizeComponent);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/diagnostics.ts
|
|
63
|
+
var MERGE_COLLISION_VIEW = "merge-collision-view";
|
|
64
|
+
var MERGE_COLLISION_COMPONENT = "merge-collision-component";
|
|
65
|
+
|
|
66
|
+
// src/merge.ts
|
|
67
|
+
function merge(fragments) {
|
|
68
|
+
const sorted = [...fragments].sort((a, b) => {
|
|
69
|
+
const aFile = a.__sourceFile ?? "";
|
|
70
|
+
const bFile = b.__sourceFile ?? "";
|
|
71
|
+
return aFile < bFile ? -1 : aFile > bFile ? 1 : 0;
|
|
72
|
+
});
|
|
73
|
+
const views = [];
|
|
74
|
+
const globalComponents = [];
|
|
75
|
+
const globalRequests = [];
|
|
76
|
+
const fileMemory = [];
|
|
77
|
+
const diagnostics = [];
|
|
78
|
+
const seenViewNames = /* @__PURE__ */ new Map();
|
|
79
|
+
const seenComponentNames = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const f of sorted) {
|
|
81
|
+
const file = f.__sourceFile ?? "<unknown>";
|
|
82
|
+
for (const v of f.views ?? []) {
|
|
83
|
+
const prev = seenViewNames.get(v.name);
|
|
84
|
+
if (prev !== void 0) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: "warning",
|
|
87
|
+
code: MERGE_COLLISION_VIEW,
|
|
88
|
+
message: `View name "${v.name}" defined in both "${prev}" and "${file}"; first occurrence wins.`,
|
|
89
|
+
file
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
seenViewNames.set(v.name, file);
|
|
93
|
+
}
|
|
94
|
+
views.push(v);
|
|
95
|
+
}
|
|
96
|
+
for (const c of f.components ?? []) {
|
|
97
|
+
const prev = seenComponentNames.get(c.name);
|
|
98
|
+
if (prev !== void 0) {
|
|
99
|
+
diagnostics.push({
|
|
100
|
+
severity: "warning",
|
|
101
|
+
code: MERGE_COLLISION_COMPONENT,
|
|
102
|
+
message: `Component name "${c.name}" defined in both "${prev}" and "${file}"; first occurrence wins.`,
|
|
103
|
+
file
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
seenComponentNames.set(c.name, file);
|
|
107
|
+
}
|
|
108
|
+
globalComponents.push(c);
|
|
109
|
+
}
|
|
110
|
+
for (const r of f.requests ?? []) {
|
|
111
|
+
globalRequests.push(r);
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(f.memory) && f.memory.length > 0) {
|
|
114
|
+
fileMemory.push({ memory: [...f.memory], sourceFile: file });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
version: 1,
|
|
119
|
+
views,
|
|
120
|
+
globalComponents,
|
|
121
|
+
globalRequests,
|
|
122
|
+
fileMemory,
|
|
123
|
+
diagnostics,
|
|
124
|
+
__brand: "Sightmap"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/routeMatch.ts
|
|
129
|
+
function canonicalizeUrl(input) {
|
|
130
|
+
let s = input;
|
|
131
|
+
const protoMatch = /^[a-z][a-z0-9+.-]*:\/\/[^/]*/i.exec(s);
|
|
132
|
+
if (protoMatch) {
|
|
133
|
+
s = s.slice(protoMatch[0].length) || "/";
|
|
134
|
+
}
|
|
135
|
+
const hash = s.indexOf("#");
|
|
136
|
+
if (hash !== -1) s = s.slice(0, hash);
|
|
137
|
+
const q = s.indexOf("?");
|
|
138
|
+
if (q !== -1) s = s.slice(0, q);
|
|
139
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
140
|
+
return s;
|
|
141
|
+
}
|
|
142
|
+
function routeMatch(pattern, url) {
|
|
143
|
+
const p = normalizePattern(pattern);
|
|
144
|
+
const u = canonicalizeUrl(url);
|
|
145
|
+
const patternSegs = splitSegments(p);
|
|
146
|
+
const urlSegs = splitSegments(u);
|
|
147
|
+
return matchSegs(patternSegs, urlSegs);
|
|
148
|
+
}
|
|
149
|
+
function normalizePattern(p) {
|
|
150
|
+
let s = p.split("/").map((seg) => seg.startsWith(":") ? "*" : seg).join("/");
|
|
151
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
152
|
+
return s;
|
|
153
|
+
}
|
|
154
|
+
function splitSegments(path) {
|
|
155
|
+
if (path === "/" || path === "") return [];
|
|
156
|
+
const trimmed = path.startsWith("/") ? path.slice(1) : path;
|
|
157
|
+
return trimmed.split("/");
|
|
158
|
+
}
|
|
159
|
+
function matchSegs(pattern, url) {
|
|
160
|
+
if (pattern.length === 0 && url.length === 0) return true;
|
|
161
|
+
if (pattern.length === 0) return false;
|
|
162
|
+
const head = pattern[0];
|
|
163
|
+
const rest = pattern.slice(1);
|
|
164
|
+
if (head === "**") {
|
|
165
|
+
if (rest.length === 0) return true;
|
|
166
|
+
for (let i = 0; i <= url.length; i++) {
|
|
167
|
+
if (matchSegs(rest, url.slice(i))) return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (url.length === 0) return false;
|
|
172
|
+
if (head === "*" || head === url[0]) {
|
|
173
|
+
return matchSegs(rest, url.slice(1));
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/resolver.ts
|
|
179
|
+
function resolveByUrl(sightmap, url, method) {
|
|
180
|
+
let matchedView = null;
|
|
181
|
+
for (const v of sightmap.views) {
|
|
182
|
+
if (routeMatch(v.route, url)) {
|
|
183
|
+
matchedView = v;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const components = [];
|
|
188
|
+
for (const c of sightmap.globalComponents) {
|
|
189
|
+
components.push(...flattenComponent(c, [], "global", void 0));
|
|
190
|
+
}
|
|
191
|
+
if (matchedView !== null && Array.isArray(matchedView.components)) {
|
|
192
|
+
for (const c of matchedView.components) {
|
|
193
|
+
components.push(...flattenComponent(c, [], "view-scoped", matchedView.name));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const requests = [];
|
|
197
|
+
const requestPool = [
|
|
198
|
+
...sightmap.globalRequests,
|
|
199
|
+
...matchedView?.requests ?? []
|
|
200
|
+
];
|
|
201
|
+
for (const req of requestPool) {
|
|
202
|
+
if (!routeMatch(req.route, url)) continue;
|
|
203
|
+
if (method !== void 0 && req.method !== void 0 && req.method !== method) continue;
|
|
204
|
+
requests.push(toResolvedRequest(req));
|
|
205
|
+
}
|
|
206
|
+
const memory = [];
|
|
207
|
+
for (const fm of sightmap.fileMemory) memory.push(...fm.memory);
|
|
208
|
+
if (matchedView?.memory) memory.push(...matchedView.memory);
|
|
209
|
+
return {
|
|
210
|
+
view: matchedView !== null ? toResolvedView(matchedView) : null,
|
|
211
|
+
components,
|
|
212
|
+
requests,
|
|
213
|
+
memory
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function resolveByName(sightmap, name) {
|
|
217
|
+
const hits = [];
|
|
218
|
+
for (const v of sightmap.views) {
|
|
219
|
+
if (v.name === name) hits.push({ type: "view", matchedAs: "name", entry: toResolvedView(v) });
|
|
220
|
+
}
|
|
221
|
+
for (const c of sightmap.globalComponents) {
|
|
222
|
+
for (const rc of flattenComponent(c, [], "global", void 0)) {
|
|
223
|
+
if (rc.name === name) hits.push({ type: "component", matchedAs: "name", entry: rc });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const v of sightmap.views) {
|
|
227
|
+
for (const c of v.components ?? []) {
|
|
228
|
+
for (const rc of flattenComponent(c, [], "view-scoped", v.name)) {
|
|
229
|
+
if (rc.name === name) hits.push({ type: "component", matchedAs: "name", entry: rc });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const r of sightmap.globalRequests) {
|
|
234
|
+
if (r.name === name) hits.push({ type: "request", matchedAs: "name", entry: toResolvedRequest(r) });
|
|
235
|
+
}
|
|
236
|
+
for (const v of sightmap.views) {
|
|
237
|
+
for (const r of v.requests ?? []) {
|
|
238
|
+
if (r.name === name) hits.push({ type: "request", matchedAs: "name", entry: toResolvedRequest(r) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return hits;
|
|
242
|
+
}
|
|
243
|
+
function resolveBySourcePath(sightmap, path) {
|
|
244
|
+
const hits = [];
|
|
245
|
+
for (const v of sightmap.views) {
|
|
246
|
+
if (v.source === path) hits.push({ type: "view", matchedAs: "path", entry: toResolvedView(v) });
|
|
247
|
+
}
|
|
248
|
+
for (const c of sightmap.globalComponents) {
|
|
249
|
+
for (const rc of flattenComponent(c, [], "global", void 0)) {
|
|
250
|
+
if (rc.source === path) hits.push({ type: "component", matchedAs: "path", entry: rc });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const v of sightmap.views) {
|
|
254
|
+
for (const c of v.components ?? []) {
|
|
255
|
+
for (const rc of flattenComponent(c, [], "view-scoped", v.name)) {
|
|
256
|
+
if (rc.source === path) hits.push({ type: "component", matchedAs: "path", entry: rc });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const r of sightmap.globalRequests) {
|
|
261
|
+
if (r.source === path) hits.push({ type: "request", matchedAs: "path", entry: toResolvedRequest(r) });
|
|
262
|
+
}
|
|
263
|
+
return hits;
|
|
264
|
+
}
|
|
265
|
+
function flattenComponent(c, parentChain, scope, scopedToView) {
|
|
266
|
+
const out = [];
|
|
267
|
+
const selector = Array.isArray(c.selector) ? c.selector : [c.selector];
|
|
268
|
+
out.push({
|
|
269
|
+
name: c.name,
|
|
270
|
+
selector,
|
|
271
|
+
...c.source !== void 0 ? { source: c.source } : {},
|
|
272
|
+
...c.description !== void 0 ? { description: c.description } : {},
|
|
273
|
+
memory: c.memory ? [...c.memory] : [],
|
|
274
|
+
parentChain: [...parentChain],
|
|
275
|
+
scope,
|
|
276
|
+
...scopedToView !== void 0 ? { scopedToView } : {},
|
|
277
|
+
definedIn: { file: "<unknown>" }
|
|
278
|
+
});
|
|
279
|
+
for (const child of c.children ?? []) {
|
|
280
|
+
out.push(...flattenComponent(child, [...parentChain, c.name], scope, scopedToView));
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
function toResolvedView(v) {
|
|
285
|
+
return {
|
|
286
|
+
name: v.name,
|
|
287
|
+
route: v.route,
|
|
288
|
+
...v.source !== void 0 ? { source: v.source } : {},
|
|
289
|
+
...v.description !== void 0 ? { description: v.description } : {},
|
|
290
|
+
memory: v.memory ? [...v.memory] : [],
|
|
291
|
+
definedIn: { file: "<unknown>" }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function toResolvedRequest(r) {
|
|
295
|
+
return {
|
|
296
|
+
name: r.name,
|
|
297
|
+
route: r.route,
|
|
298
|
+
...r.method !== void 0 ? { method: r.method } : {},
|
|
299
|
+
...r.source !== void 0 ? { source: r.source } : {},
|
|
300
|
+
...r.description !== void 0 ? { description: r.description } : {},
|
|
301
|
+
...r.request !== void 0 ? { request: { fields: r.request.fields ?? [] } } : {},
|
|
302
|
+
...r.response !== void 0 ? { response: { fields: r.response.fields ?? [] } } : {},
|
|
303
|
+
...r.headers !== void 0 ? { headers: r.headers } : {},
|
|
304
|
+
memory: r.memory ? [...r.memory] : [],
|
|
305
|
+
definedIn: { file: "<unknown>" }
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/match.ts
|
|
310
|
+
function match(sightmap, opts) {
|
|
311
|
+
return resolveByUrl(sightmap, opts.url, opts.method);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/explain.ts
|
|
315
|
+
function explain(sightmap, query, opts = {}) {
|
|
316
|
+
const byName = opts.by === "path" ? [] : resolveByName(sightmap, query);
|
|
317
|
+
const byPath = opts.by === "name" ? [] : resolveBySourcePath(sightmap, query);
|
|
318
|
+
const key = (h) => `${h.type}:${h.entry.name}`;
|
|
319
|
+
const namedKeys = new Set(byName.map(key));
|
|
320
|
+
const pathKeys = new Set(byPath.map(key));
|
|
321
|
+
const merged = [];
|
|
322
|
+
for (const h of byName) {
|
|
323
|
+
if (pathKeys.has(key(h))) {
|
|
324
|
+
merged.push(withMatchedAs(h, "name-and-path"));
|
|
325
|
+
} else {
|
|
326
|
+
merged.push(h);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const h of byPath) {
|
|
330
|
+
if (!namedKeys.has(key(h))) {
|
|
331
|
+
merged.push(h);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const filtered = opts.type !== void 0 ? merged.filter((h) => h.type === opts.type) : merged;
|
|
335
|
+
return { query, hits: filtered };
|
|
336
|
+
}
|
|
337
|
+
function withMatchedAs(h, matchedAs) {
|
|
338
|
+
switch (h.type) {
|
|
339
|
+
case "view":
|
|
340
|
+
return { type: "view", matchedAs, entry: h.entry };
|
|
341
|
+
case "component":
|
|
342
|
+
return { type: "component", matchedAs, entry: h.entry };
|
|
343
|
+
case "request":
|
|
344
|
+
return { type: "request", matchedAs, entry: h.entry };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export {
|
|
348
|
+
explain,
|
|
349
|
+
match,
|
|
350
|
+
merge,
|
|
351
|
+
parse
|
|
352
|
+
};
|
|
353
|
+
//# sourceMappingURL=browser.js.map
|