@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
@@ -5,16 +5,236 @@ 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"));
9
+ const IdStrategy_1 = require("../injection/IdStrategy");
8
10
  class PackageBuilder {
11
+ constructor() {
12
+ this.version = '1.0.0';
13
+ }
9
14
  buildPackage(data) {
10
15
  return {
11
16
  ...data,
12
17
  generatedAt: new Date().toISOString(),
13
18
  };
14
19
  }
20
+ buildArtifacts(data) {
21
+ const generatedAt = new Date().toISOString();
22
+ const policy = this.buildPolicyDocument({
23
+ ...data,
24
+ generatedAt,
25
+ });
26
+ const selectorManifest = this.buildSelectorManifest({
27
+ buildId: data.buildId,
28
+ generatedAt,
29
+ selectors: policy.selectors,
30
+ });
31
+ return {
32
+ metadata: {
33
+ version: this.version,
34
+ buildId: data.buildId,
35
+ generatedAt,
36
+ bundler: data.bundler,
37
+ },
38
+ policy,
39
+ selectorManifest,
40
+ };
41
+ }
42
+ buildPolicyDocument(data) {
43
+ const projectRoot = this.inferProjectRoot(data.routes);
44
+ const normalizedRoutes = data.routes.map((r) => ({
45
+ ...r,
46
+ filePath: this.normalizePath(r.filePath, projectRoot),
47
+ layout: r.layout ? this.normalizePath(r.layout, projectRoot) : null,
48
+ }));
49
+ const policySelectors = this.toPolicySelectors(data.selectors, projectRoot);
50
+ return {
51
+ version: this.version,
52
+ buildId: data.buildId,
53
+ generatedAt: data.generatedAt,
54
+ bundler: data.bundler,
55
+ routes: normalizedRoutes,
56
+ selectors: policySelectors,
57
+ components: data.components ?? this.toPolicyComponents(data.selectors),
58
+ flows: this.toPolicyFlows(data.flows, normalizedRoutes, policySelectors),
59
+ };
60
+ }
61
+ buildSelectorManifest(data) {
62
+ // Deduplicate by clippyId — shared components used on multiple routes appear once
63
+ // with a routes[] array listing every route where they appear.
64
+ const byId = new Map();
65
+ for (const entry of data.selectors) {
66
+ if (byId.has(entry.clippyId)) {
67
+ const existing = byId.get(entry.clippyId);
68
+ if (!existing.routes.includes(entry.route)) {
69
+ existing.routes.push(entry.route);
70
+ }
71
+ }
72
+ else {
73
+ byId.set(entry.clippyId, {
74
+ id: entry.clippyId,
75
+ selector: entry.selector,
76
+ component: entry.component,
77
+ tag: entry.tag,
78
+ label: entry.label,
79
+ routes: [entry.route],
80
+ });
81
+ }
82
+ }
83
+ return {
84
+ version: this.version,
85
+ buildId: data.buildId,
86
+ generatedAt: data.generatedAt,
87
+ selectors: Array.from(byId.values()),
88
+ };
89
+ }
15
90
  build(data) {
16
91
  const pkg = this.buildPackage(data);
17
92
  return zlib_1.default.gzipSync(JSON.stringify(pkg));
18
93
  }
94
+ buildArtifactsBuffer(artifacts) {
95
+ return zlib_1.default.gzipSync(JSON.stringify(artifacts));
96
+ }
97
+ inferProjectRoot(routes) {
98
+ const filePaths = routes.map((r) => r.filePath).filter(Boolean);
99
+ if (filePaths.length === 0)
100
+ return process.cwd();
101
+ // Find the longest common directory prefix across all route file paths
102
+ const parts = filePaths[0].replace(/\\/g, '/').split('/');
103
+ let commonParts = parts;
104
+ for (const fp of filePaths.slice(1)) {
105
+ const fpParts = fp.replace(/\\/g, '/').split('/');
106
+ const len = Math.min(commonParts.length, fpParts.length);
107
+ let i = 0;
108
+ while (i < len && commonParts[i] === fpParts[i])
109
+ i++;
110
+ commonParts = commonParts.slice(0, i);
111
+ }
112
+ return commonParts.join('/') || process.cwd();
113
+ }
114
+ normalizePath(filePath, projectRoot) {
115
+ try {
116
+ const rel = path_1.default.relative(projectRoot, filePath).replace(/\\/g, '/');
117
+ return rel.startsWith('..') ? filePath.replace(/\\/g, '/') : rel;
118
+ }
119
+ catch {
120
+ return filePath.replace(/\\/g, '/');
121
+ }
122
+ }
123
+ toPolicySelectors(selectors, projectRoot) {
124
+ return selectors
125
+ .filter((element) => !element.isNavigationLink)
126
+ .map((element, index) => {
127
+ const component = element.component || (0, IdStrategy_1.deriveComponentName)(element.filePath);
128
+ const preferred = element.selectors.find((candidate) => candidate.type === 'clippy_id') ||
129
+ element.selectors[0];
130
+ const stableLine = element.loc?.line ?? index;
131
+ const clippyId = preferred?.type === 'clippy_id'
132
+ ? this.extractClippyIdFromSelector(preferred.value) ||
133
+ this.deriveStableId(component, element.tag, stableLine)
134
+ : this.deriveStableId(component, element.tag, stableLine);
135
+ const selector = preferred?.value ||
136
+ `[data-clippy-id='${clippyId}']`;
137
+ return {
138
+ clippyId,
139
+ selector,
140
+ tag: element.tag,
141
+ component,
142
+ label: element.label,
143
+ route: element.route,
144
+ filePath: projectRoot ? this.normalizePath(element.filePath, projectRoot) : element.filePath.replace(/\\/g, '/'),
145
+ attributes: Object.entries(element.staticProps).map(([name, value]) => ({ name, value })),
146
+ candidates: element.selectors,
147
+ };
148
+ });
149
+ }
150
+ toPolicyComponents(selectors) {
151
+ const byFile = new Map();
152
+ for (const element of selectors) {
153
+ if (element.isNavigationLink)
154
+ continue;
155
+ if (!byFile.has(element.filePath))
156
+ byFile.set(element.filePath, []);
157
+ byFile.get(element.filePath).push(element);
158
+ }
159
+ return Array.from(byFile.entries()).map(([filePath, items]) => {
160
+ // Prefer the component name captured during extraction over the filename
161
+ const componentName = items.find((i) => i.component)?.component ||
162
+ this.deriveComponentName(filePath);
163
+ const route = items[0]?.route || '/';
164
+ return {
165
+ name: componentName,
166
+ filePath,
167
+ route,
168
+ stateVariables: [],
169
+ interactions: [],
170
+ };
171
+ });
172
+ }
173
+ toPolicyFlows(flows, routes, selectors) {
174
+ return flows.map((flow, flowIndex) => {
175
+ const firstRoute = flow.steps[0] || '/';
176
+ const page = flow.page || routes.find((r) => r.path === firstRoute)?.path || firstRoute;
177
+ const steps = flow.steps.map((routePath, stepIndex) => {
178
+ // For transition steps (not the first), find the edge that leads to this route
179
+ // and try to match its trigger text to a specific selector
180
+ const incomingEdge = stepIndex > 0
181
+ ? flow.edges.find((e) => e.to === routePath)
182
+ : null;
183
+ let target;
184
+ if (incomingEdge?.trigger && incomingEdge.trigger !== 'link') {
185
+ // Try to find the specific triggering element by label match
186
+ const fromRoute = incomingEdge.from;
187
+ const byLabel = selectors.find((s) => s.route === fromRoute &&
188
+ s.label?.toLowerCase() === incomingEdge.trigger.toLowerCase());
189
+ const byAttr = !byLabel
190
+ ? selectors.find((s) => s.route === fromRoute &&
191
+ s.attributes.some((a) => a.value.toLowerCase() === incomingEdge.trigger.toLowerCase()))
192
+ : null;
193
+ target = (byLabel || byAttr)?.selector;
194
+ }
195
+ // Fallback: first selector on the destination route
196
+ if (!target) {
197
+ target =
198
+ selectors.find((s) => s.route === routePath)?.selector ||
199
+ `[data-clippy-route='${routePath}']`;
200
+ }
201
+ const isFirst = stepIndex === 0;
202
+ const isInteractionStep = flow.edges.some((e) => e.from === e.to && e.trigger?.startsWith('on'));
203
+ return {
204
+ step: stepIndex + 1,
205
+ action: isFirst
206
+ ? isInteractionStep ? 'interact' : 'navigate'
207
+ : 'transition',
208
+ target,
209
+ };
210
+ });
211
+ const intentPatterns = flow.intentPatterns && flow.intentPatterns.length > 0
212
+ ? flow.intentPatterns
213
+ : [flow.name.toLowerCase()];
214
+ return {
215
+ flowId: flow.id || `flow_${flowIndex + 1}`,
216
+ page,
217
+ intentPatterns,
218
+ steps,
219
+ };
220
+ });
221
+ }
222
+ deriveComponentName(filePath) {
223
+ return (0, IdStrategy_1.deriveComponentName)(filePath);
224
+ }
225
+ deriveStableId(component, tag, line) {
226
+ return (0, IdStrategy_1.deriveClippyId)(component, tag, line);
227
+ }
228
+ extractClippyIdFromSelector(selector) {
229
+ const match = selector.match(/\[data-clippy-id=['\"]([^'\"]+)['\"]\]/);
230
+ return match?.[1] || null;
231
+ }
232
+ simpleHash(input) {
233
+ let hash = 0;
234
+ for (let i = 0; i < input.length; i++) {
235
+ hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
236
+ }
237
+ return hash.toString(36);
238
+ }
19
239
  }
20
240
  exports.PackageBuilder = PackageBuilder;
@@ -1,7 +1,15 @@
1
- import type { KnowledgePackage } from '../types';
1
+ import type { KnowledgePackage, PolicyArtifacts } from '../types';
2
2
  export declare class PackageWriter {
3
3
  write(outputDir: string, knowledgePackage: KnowledgePackage): {
4
4
  jsonPath: string;
5
5
  gzipPath: string;
6
6
  };
7
+ writeArtifacts(outputDir: string, artifacts: PolicyArtifacts, options?: {
8
+ gzip?: boolean;
9
+ }): {
10
+ policyPath: string;
11
+ selectorsPath: string;
12
+ policyGzipPath?: string;
13
+ selectorsGzipPath?: string;
14
+ };
7
15
  }
@@ -18,5 +18,31 @@ class PackageWriter {
18
18
  fs_1.default.writeFileSync(gzipPath, zlib_1.default.gzipSync(content));
19
19
  return { jsonPath, gzipPath };
20
20
  }
21
+ writeArtifacts(outputDir, artifacts, options) {
22
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
23
+ const policyPath = path_1.default.join(outputDir, 'clippy-policy.json');
24
+ const selectorsPath = path_1.default.join(outputDir, 'clippy-selectors.json');
25
+ const policyContent = JSON.stringify(artifacts.policy, null, 2);
26
+ const selectorsContent = JSON.stringify(artifacts.selectorManifest, null, 2);
27
+ fs_1.default.writeFileSync(policyPath, policyContent, 'utf-8');
28
+ fs_1.default.writeFileSync(selectorsPath, selectorsContent, 'utf-8');
29
+ const shouldGzip = options?.gzip ?? true;
30
+ if (!shouldGzip) {
31
+ return {
32
+ policyPath,
33
+ selectorsPath,
34
+ };
35
+ }
36
+ const policyGzipPath = `${policyPath}.gz`;
37
+ const selectorsGzipPath = `${selectorsPath}.gz`;
38
+ fs_1.default.writeFileSync(policyGzipPath, zlib_1.default.gzipSync(policyContent));
39
+ fs_1.default.writeFileSync(selectorsGzipPath, zlib_1.default.gzipSync(selectorsContent));
40
+ return {
41
+ policyPath,
42
+ selectorsPath,
43
+ policyGzipPath,
44
+ selectorsGzipPath,
45
+ };
46
+ }
21
47
  }
22
48
  exports.PackageWriter = PackageWriter;
@@ -0,0 +1,11 @@
1
+ import type { PolicyArtifacts } from '../types';
2
+ export interface ArtifactRequest {
3
+ endpoint: string;
4
+ payload: Buffer;
5
+ extraHeaders?: Record<string, string>;
6
+ }
7
+ declare const POLICY_ENDPOINT = "https://api.clippy.dev/v1/policy";
8
+ declare const SELECTORS_ENDPOINT = "https://api.clippy.dev/v1/selectors";
9
+ declare const BUNDLE_ENDPOINT = "https://api.clippy.dev/v1/policy-artifacts";
10
+ export declare function buildArtifactRequests(artifacts: PolicyArtifacts, mode: 'single' | 'split'): ArtifactRequest[];
11
+ export { POLICY_ENDPOINT, SELECTORS_ENDPOINT, BUNDLE_ENDPOINT };
@@ -0,0 +1,40 @@
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.BUNDLE_ENDPOINT = exports.SELECTORS_ENDPOINT = exports.POLICY_ENDPOINT = void 0;
7
+ exports.buildArtifactRequests = buildArtifactRequests;
8
+ const zlib_1 = __importDefault(require("zlib"));
9
+ const POLICY_ENDPOINT = 'https://api.clippy.dev/v1/policy';
10
+ exports.POLICY_ENDPOINT = POLICY_ENDPOINT;
11
+ const SELECTORS_ENDPOINT = 'https://api.clippy.dev/v1/selectors';
12
+ exports.SELECTORS_ENDPOINT = SELECTORS_ENDPOINT;
13
+ const BUNDLE_ENDPOINT = 'https://api.clippy.dev/v1/policy-artifacts';
14
+ exports.BUNDLE_ENDPOINT = BUNDLE_ENDPOINT;
15
+ function buildArtifactRequests(artifacts, mode) {
16
+ if (mode === 'split') {
17
+ const policyPayload = zlib_1.default.gzipSync(JSON.stringify(artifacts.policy));
18
+ const selectorsPayload = zlib_1.default.gzipSync(JSON.stringify(artifacts.selectorManifest));
19
+ return [
20
+ {
21
+ endpoint: POLICY_ENDPOINT,
22
+ payload: policyPayload,
23
+ extraHeaders: { 'X-Clippy-Artifact': 'policy' },
24
+ },
25
+ {
26
+ endpoint: SELECTORS_ENDPOINT,
27
+ payload: selectorsPayload,
28
+ extraHeaders: { 'X-Clippy-Artifact': 'selectors' },
29
+ },
30
+ ];
31
+ }
32
+ const bundle = zlib_1.default.gzipSync(JSON.stringify(artifacts));
33
+ return [
34
+ {
35
+ endpoint: BUNDLE_ENDPOINT,
36
+ payload: bundle,
37
+ extraHeaders: { 'X-Clippy-Artifact': 'bundle' },
38
+ },
39
+ ];
40
+ }
@@ -1,10 +1,15 @@
1
- import type { ClippyPluginOptions } from '../types';
1
+ import type { ClippyPluginOptions, PolicyArtifacts, UploadResult } from '../types';
2
2
  export declare class Uploader {
3
3
  private options;
4
4
  private endpoint;
5
+ private policyArtifactsEndpoint;
6
+ private policyEndpoint;
7
+ private selectorsEndpoint;
5
8
  private requestTimeoutMs;
6
9
  private maxAttempts;
7
10
  constructor(options: ClippyPluginOptions);
8
11
  upload(compressedPackage: Buffer): Promise<boolean>;
12
+ uploadArtifacts(artifacts: PolicyArtifacts): Promise<UploadResult>;
13
+ private uploadWithRetry;
9
14
  private uploadOnce;
10
15
  }
@@ -5,10 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Uploader = void 0;
7
7
  const https_1 = __importDefault(require("https"));
8
+ const UploadStrategy_1 = require("./UploadStrategy");
8
9
  class Uploader {
9
10
  constructor(options) {
10
11
  this.options = options;
11
12
  this.endpoint = 'https://api.clippy.dev/v1/knowledge';
13
+ this.policyArtifactsEndpoint = 'https://api.clippy.dev/v1/policy-artifacts';
14
+ this.policyEndpoint = 'https://api.clippy.dev/v1/policy';
15
+ this.selectorsEndpoint = 'https://api.clippy.dev/v1/selectors';
12
16
  this.requestTimeoutMs = 10000;
13
17
  this.maxAttempts = 3;
14
18
  }
@@ -18,7 +22,7 @@ class Uploader {
18
22
  let lastError = null;
19
23
  for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
20
24
  try {
21
- await this.uploadOnce(compressedPackage);
25
+ await this.uploadOnce(this.endpoint, compressedPackage);
22
26
  return true;
23
27
  }
24
28
  catch (err) {
@@ -30,7 +34,47 @@ class Uploader {
30
34
  }
31
35
  throw lastError || new Error('Upload failed after retries');
32
36
  }
33
- async uploadOnce(compressedPackage) {
37
+ async uploadArtifacts(artifacts) {
38
+ if (this.options.skipUpload) {
39
+ return {
40
+ skipped: true,
41
+ policyUploaded: false,
42
+ selectorsUploaded: false,
43
+ mode: this.options.artifactUploadMode || 'single',
44
+ };
45
+ }
46
+ const mode = this.options.artifactUploadMode || 'single';
47
+ const requests = (0, UploadStrategy_1.buildArtifactRequests)(artifacts, mode);
48
+ for (const req of requests) {
49
+ await this.uploadWithRetry(req.endpoint, req.payload, {
50
+ ...(req.extraHeaders || {}),
51
+ 'X-Build-Id': artifacts.metadata.buildId,
52
+ });
53
+ }
54
+ return {
55
+ skipped: false,
56
+ policyUploaded: true,
57
+ selectorsUploaded: true,
58
+ mode,
59
+ };
60
+ }
61
+ async uploadWithRetry(endpoint, payload, extraHeaders) {
62
+ let lastError = null;
63
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
64
+ try {
65
+ await this.uploadOnce(endpoint, payload, extraHeaders);
66
+ return;
67
+ }
68
+ catch (err) {
69
+ lastError = err;
70
+ if (attempt < this.maxAttempts) {
71
+ await delay(250 * Math.pow(2, attempt - 1));
72
+ }
73
+ }
74
+ }
75
+ throw lastError || new Error('Upload failed after retries');
76
+ }
77
+ async uploadOnce(endpoint, compressedPackage, extraHeaders) {
34
78
  return new Promise((resolve, reject) => {
35
79
  let settled = false;
36
80
  const finalize = (fn) => {
@@ -39,7 +83,7 @@ class Uploader {
39
83
  settled = true;
40
84
  fn();
41
85
  };
42
- const req = https_1.default.request(this.endpoint, {
86
+ const req = https_1.default.request(endpoint, {
43
87
  method: 'POST',
44
88
  headers: {
45
89
  Authorization: `Bearer ${this.options.apiKey}`,
@@ -47,6 +91,7 @@ class Uploader {
47
91
  'Content-Type': 'application/octet-stream',
48
92
  'Content-Encoding': 'gzip',
49
93
  'Content-Length': compressedPackage.length,
94
+ ...extraHeaders,
50
95
  },
51
96
  }, (res) => {
52
97
  if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcoder-x/plugin-shared",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",