@clidey/dory 1.0.3 → 1.0.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clidey/dory",
3
3
  "private": false,
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "description": "A CLI tool for building and previewing documentation sites",
7
7
  "keywords": [
@@ -43,17 +43,18 @@
43
43
  "build:embed:loader": "vite build -c vite.config.embed-loader.ts",
44
44
  "build:embed:widget": "vite build -c vite.config.embed-widget.ts",
45
45
  "preview": "vite preview",
46
- "test": "vitest run",
46
+ "test": "node scripts/check-trigger-children.mjs src && vitest run",
47
+ "lint:triggers": "node scripts/check-trigger-children.mjs src",
47
48
  "publish": "npm run build && npm publish --access public --no-git-checks",
48
49
  "prepublishOnly": "echo 'Skipping prepublishOnly - build handled by publish script'"
49
50
  },
50
51
  "dependencies": {
51
52
  "@algolia/autocomplete-core": "1.19.9",
52
- "@clidey/ux": "1.2.3",
53
- "@fortawesome/fontawesome-svg-core": "7.3.0",
54
- "@fortawesome/free-brands-svg-icons": "7.3.0",
55
- "@fortawesome/free-regular-svg-icons": "7.3.0",
56
- "@fortawesome/free-solid-svg-icons": "7.3.0",
53
+ "@clidey/ux": "1.5.1",
54
+ "@fortawesome/fontawesome-svg-core": "7.3.1",
55
+ "@fortawesome/free-brands-svg-icons": "7.3.1",
56
+ "@fortawesome/free-regular-svg-icons": "7.3.1",
57
+ "@fortawesome/free-solid-svg-icons": "7.3.1",
57
58
  "@fortawesome/react-fontawesome": "3.4.0",
58
59
  "@headlessui/react": "2.2.10",
59
60
  "@heroicons/react": "2.2.0",
@@ -65,11 +66,11 @@
65
66
  "classnames": "2.5.1",
66
67
  "flexsearch": "0.8.212",
67
68
  "gray-matter": "4.0.3",
68
- "katex": "0.17.0",
69
+ "katex": "0.18.3",
69
70
  "lucide-react": "1.24.0",
70
- "mermaid": "11.16.0",
71
+ "mermaid": "11.16.1",
71
72
  "motion": "12.42.2",
72
- "preact": "10.29.7",
73
+ "preact": "10.29.8",
73
74
  "preact-render-to-string": "6.7.0",
74
75
  "prism-react-renderer": "2.4.1",
75
76
  "react-highlight-words": "0.21.0",
@@ -88,7 +89,8 @@
88
89
  "devDependencies": {
89
90
  "@types/node": "26.1.1",
90
91
  "@types/react-highlight-words": "0.20.1",
91
- "vitest": "4.1.10"
92
+ "@typescript/typescript6": "6.0.2",
93
+ "vitest": "5.0.4"
92
94
  },
93
95
  "pnpm": {
94
96
  "overrides": {
@@ -96,4 +98,4 @@
96
98
  "react-redux": "9.3.0"
97
99
  }
98
100
  }
99
- }
101
+ }
@@ -0,0 +1,60 @@
1
+ // Lint guard: fail when a @clidey/ux trigger has `asChild` and its child cannot be slotted.
2
+ //
3
+ // node scripts/check-trigger-children.mjs <dir> [<dir>...]
4
+ //
5
+ // Fails on `<XTrigger asChild>` whose child is a fragment, string literal,
6
+ // array, conditional expression, multiple children, or a component on the
7
+ // deny list in trigger-children.json. Deny-listed children fail even without
8
+ // `asChild`, since wrapping them is never what the author meant.
9
+
10
+ import {
11
+ asChildAttribute,
12
+ childSlotStatus,
13
+ classifyChildren,
14
+ forEachTrigger,
15
+ listTsxFiles,
16
+ location,
17
+ parse,
18
+ tagName,
19
+ } from './trigger-children-lib.mjs';
20
+
21
+ const dirs = process.argv.slice(2).filter((arg) => !arg.startsWith('--'));
22
+ if (dirs.length === 0) {
23
+ console.error('usage: check-trigger-children.mjs <dir> [<dir>...]');
24
+ process.exit(2);
25
+ }
26
+
27
+ const errors = [];
28
+ let triggers = 0;
29
+
30
+ for (const dir of dirs) {
31
+ for (const file of listTsxFiles(dir)) {
32
+ const source = parse(file);
33
+ forEachTrigger(source, (element, imports) => {
34
+ triggers += 1;
35
+ const trigger = tagName(element);
36
+ const asChild = asChildAttribute(element);
37
+ const where = location(source, element);
38
+ const shape = classifyChildren(element);
39
+
40
+ if (shape.kind === 'element') {
41
+ if (childSlotStatus(shape.child, imports) === 'denied') {
42
+ errors.push(`${where} <${trigger}> wraps ${tagName(shape.child)}, which does not forward refs or spread props; wrap it in a <span>`);
43
+ }
44
+ return;
45
+ }
46
+ if (!asChild || asChild.value === false) return;
47
+ // A bare identifier (`{node}`) may hold an element; the library validates it at runtime.
48
+ if (shape.kind === 'expression') return;
49
+ errors.push(`${where} <${trigger} asChild> child is ${shape.kind}; asChild needs exactly one element child`);
50
+ });
51
+ }
52
+ }
53
+
54
+ if (errors.length > 0) {
55
+ console.error(`check-trigger-children: ${errors.length} problem(s) in ${triggers} trigger(s)`);
56
+ for (const line of errors) console.error(` ${line}`);
57
+ console.error('See @clidey/ux docs/triggers-and-aschild.md.');
58
+ process.exit(1);
59
+ }
60
+ console.log(`check-trigger-children: ${triggers} trigger(s) OK`);
@@ -0,0 +1,149 @@
1
+ // Shared AST helpers for codemod-trigger-aschild.mjs and check-trigger-children.mjs.
2
+ // Uses the TypeScript compiler API already installed for the frontend build.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const here = path.dirname(fileURLToPath(import.meta.url));
10
+ const require = createRequire(path.join(here, '..', 'package.json'));
11
+
12
+ function loadTypeScript() {
13
+ // TypeScript 7 ships a native compiler whose npm package has no JS compiler API
14
+ // (or only a stub); fall back to the @typescript/typescript6 compatibility package.
15
+ let candidate;
16
+ try {
17
+ candidate = require('typescript');
18
+ } catch {
19
+ candidate = undefined;
20
+ }
21
+ if (candidate && typeof candidate.createSourceFile === 'function') return candidate;
22
+ return require('@typescript/typescript6');
23
+ }
24
+
25
+ /** @type {import('typescript')} */
26
+ export const ts = loadTypeScript();
27
+
28
+ export const config = JSON.parse(fs.readFileSync(path.join(here, 'trigger-children.json'), 'utf8'));
29
+
30
+ const TRIGGERS = new Set(config.triggers);
31
+ const INFERRED_UX = new Set(config.inferredUxChildren);
32
+ const INFERRED_INTRINSICS = new Set(config.inferredIntrinsics);
33
+ const DENY = new Set(config.denyList);
34
+
35
+ /** Recursively lists .tsx files under `dir`, skipping generated and dependency folders. */
36
+ export function listTsxFiles(dir) {
37
+ const out = [];
38
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
39
+ if (entry.name === 'node_modules' || entry.name === 'generated' || entry.name.startsWith('.')) continue;
40
+ const full = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) out.push(...listTsxFiles(full));
42
+ else if (entry.name.endsWith('.tsx')) out.push(full);
43
+ }
44
+ return out;
45
+ }
46
+
47
+ export function parse(file) {
48
+ const text = fs.readFileSync(file, 'utf8');
49
+ return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
50
+ }
51
+
52
+ /** Maps local identifier → module specifier for every import in the file. */
53
+ export function importSources(source) {
54
+ const map = new Map();
55
+ for (const statement of source.statements) {
56
+ if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
57
+ const from = statement.moduleSpecifier.text;
58
+ const { name, namedBindings } = statement.importClause;
59
+ if (name) map.set(name.text, from);
60
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
61
+ for (const element of namedBindings.elements) map.set(element.name.text, from);
62
+ }
63
+ }
64
+ return map;
65
+ }
66
+
67
+ export function tagName(node) {
68
+ const name = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName;
69
+ return name.getText();
70
+ }
71
+
72
+ function isMeaningfulChild(child) {
73
+ if (ts.isJsxText(child)) return child.text.trim().length > 0;
74
+ if (ts.isJsxExpression(child)) return child.expression !== undefined;
75
+ return true;
76
+ }
77
+
78
+ /** Classifies the children of a trigger element. */
79
+ export function classifyChildren(element) {
80
+ const children = element.children.filter(isMeaningfulChild);
81
+ if (children.length === 0) return { kind: 'empty', children };
82
+ if (children.length > 1) return { kind: 'multiple', children };
83
+ const [child] = children;
84
+ if (ts.isJsxText(child)) return { kind: 'text', children, child };
85
+ if (ts.isJsxFragment(child)) return { kind: 'fragment', children, child };
86
+ if (ts.isJsxExpression(child)) {
87
+ const expr = child.expression;
88
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isTemplateExpression(expr)) {
89
+ return { kind: 'text', children, child };
90
+ }
91
+ if (ts.isConditionalExpression(expr) || ts.isBinaryExpression(expr)) return { kind: 'conditional', children, child };
92
+ if (ts.isArrayLiteralExpression(expr)) return { kind: 'array', children, child };
93
+ if (ts.isJsxElement(expr) || ts.isJsxSelfClosingElement(expr)) return { kind: 'element', children, child: expr };
94
+ if (ts.isParenthesizedExpression(expr) && (ts.isJsxElement(expr.expression) || ts.isJsxSelfClosingElement(expr.expression))) {
95
+ return { kind: 'element', children, child: expr.expression };
96
+ }
97
+ return { kind: 'expression', children, child };
98
+ }
99
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) return { kind: 'element', children, child };
100
+ return { kind: 'expression', children, child };
101
+ }
102
+
103
+ /** Returns the `asChild` attribute node and its literal value (true/false/undefined for non-literal). */
104
+ export function asChildAttribute(element) {
105
+ const attributes = element.openingElement.attributes.properties;
106
+ for (const attr of attributes) {
107
+ if (!ts.isJsxAttribute(attr) || attr.name.getText() !== 'asChild') continue;
108
+ if (!attr.initializer) return { attr, value: true };
109
+ if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
110
+ const expr = attr.initializer.expression;
111
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return { attr, value: true };
112
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) return { attr, value: false };
113
+ }
114
+ return { attr, value: undefined };
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Decides how the library will treat a child element:
121
+ * 'inferred' (slot-safe, asChild implied), 'denied' (never slot-safe), or 'unknown'.
122
+ */
123
+ export function childSlotStatus(childElement, imports) {
124
+ const name = tagName(childElement);
125
+ if (name.includes('.')) return 'unknown';
126
+ if (/^[a-z]/.test(name)) return INFERRED_INTRINSICS.has(name) ? 'inferred' : 'unknown';
127
+ const from = imports.get(name);
128
+ if (from !== config.uxPackage) return 'unknown';
129
+ if (INFERRED_UX.has(name)) return 'inferred';
130
+ if (DENY.has(name)) return 'denied';
131
+ return 'unknown';
132
+ }
133
+
134
+ /** Visits every `<XTrigger>...</XTrigger>` element in the file. */
135
+ export function forEachTrigger(source, visit) {
136
+ const imports = importSources(source);
137
+ const walk = (node) => {
138
+ if (ts.isJsxElement(node) && TRIGGERS.has(tagName(node))) {
139
+ visit(node, imports);
140
+ }
141
+ ts.forEachChild(node, walk);
142
+ };
143
+ walk(source);
144
+ }
145
+
146
+ export function location(source, node) {
147
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart());
148
+ return `${path.relative(process.cwd(), source.fileName)}:${line + 1}:${character + 1}`;
149
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "$comment": "Shared by codemod-trigger-aschild.mjs and check-trigger-children.mjs. Mirrors the slottable markers in @clidey/ux src/lib/slot.ts.",
3
+ "triggers": [
4
+ "DropdownMenuTrigger",
5
+ "TooltipTrigger",
6
+ "PopoverTrigger",
7
+ "DialogTrigger",
8
+ "SheetTrigger",
9
+ "AlertDialogTrigger",
10
+ "DrawerTrigger",
11
+ "ContextMenuTrigger"
12
+ ],
13
+ "uxPackage": "@clidey/ux",
14
+ "inferredUxChildren": [
15
+ "Button",
16
+ "Badge",
17
+ "BreadcrumbLink",
18
+ "ButtonGroupText",
19
+ "Kbd",
20
+ "SidebarMenuButton",
21
+ "SidebarMenuAction",
22
+ "SidebarMenuSubButton",
23
+ "DropdownMenuTrigger",
24
+ "TooltipTrigger",
25
+ "PopoverTrigger",
26
+ "DialogTrigger",
27
+ "SheetTrigger",
28
+ "AlertDialogTrigger",
29
+ "DrawerTrigger",
30
+ "ContextMenuTrigger"
31
+ ],
32
+ "inferredIntrinsics": ["button", "a", "div", "span", "input", "label", "li"],
33
+ "denyList": [
34
+ "CopyButton",
35
+ "ConfirmDialog",
36
+ "FormSheet",
37
+ "MultiSearchSelect",
38
+ "Tree",
39
+ "ChartContainer",
40
+ "MapContainer",
41
+ "MasterDetailPanels",
42
+ "ResizablePanelGroup",
43
+ "ResizablePanel",
44
+ "ResizableHandle",
45
+ "Toaster"
46
+ ]
47
+ }