@jskit-ai/architecture-inspector 0.1.0 → 0.1.1
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 +99 -10
- package/package.json +4 -2
- package/src/calls/cache.js +96 -0
- package/src/calls/evaluate.js +469 -0
- package/src/calls/features.js +273 -0
- package/src/calls/functions.js +204 -0
- package/src/calls/index.js +300 -0
- package/src/calls/program.js +143 -0
- package/src/calls/resolve.js +719 -0
- package/src/calls/symbols.js +178 -0
- package/src/calls/transports.js +194 -0
- package/src/cli.js +93 -14
- package/src/inspectVueArchitecture.js +457 -43
- package/src/previewLocator.js +30 -0
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Architecture Inspector
|
|
2
2
|
|
|
3
3
|
`@jskit-ai/architecture-inspector/vue` exports
|
|
4
|
-
`inspectVueArchitecture({ projectRoot, input, aliases, httpScope, apiBasePath })`
|
|
4
|
+
`inspectVueArchitecture({ projectRoot, input, aliases, httpScope, apiBasePath, calls, apiPrefixAliases })`
|
|
5
5
|
for finite static architecture inspection, and ships the `jskit-inspect-vue`
|
|
6
|
-
command. It is a plain tool
|
|
6
|
+
command and the Vue Preview locator `src/previewLocator.js`. It is a plain tool
|
|
7
|
+
package, not a JSKIT framework runtime package.
|
|
7
8
|
|
|
8
9
|
## Architecture inspection
|
|
9
10
|
|
|
@@ -16,7 +17,11 @@ Run `jskit-inspect-vue` in the project directory. It reads one bounded JSON
|
|
|
16
17
|
document from stdin and writes one `genesis.architecture-observations.v1` JSON
|
|
17
18
|
document to stdout. Errors use stderr and a failing exit status. It does not
|
|
18
19
|
import application code, start a server, generate router declarations or retain
|
|
19
|
-
a watcher. The package has no Genesis or Vibe64 dependency.
|
|
20
|
+
a watcher. The package has no Genesis or Vibe64 dependency. The command runs the
|
|
21
|
+
inspection in a child Node process whose old generation is bounded by the size
|
|
22
|
+
of the declared scripts (192 MB plus 56 MB per MiB), so memory stays near what
|
|
23
|
+
the inspection holds; a caller that starts Node with its own
|
|
24
|
+
`--max-old-space-size` keeps that bound and runs in place.
|
|
20
25
|
|
|
21
26
|
Input supplies `schema: "vibe64.architecture-inspection-input.v1"`, indexed
|
|
22
27
|
`files: [{ path, hash }]` and `uiDiscovery.discoveries`. Optional `packages`
|
|
@@ -24,7 +29,8 @@ carry `id`, `name`, `manifest`, `declarations`, `root`, `opaque` and public
|
|
|
24
29
|
`entryPoints: [{ importPath, file }]`; `packageIdentity` binds observations to
|
|
25
30
|
the same inspected public boundaries. Paths are project-relative
|
|
26
31
|
and hashes use `sha256:<hex>`. Only declared, hash-matching UTF-8 files are read;
|
|
27
|
-
symlinks and files over 4 MiB are rejected.
|
|
32
|
+
symlinks and files over 4 MiB are rejected. Optional `stackHash` and
|
|
33
|
+
`cacheDirectory` serve the call cache below; other unknown fields are ignored.
|
|
28
34
|
|
|
29
35
|
Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
|
|
30
36
|
`pageRoots` and `componentRoots`. Supported adapters are `jskit`,
|
|
@@ -49,9 +55,12 @@ Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
|
|
|
49
55
|
`default`, namespace `*` and type-only classification. These are import facts,
|
|
50
56
|
not evidence of runtime calls. Coverage remains partial; namespace use does
|
|
51
57
|
not establish which individual exports are consumed.
|
|
52
|
-
- An
|
|
58
|
+
- An HTTP scope names the application's operations and enables scoped literal
|
|
53
59
|
`fetch` uses and receivers registered through JSKIT's `defineFeature` setup
|
|
54
|
-
`http.router.register`.
|
|
60
|
+
`http.router.register`. It is the root `package.json` `name`, or
|
|
61
|
+
`--http-scope shop` when the operations should carry another name; without
|
|
62
|
+
either, HTTP observations stay unconfigured. `--api-base-path /api` prefixes
|
|
63
|
+
`http.router.register` paths. Use `--alias @=src` for declared project aliases.
|
|
55
64
|
- Actual `CrudListScreen` / `CrudAddEditScreen` uses whose bound screen comes from
|
|
56
65
|
the matching JSKIT composable can connect to exported `defineCrudJsonApiFeature`
|
|
57
66
|
controller declarations. The resource must resolve through indexed imports and
|
|
@@ -62,6 +71,85 @@ Each discovery supplies `id`, `adapter`, `surface`, `mode`, `registrations`,
|
|
|
62
71
|
overrides, dynamic operations and disabled routes do not create guessed links.
|
|
63
72
|
This shows declared entry points; runtime provider activation is not proven.
|
|
64
73
|
|
|
74
|
+
- Routes registered through the kernel's `createActionRouteScope` become entry
|
|
75
|
+
operations with their full path and, for `actionRoute`, their `actionId`.
|
|
76
|
+
The scope options must be string literals or imported constant strings;
|
|
77
|
+
each route's method, suffix and `actionId` must be literals, template
|
|
78
|
+
literals or elements of an enclosing literal array's `.map` or `for...of`
|
|
79
|
+
(at most 64). Anything else is reported as `ACTION_ROUTE_UNRESOLVED` or
|
|
80
|
+
`ACTION_ROUTE_SCOPE_UNBOUND`, never guessed.
|
|
81
|
+
|
|
82
|
+
### Function calls (`--calls`)
|
|
83
|
+
|
|
84
|
+
`--calls` (library option `calls: true`) adds `functions`, `calls` and
|
|
85
|
+
`entryPoints` and their capabilities. Without it the output is unchanged.
|
|
86
|
+
|
|
87
|
+
- **Functions.** Every function-like with a body, plus each file's top-level
|
|
88
|
+
code as `fn:<path>#module`. Other ids are `fn:<path>#<line>:<column>` at the
|
|
89
|
+
syntax node's start after decorators, `export` and comments, the same
|
|
90
|
+
position Genesis's syntax sites use. `exported: true` marks exported
|
|
91
|
+
top-level functions; `container` names an enclosing class or object.
|
|
92
|
+
- **Calls.** One TypeScript program over the declared scripts and Vue script
|
|
93
|
+
blocks only, plus TypeScript's own libraries; nothing under `node_modules` is
|
|
94
|
+
loaded. Each call is `exact` (`checker`, `di-binding`, `self`,
|
|
95
|
+
`route-action` or single-valued `argument-flow`), `probable` (other
|
|
96
|
+
`argument-flow`, `receiver-name`, or several candidates) or `unresolved`.
|
|
97
|
+
- `defineFeature` injection binds `requires` tokens to the `setup` outputs
|
|
98
|
+
that `provides` them: one provider is exact, several are ambiguous.
|
|
99
|
+
- Argument flow follows the caller. A function passed to a parameter that
|
|
100
|
+
the callee calls while it runs (in its own body, in callbacks it passes
|
|
101
|
+
on, or by forwarding it to a function that does) is an `argument-flow`
|
|
102
|
+
call of the function that passes it, recorded at that call. The callee's
|
|
103
|
+
own call of the parameter is `unresolved`, so one caller's callback is
|
|
104
|
+
never reached through another caller. A parameter called only from a
|
|
105
|
+
closure the callee stores or returns keeps every value its callers pass.
|
|
106
|
+
Argument flow is exact when it names one function for certain: a passed
|
|
107
|
+
inline function or a name bound to one function, or a parameter (also a
|
|
108
|
+
destructured factory parameter such as `createActions({ sessions })`)
|
|
109
|
+
that receives one value across all the project's calls, when that value
|
|
110
|
+
resolves exactly. Several values stay probable.
|
|
111
|
+
- Action definitions (`{ id, execute }` in a feature's `actions`, also through
|
|
112
|
+
pass-through wrappers and literal-array `.map`) become `action` entry
|
|
113
|
+
points; `request.executeAction({ actionId: "..." })` reaches their execute
|
|
114
|
+
function, and each action route's `controllerFunctionId` names it.
|
|
115
|
+
- Calls that are provably external are left out: package imports, builtins,
|
|
116
|
+
values produced by them (including `let` assignments, `for...of`,
|
|
117
|
+
destructuring and callbacks they call), Vue's `<script setup>` macros and
|
|
118
|
+
`emit`.
|
|
119
|
+
- A method call on an untyped receiver (`receiver-name`) reaches a project
|
|
120
|
+
function only when exactly one function on that side declares the name
|
|
121
|
+
(or exactly one in a file the receiver names) and no builtin member has
|
|
122
|
+
it: the members of TypeScript's libraries and of Node's `EventEmitter`,
|
|
123
|
+
`fs.Stats` and streams. Otherwise it is `unresolved`, and a builtin name
|
|
124
|
+
no project function declares is left out.
|
|
125
|
+
- At most 2,000 call records per file; beyond that `CALLS_TRUNCATED` and a
|
|
126
|
+
`partial` calls capability.
|
|
127
|
+
- **Transports.** With an HTTP scope, `useEndpointResource`, `useCommand`,
|
|
128
|
+
`getHttpWebClient().request` and client `fetch` become operation uses that
|
|
129
|
+
carry `fromFunctionId`; `parentId` is set only in page and component files.
|
|
130
|
+
Paths are partially evaluated (template literals, constants, imports,
|
|
131
|
+
parameter defaults, `usePaths().api`, `resolveScopedApiBasePath`). A key is
|
|
132
|
+
`keyCertainty: "exact"` when every route literal is matched; two or three
|
|
133
|
+
candidate routes give one `probable` use each, with ids suffixed per
|
|
134
|
+
candidate. `--api-prefix-alias /api/x=/api/app/:slug/x` (repeatable) rewrites
|
|
135
|
+
client prefixes before matching.
|
|
136
|
+
- **Cache.** With an absolute, existing `cacheDirectory` outside the project,
|
|
137
|
+
the command keeps `jskit-calls.json` there. An unchanged rerun reuses the
|
|
138
|
+
whole result. Otherwise a file keeps its calls when its own and its directly
|
|
139
|
+
imported sources are unchanged and every target still exists. Any change to a
|
|
140
|
+
manifest, lockfile, `tsconfig`/`jsconfig`, the inspector's own manifest or
|
|
141
|
+
source (even without a version bump), packages, UI discovery, options or
|
|
142
|
+
`stackHash` rebuilds everything. An absent, unusable or corrupt cache means a
|
|
143
|
+
cold run.
|
|
144
|
+
|
|
145
|
+
### Preview locator
|
|
146
|
+
|
|
147
|
+
`src/previewLocator.js` is a classic script a Preview can inline. It defines
|
|
148
|
+
`globalThis.__vibe64LocateElement(element)`, which returns `{ file, name? }` for
|
|
149
|
+
the nearest component with a source file (Vue development builds record it as
|
|
150
|
+
`__file`) that rendered the element, or `null` for plain DOM. It reads only
|
|
151
|
+
Vue's component bookkeeping, never text content or values.
|
|
152
|
+
|
|
65
153
|
Dynamic routing, template component tokens, runtime placement predicates,
|
|
66
154
|
application request wrappers and dispatcher conventions require more evidence or
|
|
67
155
|
an application-owned inspector. They remain partial or unsupported, never
|
|
@@ -72,13 +160,14 @@ Example consumer-owned Stack operation:
|
|
|
72
160
|
|
|
73
161
|
```markdown
|
|
74
162
|
|
|
75
|
-
### Inspector `vue`: Inspect Vue pages
|
|
163
|
+
### Inspector `vue`: Inspect Vue pages and function calls
|
|
76
164
|
|
|
77
165
|
- Runtime: `nodejs`
|
|
78
166
|
- Directory: `.`
|
|
79
|
-
- Command: `node` `node_modules/@jskit-ai/architecture-inspector/src/cli.js` `--alias` `@=src`
|
|
80
|
-
- Timeout seconds: `
|
|
81
|
-
- Max output bytes: `
|
|
167
|
+
- Command: `node` `node_modules/@jskit-ai/architecture-inspector/src/cli.js` `--alias` `@=src` `--calls`
|
|
168
|
+
- Timeout seconds: `90`
|
|
169
|
+
- Max output bytes: `67108864`
|
|
170
|
+
- Preview locator: `node_modules/@jskit-ai/architecture-inspector/src/previewLocator.js`
|
|
82
171
|
```
|
|
83
172
|
|
|
84
173
|
The consumer owns command admission, runtime selection, cancellation, publication
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/architecture-inspector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Static Vue architecture inspector for the portable architecture-inspection command contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/parser": "7.29.9",
|
|
21
|
-
"@
|
|
21
|
+
"@jskit-ai/kernel": "0.2.3",
|
|
22
|
+
"@vue/compiler-sfc": "3.5.44",
|
|
23
|
+
"typescript": "6.0.3"
|
|
22
24
|
},
|
|
23
25
|
"peerDependencies": {
|
|
24
26
|
"vue-router": "^5.3.1"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
// v2: calls a function makes through the callbacks it passes are its own records.
|
|
8
|
+
const CACHE_SCHEMA = "jskit.architecture-calls-cache.v2";
|
|
9
|
+
const CACHE_FILE = "jskit-calls.json";
|
|
10
|
+
// Manifests, lockfiles and compiler configurations change how every file
|
|
11
|
+
// resolves, so any change to them rebuilds everything.
|
|
12
|
+
const PROJECT_FILE_PATTERN = /(?:^|\/)(?:package\.json|package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|[jt]sconfig(?:\.[\w.-]+)?\.json)$/u;
|
|
13
|
+
const INSPECTOR_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
14
|
+
|
|
15
|
+
function digest(value) {
|
|
16
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The inspector's manifest (its version and dependency pins) and every source
|
|
20
|
+
// file, read once per run. A changed resolver changes the key even without a
|
|
21
|
+
// version bump, as in source development or a linked checkout.
|
|
22
|
+
let inspectorIdentity = null;
|
|
23
|
+
function inspectorSourceHash() {
|
|
24
|
+
if (!inspectorIdentity) {
|
|
25
|
+
const hash = createHash("sha256").update(readFileSync(path.join(INSPECTOR_ROOT, "package.json")));
|
|
26
|
+
const sourceRoot = path.join(INSPECTOR_ROOT, "src");
|
|
27
|
+
for (const file of readdirSync(sourceRoot, { recursive: true }).filter((name) => name.endsWith(".js")).sort()) {
|
|
28
|
+
hash.update(`\0${file}\0`).update(readFileSync(path.join(sourceRoot, file)));
|
|
29
|
+
}
|
|
30
|
+
inspectorIdentity = hash.digest("hex");
|
|
31
|
+
}
|
|
32
|
+
return inspectorIdentity;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The cache file inside the caller's cache directory, or null when the
|
|
36
|
+
// directory is not an existing absolute directory outside the project.
|
|
37
|
+
async function cacheLocation(projectRoot, directory) {
|
|
38
|
+
if (typeof directory !== "string" || !path.isAbsolute(directory)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const resolved = path.resolve(directory);
|
|
42
|
+
if (resolved === projectRoot || resolved.startsWith(`${projectRoot}${path.sep}`)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const entry = await stat(resolved).catch(() => null);
|
|
46
|
+
return entry?.isDirectory() ? path.join(resolved, CACHE_FILE) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readCallCache(location, key) {
|
|
50
|
+
try {
|
|
51
|
+
const cache = JSON.parse(await readFile(location, "utf8"));
|
|
52
|
+
return cache?.schema === CACHE_SCHEMA && cache.key === key && cache.files && typeof cache.files === "object" ? cache : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Written beside the final file and renamed into place; a cache that cannot
|
|
59
|
+
// be written only means the next run is cold.
|
|
60
|
+
async function writeCallCache(location, cache) {
|
|
61
|
+
const temporary = `${location}.${process.pid}.tmp`;
|
|
62
|
+
try {
|
|
63
|
+
await writeFile(temporary, JSON.stringify({ schema: CACHE_SCHEMA, ...cache }));
|
|
64
|
+
await rename(temporary, location);
|
|
65
|
+
} catch {
|
|
66
|
+
// An unusable cache directory leaves the inspection result unchanged.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Keys for one inspection: everything that affects every file, each file's
|
|
71
|
+
// own and directly imported sources, and the whole run.
|
|
72
|
+
function cacheKeys({ input, files, imports, scripts, options }) {
|
|
73
|
+
const projectFiles = [...files.values()]
|
|
74
|
+
.filter((file) => PROJECT_FILE_PATTERN.test(file.path))
|
|
75
|
+
.map((file) => [file.path, file.hash])
|
|
76
|
+
.sort();
|
|
77
|
+
const key = digest({
|
|
78
|
+
inspector: inspectorSourceHash(),
|
|
79
|
+
stackHash: input.stackHash ?? null,
|
|
80
|
+
packages: input.packages ?? null,
|
|
81
|
+
uiDiscovery: input.uiDiscovery ?? null,
|
|
82
|
+
options,
|
|
83
|
+
projectFiles
|
|
84
|
+
});
|
|
85
|
+
const importsByFile = new Map();
|
|
86
|
+
for (const use of imports) {
|
|
87
|
+
importsByFile.set(use.fromFile, (importsByFile.get(use.fromFile) || new Set()).add(use.targetFile));
|
|
88
|
+
}
|
|
89
|
+
const fileKeys = new Map(scripts.map((file) => [file, digest([
|
|
90
|
+
files.get(file).hash,
|
|
91
|
+
[...(importsByFile.get(file) || [])].sort().map((target) => [target, files.get(target)?.hash ?? null])
|
|
92
|
+
])]));
|
|
93
|
+
return { key, fileKeys, runKey: digest([key, [...fileKeys].sort()]) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { cacheKeys, cacheLocation, readCallCache, writeCallCache };
|