pointer-feedback 0.1.2

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 (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/dist/cli.js +10367 -0
  4. package/dist/vite.js +306 -0
  5. package/package.json +73 -0
package/dist/vite.js ADDED
@@ -0,0 +1,306 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/vite/hash.ts
12
+ import { createHash } from "node:crypto";
13
+ function componentHash(repoRelativePath, exportName) {
14
+ const normalised = repoRelativePath.split("\\").join("/").replace(/^\.\//, "");
15
+ return createHash("sha1").update(`${normalised}#${exportName}`).digest("hex").slice(0, 8);
16
+ }
17
+ function addToManifest(manifest, hash, entry) {
18
+ const existing = manifest[hash];
19
+ if (existing && (existing.path !== entry.path || existing.export !== entry.export)) {
20
+ throw new Error(
21
+ `pointer: hash collision on ${hash} between ${existing.path}#${existing.export} and ${entry.path}#${entry.export}. Rename one of the two components.`
22
+ );
23
+ }
24
+ manifest[hash] = entry;
25
+ }
26
+ var init_hash = __esm({
27
+ "src/vite/hash.ts"() {
28
+ "use strict";
29
+ }
30
+ });
31
+
32
+ // src/vite/transform.ts
33
+ var transform_exports = {};
34
+ __export(transform_exports, {
35
+ stampSource: () => stampSource
36
+ });
37
+ async function loadBabel() {
38
+ try {
39
+ const [parser, traverseMod, generatorMod] = await Promise.all([
40
+ import("@babel/parser"),
41
+ // @ts-ignore optional peer — types are not installed for a dependency-free CLI bundle
42
+ import("@babel/traverse"),
43
+ // @ts-ignore optional peer — same
44
+ import("@babel/generator")
45
+ ]);
46
+ const unwrap = (mod, name) => {
47
+ const fn = mod?.default?.default ?? mod?.default ?? mod;
48
+ if (typeof fn !== "function") {
49
+ throw new Error(
50
+ `${name} did not resolve to a function (got ${typeof fn}) \u2014 CJS/ESM interop problem`
51
+ );
52
+ }
53
+ return fn;
54
+ };
55
+ return {
56
+ parse: parser.parse,
57
+ traverse: unwrap(traverseMod, "@babel/traverse"),
58
+ generate: unwrap(generatorMod, "@babel/generator")
59
+ };
60
+ } catch {
61
+ throw new Error(
62
+ "the Vite plugin needs @babel/parser, @babel/traverse and @babel/generator. They ship with @vitejs/plugin-react; install them if you use a different React setup."
63
+ );
64
+ }
65
+ }
66
+ function isHostElement(node) {
67
+ const name = node?.openingElement?.name;
68
+ return name?.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
69
+ }
70
+ function alreadyStamped(node, attribute) {
71
+ return (node.openingElement.attributes ?? []).some(
72
+ (a) => a.type === "JSXAttribute" && a.name?.name === attribute
73
+ );
74
+ }
75
+ function collectRoots(node, out) {
76
+ if (!node)
77
+ return;
78
+ switch (node.type) {
79
+ case "JSXElement":
80
+ if (isHostElement(node))
81
+ out.push(node);
82
+ return;
83
+ case "JSXFragment":
84
+ for (const child of node.children ?? [])
85
+ collectRoots(child, out);
86
+ return;
87
+ case "ConditionalExpression":
88
+ collectRoots(node.consequent, out);
89
+ collectRoots(node.alternate, out);
90
+ return;
91
+ case "LogicalExpression":
92
+ collectRoots(node.right, out);
93
+ return;
94
+ case "ParenthesizedExpression":
95
+ collectRoots(node.expression, out);
96
+ return;
97
+ default:
98
+ return;
99
+ }
100
+ }
101
+ function componentNameFor(path) {
102
+ const node = path.node;
103
+ if (node.id?.name)
104
+ return node.id.name;
105
+ const parent = path.parent;
106
+ if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier")
107
+ return parent.id.name;
108
+ if (parent?.type === "CallExpression") {
109
+ const grand = path.parentPath?.parent;
110
+ if (grand?.type === "VariableDeclarator" && grand.id?.type === "Identifier")
111
+ return grand.id.name;
112
+ }
113
+ if (parent?.type === "ExportDefaultDeclaration")
114
+ return "default";
115
+ return null;
116
+ }
117
+ async function stampSource(code, repoRelativePath, attribute) {
118
+ if (repoRelativePath.endsWith(".vue")) {
119
+ return stampVue(code, repoRelativePath, attribute);
120
+ }
121
+ const { parse, traverse, generate } = await loadBabel();
122
+ const ast = parse(code, {
123
+ sourceType: "module",
124
+ plugins: ["jsx", "typescript", "decorators-legacy", "classProperties"]
125
+ });
126
+ const components = {};
127
+ let changed = false;
128
+ const visitComponent = (path) => {
129
+ const name = componentNameFor(path);
130
+ if (!name || !/^[A-Z]|^default$/.test(name))
131
+ return;
132
+ const roots = [];
133
+ const body = path.node.body;
134
+ if (body?.type === "BlockStatement") {
135
+ for (const stmt of body.body) {
136
+ if (stmt.type === "ReturnStatement")
137
+ collectRoots(stmt.argument, roots);
138
+ }
139
+ } else {
140
+ collectRoots(body, roots);
141
+ }
142
+ if (roots.length === 0)
143
+ return;
144
+ const hash = componentHash(repoRelativePath, name);
145
+ components[hash] = { path: repoRelativePath, export: name };
146
+ for (const el of roots) {
147
+ if (alreadyStamped(el, attribute))
148
+ continue;
149
+ el.openingElement.attributes.push({
150
+ type: "JSXAttribute",
151
+ name: { type: "JSXIdentifier", name: attribute },
152
+ value: { type: "StringLiteral", value: hash }
153
+ });
154
+ changed = true;
155
+ }
156
+ };
157
+ traverse(ast, {
158
+ FunctionDeclaration: visitComponent,
159
+ FunctionExpression: visitComponent,
160
+ ArrowFunctionExpression: visitComponent
161
+ });
162
+ if (!changed)
163
+ return { code, changed: false, components };
164
+ return { code: generate(ast, { retainLines: true }, code).code, changed: true, components };
165
+ }
166
+ function stampVue(code, repoRelativePath, attribute) {
167
+ const name = repoRelativePath.split("/").pop().replace(/\.vue$/, "");
168
+ const hash = componentHash(repoRelativePath, name);
169
+ const components = { [hash]: { path: repoRelativePath, export: name } };
170
+ const match = code.match(/<template>([\s\S]*?)<\/template>/);
171
+ if (!match)
172
+ return { code, changed: false, components };
173
+ let changed = false;
174
+ const stamped = match[1].replace(/<([a-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g, (whole, tag, attrs, selfClose) => {
175
+ if (attrs.includes(attribute))
176
+ return whole;
177
+ changed = true;
178
+ return `<${tag}${attrs} ${attribute}="${hash}"${selfClose}>`;
179
+ });
180
+ if (!changed)
181
+ return { code, changed: false, components };
182
+ return { code: code.replace(match[1], stamped), changed: true, components };
183
+ }
184
+ var init_transform = __esm({
185
+ "src/vite/transform.ts"() {
186
+ "use strict";
187
+ init_hash();
188
+ }
189
+ });
190
+
191
+ // src/vite/index.ts
192
+ init_hash();
193
+ init_hash();
194
+ import { promises as fs } from "node:fs";
195
+ import { dirname, relative, resolve, sep } from "node:path";
196
+ import { execFileSync } from "node:child_process";
197
+ var DEFAULT_ATTR = "data-component-source";
198
+ var DEFAULT_MANIFEST = ".pointer/manifest.json";
199
+ function gitRoot(fallback) {
200
+ try {
201
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: fallback, encoding: "utf8" }).trim();
202
+ } catch {
203
+ return fallback;
204
+ }
205
+ }
206
+ function headSha() {
207
+ try {
208
+ return execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function toPosix(p) {
214
+ return p.split(sep).join("/");
215
+ }
216
+ function pointerSource(options = {}) {
217
+ const attribute = options.attribute ?? DEFAULT_ATTR;
218
+ const enabled = options.enabled ?? true;
219
+ const wantsBuildSha = options.buildSha ?? true;
220
+ let root = process.cwd();
221
+ let repoRoot = root;
222
+ let manifestPath = DEFAULT_MANIFEST;
223
+ const manifest = {};
224
+ const include = options.include ?? ["**/*.jsx", "**/*.tsx", "**/*.vue"];
225
+ const exclude = options.exclude ?? [];
226
+ function matches(id) {
227
+ if (!enabled)
228
+ return false;
229
+ if (id.includes("node_modules"))
230
+ return false;
231
+ const clean = id.split("?")[0];
232
+ const ext = clean.slice(clean.lastIndexOf("."));
233
+ if (![".jsx", ".tsx", ".vue"].includes(ext))
234
+ return false;
235
+ if (exclude.some((pattern) => clean.includes(pattern.replace(/\*/g, ""))))
236
+ return false;
237
+ return true;
238
+ }
239
+ async function writeManifest() {
240
+ if (!enabled)
241
+ return;
242
+ const target = resolve(repoRoot, manifestPath);
243
+ await fs.mkdir(dirname(target), { recursive: true });
244
+ const entries = Object.fromEntries(
245
+ Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b)).map(([hash, entry]) => [hash, { path: entry.path, component: entry.export }])
246
+ );
247
+ const payload = { version: 1, entries };
248
+ const prev = target.replace(/\.json$/, ".prev.json");
249
+ try {
250
+ const existing = await fs.readFile(target, "utf8");
251
+ await fs.writeFile(prev, existing, "utf8");
252
+ } catch {
253
+ }
254
+ const tmp = `${target}.tmp`;
255
+ await fs.writeFile(tmp, JSON.stringify(payload, null, 2) + "\n", "utf8");
256
+ await fs.rename(tmp, target);
257
+ }
258
+ let debounce = null;
259
+ return {
260
+ name: "pointer-source",
261
+ enforce: "pre",
262
+ configResolved(config) {
263
+ root = config.root ?? process.cwd();
264
+ repoRoot = gitRoot(root);
265
+ manifestPath = options.manifest ?? DEFAULT_MANIFEST;
266
+ },
267
+ async transform(code, id) {
268
+ if (!matches(id))
269
+ return null;
270
+ const relPath = toPosix(relative(repoRoot, id.split("?")[0]));
271
+ const { stampSource: stampSource2 } = await Promise.resolve().then(() => (init_transform(), transform_exports));
272
+ try {
273
+ const result = await stampSource2(code, relPath, attribute);
274
+ for (const [hash, entry] of Object.entries(result.components)) {
275
+ addToManifest(manifest, hash, entry);
276
+ }
277
+ if (debounce)
278
+ clearTimeout(debounce);
279
+ debounce = setTimeout(() => void writeManifest(), 250);
280
+ return result.changed ? { code: result.code, map: null } : null;
281
+ } catch (err) {
282
+ this.warn?.(`pointer: could not stamp ${relPath}: ${err?.message ?? err}`);
283
+ return null;
284
+ }
285
+ },
286
+ transformIndexHtml(html) {
287
+ if (!enabled || wantsBuildSha === false)
288
+ return html;
289
+ const sha = typeof wantsBuildSha === "string" ? wantsBuildSha : headSha();
290
+ if (!sha)
291
+ return html;
292
+ if (html.includes("data-build-sha"))
293
+ return html;
294
+ return html.replace(/<html(\s|>)/, `<html data-build-sha="${sha}"$1`);
295
+ },
296
+ async buildEnd() {
297
+ if (debounce)
298
+ clearTimeout(debounce);
299
+ await writeManifest();
300
+ }
301
+ };
302
+ }
303
+ export {
304
+ componentHash,
305
+ pointerSource as default
306
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "pointer-feedback",
3
+ "version": "0.1.2",
4
+ "description": "Click-to-comment feedback for your web app: install the widget, then turn pending comments into AI apply prompts.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Moamen",
8
+ "homepage": "https://pointer.moamen.work",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/moamen-ui/poitner-api.git",
12
+ "directory": "cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/moamen-ui/poitner-api/issues"
16
+ },
17
+ "keywords": [
18
+ "feedback",
19
+ "widget",
20
+ "comments",
21
+ "bug-report",
22
+ "ai",
23
+ "claude",
24
+ "mcp",
25
+ "cli"
26
+ ],
27
+ "bin": {
28
+ "pointer": "dist/cli.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "build": "node build.mjs",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "node --import tsx --test test/*.test.ts test/**/*.test.ts",
42
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^18.19.130",
46
+ "esbuild": "^0.20.2",
47
+ "tsx": "^4.23.13",
48
+ "typescript": "^5.9.3"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "1.6.0"
52
+ },
53
+ "exports": {
54
+ ".": "./dist/cli.js",
55
+ "./vite": "./dist/vite.js"
56
+ },
57
+ "peerDependencies": {
58
+ "@babel/parser": ">=7",
59
+ "@babel/traverse": ">=7",
60
+ "@babel/generator": ">=7"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@babel/parser": {
64
+ "optional": true
65
+ },
66
+ "@babel/traverse": {
67
+ "optional": true
68
+ },
69
+ "@babel/generator": {
70
+ "optional": true
71
+ }
72
+ }
73
+ }