@dcoder-x/plugin-shared 0.1.3 → 0.1.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.
Files changed (36) hide show
  1. package/dist/buildId.d.ts +5 -0
  2. package/dist/buildId.js +17 -0
  3. package/dist/extractors/ComponentContextResolver.d.ts +37 -0
  4. package/dist/extractors/ComponentContextResolver.js +167 -0
  5. package/dist/extractors/ComponentExtractor.d.ts +6 -1
  6. package/dist/extractors/ComponentExtractor.js +124 -11
  7. package/dist/extractors/FlowInferrer.d.ts +5 -2
  8. package/dist/extractors/FlowInferrer.js +117 -14
  9. package/dist/extractors/InteractionGraphExtractor.d.ts +28 -0
  10. package/dist/extractors/InteractionGraphExtractor.js +333 -0
  11. package/dist/extractors/SelectorGenerator.d.ts +6 -1
  12. package/dist/extractors/SelectorGenerator.js +28 -3
  13. package/dist/index.d.ts +7 -1
  14. package/dist/index.js +21 -0
  15. package/dist/injection/ClippyIdInjector.d.ts +17 -0
  16. package/dist/injection/ClippyIdInjector.js +164 -0
  17. package/dist/injection/HtmlTagGuards.d.ts +3 -0
  18. package/dist/injection/HtmlTagGuards.js +41 -0
  19. package/dist/injection/IdStrategy.d.ts +36 -0
  20. package/dist/injection/IdStrategy.js +148 -0
  21. package/dist/types.d.ts +108 -1
  22. package/dist/upload/Adapter.d.ts +3 -0
  23. package/dist/upload/Adapter.js +13 -0
  24. package/dist/upload/BackendAdapter.d.ts +30 -0
  25. package/dist/upload/BackendAdapter.js +42 -0
  26. package/dist/upload/BackendUploadAdapter.d.ts +13 -0
  27. package/dist/upload/BackendUploadAdapter.js +51 -0
  28. package/dist/upload/PackageBuilder.d.ts +34 -1
  29. package/dist/upload/PackageBuilder.js +220 -0
  30. package/dist/upload/PackageWriter.d.ts +9 -1
  31. package/dist/upload/PackageWriter.js +26 -0
  32. package/dist/upload/UploadStrategy.d.ts +11 -0
  33. package/dist/upload/UploadStrategy.js +40 -0
  34. package/dist/upload/Uploader.d.ts +6 -1
  35. package/dist/upload/Uploader.js +48 -3
  36. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ import type { NodePath } from '@babel/traverse';
