@dcoder-x/plugin-shared 0.1.4 → 0.1.6
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/dist/extractors/ComponentExtractor.js +70 -11
- package/dist/extractors/FlowInferrer.d.ts +19 -0
- package/dist/extractors/FlowInferrer.js +156 -18
- package/dist/extractors/InteractionGraphExtractor.d.ts +7 -13
- package/dist/extractors/InteractionGraphExtractor.js +143 -55
- package/dist/extractors/SelectorGenerator.d.ts +2 -6
- package/dist/extractors/SelectorGenerator.js +11 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/injection/ClippyIdInjector.d.ts +6 -1
- package/dist/injection/ClippyIdInjector.js +100 -19
- package/dist/injection/IdStrategy.d.ts +35 -1
- package/dist/injection/IdStrategy.js +112 -2
- package/dist/types.d.ts +7 -1
- package/dist/upload/PackageBuilder.d.ts +13 -1
- package/dist/upload/PackageBuilder.js +146 -38
- package/package.json +1 -1
|
@@ -3,8 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GENERIC_COMPONENT_NAMES = void 0;
|
|
6
7
|
exports.deriveComponentName = deriveComponentName;
|
|
8
|
+
exports.inferRouteFromFilePath = inferRouteFromFilePath;
|
|
9
|
+
exports.deriveRouteComponentName = deriveRouteComponentName;
|
|
10
|
+
exports.hashRoutePath = hashRoutePath;
|
|
11
|
+
exports.sanitizeLabelForId = sanitizeLabelForId;
|
|
7
12
|
exports.deriveClippyId = deriveClippyId;
|
|
13
|
+
exports.findEnclosingComponentName = findEnclosingComponentName;
|
|
8
14
|
const path_1 = __importDefault(require("path"));
|
|
9
15
|
function deriveComponentName(filePath, fallback) {
|
|
10
16
|
if (fallback)
|
|
@@ -15,11 +21,115 @@ function deriveComponentName(filePath, fallback) {
|
|
|
15
21
|
}
|
|
16
22
|
return normalizeComponentName(base);
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Generic Next.js component names that should be replaced by a route-derived name.
|
|
26
|
+
*/
|
|
27
|
+
exports.GENERIC_COMPONENT_NAMES = new Set([
|
|
28
|
+
'Page', 'Layout', 'App', 'Index', 'Component', 'Default',
|
|
29
|
+
'Loading', 'Error', 'NotFound', 'Template', 'Root',
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Infer the route path from an absolute file path.
|
|
33
|
+
* Handles both App Router (app/…/page.tsx) and Pages Router (pages/….tsx).
|
|
34
|
+
* Returns null if the file is not a route file.
|
|
35
|
+
*/
|
|
36
|
+
function inferRouteFromFilePath(filePath) {
|
|
37
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
38
|
+
const appMatch = normalized.match(/\/app(\/.*?)\/page\.[jt]sx?$/);
|
|
39
|
+
if (appMatch) {
|
|
40
|
+
const segment = appMatch[1]
|
|
41
|
+
.replace(/\/\([^)]+\)/g, ''); // strip route groups like (auth)
|
|
42
|
+
return segment || '/';
|
|
43
|
+
}
|
|
44
|
+
const pagesMatch = normalized.match(/\/pages(\/.*?)\.[jt]sx?$/);
|
|
45
|
+
if (pagesMatch) {
|
|
46
|
+
const segment = pagesMatch[1].replace(/\/index$/, '') || '/';
|
|
47
|
+
return segment;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Convert a route path to a PascalCase component name.
|
|
53
|
+
* /admin/transactions → AdminTransactions
|
|
54
|
+
* /dashboard/forms/[id] → DashboardForms
|
|
55
|
+
* /auth/forgot-password → AuthForgotPassword
|
|
56
|
+
*/
|
|
57
|
+
function deriveRouteComponentName(routePath) {
|
|
58
|
+
const name = routePath
|
|
59
|
+
.split('/')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.filter(s => !s.startsWith('[') && !s.startsWith(':') && !s.startsWith('$'))
|
|
62
|
+
.map(s => s
|
|
63
|
+
.split('-')
|
|
64
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
65
|
+
.join(''))
|
|
66
|
+
.join('');
|
|
67
|
+
return name || 'Page';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compute a short route hash for use in stable IDs.
|
|
71
|
+
* Takes a route path (e.g., "/admin/transactions") and produces a 4-char hash.
|
|
72
|
+
*/
|
|
73
|
+
function hashRoutePath(routePath) {
|
|
74
|
+
let hash = 0;
|
|
75
|
+
for (let i = 0; i < routePath.length; i++) {
|
|
76
|
+
hash = (hash << 5) - hash + routePath.charCodeAt(i);
|
|
77
|
+
hash = hash & hash;
|
|
78
|
+
}
|
|
79
|
+
return Math.abs(hash).toString(36).slice(0, 4);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Sanitize a label string for embedding in a CSS/HTML attribute ID.
|
|
83
|
+
* TitleCase, max 2 words, alphanumeric only, minimum 2 chars.
|
|
84
|
+
* Returns null if no usable text can be extracted.
|
|
85
|
+
*/
|
|
86
|
+
function sanitizeLabelForId(label) {
|
|
87
|
+
const words = label
|
|
88
|
+
.replace(/[^A-Za-z0-9\s]/g, ' ')
|
|
89
|
+
.trim()
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.slice(0, 2)
|
|
93
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
|
|
94
|
+
const result = words.join('');
|
|
95
|
+
return result.length >= 2 ? result : null;
|
|
96
|
+
}
|
|
97
|
+
function deriveClippyId(componentName, tagName, lineNumber, routePath, labelText) {
|
|
19
98
|
const sanitizedComponent = normalizeComponentName(componentName);
|
|
20
99
|
const normalizedTag = tagName.replace(/[^a-z0-9]/gi, '') || 'element';
|
|
100
|
+
const sanitizedLabel = labelText ? sanitizeLabelForId(labelText) : null;
|
|
21
101
|
const stableLine = lineNumber > 0 ? lineNumber : 0;
|
|
22
|
-
|
|
102
|
+
const parts = [sanitizedComponent];
|
|
103
|
+
if (sanitizedLabel)
|
|
104
|
+
parts.push(sanitizedLabel);
|
|
105
|
+
parts.push(normalizedTag);
|
|
106
|
+
parts.push(String(stableLine));
|
|
107
|
+
return parts.join('-');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Walk the AST upward to find the enclosing React component name.
|
|
111
|
+
* Exported here so both the injector and the extractor use the same logic.
|
|
112
|
+
*/
|
|
113
|
+
function findEnclosingComponentName(path) {
|
|
114
|
+
let current = path;
|
|
115
|
+
while (current) {
|
|
116
|
+
if (current.isFunctionDeclaration() && current.node.id?.name) {
|
|
117
|
+
return current.node.id.name;
|
|
118
|
+
}
|
|
119
|
+
if (current.isClassDeclaration() && current.node.id?.name) {
|
|
120
|
+
return current.node.id.name;
|
|
121
|
+
}
|
|
122
|
+
if (current.isVariableDeclarator() && current.node.id?.type === 'Identifier') {
|
|
123
|
+
return current.node.id.name;
|
|
124
|
+
}
|
|
125
|
+
if ((current.isArrowFunctionExpression() || current.isFunctionExpression()) &&
|
|
126
|
+
current.parentPath?.isVariableDeclarator() &&
|
|
127
|
+
current.parentPath.node.id?.type === 'Identifier') {
|
|
128
|
+
return current.parentPath.node.id.name;
|
|
129
|
+
}
|
|
130
|
+
current = current.parentPath;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
23
133
|
}
|
|
24
134
|
function normalizeComponentName(value) {
|
|
25
135
|
const sanitized = value
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export interface DiscoveredElement {
|
|
|
38
38
|
nearbyText: string[];
|
|
39
39
|
semanticRole: string;
|
|
40
40
|
interactions: string[];
|
|
41
|
+
label?: string;
|
|
42
|
+
component?: string;
|
|
43
|
+
isNavigationLink?: boolean;
|
|
41
44
|
loc?: {
|
|
42
45
|
line: number;
|
|
43
46
|
};
|
|
@@ -62,6 +65,7 @@ export interface InferredFlow {
|
|
|
62
65
|
page: string;
|
|
63
66
|
steps: string[];
|
|
64
67
|
edges: FlowEdge[];
|
|
68
|
+
intentPatterns?: string[];
|
|
65
69
|
}
|
|
66
70
|
export interface ArtifactMetadata {
|
|
67
71
|
version: string;
|
|
@@ -91,6 +95,7 @@ export interface PolicySelectorEntry {
|
|
|
91
95
|
selector: string;
|
|
92
96
|
tag: string;
|
|
93
97
|
component: string;
|
|
98
|
+
label?: string;
|
|
94
99
|
route: string;
|
|
95
100
|
filePath: string;
|
|
96
101
|
attributes: Array<{
|
|
@@ -136,7 +141,8 @@ export interface SelectorManifestEntry {
|
|
|
136
141
|
selector: string;
|
|
137
142
|
component: string;
|
|
138
143
|
tag: string;
|
|
139
|
-
|
|
144
|
+
label?: string;
|
|
145
|
+
routes: string[];
|
|
140
146
|
}
|
|
141
147
|
export interface SelectorManifest {
|
|
142
148
|
version: string;
|
|
@@ -3,6 +3,7 @@ export declare class PackageBuilder {
|
|
|
3
3
|
private version;
|
|
4
4
|
buildPackage(data: Omit<KnowledgePackage, 'generatedAt'>): KnowledgePackage;
|
|
5
5
|
buildArtifacts(data: {
|
|
6
|
+
projectRoot?: string;
|
|
6
7
|
buildId: string;
|
|
7
8
|
bundler: 'webpack' | 'vite';
|
|
8
9
|
routes: DiscoveredRoute[];
|
|
@@ -11,6 +12,7 @@ export declare class PackageBuilder {
|
|
|
11
12
|
components?: PolicyComponent[];
|
|
12
13
|
}): PolicyArtifacts;
|
|
13
14
|
buildPolicyDocument(data: {
|
|
15
|
+
projectRoot?: string;
|
|
14
16
|
buildId: string;
|
|
15
17
|
generatedAt: string;
|
|
16
18
|
bundler: 'webpack' | 'vite';
|
|
@@ -26,11 +28,21 @@ export declare class PackageBuilder {
|
|
|
26
28
|
}): SelectorManifest;
|
|
27
29
|
build(data: Omit<KnowledgePackage, 'generatedAt'>): Buffer;
|
|
28
30
|
buildArtifactsBuffer(artifacts: PolicyArtifacts): Buffer;
|
|
31
|
+
/**
|
|
32
|
+
* Compute a fallback project root from the common ancestor of route file paths.
|
|
33
|
+
* Used only when the adapter does not provide an explicit projectRoot.
|
|
34
|
+
*/
|
|
35
|
+
private inferProjectRoot;
|
|
36
|
+
private normalizePath;
|
|
37
|
+
/**
|
|
38
|
+
* Remove design-system primitives and other shared components that carry no
|
|
39
|
+
* state or interaction data. Normalises filePaths against the project root.
|
|
40
|
+
*/
|
|
41
|
+
private filterMeaningfulComponents;
|
|
29
42
|
private toPolicySelectors;
|
|
30
43
|
private toPolicyComponents;
|
|
31
44
|
private toPolicyFlows;
|
|
32
45
|
private deriveComponentName;
|
|
33
46
|
private deriveStableId;
|
|
34
47
|
private extractClippyIdFromSelector;
|
|
35
|
-
private simpleHash;
|
|
36
48
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PackageBuilder = void 0;
|
|
7
7
|
const zlib_1 = __importDefault(require("zlib"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const IdStrategy_1 = require("../injection/IdStrategy");
|
|
9
10
|
class PackageBuilder {
|
|
10
11
|
constructor() {
|
|
@@ -39,31 +40,58 @@ class PackageBuilder {
|
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
buildPolicyDocument(data) {
|
|
42
|
-
|
|
43
|
+
// Use the adapter-provided project root (most reliable).
|
|
44
|
+
// Fall back to computing the common ancestor of route paths only when unavailable.
|
|
45
|
+
const projectRoot = data.projectRoot || this.inferProjectRoot(data.routes);
|
|
46
|
+
const normalizedRoutes = data.routes.map((r) => ({
|
|
47
|
+
...r,
|
|
48
|
+
filePath: this.normalizePath(r.filePath, projectRoot),
|
|
49
|
+
layout: r.layout ? this.normalizePath(r.layout, projectRoot) : null,
|
|
50
|
+
}));
|
|
51
|
+
const policySelectors = this.toPolicySelectors(data.selectors, projectRoot);
|
|
52
|
+
// Fix 2: only keep components that carry meaningful data (state or interactions).
|
|
53
|
+
// Pure design-system primitives (Button, Card, Input, etc.) with empty arrays
|
|
54
|
+
// are excluded — they bloat the policy without adding actionable information.
|
|
55
|
+
const rawComponents = data.components ?? this.toPolicyComponents(data.selectors, projectRoot);
|
|
56
|
+
const meaningfulComponents = this.filterMeaningfulComponents(rawComponents, projectRoot);
|
|
43
57
|
return {
|
|
44
58
|
version: this.version,
|
|
45
59
|
buildId: data.buildId,
|
|
46
60
|
generatedAt: data.generatedAt,
|
|
47
61
|
bundler: data.bundler,
|
|
48
|
-
routes:
|
|
62
|
+
routes: normalizedRoutes,
|
|
49
63
|
selectors: policySelectors,
|
|
50
|
-
components:
|
|
51
|
-
flows: this.toPolicyFlows(data.flows,
|
|
64
|
+
components: meaningfulComponents,
|
|
65
|
+
flows: this.toPolicyFlows(data.flows, normalizedRoutes, policySelectors),
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
68
|
buildSelectorManifest(data) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
// Deduplicate by clippyId — shared components used on multiple routes appear once
|
|
70
|
+
// with a routes[] array listing every route where they appear.
|
|
71
|
+
const byId = new Map();
|
|
72
|
+
for (const entry of data.selectors) {
|
|
73
|
+
if (byId.has(entry.clippyId)) {
|
|
74
|
+
const existing = byId.get(entry.clippyId);
|
|
75
|
+
if (!existing.routes.includes(entry.route)) {
|
|
76
|
+
existing.routes.push(entry.route);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
byId.set(entry.clippyId, {
|
|
81
|
+
id: entry.clippyId,
|
|
82
|
+
selector: entry.selector,
|
|
83
|
+
component: entry.component,
|
|
84
|
+
tag: entry.tag,
|
|
85
|
+
label: entry.label,
|
|
86
|
+
routes: [entry.route],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
62
90
|
return {
|
|
63
91
|
version: this.version,
|
|
64
92
|
buildId: data.buildId,
|
|
65
93
|
generatedAt: data.generatedAt,
|
|
66
|
-
selectors,
|
|
94
|
+
selectors: Array.from(byId.values()),
|
|
67
95
|
};
|
|
68
96
|
}
|
|
69
97
|
build(data) {
|
|
@@ -73,9 +101,71 @@ class PackageBuilder {
|
|
|
73
101
|
buildArtifactsBuffer(artifacts) {
|
|
74
102
|
return zlib_1.default.gzipSync(JSON.stringify(artifacts));
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Path helpers
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Compute a fallback project root from the common ancestor of route file paths.
|
|
109
|
+
* Used only when the adapter does not provide an explicit projectRoot.
|
|
110
|
+
*/
|
|
111
|
+
inferProjectRoot(routes) {
|
|
112
|
+
const filePaths = routes.map((r) => r.filePath).filter(Boolean);
|
|
113
|
+
if (filePaths.length === 0)
|
|
114
|
+
return process.cwd();
|
|
115
|
+
const parts = filePaths[0].replace(/\\/g, '/').split('/');
|
|
116
|
+
let commonParts = parts;
|
|
117
|
+
for (const fp of filePaths.slice(1)) {
|
|
118
|
+
const fpParts = fp.replace(/\\/g, '/').split('/');
|
|
119
|
+
const len = Math.min(commonParts.length, fpParts.length);
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < len && commonParts[i] === fpParts[i])
|
|
122
|
+
i++;
|
|
123
|
+
commonParts = commonParts.slice(0, i);
|
|
124
|
+
}
|
|
125
|
+
// Common ancestor of route files is typically the `app/` dir.
|
|
126
|
+
// Go one level up to reach the actual project root.
|
|
127
|
+
if (commonParts.length > 0) {
|
|
128
|
+
const candidate = commonParts.join('/');
|
|
129
|
+
const parent = candidate.includes('/') ? candidate.replace(/\/[^/]+$/, '') : candidate;
|
|
130
|
+
if (parent)
|
|
131
|
+
return parent;
|
|
132
|
+
}
|
|
133
|
+
return process.cwd();
|
|
134
|
+
}
|
|
135
|
+
normalizePath(filePath, projectRoot) {
|
|
136
|
+
try {
|
|
137
|
+
const rel = path_1.default.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
138
|
+
// Only use the relative path if it doesn't escape above the project root
|
|
139
|
+
return rel.startsWith('..') ? filePath.replace(/\\/g, '/') : rel;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return filePath.replace(/\\/g, '/');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Component filtering (Fix 2)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Remove design-system primitives and other shared components that carry no
|
|
150
|
+
* state or interaction data. Normalises filePaths against the project root.
|
|
151
|
+
*/
|
|
152
|
+
filterMeaningfulComponents(components, projectRoot) {
|
|
153
|
+
return components
|
|
154
|
+
.filter((c) => (c.stateVariables && c.stateVariables.length > 0) ||
|
|
155
|
+
(c.interactions && c.interactions.length > 0))
|
|
156
|
+
.map((c) => ({
|
|
157
|
+
...c,
|
|
158
|
+
filePath: this.normalizePath(c.filePath, projectRoot),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Selector conversion
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
toPolicySelectors(selectors, projectRoot) {
|
|
165
|
+
return selectors
|
|
166
|
+
.filter((element) => !element.isNavigationLink)
|
|
167
|
+
.map((element, index) => {
|
|
168
|
+
const component = element.component || (0, IdStrategy_1.deriveComponentName)(element.filePath);
|
|
79
169
|
const preferred = element.selectors.find((candidate) => candidate.type === 'clippy_id') ||
|
|
80
170
|
element.selectors[0];
|
|
81
171
|
const stableLine = element.loc?.line ?? index;
|
|
@@ -83,65 +173,90 @@ class PackageBuilder {
|
|
|
83
173
|
? this.extractClippyIdFromSelector(preferred.value) ||
|
|
84
174
|
this.deriveStableId(component, element.tag, stableLine)
|
|
85
175
|
: this.deriveStableId(component, element.tag, stableLine);
|
|
86
|
-
const selector = preferred?.value ||
|
|
87
|
-
`[data-clippy-id='${clippyId}']`;
|
|
176
|
+
const selector = preferred?.value || `[data-clippy-id='${clippyId}']`;
|
|
88
177
|
return {
|
|
89
178
|
clippyId,
|
|
90
179
|
selector,
|
|
91
180
|
tag: element.tag,
|
|
92
181
|
component,
|
|
182
|
+
label: element.label,
|
|
93
183
|
route: element.route,
|
|
94
|
-
filePath:
|
|
184
|
+
filePath: projectRoot
|
|
185
|
+
? this.normalizePath(element.filePath, projectRoot)
|
|
186
|
+
: element.filePath.replace(/\\/g, '/'),
|
|
95
187
|
attributes: Object.entries(element.staticProps).map(([name, value]) => ({ name, value })),
|
|
96
188
|
candidates: element.selectors,
|
|
97
189
|
};
|
|
98
190
|
});
|
|
99
191
|
}
|
|
100
|
-
toPolicyComponents(selectors) {
|
|
192
|
+
toPolicyComponents(selectors, projectRoot) {
|
|
101
193
|
const byFile = new Map();
|
|
102
194
|
for (const element of selectors) {
|
|
195
|
+
if (element.isNavigationLink)
|
|
196
|
+
continue;
|
|
103
197
|
if (!byFile.has(element.filePath))
|
|
104
198
|
byFile.set(element.filePath, []);
|
|
105
199
|
byFile.get(element.filePath).push(element);
|
|
106
200
|
}
|
|
107
201
|
return Array.from(byFile.entries()).map(([filePath, items]) => {
|
|
108
|
-
const componentName = this.deriveComponentName(filePath);
|
|
202
|
+
const componentName = items.find((i) => i.component)?.component || this.deriveComponentName(filePath);
|
|
109
203
|
const route = items[0]?.route || '/';
|
|
110
204
|
return {
|
|
111
205
|
name: componentName,
|
|
112
|
-
filePath,
|
|
206
|
+
filePath: projectRoot ? this.normalizePath(filePath, projectRoot) : filePath.replace(/\\/g, '/'),
|
|
113
207
|
route,
|
|
114
208
|
stateVariables: [],
|
|
115
209
|
interactions: [],
|
|
116
210
|
};
|
|
117
211
|
});
|
|
118
212
|
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Flow conversion
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
119
216
|
toPolicyFlows(flows, routes, selectors) {
|
|
120
217
|
return flows.map((flow, flowIndex) => {
|
|
121
218
|
const firstRoute = flow.steps[0] || '/';
|
|
122
|
-
const page = flow.page || routes.find((
|
|
123
|
-
const hasInteraction = flow.edges.some((edge) => edge.from === edge.to && edge.trigger?.startsWith('on'));
|
|
219
|
+
const page = flow.page || routes.find((r) => r.path === firstRoute)?.path || firstRoute;
|
|
124
220
|
const steps = flow.steps.map((routePath, stepIndex) => {
|
|
125
|
-
const
|
|
126
|
-
|
|
221
|
+
const incomingEdge = stepIndex > 0 ? flow.edges.find((e) => e.to === routePath) : null;
|
|
222
|
+
let target;
|
|
223
|
+
if (incomingEdge?.trigger && incomingEdge.trigger !== 'link') {
|
|
224
|
+
const fromRoute = incomingEdge.from;
|
|
225
|
+
const byLabel = selectors.find((s) => s.route === fromRoute &&
|
|
226
|
+
s.label?.toLowerCase() === incomingEdge.trigger.toLowerCase());
|
|
227
|
+
const byAttr = !byLabel
|
|
228
|
+
? selectors.find((s) => s.route === fromRoute &&
|
|
229
|
+
s.attributes.some((a) => a.value.toLowerCase() === incomingEdge.trigger.toLowerCase()))
|
|
230
|
+
: null;
|
|
231
|
+
target = (byLabel || byAttr)?.selector;
|
|
232
|
+
}
|
|
233
|
+
if (!target) {
|
|
234
|
+
target =
|
|
235
|
+
selectors.find((s) => s.route === routePath)?.selector ||
|
|
236
|
+
`[data-clippy-route='${routePath}']`;
|
|
237
|
+
}
|
|
238
|
+
const isFirst = stepIndex === 0;
|
|
239
|
+
const isInteractionStep = flow.edges.some((e) => e.from === e.to && e.trigger?.startsWith('on'));
|
|
127
240
|
return {
|
|
128
241
|
step: stepIndex + 1,
|
|
129
|
-
action:
|
|
130
|
-
? hasInteraction
|
|
131
|
-
? 'interact'
|
|
132
|
-
: 'navigate'
|
|
133
|
-
: 'transition',
|
|
242
|
+
action: isFirst ? (isInteractionStep ? 'interact' : 'navigate') : 'transition',
|
|
134
243
|
target,
|
|
135
244
|
};
|
|
136
245
|
});
|
|
246
|
+
const intentPatterns = flow.intentPatterns && flow.intentPatterns.length > 0
|
|
247
|
+
? flow.intentPatterns
|
|
248
|
+
: [flow.name.toLowerCase()];
|
|
137
249
|
return {
|
|
138
250
|
flowId: flow.id || `flow_${flowIndex + 1}`,
|
|
139
251
|
page,
|
|
140
|
-
intentPatterns
|
|
252
|
+
intentPatterns,
|
|
141
253
|
steps,
|
|
142
254
|
};
|
|
143
255
|
});
|
|
144
256
|
}
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Helpers
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
145
260
|
deriveComponentName(filePath) {
|
|
146
261
|
return (0, IdStrategy_1.deriveComponentName)(filePath);
|
|
147
262
|
}
|
|
@@ -152,12 +267,5 @@ class PackageBuilder {
|
|
|
152
267
|
const match = selector.match(/\[data-clippy-id=['\"]([^'\"]+)['\"]\]/);
|
|
153
268
|
return match?.[1] || null;
|
|
154
269
|
}
|
|
155
|
-
simpleHash(input) {
|
|
156
|
-
let hash = 0;
|
|
157
|
-
for (let i = 0; i < input.length; i++) {
|
|
158
|
-
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
|
|
159
|
-
}
|
|
160
|
-
return hash.toString(36);
|
|
161
|
-
}
|
|
162
270
|
}
|
|
163
271
|
exports.PackageBuilder = PackageBuilder;
|