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