2
+ export declare function deriveComponentName(filePath: string, fallback?: string): string;
3
+ /**
4
+ * Generic Next.js component names that should be replaced by a route-derived name.
5
+ */
6
+ export declare const GENERIC_COMPONENT_NAMES: Set<string>;
7
+ /**
8
+ * Infer the route path from an absolute file path.
9
+ * Handles both App Router (app/…/page.tsx) and Pages Router (pages/….tsx).
10
+ * Returns null if the file is not a route file.
11
+ */
12
+ export declare function inferRouteFromFilePath(filePath: string): string | null;
13
+ /**
14
+ * Convert a route path to a PascalCase component name.
15
+ * /admin/transactions → AdminTransactions
16
+ * /dashboard/forms/[id] → DashboardForms
17
+ * /auth/forgot-password → AuthForgotPassword
18
+ */
19
+ export declare function deriveRouteComponentName(routePath: string): string;
20
+ /**
21
+ * Compute a short route hash for use in stable IDs.
22
+ * Takes a route path (e.g., "/admin/transactions") and produces a 4-char hash.
23
+ */
24
+ export declare function hashRoutePath(routePath: string): string;
25
+ /**
26
+ * Sanitize a label string for embedding in a CSS/HTML attribute ID.
27
+ * TitleCase, max 2 words, alphanumeric only, minimum 2 chars.
28
+ * Returns null if no usable text can be extracted.
29
+ */
30
+ export declare function sanitizeLabelForId(label: string): string | null;
31
+ export declare function deriveClippyId(componentName: string, tagName: string, lineNumber: number, routePath?: string, labelText?: string): string;
32
+ /**
33
+ * Walk the AST upward to find the enclosing React component name.
34
+ * Exported here so both the injector and the extractor use the same logic.
35
+ */
36
+ export declare function findEnclosingComponentName(path: NodePath<any>): string | null;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GENERIC_COMPONENT_NAMES = void 0;
7
+ exports.deriveComponentName = deriveComponentName;
8
+ exports.inferRouteFromFilePath = inferRouteFromFilePath;
9
+ exports.deriveRouteComponentName = deriveRouteComponentName;
10
+ exports.hashRoutePath = hashRoutePath;
11
+ exports.sanitizeLabelForId = sanitizeLabelForId;
12
+ exports.deriveClippyId = deriveClippyId;
13
+ exports.findEnclosingComponentName = findEnclosingComponentName;
14
+ const path_1 = __importDefault(require("path"));
15
+ function deriveComponentName(filePath, fallback) {
16
+ if (fallback)
17
+ return fallback;
18
+ const base = path_1.default.basename(filePath, path_1.default.extname(filePath));
19
+ if (!base || base === 'index') {
20
+ return 'UnknownComponent';
21
+ }
22
+ return normalizeComponentName(base);
23
+ }
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) {
98
+ const sanitizedComponent = normalizeComponentName(componentName);
99
+ const normalizedTag = tagName.replace(/[^a-z0-9]/gi, '') || 'element';
100
+ const sanitizedLabel = labelText ? sanitizeLabelForId(labelText) : null;
101
+ const stableLine = lineNumber > 0 ? lineNumber : 0;
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;
133
+ }
134
+ function normalizeComponentName(value) {
135
+ const sanitized = value
136
+ .replace(/[^A-Za-z0-9]+/g, ' ')
137
+ .trim()
138
+ .split(/\s+/)
139
+ .filter(Boolean)
140
+ .map((segment) => segment
141
+ .replace(/[^A-Za-z0-9]/g, '')
142
+ .replace(/^[^A-Za-z]+/, '')
143
+ .replace(/([a-z])([A-Z])/g, '$1$2'))
144
+ .filter(Boolean)
145
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
146
+ .join('');
147
+ return sanitized || 'Component';
148
+ }
package/dist/types.d.ts CHANGED
@@ -4,6 +4,11 @@ export interface ClippyPluginOptions {
4
4
  skipUpload?: boolean;
5
5
  productionOnly?: boolean;
6
6
  localOutputDir?: string;
7
+ artifactUploadMode?: 'single' | 'split';
8
+ legacyPackageMode?: boolean;
9
+ uploadAdapter?: {
10
+ uploadArtifacts?: (artifacts: PolicyArtifacts) => Promise<UploadResult>;
11
+ };
7
12
  }
