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 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` to open the hosted Chisel sign-in page. Connecting your project
48
- to the hosted workspace is not available yet.
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 url = "https://chisel-review.vercel.app/chisel/start";
31
- console.log(`Open ${url} to sign in to Chisel.`);
32
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
33
- const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
34
- const browser = spawn(command, args, { detached: true, stdio: "ignore" });
35
- browser.on("error", () => console.error("Could not open a browser. Open the link above instead."));
36
- browser.unref();
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,3 @@
1
+ export declare function HostedChisel({ projectId }: {
2
+ projectId: string;
3
+ }): import("react").JSX.Element;
@@ -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",
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"