use-chisel 0.1.3 → 0.1.4
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/README.md +14 -2
- package/bin/chisel.js +81 -8
- package/dist/hosted-entry.d.ts +3 -0
- package/dist/hosted-entry.js +1 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -44,8 +44,20 @@ Supports Next.js 14 through 16 with React 18 or 19, including both App Router an
|
|
|
44
44
|
npm install use-chisel
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Run `npx chisel`
|
|
48
|
-
|
|
47
|
+
Run `npx chisel` from a Next.js App Router project to sign in and connect it to
|
|
48
|
+
hosted Chisel. The command adds a `/chisel` route and a preview component to the
|
|
49
|
+
app layout, and saves the project's public ID in `.chisel.json`. It does not
|
|
50
|
+
overwrite an existing `/chisel` route. Restart your development server and open
|
|
51
|
+
`/chisel` to sign in and review the site. Shared comments are stored by Chisel's
|
|
52
|
+
hosted service; project credentials and Supabase service keys are not required
|
|
53
|
+
in the client project. The hosted workspace currently supports text comments,
|
|
54
|
+
replies, resolution, and review links; rich text, attachments, presence, and
|
|
55
|
+
unread indicators are not yet available there.
|
|
56
|
+
|
|
57
|
+
The generated route defaults off in production. To use it on a deployed review
|
|
58
|
+
site, set `CHISEL_ENABLED=1` for that deployment and redeploy. Hosted connections
|
|
59
|
+
require HTTPS, except on localhost. Automatic setup currently requires an App
|
|
60
|
+
Router layout containing a single `</body>` tag.
|
|
49
61
|
|
|
50
62
|
Run `npx chisel enable` or `npx chisel disable` in your project to update its
|
|
51
63
|
local `.env.local` settings. Restart the development server afterwards. These
|
package/bin/chisel.js
CHANGED
|
@@ -1,8 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import { createServer } from "node:http";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
|
|
8
|
+
function openBrowser(url) {
|
|
9
|
+
console.log(`Open ${url} to continue in your browser.`);
|
|
10
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
11
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
12
|
+
const browser = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
13
|
+
browser.on("error", () => console.error("Could not open a browser. Open the link above instead."));
|
|
14
|
+
browser.unref();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scaffoldInfo() {
|
|
18
|
+
const app = ["app", "src/app"].find(dir => existsSync(dir));
|
|
19
|
+
const layout = app && ["layout.tsx", "layout.jsx", "layout.js"].map(file => join(app, file)).find(existsSync);
|
|
20
|
+
if (!layout) throw new Error("Chisel currently sets up Next.js App Router projects with an app/layout file.");
|
|
21
|
+
const source = readFileSync(layout, "utf8");
|
|
22
|
+
if ((source.match(/<\/body\s*>/g) || []).length !== 1 || source.includes("ChiselConnectedPreview")) throw new Error("Could not safely update the app layout. Add ChiselPreview to the layout and create the /chisel route manually.");
|
|
23
|
+
const directory = join(app, "chisel");
|
|
24
|
+
if (["page.tsx", "page.jsx", "page.js"].some(file => existsSync(join(directory, file)))) throw new Error("The /chisel route already exists. Chisel will not overwrite it.");
|
|
25
|
+
return { layout, source, directory, extension: layout.endsWith(".tsx") ? "tsx" : "jsx" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function scaffoldProject(info, projectId, configFile) {
|
|
29
|
+
const closingBody = info.source.match(/<\/body\s*>/)[0];
|
|
30
|
+
const updated = info.source.replace(closingBody, `<ChiselConnectedPreview />\n ${closingBody}`);
|
|
31
|
+
const directive = updated.match(/^\s*["']use client["'];?\s*/)?.[0] || "";
|
|
32
|
+
const layout = `${directive}import { ChiselPreview as ChiselConnectedPreview } from "use-chisel/workspace";\n${updated.slice(directive.length)}`;
|
|
33
|
+
const page = `import { notFound } from "next/navigation";\nimport { HostedChisel } from "use-chisel/hosted";\n\nexport default function ChiselPage() {\n const enabled = process.env.CHISEL_ENABLED === undefined ? process.env.NODE_ENV !== "production" : process.env.CHISEL_ENABLED === "1";\n if (!enabled) notFound();\n return <HostedChisel projectId="${projectId}" />;\n}\n`;
|
|
34
|
+
const pageFile = join(info.directory, `page.${info.extension}`);
|
|
35
|
+
if (readFileSync(info.layout, "utf8") !== info.source) throw new Error("The layout changed during setup. Run npx chisel again.");
|
|
36
|
+
mkdirSync(info.directory, { recursive: true });
|
|
37
|
+
writeFileSync(pageFile, page, { flag: "wx" });
|
|
38
|
+
let layoutWritten = false;
|
|
39
|
+
try {
|
|
40
|
+
writeFileSync(info.layout, layout); layoutWritten = true;
|
|
41
|
+
writeFileSync(configFile, JSON.stringify({ projectId }, null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (layoutWritten && existsSync(info.layout) && readFileSync(info.layout, "utf8") === layout) writeFileSync(info.layout, info.source);
|
|
44
|
+
if (existsSync(pageFile) && readFileSync(pageFile, "utf8") === page) unlinkSync(pageFile);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
6
49
|
const action = process.argv[2];
|
|
7
50
|
if (action === "enable" || action === "disable") {
|
|
8
51
|
if (!existsSync("package.json")) {
|
|
@@ -27,11 +70,41 @@ if (action === "enable" || action === "disable") {
|
|
|
27
70
|
console.error("Usage: npx chisel [enable|disable]");
|
|
28
71
|
process.exitCode = 1;
|
|
29
72
|
} else {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
73
|
+
const configFile = join(process.cwd(), ".chisel.json");
|
|
74
|
+
if (!existsSync("package.json")) {
|
|
75
|
+
openBrowser("https://chisel-review.vercel.app/chisel/start");
|
|
76
|
+
} else if (existsSync(configFile)) {
|
|
77
|
+
console.log("This project is already connected. Start your development server and open /chisel.");
|
|
78
|
+
} else {
|
|
79
|
+
let name, info;
|
|
80
|
+
try { name = JSON.parse(readFileSync("package.json", "utf8")).name || "My project"; }
|
|
81
|
+
catch { console.error("Could not read this project's package.json."); process.exit(1); }
|
|
82
|
+
try { info = scaffoldInfo(); }
|
|
83
|
+
catch (error) { console.error(error.message); process.exit(1); }
|
|
84
|
+
if (typeof name !== "string" || !name.trim()) name = "My project";
|
|
85
|
+
const state = randomBytes(32).toString("hex"), requestId = randomUUID();
|
|
86
|
+
const server = createServer((request, response) => {
|
|
87
|
+
let url;
|
|
88
|
+
try { url = new URL(request.url || "/", "http://127.0.0.1"); }
|
|
89
|
+
catch { response.writeHead(400).end("Invalid connection request."); return; }
|
|
90
|
+
const project = url.searchParams.get("project");
|
|
91
|
+
if (request.method !== "GET" || url.pathname !== "/" || url.searchParams.get("state") !== state || !project || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(project)) {
|
|
92
|
+
response.writeHead(400).end("Invalid connection request."); return;
|
|
93
|
+
}
|
|
94
|
+
try { scaffoldProject(info, project, configFile); }
|
|
95
|
+
catch (error) { console.error(`Could not finish project setup: ${error.message}`); response.writeHead(500).end("Could not finish project setup. Return to your terminal."); return; }
|
|
96
|
+
response.writeHead(200, { "Content-Type": "text/plain", "Cache-Control": "no-store" }).end("Project connected. You can return to your terminal.");
|
|
97
|
+
console.log("Project connected. Start your development server and open /chisel.");
|
|
98
|
+
clearTimeout(timeout); server.close();
|
|
99
|
+
});
|
|
100
|
+
server.on("error", () => { console.error("Could not start the local connection listener."); process.exitCode = 1; });
|
|
101
|
+
server.listen(0, "127.0.0.1", () => {
|
|
102
|
+
const url = new URL("https://chisel-review.vercel.app/chisel/start");
|
|
103
|
+
url.search = new URLSearchParams({ state, port: String(server.address().port), request: requestId, name: name.trim().slice(0, 120) }).toString();
|
|
104
|
+
openBrowser(url.toString());
|
|
105
|
+
console.log("Waiting for project confirmation. Press Ctrl+C to cancel.");
|
|
106
|
+
});
|
|
107
|
+
const timeout = setTimeout(() => { console.error("Project connection timed out. Run npx chisel again to retry."); server.close(); process.exitCode = 1; }, 300000);
|
|
108
|
+
timeout.unref();
|
|
109
|
+
}
|
|
37
110
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as i,jsxs as l,Fragment as L}from"react/jsx-runtime";import{useCallback as R,useEffect as b,useMemo as j,useRef as w,useState as f}from"react";import{ChiselWorkspace as I,ChiselWorkspaceStyles as E}from"use-chisel/workspace";var u="https://chisel-review.vercel.app";function N({projectId:h}){let a=`chisel:session:${h}`,r=w(""),[c,C]=f(null),[P,d]=f(!0),[v,p]=f(""),[x,y]=f(!1),S=w(null);S.current=c;let m=w(null),g=w(""),o=R(async(t,e)=>{let n=await fetch(`${u}/chisel/api/hosted/comments`,{method:t,headers:{Authorization:`Bearer ${r.current}`,...e?{"Content-Type":"application/json","X-Chisel-Protocol":"2"}:{}},...e?{body:JSON.stringify(e)}:{}}),s=await n.json();if(!n.ok)throw n.status===401&&S.current&&y(!0),Object.assign(new Error(s.error||"Chisel is unavailable. Please try again."),{status:n.status});if(s.siteId&&s.siteId!==h)throw new Error("This session belongs to another project.");return s},[h]),T=j(()=>({async list(){return(await o("GET")).comments},async mutate(t){return(await o("POST",t)).comment},async delete(t){await o("POST",{type:"delete",id:t})}}),[o]);b(()=>{if(r.current=sessionStorage.getItem(a)||"",!r.current){d(!1);return}let t=!0;return o("GET").then(e=>{t&&C(e)}).catch(()=>{t&&(r.current="",sessionStorage.removeItem(a))}).finally(()=>{t&&d(!1)}),()=>{t=!1}},[a,o]),b(()=>{let t=e=>{e.origin!==u||e.source!==m.current||e.data?.type!=="chisel:session"||e.data.state!==g.current||!/^[0-9a-f]{64}$/.test(e.data.token)||(r.current=e.data.token,sessionStorage.setItem(a,r.current),g.current="",m.current=null,S.current||d(!0),p(""),o("GET").then(n=>{C(n),y(!1);let s=new URL(window.location.href);s.searchParams.delete("invite"),window.history.replaceState(window.history.state,"",s)}).catch(n=>{r.current="",sessionStorage.removeItem(a),p(n instanceof Error?n.message:"Could not connect to Chisel.")}).finally(()=>d(!1)))};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)},[a,o]);function k(){let t=crypto.getRandomValues(new Uint8Array(32));g.current=Array.from(t,s=>s.toString(16).padStart(2,"0")).join("");let e=new URL("/chisel/connect",u);e.searchParams.set("site",h),e.searchParams.set("origin",window.location.origin),e.searchParams.set("state",g.current);let n=new URLSearchParams(window.location.search).get("invite");n&&e.searchParams.set("invite",n),m.current=window.open(e.toString(),"chisel-sign-in","popup,width=520,height=700"),m.current||p("Allow pop-ups for this site, then try again.")}return P?l("main",{className:"chisel-sign-in",children:[i(E,{}),i("section",{children:i("p",{children:"Connecting to Chisel\u2026"})})]}):c?l(L,{children:[i(I,{user:c.user,commentStore:T,canManageComments:c.role==="owner",getReviewLink:c.role==="owner"?async(t=!1)=>{let e=await fetch(`${u}/chisel/api/hosted/invite`,{method:"POST",headers:{Authorization:`Bearer ${r.current}`,"Content-Type":"application/json","X-Chisel-Protocol":"2"},body:JSON.stringify({rotate:t})}),n=await e.json();if(e.status===401&&y(!0),!e.ok||typeof n.invite!="string")throw new Error(n.error||"Could not create a review link.");let s=new URL("/chisel",window.location.origin);return s.searchParams.set("mode","comment"),s.searchParams.set("invite",n.invite),s.toString()}:void 0,validateSession:async()=>!!r.current,signOut:async()=>{let t=await fetch(`${u}/chisel/api/hosted/logout`,{method:"DELETE",headers:{Authorization:`Bearer ${r.current}`,"X-Chisel-Protocol":"2"}});if(!t.ok&&t.status!==401)throw new Error("Sign out failed.");r.current="",sessionStorage.removeItem(a)}}),x&&l("div",{role:"alert",style:{position:"fixed",bottom:16,right:16,zIndex:999999,background:"white",color:"black",padding:12,borderRadius:8,boxShadow:"0 2px 12px #0003"},children:["Your Chisel session expired. ",i("button",{type:"button",onClick:k,children:"Sign in again"})]})]}):l("main",{className:"chisel-sign-in",children:[i(E,{}),l("section",{children:[i("strong",{className:"chisel-sign-in-brand",children:"chisel"}),i("h1",{children:"Sign in to review"}),i("p",{children:"Connect this website to your Chisel account."}),i("button",{onClick:k,children:"Continue with Chisel"}),v&&i("p",{role:"alert",children:v})]})]})}export{N as HostedChisel};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-chisel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Dev-only visual editing layer for Next.js apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"chisel": "./bin/chisel.js"
|
|
26
26
|
},
|
|
27
27
|
"exports": {
|
|
28
|
+
"./hosted": {
|
|
29
|
+
"types": "./dist/hosted-entry.d.ts",
|
|
30
|
+
"import": "./dist/hosted-entry.js"
|
|
31
|
+
},
|
|
28
32
|
"./workspace": {
|
|
29
33
|
"types": "./dist/workspace-entry.d.ts",
|
|
30
34
|
"import": "./dist/workspace-entry.js"
|
|
@@ -38,6 +42,7 @@
|
|
|
38
42
|
"bin/chisel.js",
|
|
39
43
|
"dist/index.js",
|
|
40
44
|
"dist/workspace-entry.js",
|
|
45
|
+
"dist/hosted-entry.js",
|
|
41
46
|
"dist/**/*.d.ts",
|
|
42
47
|
"README.md"
|
|
43
48
|
],
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
"access": "public"
|
|
46
51
|
},
|
|
47
52
|
"scripts": {
|
|
48
|
-
"build": "tsc -p tsconfig.json && esbuild dist/index.js dist/workspace-entry.js --bundle --minify --format=esm --platform=browser --external:react --external:react/jsx-runtime --external:react-dom --external:use-chisel --define:process.env.NODE_ENV=process.env.NODE_ENV --outdir=dist --allow-overwrite",
|
|
53
|
+
"build": "tsc -p tsconfig.json && esbuild dist/index.js dist/workspace-entry.js dist/hosted-entry.js --bundle --minify --format=esm --platform=browser --external:react --external:react/jsx-runtime --external:react-dom --external:use-chisel --define:process.env.NODE_ENV=process.env.NODE_ENV --outdir=dist --allow-overwrite",
|
|
49
54
|
"prepack": "npm run build",
|
|
50
55
|
"test": "vitest run",
|
|
51
56
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|