@camox/cli 0.42.0 → 0.44.0

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.
@@ -7,9 +7,9 @@ interface AuthToken {
7
7
  }
8
8
  declare function normalizeUrl(url: string): string;
9
9
  /** Look up stored credentials for a specific Camox authentication backend. */
10
- declare function readAuthTokenForUrl(authenticationUrl: string): AuthToken | null;
11
- declare function writeAuthTokenForUrl(authenticationUrl: string, token: AuthToken): void;
12
- declare function removeAuthTokenForUrl(authenticationUrl: string): void;
10
+ declare function readAuthTokenForUrl(authenticationUrl: string, cwd?: string): AuthToken | null;
11
+ declare function writeAuthTokenForUrl(authenticationUrl: string, token: AuthToken, cwd?: string): void;
12
+ declare function removeAuthTokenForUrl(authenticationUrl: string, cwd?: string): void;
13
13
  declare function verifyOneTimeToken(apiUrl: string, token: string): Promise<AuthToken>;
14
14
  //#endregion
15
15
  export { AuthToken, normalizeUrl, readAuthTokenForUrl, removeAuthTokenForUrl, verifyOneTimeToken, writeAuthTokenForUrl };
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { a as writeAuthTokenForUrl, i as verifyOneTimeToken, n as readAuthTokenForUrl, r as removeAuthTokenForUrl, t as normalizeUrl } from "../auth-core-cCuk5aZy.mjs";
2
+ import { a as writeAuthTokenForUrl, i as verifyOneTimeToken, n as readAuthTokenForUrl, r as removeAuthTokenForUrl, t as normalizeUrl } from "../auth-core-4Jdf5q-u.mjs";
3
3
  export { normalizeUrl, readAuthTokenForUrl, removeAuthTokenForUrl, verifyOneTimeToken, writeAuthTokenForUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/cli",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "camox": "./dist/index.mjs"
@@ -31,7 +31,7 @@
31
31
  "@types/node": "^24.12.4",
32
32
  "@typescript/native-preview": "7.0.0-dev.20260412.1",
33
33
  "vite-plus": "^0.1.21",
34
- "@camox/api-contract": "0.42.0"
34
+ "@camox/api-contract": "0.44.0"
35
35
  },
36
36
  "nx": {
37
37
  "tags": [
@@ -42,6 +42,7 @@
42
42
  "build": "node scripts/sync-templates.mjs && vp pack",
43
43
  "dev": "node scripts/sync-templates.mjs --watch --build",
44
44
  "lint": "vp lint",
45
+ "test": "vp pack && node --test test/*.test.mjs",
45
46
  "check": "tsgo --noEmit && vp lint --fix"
46
47
  }
47
48
  }
@@ -8,6 +8,7 @@
8
8
  "dev": "vp dev --port 7400",
9
9
  "start": "node .output/server/index.mjs",
10
10
  "build": "vp build",
11
+ "test:production": "node scripts/production-smoke.mjs",
11
12
  "release": "camox release",
12
13
  "serve": "vp preview",
13
14
  "lint": "vp lint",
