@pikku/inspector 0.12.99 → 0.12.102
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/CHANGELOG.md +31 -0
- package/dist/add/add-functions.js +17 -4
- package/dist/add/add-scope.d.ts +7 -0
- package/dist/add/add-scope.js +58 -0
- package/dist/add/add-trigger.js +6 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/contract-hashes.js +4 -2
- package/dist/utils/load-addon-functions-meta.js +10 -1
- package/dist/utils/read-jsx-literal-text.d.ts +5 -0
- package/dist/utils/read-jsx-literal-text.js +126 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.12.102
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 3c0b151: `pikku validate` checks block libraries. The project's `pikku.config.json` lists them by package name under `blocks`; each listed package's `src/blocks` is checked. A block that renders a literal string is an error, because its words have to come from messages, and a block with no `.stories.tsx` is a warning. A block with no errors is reported as productized. A listed name that matches no package, or a package with no `src/blocks`, is an error.
|
|
6
|
+
- 0963b73: Every scope tree now records where it came from: `origin` on `ScopeDefinitionMeta` is `{ kind: 'app' }` for the app's own `defineScope`, `{ kind: 'generated' }` for a tree the CLI wrote into the project (a `.gen.ts`, or the `app` root derived from personas), and `{ kind: 'addon', package, displayName }` for one an installed addon declared. An addon's build stamps its own package and display name on its trees, and the host keeps that name while recording the package it actually installed. `MetaService.getScopesMeta()` reads the result, and the console's all-meta payload carries it as `scopes`.
|
|
7
|
+
|
|
8
|
+
The console's Scopes page becomes **Permissions** and is grouped by that origin: the app's own permissions first, then what Pikku generated into the project, then one card per addon (Pikku's own before third-party, each alphabetical), each card folding to its header — the app's own starts open, every addon's starts folded, and any search or filter opens them all. Above the cards, two filters narrow the page to what one role is given (or to what no role is given yet) and to one source. A permission group no longer says "Not part of any role yet" above lines that are each given to a role.
|
|
9
|
+
|
|
10
|
+
- Updated dependencies [0c865d9]
|
|
11
|
+
- Updated dependencies [c36b066]
|
|
12
|
+
- Updated dependencies [0963b73]
|
|
13
|
+
- @pikku/core@0.12.137
|
|
14
|
+
|
|
15
|
+
## 0.12.101
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 9112494: `pikku knowledge validate` reports code that no note describes as a warning rather than info, and raises one `PKU960` diagnostic for it, so `--fail-on-warn` can gate on it and the "consistent" line no longer prints while notes are missing. A plain run still exits 0: only errors fail it.
|
|
20
|
+
|
|
21
|
+
## 0.12.100
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 149faae: Webhook `receive` steps lose their boilerplate. `pikkuWebhookReceive` (from `#pikku/trigger` or `#pikku/addon/trigger`) declares one: it is public, typed to the raw request with a required `http`, and never registered as an RPC, and the inspector rejects a `receive` declared with any other wrapper. `parseJson` (in `@pikku/core/utils`, generated as `#pikku/utils` and `#pikku/addon/utils`) parses text or bytes and answers a body that is not JSON with a 400. A webhook source whose `method` includes `'head'` answers HEAD probes itself with a 200.
|
|
26
|
+
|
|
27
|
+
`receive` no longer returns `{ respond }`: a handshake returns nothing and writes its answer to `http.response`, like any other HTTP function. The webhook route no longer returns a fetch `Response` either. An HTTP function that returns nothing is now answered with whatever status its response has (200 unless it set another), no longer a forced 204. A webhook receive is also left out of contract versioning, since nothing but its own route calls it.
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [149faae]
|
|
30
|
+
- @pikku/core@0.12.136
|
|
31
|
+
|
|
1
32
|
## 0.12.99
|
|
2
33
|
|
|
3
34
|
### Patch Changes
|
|
@@ -361,6 +361,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
361
361
|
const pikkuFuncPattern = /pikku.*func/i;
|
|
362
362
|
if (!pikkuFuncPattern.test(expression.text) &&
|
|
363
363
|
expression.text !== 'pikkuScenario' &&
|
|
364
|
+
expression.text !== 'pikkuWebhookReceive' &&
|
|
364
365
|
!SCENARIO_STEP_KINDS.has(expression.text)) {
|
|
365
366
|
return;
|
|
366
367
|
}
|
|
@@ -373,6 +374,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
373
374
|
// analyzer can tell it apart from an application workflow without having to
|
|
374
375
|
// reach into the workflow graph.
|
|
375
376
|
const isScenario = expression.text === 'pikkuScenario';
|
|
377
|
+
// A webhook source's `receive`. Only its source's route runs it, with the
|
|
378
|
+
// raw request, so like a scenario step it is bundled but never RPC-registered.
|
|
379
|
+
const isWebhookReceive = expression.text === 'pikkuWebhookReceive';
|
|
380
|
+
const notRpc = isScenarioStep || isWebhookReceive;
|
|
376
381
|
// only handle calls like pikkuFunc(...)
|
|
377
382
|
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
378
383
|
return;
|
|
@@ -817,6 +822,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
817
822
|
inputTypes = result.types;
|
|
818
823
|
}
|
|
819
824
|
}
|
|
825
|
+
// A receive's input is always the raw request, which no schema describes.
|
|
826
|
+
if (isWebhookReceive)
|
|
827
|
+
inputNames = [];
|
|
820
828
|
// --- Output Extraction ---
|
|
821
829
|
let outputNames = [];
|
|
822
830
|
if (outputSchemaRef) {
|
|
@@ -1054,11 +1062,16 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1054
1062
|
inputs: inputNames.filter((n) => n !== 'void') ?? null,
|
|
1055
1063
|
outputs: outputNames.filter((n) => n !== 'void') ?? null,
|
|
1056
1064
|
expose: expose || undefined,
|
|
1057
|
-
auth:
|
|
1065
|
+
auth: isWebhookReceive
|
|
1066
|
+
? false
|
|
1067
|
+
: typeof auth === 'boolean'
|
|
1068
|
+
? auth
|
|
1069
|
+
: undefined,
|
|
1058
1070
|
permissionsInBody: permissionsInBody || undefined,
|
|
1059
1071
|
audit,
|
|
1060
1072
|
remote: remote || undefined,
|
|
1061
1073
|
scenarioStep: isScenarioStep || undefined,
|
|
1074
|
+
webhookReceive: isWebhookReceive || undefined,
|
|
1062
1075
|
scenario: isScenario || undefined,
|
|
1063
1076
|
mcp: mcpEnabled || undefined,
|
|
1064
1077
|
readonly: readonly_ || undefined,
|
|
@@ -1173,7 +1186,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1173
1186
|
logger.error(`• Function with explicit name '${name}' is not exported, this is not allowed.`);
|
|
1174
1187
|
return;
|
|
1175
1188
|
}
|
|
1176
|
-
if (remote && !
|
|
1189
|
+
if (remote && !notRpc) {
|
|
1177
1190
|
state.rpc.invokedFunctions.add(pikkuFuncId);
|
|
1178
1191
|
// The consumer-facing surface a wireRemoteAddon imports (mirrors exposedMeta)
|
|
1179
1192
|
state.rpc.remoteMeta[name] = pikkuFuncId;
|
|
@@ -1182,7 +1195,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1182
1195
|
exportedName,
|
|
1183
1196
|
});
|
|
1184
1197
|
}
|
|
1185
|
-
if (expose && !
|
|
1198
|
+
if (expose && !notRpc) {
|
|
1186
1199
|
state.rpc.exposedMeta[name] = pikkuFuncId;
|
|
1187
1200
|
state.rpc.exposedFiles.set(name, {
|
|
1188
1201
|
path: node.getSourceFile().fileName,
|
|
@@ -1191,7 +1204,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
1191
1204
|
// Track exposed RPC function for service aggregation
|
|
1192
1205
|
state.serviceAggregation.usedFunctions.add(pikkuFuncId);
|
|
1193
1206
|
}
|
|
1194
|
-
if (!
|
|
1207
|
+
if (!notRpc) {
|
|
1195
1208
|
// We add it to internal meta to allow autocomplete for everything
|
|
1196
1209
|
state.rpc.internalMeta[name] = pikkuFuncId;
|
|
1197
1210
|
if (version !== undefined) {
|
package/dist/add/add-scope.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { AddWiring } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Folds `declareScopes` ids into `state.scopes.definitions`. An id the
|
|
4
|
+
* definitions already hold (an addon's tree, a `defineScope`) is left alone;
|
|
5
|
+
* a missing one is added as a bare node under its root, creating the root when
|
|
6
|
+
* nothing declared it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const applyDeclaredScopes: (state: Parameters<AddWiring>[3]) => void;
|
|
2
9
|
export declare const addScope: AddWiring;
|
package/dist/add/add-scope.js
CHANGED
|
@@ -84,11 +84,63 @@ const extractScopeNodes = (obj, scopeName, logger) => {
|
|
|
84
84
|
const unwrapAs = (node) => ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
|
|
85
85
|
? unwrapAs(node.expression)
|
|
86
86
|
: node;
|
|
87
|
+
/**
|
|
88
|
+
* Records the ids of a `declareScopes([...])` call. They are merged into the
|
|
89
|
+
* definitions later (`applyDeclaredScopes`), after addon scope trees have
|
|
90
|
+
* loaded, so an addon's richer declaration is never shadowed by a bare id.
|
|
91
|
+
*/
|
|
92
|
+
const addDeclareScopes = (logger, node, state) => {
|
|
93
|
+
const arg = node.arguments[0] && unwrapAs(node.arguments[0]);
|
|
94
|
+
if (!arg || !ts.isArrayLiteralExpression(arg)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const sourceFile = node.getSourceFile().fileName;
|
|
98
|
+
for (const element of arg.elements) {
|
|
99
|
+
if (!ts.isStringLiteralLike(element)) {
|
|
100
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'declareScopes takes string literal scope ids.');
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const id = element.text;
|
|
104
|
+
if (!id.split(SEPARATOR).every((s) => isValidSegment(s, id, logger))) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
state.scopes.declared ??= new Map();
|
|
108
|
+
if (!state.scopes.declared.has(id)) {
|
|
109
|
+
state.scopes.declared.set(id, sourceFile);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
state.scopes.files.add(sourceFile);
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Folds `declareScopes` ids into `state.scopes.definitions`. An id the
|
|
116
|
+
* definitions already hold (an addon's tree, a `defineScope`) is left alone;
|
|
117
|
+
* a missing one is added as a bare node under its root, creating the root when
|
|
118
|
+
* nothing declared it.
|
|
119
|
+
*/
|
|
120
|
+
export const applyDeclaredScopes = (state) => {
|
|
121
|
+
for (const [id, sourceFile] of state.scopes.declared ?? []) {
|
|
122
|
+
const [rootName, ...rest] = id.split(SEPARATOR);
|
|
123
|
+
let root = state.scopes.definitions.find((d) => d.name === rootName);
|
|
124
|
+
if (!root) {
|
|
125
|
+
root = { name: rootName, sourceFile };
|
|
126
|
+
state.scopes.definitions.push(root);
|
|
127
|
+
}
|
|
128
|
+
let level = root;
|
|
129
|
+
for (const segment of rest) {
|
|
130
|
+
level.scopes ??= {};
|
|
131
|
+
level = level.scopes[segment] ??= {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
87
135
|
export const addScope = (logger, node, checker, state, _options) => {
|
|
88
136
|
if (!ts.isCallExpression(node)) {
|
|
89
137
|
return;
|
|
90
138
|
}
|
|
91
139
|
const expression = node.expression;
|
|
140
|
+
if (ts.isIdentifier(expression) && expression.text === 'declareScopes') {
|
|
141
|
+
addDeclareScopes(logger, node, state);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
92
144
|
if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
|
|
93
145
|
return;
|
|
94
146
|
}
|
|
@@ -101,6 +153,11 @@ export const addScope = (logger, node, checker, state, _options) => {
|
|
|
101
153
|
return;
|
|
102
154
|
}
|
|
103
155
|
const sourceFile = node.getSourceFile().fileName;
|
|
156
|
+
// A `.gen.ts` is one the CLI wrote into the project, the same test
|
|
157
|
+
// `claimSingleDeclaration` exempts it by.
|
|
158
|
+
const origin = sourceFile.endsWith('.gen.ts')
|
|
159
|
+
? { kind: 'generated' }
|
|
160
|
+
: { kind: 'app' };
|
|
104
161
|
// Roots are keyed exactly like the nodes beneath them, so each property of
|
|
105
162
|
// the call's single argument is one tree.
|
|
106
163
|
for (const prop of unwrapped.properties) {
|
|
@@ -141,6 +198,7 @@ export const addScope = (logger, node, checker, state, _options) => {
|
|
|
141
198
|
description: descriptionValue || undefined,
|
|
142
199
|
scopes,
|
|
143
200
|
sourceFile,
|
|
201
|
+
origin,
|
|
144
202
|
});
|
|
145
203
|
}
|
|
146
204
|
};
|
package/dist/add/add-trigger.js
CHANGED
|
@@ -184,6 +184,12 @@ const addWireTriggerWebhookSource = (logger, node, checker, state, firstArg) =>
|
|
|
184
184
|
*/
|
|
185
185
|
export const validateWebhookSourceTriggers = (logger, state) => {
|
|
186
186
|
const sources = state.triggers.webhookSourceMeta;
|
|
187
|
+
for (const source of Object.values(sources)) {
|
|
188
|
+
const meta = source.receive && state.functions.meta[source.receive];
|
|
189
|
+
if (meta && meta.funcWrapper && !meta.webhookReceive) {
|
|
190
|
+
logger.error(`Webhook source '${source.name}' receives with '${source.receive}', a ${meta.funcWrapper}. Declare it with pikkuWebhookReceive so it is never callable as an RPC.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
187
193
|
for (const trigger of Object.keys(state.triggers.meta)) {
|
|
188
194
|
const colon = trigger.indexOf(':');
|
|
189
195
|
if (colon === -1)
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ export declare enum ErrorCode {
|
|
|
83
83
|
ADDON_WIRING_NOT_ALLOWED = "PKU920",
|
|
84
84
|
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
|
|
85
85
|
RPC_INVOCATION_TYPE_CAST = "PKU940",
|
|
86
|
+
KNOWLEDGE_NOTE_MISSING = "PKU960",
|
|
86
87
|
SECRET_SERVICE_ALIASED = "PKU950",
|
|
87
88
|
SECRET_NOT_DECLARED = "PKU951",
|
|
88
89
|
SECRET_KEY_NOT_STATIC = "PKU952",
|
package/dist/error-codes.js
CHANGED
|
@@ -108,6 +108,8 @@ export var ErrorCode;
|
|
|
108
108
|
ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
|
|
109
109
|
ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
|
|
110
110
|
ErrorCode["RPC_INVOCATION_TYPE_CAST"] = "PKU940";
|
|
111
|
+
// Knowledge base errors
|
|
112
|
+
ErrorCode["KNOWLEDGE_NOTE_MISSING"] = "PKU960";
|
|
111
113
|
// Secret boundary errors
|
|
112
114
|
ErrorCode["SECRET_SERVICE_ALIASED"] = "PKU950";
|
|
113
115
|
ErrorCode["SECRET_NOT_DECLARED"] = "PKU951";
|
package/dist/index.d.ts
CHANGED
|
@@ -25,5 +25,6 @@ export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
|
|
|
25
25
|
export type { SerializedWorkflowGraph, SerializedWorkflowGraphs, } from './utils/workflow/graph/index.js';
|
|
26
26
|
export { resolveCoreType } from './utils/resolve-core-type.js';
|
|
27
27
|
export { readModuleSpecifiers } from './utils/read-module-specifiers.js';
|
|
28
|
+
export { readJsxLiteralText } from './utils/read-jsx-literal-text.js';
|
|
28
29
|
export { readTsconfigOutDir } from './utils/read-tsconfig-out-dir.js';
|
|
29
30
|
export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,6 @@ export { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
|
14
14
|
export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
|
|
15
15
|
export { resolveCoreType } from './utils/resolve-core-type.js';
|
|
16
16
|
export { readModuleSpecifiers } from './utils/read-module-specifiers.js';
|
|
17
|
+
export { readJsxLiteralText } from './utils/read-jsx-literal-text.js';
|
|
17
18
|
export { readTsconfigOutDir } from './utils/read-tsconfig-out-dir.js';
|
|
18
19
|
export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
|
package/dist/inspector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { applyDeclaredScopes } from './add/add-scope.js';
|
|
2
3
|
import { performance } from 'perf_hooks';
|
|
3
4
|
import { dirname, relative, resolve } from 'path';
|
|
4
5
|
import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
|
|
@@ -428,6 +429,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
428
429
|
validateCredentialOverrides(logger, state);
|
|
429
430
|
validateRemoteAddonDependencies(logger, state);
|
|
430
431
|
validateRemoteAddonAuth(logger, state);
|
|
432
|
+
applyDeclaredScopes(state);
|
|
431
433
|
validateScopeReferences(logger, state);
|
|
432
434
|
validateSystemRoleScopes(logger, state);
|
|
433
435
|
validatePersonaRoles(logger, state);
|
package/dist/types.d.ts
CHANGED
|
@@ -731,6 +731,8 @@ export interface InspectorState {
|
|
|
731
731
|
scopes: {
|
|
732
732
|
definitions: ScopeDefinitions;
|
|
733
733
|
files: Set<string>;
|
|
734
|
+
/** Ids from `declareScopes([...])`, merged into `definitions` once addon scopes are loaded. */
|
|
735
|
+
declared?: Map<string, string>;
|
|
734
736
|
};
|
|
735
737
|
systemRoles: {
|
|
736
738
|
definitions: SystemRoleDefinitions;
|
|
@@ -71,7 +71,9 @@ function resolveSchema(typeNames, allSchemas, typesMap) {
|
|
|
71
71
|
export function buildCurrentContracts(functionsMeta, allSchemas, typesMap) {
|
|
72
72
|
const result = new Map();
|
|
73
73
|
for (const [funcId, meta] of Object.entries(functionsMeta)) {
|
|
74
|
-
|
|
74
|
+
// A webhook receive has no callers but its own route, so there is no
|
|
75
|
+
// contract for a version to protect.
|
|
76
|
+
if (meta.remote === true || meta.webhookReceive) {
|
|
75
77
|
continue;
|
|
76
78
|
}
|
|
77
79
|
const parsed = parseVersionedId(funcId);
|
|
@@ -251,7 +253,7 @@ export function updateManifest(existing, currentContracts) {
|
|
|
251
253
|
export function extractContractsFromMeta(functionsMeta) {
|
|
252
254
|
const result = new Map();
|
|
253
255
|
for (const [funcId, meta] of Object.entries(functionsMeta)) {
|
|
254
|
-
if (meta.remote === true || !meta.contractHash) {
|
|
256
|
+
if (meta.remote === true || meta.webhookReceive || !meta.contractHash) {
|
|
255
257
|
continue;
|
|
256
258
|
}
|
|
257
259
|
const parsed = parseVersionedId(funcId);
|
|
@@ -296,7 +296,16 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
296
296
|
for (const [key, def] of Object.entries(scopesMeta)) {
|
|
297
297
|
const existing = state.scopes.definitions.find((d) => d.name === key);
|
|
298
298
|
if (!existing) {
|
|
299
|
-
|
|
299
|
+
// The addon's build stamped its own name on the trees it declared;
|
|
300
|
+
// the package it was installed as is the one this host knows.
|
|
301
|
+
const origin = {
|
|
302
|
+
kind: 'addon',
|
|
303
|
+
package: decl.package,
|
|
304
|
+
displayName: def.origin?.kind === 'addon'
|
|
305
|
+
? def.origin.displayName
|
|
306
|
+
: undefined,
|
|
307
|
+
};
|
|
308
|
+
state.scopes.definitions.push({ ...def, origin });
|
|
300
309
|
logger.debug(`Loaded addon scope '${key}' from ${decl.package}`);
|
|
301
310
|
}
|
|
302
311
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const TEXT_ATTRIBUTES = new Set([
|
|
3
|
+
'placeholder',
|
|
4
|
+
'title',
|
|
5
|
+
'alt',
|
|
6
|
+
'aria-label',
|
|
7
|
+
'label',
|
|
8
|
+
'description',
|
|
9
|
+
]);
|
|
10
|
+
const TEXT_PROPERTIES = new Set([
|
|
11
|
+
...TEXT_ATTRIBUTES,
|
|
12
|
+
'text',
|
|
13
|
+
'body',
|
|
14
|
+
'why',
|
|
15
|
+
'hint',
|
|
16
|
+
'caption',
|
|
17
|
+
'heading',
|
|
18
|
+
'subtitle',
|
|
19
|
+
'message',
|
|
20
|
+
'tooltip',
|
|
21
|
+
]);
|
|
22
|
+
const NON_TEXT_ATTRIBUTES = new Set([
|
|
23
|
+
'className',
|
|
24
|
+
'class',
|
|
25
|
+
'style',
|
|
26
|
+
'd',
|
|
27
|
+
'viewBox',
|
|
28
|
+
'preserveAspectRatio',
|
|
29
|
+
'points',
|
|
30
|
+
'transform',
|
|
31
|
+
'fill',
|
|
32
|
+
'stroke',
|
|
33
|
+
'href',
|
|
34
|
+
'src',
|
|
35
|
+
'rel',
|
|
36
|
+
'target',
|
|
37
|
+
'role',
|
|
38
|
+
'to',
|
|
39
|
+
'id',
|
|
40
|
+
'name',
|
|
41
|
+
'type',
|
|
42
|
+
'value',
|
|
43
|
+
'key',
|
|
44
|
+
]);
|
|
45
|
+
const isProse = (text) => /^\p{Lu}[^\s]*(\s+[^\s]+)+/u.test(text.trim()) &&
|
|
46
|
+
!/[:/[\]=_#]/.test(text.split(/\s+/)[0] ?? '');
|
|
47
|
+
const isSentence = (text) => /\p{L}+\s+\p{L}+/u.test(text);
|
|
48
|
+
const hasLetter = (text) => /\p{L}/u.test(text);
|
|
49
|
+
const isLiteral = (node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
|
|
50
|
+
const rendered = (node) => {
|
|
51
|
+
if (isLiteral(node))
|
|
52
|
+
return [node];
|
|
53
|
+
if (ts.isParenthesizedExpression(node))
|
|
54
|
+
return rendered(node.expression);
|
|
55
|
+
if (ts.isConditionalExpression(node)) {
|
|
56
|
+
return [...rendered(node.whenTrue), ...rendered(node.whenFalse)];
|
|
57
|
+
}
|
|
58
|
+
if (ts.isBinaryExpression(node)) {
|
|
59
|
+
const operator = node.operatorToken.kind;
|
|
60
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
61
|
+
return rendered(node.right);
|
|
62
|
+
}
|
|
63
|
+
if (operator === ts.SyntaxKind.BarBarToken ||
|
|
64
|
+
operator === ts.SyntaxKind.QuestionQuestionToken) {
|
|
65
|
+
return [...rendered(node.left), ...rendered(node.right)];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
};
|
|
70
|
+
export const readJsxLiteralText = (fileName, content) => {
|
|
71
|
+
const source = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
72
|
+
const found = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const add = (node, text) => {
|
|
75
|
+
if (!hasLetter(text) || seen.has(node.getStart(source)))
|
|
76
|
+
return;
|
|
77
|
+
seen.add(node.getStart(source));
|
|
78
|
+
const { line } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
79
|
+
found.push({ text: text.trim(), line: line + 1 });
|
|
80
|
+
};
|
|
81
|
+
const visit = (node) => {
|
|
82
|
+
if (ts.isJsxText(node)) {
|
|
83
|
+
add(node, node.text);
|
|
84
|
+
}
|
|
85
|
+
else if (ts.isJsxExpression(node) &&
|
|
86
|
+
node.expression &&
|
|
87
|
+
(ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
88
|
+
for (const literal of rendered(node.expression))
|
|
89
|
+
add(literal, literal.text);
|
|
90
|
+
}
|
|
91
|
+
else if (ts.isJsxAttribute(node) && node.initializer) {
|
|
92
|
+
const name = node.name.getText(source);
|
|
93
|
+
const value = ts.isJsxExpression(node.initializer)
|
|
94
|
+
? node.initializer.expression
|
|
95
|
+
: node.initializer;
|
|
96
|
+
if (value &&
|
|
97
|
+
(ts.isStringLiteral(value) ||
|
|
98
|
+
ts.isNoSubstitutionTemplateLiteral(value)) &&
|
|
99
|
+
(TEXT_ATTRIBUTES.has(name) ||
|
|
100
|
+
(!NON_TEXT_ATTRIBUTES.has(name) &&
|
|
101
|
+
!name.startsWith('data-') &&
|
|
102
|
+
isSentence(value.text)))) {
|
|
103
|
+
add(value, value.text);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (ts.isPropertyAssignment(node) &&
|
|
107
|
+
TEXT_PROPERTIES.has(node.name.getText(source)) &&
|
|
108
|
+
(ts.isStringLiteral(node.initializer) ||
|
|
109
|
+
ts.isNoSubstitutionTemplateLiteral(node.initializer))) {
|
|
110
|
+
add(node.initializer, node.initializer.text);
|
|
111
|
+
}
|
|
112
|
+
if (isLiteral(node) &&
|
|
113
|
+
isProse(node.text) &&
|
|
114
|
+
!ts.isImportDeclaration(node.parent) &&
|
|
115
|
+
!ts.isExportDeclaration(node.parent) &&
|
|
116
|
+
!ts.isLiteralTypeNode(node.parent) &&
|
|
117
|
+
!(ts.isPropertyAssignment(node.parent) && node.parent.name === node) &&
|
|
118
|
+
!(ts.isJsxAttribute(node.parent) &&
|
|
119
|
+
NON_TEXT_ATTRIBUTES.has(node.parent.name.getText(source)))) {
|
|
120
|
+
add(node, node.text);
|
|
121
|
+
}
|
|
122
|
+
ts.forEachChild(node, visit);
|
|
123
|
+
};
|
|
124
|
+
visit(source);
|
|
125
|
+
return found;
|
|
126
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.102",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"zod": "^4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@pikku/core": "^0.12.
|
|
57
|
+
"@pikku/core": "^0.12.137"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@pikku/core": "^0.12.
|
|
60
|
+
"@pikku/core": "^0.12.137",
|
|
61
61
|
"@types/node": "^24.13.3"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|