@pikku/inspector 0.12.101 → 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 CHANGED
@@ -1,3 +1,17 @@
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
+
1
15
  ## 0.12.101
2
16
 
3
17
  ### Patch Changes
@@ -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;
@@ -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/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;
@@ -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
- state.scopes.definitions.push(def);
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,5 @@
1
+ export type JsxLiteralText = {
2
+ text: string;
3
+ line: number;
4
+ };
5
+ export declare const readJsxLiteralText: (fileName: string, content: string) => JsxLiteralText[];
@@ -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.101",
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.136"
57
+ "@pikku/core": "^0.12.137"
58
58
  },
59
59
  "devDependencies": {
60
- "@pikku/core": "^0.12.136",
60
+ "@pikku/core": "^0.12.137",
61
61
  "@types/node": "^24.13.3"
62
62
  },
63
63
  "engines": {