@@ -0,0 +1,35 @@
1
+ # Production runtime smoke test
2
+
3
+ From the repository root:
4
+
5
+ ```sh
6
+ pnpm exec nx run camox:build
7
+ pnpm --filter @camox/template-default test:production
8
+ ```
9
+
10
+ The test builds a temporary copy of the starter using Nitro's Node middleware
11
+ preset, moves only `.output` into an isolated deployment, removes build-time
12
+ `node_modules` links, and serves a real HTTP request. It asserts that React/Base UI
13
+ can render HTML and Camox's OG renderer can produce a PNG. No Camox API, real auth,
14
+ application-level dependency tracing overrides, or direct Takumi dependency is
15
+ needed. A temporary home directory supplies fake build authentication without
16
+ modifying the developer's credentials. CI runs this test after building the SDK.
17
+
18
+ ## Issue #106 investigation
19
+
20
+ - **Takumi:** the original `createRequire(import.meta.url).resolve(...)` survives
21
+ bundling, but the WASM package/asset is absent from the deployment. The isolated
22
+ smoke test reproduced `Cannot find module '@takumi-rs/wasm/takumi_wasm_bg.wasm'`
23
+ before the fix. Camox now uses a static module import so Nitro's WASM loader
24
+ owns asset inclusion, including when dependencies are fully bundled. The
25
+ server-only guard in `createLayout` prevents this import reaching client builds.
26
+ - **tslib:** a package containing only `package.json` and `tslib.es6.mjs`, as
27
+ reported, reproduces Node's missing `tslib/modules/index.js` error: Node selects
28
+ that entry from the package exports. The complete upstream package is valid;
29
+ the partial traced copy is not. The current starter build did not independently
30
+ reproduce that partial copy. Camox now adds `tslib` to Nitro's `noExternals`,
31
+ keeping its existing ESM alias and removing reliance on partial package tracing.
32
+ Plugin tests cover preservation of user options, including `noExternals: true`.
33
+
34
+ These are SDK integration fixes, not starter workarounds. Applications should not
35
+ need to add Takumi as a direct dependency or configure `traceDeps`/`noExternals`.
@@ -0,0 +1,124 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ const temp = await mkdtemp(join(tmpdir(), "camox-production-"));
11
+
12
+ try {
13
+ const root = join(temp, "app");
14
+ const home = join(temp, "home");
15
+ await mkdir(join(home, ".camox"), { recursive: true });
16
+ await writeFile(
17
+ join(home, ".camox/auth.json"),
18
+ JSON.stringify({
19
+ "http://localhost:3274": { token: "smoke", name: "Smoke", email: "smoke@example.com" },
20
+ }),
21
+ );
22
+ await mkdir(root);
23
+ await mkdir(join(temp, "node_modules"));
24
+ // Nitro resolves its builder from the project root. Match Vite+'s own Vite
25
+ // dependency without relying on workspace hoisting in this temporary app.
26
+ const vitePackage = createRequire(import.meta.resolve("vite-plus")).resolve("vite/package.json");
27
+ await symlink(dirname(vitePackage), join(temp, "node_modules/vite"), "dir");
28
+ for (const file of ["src", "vite.config.ts", "tsconfig.json", "package.json"]) {
29
+ await cp(join(appRoot, file), join(root, file), { recursive: true });
30
+ }
31
+ await symlink(join(appRoot, "node_modules"), join(root, "node_modules"), "dir");
32
+ const configPath = join(root, "vite.config.ts");
33
+ await writeFile(
34
+ configPath,
35
+ (await readFile(configPath, "utf8")).replace(
36
+ "nitro()",
37
+ 'nitro({ routes: { "/smoke": "./routes/smoke.ts" } })',
38
+ ),
39
+ );
40
+ await mkdir(join(root, "routes"), { recursive: true });
41
+ await writeFile(
42
+ join(root, "routes/smoke.ts"),
43
+ `import { Dialog } from "@base-ui/react/dialog";
44
+ import { createElement } from "react";
45
+ import { renderToStaticMarkup } from "react-dom/server";
46
+ import { ImageResponse } from "camox/_internal/imageResponse";
47
+
48
+ export default async () => {
49
+ const element = createElement("div", { style: { color: "black", background: "white" } }, "Production smoke");
50
+ const image = new ImageResponse(element, { width: 320, height: 160 });
51
+ const bytes = new Uint8Array(await image.arrayBuffer());
52
+ const html = renderToStaticMarkup(createElement(Dialog.Root, null,
53
+ createElement(Dialog.Trigger, null, "Production smoke")));
54
+ return Response.json({ html, png: Array.from(bytes.slice(0, 8)) });
55
+ };
56
+ `,
57
+ );
58
+ const build = spawnSync(
59
+ process.execPath,
60
+ [
61
+ "--input-type=module",
62
+ "-e",
63
+ `
64
+ const { createBuilder } = await import(${JSON.stringify(import.meta.resolve("vite-plus"))});
65
+ const builder = await createBuilder({ root: ${JSON.stringify(root)} });
66
+ await builder.buildApp();
67
+ `,
68
+ ],
69
+ {
70
+ cwd: root,
71
+ env: {
72
+ ...process.env,
73
+ HOME: home,
74
+ CAMOX_INTERNAL_RELEASE: "0",
75
+ NITRO_PRESET: "node-middleware",
76
+ },
77
+ encoding: "utf8",
78
+ timeout: 180_000,
79
+ killSignal: "SIGKILL",
80
+ },
81
+ );
82
+ assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
83
+
84
+ // Move only the deployment artifact away from all workspace node_modules.
85
+ const deployment = join(temp, "deployment");
86
+ await cp(join(root, ".output"), deployment, { recursive: true });
87
+ await rm(root, { recursive: true, force: true });
88
+ await rm(join(temp, "node_modules"), { recursive: true, force: true });
89
+ const runtime = spawnSync(
90
+ process.execPath,
91
+ [
92
+ "--input-type=module",
93
+ "-e",
94
+ `
95
+ import assert from "node:assert/strict";
96
+ import { createServer } from "node:http";
97
+ const { middleware } = await import("./server/index.mjs");
98
+ const server = createServer(middleware);
99
+ await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
100
+ try {
101
+ const response = await fetch("http://127.0.0.1:" + server.address().port + "/smoke");
102
+ assert.equal(response.status, 200, await response.clone().text());
103
+ const result = await response.json();
104
+ assert.match(result.html, /Production smoke/);
105
+ assert.deepEqual(result.png, [137, 80, 78, 71, 13, 10, 26, 10]);
106
+ } finally {
107
+ server.closeAllConnections();
108
+ server.close();
109
+ }
110
+ `,
111
+ ],
112
+ {
113
+ cwd: deployment,
114
+ env: { ...process.env, NODE_PATH: "", NODE_ENV: "production" },
115
+ encoding: "utf8",
116
+ timeout: 30_000,
117
+ killSignal: "SIGKILL",
118
+ },
119
+ );
120
+ assert.equal(runtime.status, 0, `${runtime.stdout}\n${runtime.stderr}`);
121
+ console.log("Production server rendered HTML and a Takumi PNG from an isolated deployment.");
122
+ } finally {
123
+ await rm(temp, { recursive: true, force: true });
124
+ }
@@ -14,8 +14,8 @@ export default defineConfig({
14
14
  projectSlug: "camox-template-default-01", // camox-cli:replace-slug
15
15
  // camox-cli:dev-only-start
16
16
  _internal: {
17
- authenticationUrl: "http://localhost:3274",
18
- apiUrl: "http://localhost:8787",
17
+ authenticationUrl: process.env.VITE_DASHBOARD_URL ?? "http://localhost:3274",
18
+ apiUrl: process.env.VITE_API_URL ?? "http://localhost:8787",
19
19
  },
20
20
  // camox-cli:dev-only-end
21
21
  }),
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- //#region src/lib/auth-core.ts
6
- const AUTH_DIR = path.join(os.homedir(), ".camox");
7
- const AUTH_FILE = path.join(AUTH_DIR, "auth.json");
8
- function normalizeUrl(url) {
9
- return url.replace(/\/+$/, "");
10
- }
11
- function readAllTokens() {
12
- try {
13
- return JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
14
- } catch {
15
- return {};
16
- }
17
- }
18
- function writeAllTokens(tokens) {
19
- fs.mkdirSync(AUTH_DIR, { recursive: true });
20
- fs.writeFileSync(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 384 });
21
- }
22
- /** Look up stored credentials for a specific Camox authentication backend. */
23
- function readAuthTokenForUrl(authenticationUrl) {
24
- const entry = readAllTokens()[normalizeUrl(authenticationUrl)];
25
- if (entry?.token && entry?.name && typeof entry.email === "string") return entry;
26
- return null;
27
- }
28
- function writeAuthTokenForUrl(authenticationUrl, token) {
29
- const tokens = readAllTokens();
30
- tokens[normalizeUrl(authenticationUrl)] = token;
31
- writeAllTokens(tokens);
32
- }
33
- function removeAuthTokenForUrl(authenticationUrl) {
34
- const tokens = readAllTokens();
35
- delete tokens[normalizeUrl(authenticationUrl)];
36
- if (Object.keys(tokens).length > 0) {
37
- writeAllTokens(tokens);
38
- return;
39
- }
40
- try {
41
- fs.unlinkSync(AUTH_FILE);
42
- } catch {}
43
- }
44
- async function verifyOneTimeToken(apiUrl, token) {
45
- const res = await fetch(`${normalizeUrl(apiUrl)}/api/auth/one-time-token/verify`, {
46
- method: "POST",
47
- headers: { "Content-Type": "application/json" },
48
- body: JSON.stringify({ token })
49
- });
50
- if (!res.ok) throw new Error(`OTT verification failed: ${res.status}`);
51
- const data = await res.json();
52
- const user = data.user;
53
- if (!user?.name) throw new Error("No user info in verification response");
54
- const sessionToken = data.session?.token;
55
- if (!sessionToken) throw new Error("No session token in verification response");
56
- return {
57
- name: user.name,
58
- email: user.email ?? "",
59
- token: sessionToken
60
- };
61
- }
62
- //#endregion
63
- export { writeAuthTokenForUrl as a, verifyOneTimeToken as i, readAuthTokenForUrl as n, removeAuthTokenForUrl as r, normalizeUrl as t };