8
13
  export type ModuleGraphSource = {
9
14
  type: 'webpack';
@@ -33,9 +38,15 @@ export interface DiscoveredElement {
33
38
  nearbyText: string[];
34
39
  semanticRole: string;
35
40
  interactions: string[];
41
+ label?: string;
42
+ component?: string;
43
+ isNavigationLink?: boolean;
44
+ loc?: {
45
+ line: number;
46
+ };
36
47
  }
37
48
  export interface SelectorCandidate {
38
- type: 'testid' | 'aria' | 'id' | 'semantic' | 'text' | 'structural';
49
+ type: 'clippy_id' | 'testid' | 'aria' | 'id' | 'semantic' | 'text' | 'structural';
39
50
  value: string;
40
51
  confidence: number;
41
52
  }
@@ -51,8 +62,104 @@ export interface FlowEdge {
51
62
  export interface InferredFlow {
52
63
  id: string;
53
64
  name: string;
65
+ page: string;
54
66
  steps: string[];
55
67
  edges: FlowEdge[];
68
+ intentPatterns?: string[];
69
+ }
70
+ export interface ArtifactMetadata {
71
+ version: string;
72
+ buildId: string;
73
+ generatedAt: string;
74
+ bundler: 'webpack' | 'vite';
75
+ }
76
+ export interface TriggerSpec {
77
+ event: string;
78
+ element: string;
79
+ setsState?: string;
80
+ }
81
+ export interface EffectSpec {
82
+ type: 'conditionalRender' | 'asyncEffect' | 'contextDependency';
83
+ rendersWhenTrue?: string;
84
+ rendersWhenFalse?: string;
85
+ waitStrategy: 'elementAppears' | 'domSettle' | 'none';
86
+ selector?: string;
87
+ settleMs?: number;
88
+ }
89
+ export interface ComponentInteraction {
90
+ trigger: TriggerSpec;
91
+ effect: EffectSpec;
92
+ }
93
+ export interface PolicySelectorEntry {
94
+ clippyId: string;
95
+ selector: string;
96
+ tag: string;
97
+ component: string;
98
+ label?: string;
99
+ route: string;
100
+ filePath: string;
101
+ attributes: Array<{
102
+ name: string;
103
+ value: string;
104
+ }>;
105
+ candidates: SelectorCandidate[];
106
+ }
107
+ export interface PolicyFlowStep {
108
+ step: number;
109
+ action: string;
110
+ target: string;
111
+ triggers?: Array<{
112
+ renders: string;
113
+ waitStrategy: 'elementAppears' | 'domSettle' | 'none';
114
+ }>;
115
+ }
116
+ export interface PolicyFlow {
117
+ flowId: string;
118
+ page: string;
119
+ intentPatterns: string[];
120
+ steps: PolicyFlowStep[];
121
+ }
122
+ export interface PolicyComponent {
123
+ name: string;
124
+ filePath: string;
125
+ route: string;
126
+ stateVariables: Array<{
127
+ name: string;
128
+ setter?: string;
129
+ initialValue?: string;
130
+ }>;
131
+ interactions: ComponentInteraction[];
132
+ }
133
+ export interface PolicyDocument extends ArtifactMetadata {
134
+ routes: DiscoveredRoute[];
135
+ selectors: PolicySelectorEntry[];
136
+ components: PolicyComponent[];
137
+ flows: PolicyFlow[];
138
+ }
139
+ export interface SelectorManifestEntry {
140
+ id: string;
141
+ selector: string;
142
+ component: string;
143
+ tag: string;
144
+ label?: string;
145
+ routes: string[];
146
+ }
147
+ export interface SelectorManifest {
148
+ version: string;
149
+ buildId: string;
150
+ generatedAt: string;
151
+ selectors: SelectorManifestEntry[];
152
+ }
153
+ export interface PolicyArtifacts {
154
+ metadata: ArtifactMetadata;
155
+ policy: PolicyDocument;
156
+ selectorManifest: SelectorManifest;
157
+ }
158
+ export interface UploadResult {
159
+ skipped: boolean;
160
+ policyUploaded: boolean;
161
+ selectorsUploaded: boolean;
162
+ mode: 'single' | 'split';
56
163
  }
57
164
  export interface KnowledgePackage {
58
165
  buildId: string;
@@ -0,0 +1,3 @@
1
+ import type { ClippyPluginOptions, PolicyArtifacts, UploadResult } from '../types';
2
+ export declare function performUpload(artifacts: PolicyArtifacts, options: ClippyPluginOptions): Promise<UploadResult>;
3
+ export default performUpload;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.performUpload = performUpload;
4
+ const Uploader_1 = require("./Uploader");
5
+ async function performUpload(artifacts, options) {
6
+ // Allow adapters to supply a custom upload implementation
7
+ if (options && options.uploadAdapter && typeof options.uploadAdapter.uploadArtifacts === 'function') {
8
+ return options.uploadAdapter.uploadArtifacts(artifacts);
9
+ }
10
+ const uploader = new Uploader_1.Uploader(options);
11
+ return uploader.uploadArtifacts(artifacts);
12
+ }
13
+ exports.default = performUpload;
@@ -0,0 +1,30 @@
1
+ import type { PolicyArtifacts } from '../types';
2
+ export interface SiteComponentRegistryUpload {
3
+ domain: string;
4
+ components: {
5
+ version: string;
6
+ generatedAt: string;
7
+ selectors: Array<{
8
+ clippyId: string;
9
+ selector: string;
10
+ tag: string;
11
+ component: string;
12
+ file: string;
13
+ attributes?: Array<{
14
+ name: string;
15
+ value: string | null;
16
+ }>;
17
+ }>;
18
+ components: Array<{
19
+ name: string;
20
+ file: string;
21
+ stateVariables?: any[];
22
+ interactions?: any[];
23
+ conditionalRenders?: any[];
24
+ effects?: any[];
25
+ contextUsages?: any[];
26
+ }>;
27
+ };
28
+ }
29
+ export declare function mapArtifactsToSiteRegistry(artifacts: PolicyArtifacts, domain: string): SiteComponentRegistryUpload;
30
+ export default mapArtifactsToSiteRegistry;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapArtifactsToSiteRegistry = mapArtifactsToSiteRegistry;
4
+ // Convert internal PolicyArtifacts -> backend SiteComponentRegistryUpload.
5
+ // This is intentionally a best-effort mapping: the plugin has richer
6
+ // fields (routes, flows, selector candidates). The adapter flattens
7
+ // and retains the core selectors/components the backend requires.
8
+ function mapArtifactsToSiteRegistry(artifacts, domain) {
9
+ const generatedAt = artifacts.metadata.generatedAt;
10
+ const selectors = (artifacts.policy.selectors || []).map((s) => ({
11
+ clippyId: s.clippyId,
12
+ selector: s.selector,
13
+ tag: s.tag,
14
+ component: s.component,
15
+ file: s.filePath,
16
+ attributes: s.attributes || [],
17
+ }));
18
+ const components = (artifacts.policy.components || []).map((c) => ({
19
+ name: c.name,
20
+ file: c.filePath,
21
+ stateVariables: c.stateVariables || [],
22
+ interactions: c.interactions || [],
23
+ // Best-effort placeholders: preserve array shape expected by backend.
24
+ conditionalRenders: [],
25
+ effects: (c.interactions || []).map((it) => ({
26
+ dependencies: [],
27
+ asyncCalls: [],
28
+ runsOnMount: false,
29
+ })),
30
+ contextUsages: [],
31
+ }));
32
+ return {
33
+ domain,
34
+ components: {
35
+ version: artifacts.metadata.version,
36
+ generatedAt,
37
+ selectors,
38
+ components,
39
+ },
40
+ };
41
+ }
42
+ exports.default = mapArtifactsToSiteRegistry;
@@ -0,0 +1,13 @@
1
+ import type { PolicyArtifacts, UploadResult } from '../types';
2
+ export interface BackendUploadOptions {
3
+ /** Full URL to POST the SiteComponentRegistryUpload to, e.g. http://localhost:3000/website/registry */
4
+ url: string;
5
+ /** Authorization header value (full header string, e.g. 'Bearer ...') */
6
+ authorization?: string;
7
+ /** Domain to include in the payload (required by backend contract) */
8
+ domain: string;
9
+ }
10
+ export declare function createBackendUploadAdapter(opts: BackendUploadOptions): {
11
+ uploadArtifacts(artifacts: PolicyArtifacts): Promise<UploadResult>;
12
+ };
13
+ export default createBackendUploadAdapter;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBackendUploadAdapter = createBackendUploadAdapter;
4
+ const BackendAdapter_1 = require("./BackendAdapter");
5
+ function createBackendUploadAdapter(opts) {
6
+ if (!opts || !opts.url)
7
+ throw new Error('BackendUploadAdapter requires a target url');
8
+ if (!opts.domain)
9
+ throw new Error('BackendUploadAdapter requires a domain');
10
+ return {
11
+ async uploadArtifacts(artifacts) {
12
+ const payload = (0, BackendAdapter_1.mapArtifactsToSiteRegistry)(artifacts, opts.domain);
13
+ // Use global fetch when available (Node 18+), otherwise try dynamic import
14
+ const headers = { 'Content-Type': 'application/json' };
15
+ if (opts.authorization)
16
+ headers['authorization'] = opts.authorization;
17
+ if (typeof fetch === 'function') {
18
+ const res = await fetch(opts.url, {
19
+ method: 'POST',
20
+ headers,
21
+ body: JSON.stringify(payload),
22
+ });
23
+ return {
24
+ skipped: false,
25
+ policyUploaded: res.ok,
26
+ selectorsUploaded: res.ok,
27
+ mode: 'single',
28
+ };
29
+ }
30
+ // Fallback to http/https
31
+ const { URL } = await import('url');
32
+ const parsed = new URL(opts.url);
33
+ const lib = parsed.protocol === 'https:' ? await import('https') : await import('http');
34
+ return new Promise((resolve, reject) => {
35
+ const req = lib.request(opts.url, {
36
+ method: 'POST',
37
+ headers,
38
+ }, (res) => {
39
+ const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
40
+ // consume body
41
+ res.on('data', () => { });
42
+ res.on('end', () => resolve({ skipped: false, policyUploaded: !!ok, selectorsUploaded: !!ok, mode: 'single' }));
43
+ });
44
+ req.on('error', (err) => reject(err));
45
+ req.write(JSON.stringify(payload));
46
+ req.end();
47
+ });
48
+ },
49
+ };
50
+ }
51
+ exports.default = createBackendUploadAdapter;
@@ -1,5 +1,38 @@
1
- import type { KnowledgePackage } from '../types';
1
+ import type { ElementWithSelectors, InferredFlow, KnowledgePackage, PolicyArtifacts, PolicyComponent, PolicyDocument, PolicySelectorEntry, SelectorManifest, DiscoveredRoute } from '../types';
2
2
  export declare class PackageBuilder {
3
+ private version;
3
4
  buildPackage(data: Omit<KnowledgePackage, 'generatedAt'>): KnowledgePackage;
5
+ buildArtifacts(data: {
6
+ buildId: string;
7
+ bundler: 'webpack' | 'vite';
8
+ routes: DiscoveredRoute[];
9
+ selectors: ElementWithSelectors[];
10
+ flows: InferredFlow[];
11
+ components?: PolicyComponent[];
12
+ }): PolicyArtifacts;
13
+ buildPolicyDocument(data: {
14
+ buildId: string;
15
+ generatedAt: string;
16
+ bundler: 'webpack' | 'vite';
17
+ routes: DiscoveredRoute[];
18
+ selectors: ElementWithSelectors[];
19
+ flows: InferredFlow[];
20
+ components?: PolicyComponent[];
21
+ }): PolicyDocument;
22
+ buildSelectorManifest(data: {
23
+ buildId: string;
24
+ generatedAt: string;
25
+ selectors: PolicySelectorEntry[];
26
+ }): SelectorManifest;
4
27
  build(data: Omit<KnowledgePackage, 'generatedAt'>): Buffer;
28
+ buildArtifactsBuffer(artifacts: PolicyArtifacts): Buffer;
29
+ private inferProjectRoot;
30
+ private normalizePath;
31
+ private toPolicySelectors;
32
+ private toPolicyComponents;
33
+ private toPolicyFlows;
34
+ private deriveComponentName;
35
+ private deriveStableId;
36
+ private extractClippyIdFromSelector;
37
+ private simpleHash;
5
38
  }