creght-cli 0.1.16

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 ADDED
@@ -0,0 +1,495 @@
1
+ # Cregh CLI
2
+
3
+ Cregh CLI is a thin local bridge for syncing site code between a local directory and Cregh.
4
+
5
+ The CLI can also run a local Vite preview for pulled Cregh projects. Cregh remains responsible for cloud rendering, CMS, assets, and the realtime preview environment.
6
+
7
+ ## Install
8
+
9
+ Using npm:
10
+
11
+ ```bash
12
+ npm install -g creght-cli
13
+ ```
14
+
15
+ Build from source:
16
+
17
+ ```bash
18
+ cd /Users/bysir/dev/bysir/creght-cli
19
+ go build -o creght ./cmd/creght
20
+ ```
21
+
22
+ Optional:
23
+
24
+ ```bash
25
+ mv ./creght /usr/local/bin/creght
26
+ ```
27
+
28
+ ## Login
29
+
30
+ For production:
31
+
32
+ ```bash
33
+ creght login
34
+ ```
35
+
36
+ For local development:
37
+
38
+ ```bash
39
+ CREGHT_API_HOST=http://localhost:8433 creght login --web=http://localhost:5173
40
+ ```
41
+
42
+ The command opens a browser authorization page. After authorization succeeds, the CLI stores the token in:
43
+
44
+ ```text
45
+ ~/Library/Application Support/creght/config.json
46
+ ```
47
+
48
+ The config file contains the API host and CLI token.
49
+
50
+ When `--web` is omitted, the CLI uses `CREGHT_WEB_HOST` if set. For local API hosts such as `localhost` or `127.0.0.1`, it defaults to `http://localhost:5173`.
51
+ For production, the default API host and default web host are both `https://creght.cn`.
52
+
53
+ ## Logout
54
+
55
+ Remove the saved CLI config:
56
+
57
+ ```bash
58
+ creght logout
59
+ ```
60
+
61
+ This clears the saved token and any saved API host. The next command will use the production default unless you set `CREGHT_API_HOST`.
62
+
63
+ ## List Projects
64
+
65
+ ```bash
66
+ creght project list
67
+ ```
68
+
69
+ For local development:
70
+
71
+ ```bash
72
+ CREGHT_API_HOST=http://localhost:8433 creght project list
73
+ ```
74
+
75
+ Example output:
76
+
77
+ ```text
78
+ project_id Project Name
79
+ project_id/site_id Site Name
80
+ ```
81
+
82
+ Use the `project_id/site_id` value with `pull`, `push`, and `sync`.
83
+
84
+ ## Create Project
85
+
86
+ Create a new project:
87
+
88
+ ```bash
89
+ creght project create --name="My Project"
90
+ ```
91
+
92
+ For local development:
93
+
94
+ ```bash
95
+ CREGHT_API_HOST=http://localhost:8433 creght project create --name="My Project"
96
+ ```
97
+
98
+ You can also create from an existing project or template when the backend allows it:
99
+
100
+ ```bash
101
+ creght project create --name="My Project" --from_id=<project_id>
102
+ creght project create --name="My Project" --tpl_id=<template_id>
103
+ ```
104
+
105
+ ## Pull Site Code
106
+
107
+ Download the current remote site files into a local directory:
108
+
109
+ ```bash
110
+ creght pull --site_id=<project_id>/<site_id> --dir=./mysite
111
+ ```
112
+
113
+ For local development:
114
+
115
+ ```bash
116
+ CREGHT_API_HOST=http://localhost:8433 creght pull --site_id=<project_id>/<site_id> --dir=./mysite
117
+ ```
118
+
119
+ The command writes remote files such as `/page/...`, `/component/...`, and `creght.config.ts` into the target directory.
120
+
121
+ ## Local Vite Preview
122
+
123
+ Cregh projects pulled by the CLI usually do not have their own `package.json`
124
+ or `node_modules`. The local preview plugin therefore uses Vite only for local
125
+ file serving and TSX transpilation; third-party packages continue to resolve
126
+ through the Cregh import map, matching the Web editor preview model. In
127
+ `creght dev`, the CLI loads the platform import map from server system info and
128
+ passes it to the Vite plugin; the plugin's local map is only a fallback.
129
+
130
+ Install Vite in the local project folder:
131
+
132
+ ```bash
133
+ cd ./mysite
134
+ npm init -y
135
+ npm install -D vite esbuild creght-cli
136
+ ```
137
+
138
+ Create `vite.config.mjs`:
139
+
140
+ ```js
141
+ import { defineConfig } from 'vite'
142
+ import creght from 'creght-cli/vite'
143
+
144
+ export default defineConfig({
145
+ plugins: [
146
+ creght({
147
+ apiHost: 'https://creght.cn',
148
+ projectId: '<project_id>',
149
+ // token: process.env.CREGHT_TOKEN,
150
+ }),
151
+ ],
152
+ })
153
+ ```
154
+
155
+ Run it:
156
+
157
+ ```bash
158
+ npx vite --host 0.0.0.0
159
+ ```
160
+
161
+ The plugin maps `/page/Index.tsx` to `/`, `/page/About.tsx` to `/about`, starts
162
+ from the platform import map, merges `creght.config.ts` import-map entries,
163
+ loads `/index.css` through the Tailwind browser runtime, proxies local `/api/*`
164
+ requests to `apiHost`, calls page `getServerSideProps()` in the browser for a
165
+ preview-only first render, and uses Vite HMR to re-import the current page
166
+ module after local file changes without a full page reload.
167
+
168
+ ## Push Local Changes
169
+
170
+ Push the current local directory snapshot to Cregh and exit:
171
+
172
+ ```bash
173
+ creght push --site_id=<project_id>/<site_id> --dir=./mysite
174
+ ```
175
+
176
+ For local development:
177
+
178
+ ```bash
179
+ CREGHT_API_HOST=http://localhost:8433 creght push --site_id=<project_id>/<site_id> --dir=./mysite
180
+ ```
181
+
182
+ The CLI scans the local directory and calls the existing Cregh `site_action`
183
+ API to create or update remote files.
184
+
185
+ ## Sync Local Changes
186
+
187
+ Run watch mode for a local directory:
188
+
189
+ ```bash
190
+ creght sync --site_id=<project_id>/<site_id> --dir=./mysite
191
+ ```
192
+
193
+ For local development:
194
+
195
+ ```bash
196
+ CREGHT_API_HOST=http://localhost:8433 creght sync --site_id=<project_id>/<site_id> --dir=./mysite
197
+ ```
198
+
199
+ `sync` first pushes the current local snapshot, then keeps running and
200
+ automatically listens for local file changes. When a file is changed locally,
201
+ the CLI calls the existing Cregh `site_action` API and updates the remote site
202
+ in realtime. The command also prints the remote preview URL when available.
203
+
204
+ ## Local Web Editor Bidirectional Sync
205
+
206
+ Run local files and the online Cregh editor against the same cloud realtime
207
+ files:
208
+
209
+ ```bash
210
+ creght dev --site_id=<project_id>/<site_id> --dir=./mysite
211
+ ```
212
+
213
+ For local backend or web development:
214
+
215
+ ```bash
216
+ CREGHT_API_HOST=http://localhost:8433 creght dev --web=http://localhost:5173 --site_id=<project_id>/<site_id> --dir=./mysite
217
+ ```
218
+
219
+ The command prints the online Web editor URL, pushes local file changes to
220
+ Cregh, and listens to the existing WebSocket collaboration channel so editor
221
+ changes are written back to the local directory. MVP conflict handling is last
222
+ write wins.
223
+
224
+ `dev` also starts a local Vite preview by default:
225
+
226
+ ```text
227
+ VITE v8.0.14 ready in 529 ms
228
+ ➜ Local: http://localhost:5173/
229
+ Local Vite: started (preferred http://localhost:5173; use the Vite Local URL above)
230
+ ```
231
+
232
+ Use `--preview-port` or `--preview-host` to change the preferred local preview
233
+ address. If that port is occupied, Vite uses its normal auto-port behavior and
234
+ prints the actual URL in the terminal:
235
+
236
+ ```bash
237
+ creght dev --site_id=<project_id>/<site_id> --dir=./mysite --preview-port=5174
238
+ ```
239
+
240
+ Disable the local preview when you only want file sync:
241
+
242
+ ```bash
243
+ creght dev --site_id=<project_id>/<site_id> --dir=./mysite --no-preview
244
+ ```
245
+
246
+ The preview uses the bundled `creght-cli/vite` plugin. If the site directory
247
+ has `node_modules/.bin/vite`, that local Vite is used; otherwise the CLI starts
248
+ a hidden temporary Vite runtime under `.creght/` and installs `vite` plus
249
+ `esbuild` there.
250
+
251
+ Local file changes are pushed through Vite HMR as a React root re-render. This
252
+ avoids a browser-level refresh, but it is not yet full React Fast Refresh and
253
+ does not guarantee component state preservation.
254
+
255
+ ## Open Preview
256
+
257
+ Open the remote preview URL for a site in the browser:
258
+
259
+ ```bash
260
+ creght preview --site_id=<project_id>/<site_id>
261
+ ```
262
+
263
+ For local development:
264
+
265
+ ```bash
266
+ CREGHT_API_HOST=http://localhost:8433 creght preview --site_id=<project_id>/<site_id>
267
+ ```
268
+
269
+ ## Publish Site
270
+
271
+ Publish a site:
272
+
273
+ ```bash
274
+ creght publish --site_id=<project_id>/<site_id>
275
+ ```
276
+
277
+ With a publish note:
278
+
279
+ ```bash
280
+ creght publish --site_id=<project_id>/<site_id> --note="Update homepage copy"
281
+ ```
282
+
283
+ For local development:
284
+
285
+ ```bash
286
+ CREGHT_API_HOST=http://localhost:8433 creght publish --site_id=<project_id>/<site_id>
287
+ ```
288
+
289
+ ## Manage CMS Collections
290
+
291
+ List CMS collections:
292
+
293
+ ```bash
294
+ creght cms collections --site_id=<project_id>/<site_id>
295
+ ```
296
+
297
+ Create a collection from a JSON Schema file:
298
+
299
+ ```bash
300
+ creght cms collection create --site_id=<project_id>/<site_id> --key=blogs --name="Blogs" --schema=./blogs.schema.json
301
+ ```
302
+
303
+ Update or delete by collection key or id:
304
+
305
+ ```bash
306
+ creght cms collection get --site_id=<project_id>/<site_id> --key=blogs
307
+ creght cms collection update --site_id=<project_id>/<site_id> --key=blogs --schema=./blogs.schema.json
308
+ creght cms collection delete --site_id=<project_id>/<site_id> --key=blogs
309
+ ```
310
+
311
+ `--schema` can point to either a raw JSON Schema object or a full collection JSON object containing fields such as `key`, `name`, `desc`, and `json_schema`.
312
+
313
+ ## Manage CMS Content
314
+
315
+ List, get, create, update, and delete content entries:
316
+
317
+ ```bash
318
+ creght content list --site_id=<project_id>/<site_id> --collection=blogs
319
+ creght content get --site_id=<project_id>/<site_id> --collection=blogs --slug=hello-world
320
+ creght content create --site_id=<project_id>/<site_id> --collection=blogs --data=./content.json --slug=hello-world
321
+ creght content update --site_id=<project_id>/<site_id> --collection=blogs --id=<content_id> --data=./content.json
322
+ creght content delete --site_id=<project_id>/<site_id> --collection=blogs --id=<content_id>
323
+ ```
324
+
325
+ `--data` can point to either a plain CMS content body or a full content object. A plain content body may include a business field named `body`. The CLI treats JSON as a full content object only when it includes wrapper fields such as `id`, `slug`, `content_app_id`, `json_schema`, `status`, `sort`, or `tags`.
326
+
327
+ If your business JSON has a top-level `slug`, do not pass it as plain body JSON because `slug` is a content wrapper field. Either pass the slug as a flag and omit it from `--data`:
328
+
329
+ ```bash
330
+ creght content create --site_id=<project_id>/<site_id> --collection=prompts --data=./content-body.json --slug=typography-v02
331
+ ```
332
+
333
+ Or use a full content object and put business fields under `body`:
334
+
335
+ ```json
336
+ {
337
+ "slug": "typography-v02",
338
+ "body": {
339
+ "title": "Typography V.02",
340
+ "description": "100vh",
341
+ "tags": ["skill"]
342
+ }
343
+ }
344
+ ```
345
+
346
+ ## Manage Forms
347
+
348
+ List, create, update, and delete forms:
349
+
350
+ ```bash
351
+ creght form list --site_id=<project_id>/<site_id>
352
+ creght form create --site_id=<project_id>/<site_id> --key=contact-form --name="Contact form" --schema=./contact.schema.json
353
+ creght form get --site_id=<project_id>/<site_id> --key=contact-form
354
+ creght form update --site_id=<project_id>/<site_id> --key=contact-form --schema=./contact.schema.json
355
+ creght form delete --site_id=<project_id>/<site_id> --key=contact-form
356
+ ```
357
+
358
+ Inspect and delete form submissions:
359
+
360
+ ```bash
361
+ creght form logs --site_id=<project_id>/<site_id> --key=contact-form
362
+ creght form log get --site_id=<project_id>/<site_id> --key=contact-form --log_id=<log_id>
363
+ creght form log delete --site_id=<project_id>/<site_id> --key=contact-form --log_id=<log_id>
364
+ ```
365
+
366
+ Submit a form payload through the platform API:
367
+
368
+ ```bash
369
+ creght form submit --site_id=<project_id>/<site_id> --key=contact-form --data=./payload.json
370
+ ```
371
+
372
+ After creating or changing CMS collections or forms, run `creght pull` again to refresh generated files such as `/types/cms.d.ts` and `/types/form.d.ts` before writing code that imports those types.
373
+
374
+ ## Upload Assets
375
+
376
+ Upload a local file through the Cregh site asset flow:
377
+
378
+ ```bash
379
+ creght upload --site_id=<project_id>/<site_id> --file=./image.png
380
+ ```
381
+
382
+ The command prints the public file URL by default. Use `--json` to inspect the
383
+ full upload metadata, including `site_path`, a stable `/_assets/...` path that
384
+ can be used from Cregh site code:
385
+
386
+ ```bash
387
+ creght upload --site_id=<project_id>/<site_id> --file=./image.png --json
388
+ ```
389
+
390
+ Optional flags:
391
+
392
+ ```bash
393
+ creght upload --site_id=<project_id>/<site_id> --file=./image.png --name=hero.png --mimetype=image/png
394
+ ```
395
+
396
+ ## Push And Sync Boundary
397
+
398
+ The current MVP push/sync mode is one-way:
399
+
400
+ ```text
401
+ local directory -> Cregh remote site
402
+ ```
403
+
404
+ `push` fetches the remote file list to build the local path to remote file id
405
+ mapping, scans the local directory, uploads the current local snapshot, and then
406
+ exits.
407
+
408
+ `sync` is watch mode. It performs the same initial local snapshot push, then
409
+ keeps running and automatically listens for later local changes.
410
+
411
+ Neither command pulls Web editor changes back to the local directory while
412
+ running. If you edit the same site in the Web editor, run `pull` manually or
413
+ restart from a clean local copy before continuing.
414
+
415
+ Use a test project/site while validating the CLI. Do not run `push` or `sync`
416
+ against production content unless the local directory is intended to be the
417
+ source of truth.
418
+
419
+ ## Commands
420
+
421
+ Cregh CLI is a local bridge for Cregh site code. It can authenticate with
422
+ Cregh, list projects and sites, pull remote site files into a local directory,
423
+ push local files back to Cregh, watch local files for realtime sync, open the
424
+ remote preview, and publish a site.
425
+
426
+ The CLI commands still use the Cregh backend and web app for the canonical
427
+ preview. The Vite plugin is a local development helper and intentionally does
428
+ not implement full production SSR.
429
+
430
+ ```bash
431
+ creght login [--web=https://creght.cn]
432
+ creght logout
433
+ creght project list
434
+ creght pull --site_id=<project_id>/<site_id> --dir=./mysite
435
+ creght push --site_id=<project_id>/<site_id> --dir=./mysite
436
+ creght sync --site_id=<project_id>/<site_id> --dir=./mysite
437
+ creght dev --site_id=<project_id>/<site_id> --dir=./mysite [--web=https://creght.cn]
438
+ creght preview --site_id=<project_id>/<site_id>
439
+ creght publish --site_id=<project_id>/<site_id> [--note=<note>]
440
+ creght cms collections --site_id=<project_id>/<site_id>
441
+ creght cms collection create --site_id=<project_id>/<site_id> --key=<key> --name=<name> --schema=./schema.json
442
+ creght content list --site_id=<project_id>/<site_id> --collection=<key>
443
+ creght content create --site_id=<project_id>/<site_id> --collection=<key> --data=./content.json
444
+ creght form list --site_id=<project_id>/<site_id>
445
+ creght form create --site_id=<project_id>/<site_id> --key=<key> --name=<name> --schema=./schema.json
446
+ creght upload --site_id=<project_id>/<site_id> --file=./image.png
447
+ creght version
448
+ ```
449
+
450
+ Command meanings:
451
+
452
+ - `login`: Authenticate this machine with Cregh and save a CLI token.
453
+ - `logout`: Remove the saved CLI token and API host configuration.
454
+ - `project`: List available projects and sites. Use `project_id/site_id` with site commands. Also supports `project create`.
455
+ - `pull`: Download the current remote site files into a local directory.
456
+ - `push`: Push the current local directory snapshot to the remote site.
457
+ - `sync`: Watch mode; push the current snapshot, then keep listening for local changes.
458
+ - `dev`: Bidirectionally sync local files with cloud realtime files and the online Web editor.
459
+ - `preview`: Open the remote preview URL for a site in the browser.
460
+ - `publish`: Publish a site to make the current remote site version live.
461
+ - `cms`: Manage CMS collections.
462
+ - `content`: Manage CMS content entries.
463
+ - `form`: Manage forms and form submissions.
464
+ - `upload`: Upload a local file as a Cregh site asset and print its URL.
465
+ - `version`: Print the installed CLI version.
466
+
467
+ ## Release
468
+
469
+ GitHub Releases are created by GitHub Actions when a tag matching `v*` is pushed.
470
+ The same workflow publishes the npm package `creght-cli`.
471
+
472
+ The release workflow builds binaries for:
473
+
474
+ - macOS: `darwin/amd64`, `darwin/arm64`
475
+ - Linux: `linux/amd64`, `linux/arm64`
476
+ - Windows: `windows/amd64`, `windows/arm64`
477
+
478
+ Create and push a release tag:
479
+
480
+ ```bash
481
+ git tag v0.1.0
482
+ git push origin v0.1.0
483
+ ```
484
+
485
+ Before pushing a release tag, make sure `package.json` has the same version as the
486
+ tag without the leading `v`, and configure npm Trusted Publishing for `creght-cli`
487
+ with GitHub repository `creght/creght-cli` and workflow filename `release.yml`.
488
+
489
+ If this repository is mirrored to GitHub with a different remote name, push the tag to that remote:
490
+
491
+ ```bash
492
+ git remote add github git@github.com:creght-dev/creght-cli.git
493
+ git push github main
494
+ git push github v0.1.0
495
+ ```
package/bin/creght.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require("node:child_process");
4
+ const path = require("node:path");
5
+
6
+ const platform = process.platform;
7
+ const arch = process.arch;
8
+ const exe = process.platform === "win32" ? "creght.exe" : "creght";
9
+ const binary = path.join(__dirname, "..", "vendor", `${platform}-${arch}`, exe);
10
+
11
+ const result = spawnSync(binary, process.argv.slice(2), {
12
+ stdio: "inherit",
13
+ });
14
+
15
+ if (result.error) {
16
+ if (result.error.code === "ENOENT") {
17
+ console.error(
18
+ "Cregh CLI binary is missing. Reinstall creght-cli and try again.",
19
+ );
20
+ } else {
21
+ console.error(result.error.message);
22
+ }
23
+ process.exit(1);
24
+ }
25
+
26
+ if (typeof result.status === "number") {
27
+ process.exit(result.status);
28
+ }
29
+
30
+ process.exit(result.signal ? 1 : 0);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "creght-cli",
3
+ "version": "0.1.16",
4
+ "description": "Cregh CLI for syncing local site code with Cregh.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/creght-dev/creght-cli#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/creght-dev/creght-cli.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/creght-dev/creght-cli/issues"
13
+ },
14
+ "bin": {
15
+ "creght": "bin/creght.js"
16
+ },
17
+ "exports": {
18
+ "./vite": {
19
+ "types": "./vite/index.d.ts",
20
+ "import": "./vite/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "prepare:npm-binaries": "node scripts/prepare-npm-binaries.js",
25
+ "test:npm": "node scripts/install.js --check"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "scripts/",
30
+ "vite/",
31
+ "vendor/",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "peerDependencies": {
38
+ "esbuild": ">=0.20",
39
+ "vite": ">=5"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "esbuild": {
43
+ "optional": true
44
+ },
45
+ "vite": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "os": [
50
+ "darwin",
51
+ "linux",
52
+ "win32"
53
+ ],
54
+ "cpu": [
55
+ "x64",
56
+ "arm64"
57
+ ]
58
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const packageRoot = path.join(__dirname, "..");
7
+ const platform = process.platform;
8
+ const arch = process.arch;
9
+ const exe = platform === "win32" ? "creght.exe" : "creght";
10
+ const binary = path.join(packageRoot, "vendor", `${platform}-${arch}`, exe);
11
+ const checkOnly = process.argv.includes("--check");
12
+
13
+ if (checkOnly) {
14
+ const exists = fs.existsSync(binary);
15
+ console.log(`Cregh CLI binary path for this platform: ${binary}`);
16
+ if (!exists) {
17
+ console.log("Binary is not present in this local checkout. CI adds release binaries before npm publish.");
18
+ }
19
+ process.exit(0);
20
+ }
21
+
22
+ if (!fs.existsSync(binary)) {
23
+ console.error(
24
+ `Cregh CLI binary is missing for ${platform}-${arch}. Reinstall creght-cli and try again.`,
25
+ );
26
+ process.exit(1);
27
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ const packageRoot = path.join(__dirname, "..");
9
+ const distDir = path.join(packageRoot, "dist");
10
+ const vendorDir = path.join(packageRoot, "vendor");
11
+ const version = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
12
+
13
+ const targets = [
14
+ { goos: "darwin", goarch: "amd64", npm: "darwin-x64", ext: "tar.gz", exe: "creght" },
15
+ { goos: "darwin", goarch: "arm64", npm: "darwin-arm64", ext: "tar.gz", exe: "creght" },
16
+ { goos: "linux", goarch: "amd64", npm: "linux-x64", ext: "tar.gz", exe: "creght" },
17
+ { goos: "linux", goarch: "arm64", npm: "linux-arm64", ext: "tar.gz", exe: "creght" },
18
+ { goos: "windows", goarch: "amd64", npm: "win32-x64", ext: "zip", exe: "creght.exe" },
19
+ { goos: "windows", goarch: "arm64", npm: "win32-arm64", ext: "zip", exe: "creght.exe" },
20
+ ];
21
+
22
+ fs.rmSync(vendorDir, { recursive: true, force: true });
23
+ fs.mkdirSync(vendorDir, { recursive: true });
24
+
25
+ for (const target of targets) {
26
+ const archive = path.join(distDir, `creght_${version}_${target.goos}_${target.goarch}.${target.ext}`);
27
+ if (!fs.existsSync(archive)) {
28
+ throw new Error(`missing release archive: ${archive}`);
29
+ }
30
+
31
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "creght-npm-"));
32
+ try {
33
+ extract(archive, tmpDir, target.ext);
34
+ const source = path.join(tmpDir, target.exe);
35
+ if (!fs.existsSync(source)) {
36
+ throw new Error(`archive did not contain ${target.exe}: ${archive}`);
37
+ }
38
+
39
+ const targetDir = path.join(vendorDir, target.npm);
40
+ fs.mkdirSync(targetDir, { recursive: true });
41
+ const destination = path.join(targetDir, target.exe);
42
+ fs.copyFileSync(source, destination);
43
+ if (target.exe === "creght") {
44
+ fs.chmodSync(destination, 0o755);
45
+ }
46
+ } finally {
47
+ fs.rmSync(tmpDir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ function extract(archive, destination, ext) {
52
+ const command = ext === "zip" ? "unzip" : "tar";
53
+ const args = ext === "zip" ? ["-q", archive, "-d", destination] : ["-xzf", archive, "-C", destination];
54
+ const result = spawnSync(command, args, { stdio: "inherit" });
55
+
56
+ if (result.error) {
57
+ throw result.error;
58
+ }
59
+ if (result.status !== 0) {
60
+ throw new Error(`${command} exited with status ${result.status}`);
61
+ }
62
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,55 @@
1
+ function mergeExternalDeps(externalValue, requiredDeps) {
2
+ try {
3
+ externalValue = decodeURIComponent(externalValue)
4
+ } catch {
5
+ // Keep the original value and continue merging invalid encoded external lists.
6
+ }
7
+
8
+ const seen = new Set()
9
+ const deps = externalValue
10
+ .split(',')
11
+ .map((dep) => dep.trim())
12
+ .filter(Boolean)
13
+ .filter((dep) => {
14
+ if (seen.has(dep)) return false
15
+ seen.add(dep)
16
+ return true
17
+ })
18
+
19
+ for (const dep of requiredDeps) {
20
+ if (seen.has(dep)) continue
21
+ seen.add(dep)
22
+ deps.push(dep)
23
+ }
24
+
25
+ return deps.join(',')
26
+ }
27
+
28
+ export function normalizeImportMapExternal(specifier, url) {
29
+ if (!/^https?:\/\//i.test(url)) return url
30
+ if (
31
+ specifier === 'react' ||
32
+ specifier.startsWith('react/') ||
33
+ specifier === 'react-dom' ||
34
+ specifier.startsWith('react-dom/')
35
+ ) {
36
+ return url
37
+ }
38
+
39
+ const requiredDeps = ['react', 'react-dom']
40
+ const hashIndex = url.indexOf('#')
41
+ const beforeHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url
42
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : ''
43
+ const hasPrefixSlash = specifier.endsWith('/') && beforeHash.endsWith('/')
44
+ const externalTarget = hasPrefixSlash ? beforeHash.slice(0, -1) : beforeHash
45
+ const externalSuffix = hasPrefixSlash ? '/' : ''
46
+ const externalMatch = externalTarget.match(/([?&])external=([^&#]*)/)
47
+
48
+ if (externalMatch && externalMatch.index !== undefined) {
49
+ const start = externalMatch.index + externalMatch[1].length + 'external='.length
50
+ const end = start + externalMatch[2].length
51
+ return `${externalTarget.slice(0, start)}${mergeExternalDeps(externalMatch[2], requiredDeps)}${externalTarget.slice(end)}${externalSuffix}${hash}`
52
+ }
53
+
54
+ return `${externalTarget}${hasPrefixSlash || externalTarget.includes('?') ? '&' : '?'}external=${requiredDeps.join(',')}${externalSuffix}${hash}`
55
+ }
@@ -0,0 +1,28 @@
1
+ import type { Plugin } from 'vite'
2
+
3
+ export type CreghVitePluginOptions = {
4
+ /**
5
+ * Local Cregh project root. Defaults to Vite's root.
6
+ */
7
+ root?: string
8
+ /**
9
+ * Cregh API host used by the local /api proxy at runtime.
10
+ */
11
+ apiHost?: string
12
+ /**
13
+ * Project id for runtime CMS/form requests.
14
+ */
15
+ projectId?: string
16
+ /**
17
+ * Optional CLI auth token. When set, local /api proxy sends it as Bearer.
18
+ */
19
+ token?: string
20
+ /**
21
+ * Platform import map entries. `creght dev` fills this from server system info.
22
+ * Manually configured entries override the built-in fallback when no server map is provided.
23
+ */
24
+ importMap?: Record<string, string>
25
+ }
26
+
27
+ export declare function creght(options?: CreghVitePluginOptions): Plugin
28
+ export default creght
package/vite/index.js ADDED
@@ -0,0 +1,546 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { transform as esbuildTransform } from 'esbuild'
4
+ import { normalizeImportMapExternal } from './import-map.js'
5
+
6
+ const modulePrefix = '/@creght/module'
7
+ const assetPrefix = '/@creght/asset'
8
+ const runtimePrefix = '/@creght/runtime'
9
+ const pageExts = ['.tsx', '.ts', '.jsx', '.js']
10
+ const sourceExts = new Set([...pageExts, '.css'])
11
+
12
+ const fallbackImportMap = {
13
+ react: 'https://esm.talizen.com/react@19.2.4?dev',
14
+ 'react/': 'https://esm.talizen.com/react@19.2.4&dev/',
15
+ 'react-dom': 'https://esm.talizen.com/react-dom@19.2.4?dev',
16
+ 'react-dom/': 'https://esm.talizen.com/react-dom@19.2.4&dev/',
17
+ 'react-dom/client': 'https://esm.talizen.com/react-dom@19.2.4/client?dev',
18
+ 'class-variance-authority': 'https://esm.talizen.com/class-variance-authority@0.7.1',
19
+ clsx: 'https://esm.talizen.com/clsx@2.1.1',
20
+ 'tailwind-merge': 'https://esm.talizen.com/tailwind-merge@3.5.0',
21
+ '@radix-ui/react-slot': 'https://esm.talizen.com/@radix-ui/react-slot@1.2.4',
22
+ 'lucide-react': 'https://esm.talizen.com/lucide-react@0.577.0?dev&external=react,react-dom',
23
+ motion: 'https://esm.talizen.com/motion@12.38.0?dev',
24
+ 'motion/react': 'https://esm.talizen.com/motion@12.38.0/react?dev&external=react,react-dom',
25
+ 'framer-motion': 'https://esm.talizen.com/framer-motion@12.38.0?dev&external=react,react-dom',
26
+ three: 'https://esm.talizen.com/three@0.167.1',
27
+ 'three/': 'https://esm.talizen.com/three@0.167.1/',
28
+ '@react-three/fiber': 'https://esm.talizen.com/@react-three/fiber@9.3.0?external=react,react-dom,three',
29
+ '@react-three/drei': 'https://esm.talizen.com/@react-three/drei@10.7.4?external=react,react-dom,three,@react-three',
30
+ talizen: 'https://esm.talizen.com/talizen@0.1.4',
31
+ 'talizen/': 'https://esm.talizen.com/talizen@0.1.4/',
32
+ }
33
+
34
+ const escapeHtml = (s) => String(s)
35
+ .replaceAll('&', '&amp;')
36
+ .replaceAll('<', '&lt;')
37
+ .replaceAll('>', '&gt;')
38
+ .replaceAll('"', '&quot;')
39
+
40
+ const jsonScript = (value) => JSON.stringify(value).replaceAll('<', '\\u003c')
41
+
42
+ const transformWithEsbuild = (source, filename, options) =>
43
+ esbuildTransform(source, { ...options, sourcefile: filename })
44
+
45
+ const isRelativeSpecifier = (specifier) =>
46
+ specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/')
47
+
48
+ const contentTypeForPath = (file) => {
49
+ switch (path.extname(file).toLowerCase()) {
50
+ case '.css':
51
+ return 'text/css; charset=utf-8'
52
+ case '.js':
53
+ case '.mjs':
54
+ return 'text/javascript; charset=utf-8'
55
+ case '.json':
56
+ return 'application/json; charset=utf-8'
57
+ case '.svg':
58
+ return 'image/svg+xml'
59
+ case '.png':
60
+ return 'image/png'
61
+ case '.jpg':
62
+ case '.jpeg':
63
+ return 'image/jpeg'
64
+ case '.gif':
65
+ return 'image/gif'
66
+ case '.webp':
67
+ return 'image/webp'
68
+ case '.woff':
69
+ return 'font/woff'
70
+ case '.woff2':
71
+ return 'font/woff2'
72
+ default:
73
+ return 'application/octet-stream'
74
+ }
75
+ }
76
+
77
+ const normalizeProjectPath = (value) => {
78
+ const out = path.posix.normalize('/' + value.replaceAll(path.sep, '/').replace(/^\/+/, ''))
79
+ if (out.startsWith('/../')) {
80
+ throw new Error(`unsafe Cregh path: ${value}`)
81
+ }
82
+ return out
83
+ }
84
+
85
+ async function exists(file) {
86
+ try {
87
+ await fs.access(file)
88
+ return true
89
+ } catch {
90
+ return false
91
+ }
92
+ }
93
+
94
+ async function resolveFile(projectRoot, projectPath) {
95
+ const clean = normalizeProjectPath(projectPath)
96
+ const abs = path.join(projectRoot, clean.slice(1))
97
+ if (await exists(abs)) return { projectPath: clean, abs }
98
+
99
+ for (const ext of pageExts) {
100
+ if (await exists(abs + ext)) {
101
+ return { projectPath: clean + ext, abs: abs + ext }
102
+ }
103
+ }
104
+
105
+ for (const ext of pageExts) {
106
+ const indexPath = path.join(abs, 'Index' + ext)
107
+ if (await exists(indexPath)) {
108
+ return { projectPath: normalizeProjectPath(path.posix.join(clean, 'Index' + ext)), abs: indexPath }
109
+ }
110
+ }
111
+
112
+ throw new Error(`Cregh module not found: ${projectPath}`)
113
+ }
114
+
115
+ async function readSiteConfig(projectRoot) {
116
+ for (const name of ['creght.config.ts', 'creght.config.js', 'talizen.config.ts', 'talizen.config.js', 'folia.config.ts', 'folia.config.js']) {
117
+ const abs = path.join(projectRoot, name)
118
+ if (!(await exists(abs))) continue
119
+
120
+ try {
121
+ const source = await fs.readFile(abs, 'utf8')
122
+ const transformed = await transformWithEsbuild(source, abs, {
123
+ loader: name.endsWith('.ts') ? 'ts' : 'js',
124
+ format: 'cjs',
125
+ target: 'es2020',
126
+ })
127
+ const module = { exports: {} }
128
+ const require = (specifier) => {
129
+ if (specifier === 'creght' || specifier === 'creght/config' || specifier === 'talizen' || specifier === 'talizen/config') {
130
+ return { defineConfig: (config) => config }
131
+ }
132
+ if (specifier.startsWith('creght/')) return {}
133
+ if (specifier.startsWith('talizen/')) return {}
134
+ throw new Error(`[creght.config] unsupported import: ${specifier}`)
135
+ }
136
+ const fn = new Function('module', 'exports', 'require', 'defineConfig', transformed.code)
137
+ fn(module, module.exports, require, (config) => config)
138
+ const value = module.exports?.default ?? module.exports
139
+ return value && typeof value === 'object' ? value : {}
140
+ } catch (err) {
141
+ console.warn(`[creght vite] failed to evaluate ${name}:`, err)
142
+ return {}
143
+ }
144
+ }
145
+
146
+ return {}
147
+ }
148
+
149
+ async function readIndexCss(projectRoot, config) {
150
+ if (typeof config.tailwindCss === 'string') return config.tailwindCss
151
+
152
+ const cssPath = path.join(projectRoot, 'index.css')
153
+ if (!(await exists(cssPath))) return ''
154
+ return fs.readFile(cssPath, 'utf8')
155
+ }
156
+
157
+ async function buildImportMap(projectRoot, options) {
158
+ const config = await readSiteConfig(projectRoot)
159
+ const userImports = config.importMap?.imports || {}
160
+ const imports = Object.keys(options.importMap || {}).length > 0
161
+ ? { ...options.importMap }
162
+ : { ...fallbackImportMap }
163
+
164
+ for (const [specifier, url] of Object.entries(userImports)) {
165
+ imports[specifier] = normalizeImportMapExternal(specifier, String(url))
166
+ }
167
+ for (const [specifier, url] of Object.entries(options.importMap || {})) {
168
+ imports[specifier] = normalizeImportMapExternal(specifier, String(url))
169
+ }
170
+
171
+ return { config, imports }
172
+ }
173
+
174
+ async function listPages(projectRoot) {
175
+ const pageRoot = path.join(projectRoot, 'page')
176
+ if (!(await exists(pageRoot))) return []
177
+
178
+ const out = []
179
+ async function walk(dir) {
180
+ const entries = await fs.readdir(dir, { withFileTypes: true })
181
+ for (const entry of entries) {
182
+ const abs = path.join(dir, entry.name)
183
+ if (entry.isDirectory()) {
184
+ await walk(abs)
185
+ continue
186
+ }
187
+ const ext = path.extname(entry.name)
188
+ if (!pageExts.includes(ext)) continue
189
+ if (entry.name.includes('.canvas.')) continue
190
+ const rel = normalizeProjectPath(path.relative(projectRoot, abs))
191
+ out.push({ file: rel, route: routeFromPagePath(rel) })
192
+ }
193
+ }
194
+ await walk(pageRoot)
195
+ return out.sort((a, b) => a.route.localeCompare(b.route))
196
+ }
197
+
198
+ function routeFromPagePath(file) {
199
+ const withoutExt = file.replace(/^\/page\//, '').replace(/\.[^.]+$/, '')
200
+ if (withoutExt === 'Index') return '/'
201
+ return '/' + withoutExt
202
+ .replace(/\/Index$/, '')
203
+ .split('/')
204
+ .map((segment) => segment.startsWith('[') && segment.endsWith(']') ? `:${segment.slice(1, -1)}` : segment.toLowerCase())
205
+ .join('/')
206
+ }
207
+
208
+ function matchRoute(routes, pathname) {
209
+ const normalized = pathname !== '/' ? pathname.replace(/\/+$/, '') : '/'
210
+ const exact = routes.find((r) => r.route === normalized)
211
+ if (exact) return { ...exact, params: {} }
212
+
213
+ for (const route of routes) {
214
+ const routeParts = route.route.split('/').filter(Boolean)
215
+ const pathParts = normalized.split('/').filter(Boolean)
216
+ if (routeParts.length !== pathParts.length) continue
217
+ const params = {}
218
+ let ok = true
219
+ for (let i = 0; i < routeParts.length; i++) {
220
+ const part = routeParts[i]
221
+ if (part.startsWith(':')) {
222
+ params[part.slice(1)] = decodeURIComponent(pathParts[i])
223
+ continue
224
+ }
225
+ if (part !== pathParts[i].toLowerCase()) {
226
+ ok = false
227
+ break
228
+ }
229
+ }
230
+ if (ok) return { ...route, params }
231
+ }
232
+
233
+ return { ...(routes.find((r) => r.route === '/') || routes[0]), params: {} }
234
+ }
235
+
236
+ function rewriteModuleSpecifiers(code, importerPath) {
237
+ const rewrite = (specifier) => {
238
+ if (!isRelativeSpecifier(specifier)) return specifier
239
+ const [rawPath, rawQuery = ''] = specifier.split('?')
240
+ const importerDir = path.posix.dirname(importerPath)
241
+ const resolved = normalizeProjectPath(path.posix.join(importerDir, rawPath))
242
+ const target = rawQuery ? `${resolved}?${rawQuery}` : resolved
243
+ return `${modulePrefix}?path=${encodeURIComponent(target)}`
244
+ }
245
+
246
+ return code
247
+ .replace(/(from\s*["'])([^"']+)(["'])/g, (_, before, specifier, after) => `${before}${rewrite(specifier)}${after}`)
248
+ .replace(/(import\s*["'])([^"']+)(["'])/g, (_, before, specifier, after) => `${before}${rewrite(specifier)}${after}`)
249
+ .replace(/(import\s*\(\s*["'])([^"']+)(["']\s*\))/g, (_, before, specifier, after) => `${before}${rewrite(specifier)}${after}`)
250
+ }
251
+
252
+ async function transformProjectModule(projectRoot, projectPath) {
253
+ const [pathWithoutQuery, query = ''] = projectPath.split('?')
254
+ const { projectPath: resolvedProjectPath, abs } = await resolveFile(projectRoot, pathWithoutQuery)
255
+ const ext = path.extname(abs).toLowerCase()
256
+
257
+ if (query === 'raw') {
258
+ const source = await fs.readFile(abs, 'utf8')
259
+ return `export default ${JSON.stringify(source)};\n`
260
+ }
261
+ if (query === 'url') {
262
+ return `export default ${JSON.stringify(`${assetPrefix}?path=${encodeURIComponent(resolvedProjectPath)}`)};\n`
263
+ }
264
+ if (!sourceExts.has(ext)) {
265
+ return `export default ${JSON.stringify(`${assetPrefix}?path=${encodeURIComponent(resolvedProjectPath)}`)};\n`
266
+ }
267
+
268
+ const source = await fs.readFile(abs, 'utf8')
269
+ if (abs.endsWith('.css')) {
270
+ return `
271
+ const css = ${JSON.stringify(source)};
272
+ let style = document.querySelector('style[data-talizen-css-module="${resolvedProjectPath}"]');
273
+ if (!style) {
274
+ style = document.createElement('style');
275
+ style.dataset.talizenCssModule = ${JSON.stringify(resolvedProjectPath)};
276
+ document.head.appendChild(style);
277
+ }
278
+ style.textContent = css;
279
+ export default css;
280
+ `
281
+ }
282
+
283
+ const loader = abs.endsWith('.tsx') ? 'tsx' : abs.endsWith('.ts') ? 'ts' : abs.endsWith('.jsx') ? 'jsx' : 'js'
284
+ const result = await transformWithEsbuild(source, abs, {
285
+ loader,
286
+ jsx: 'automatic',
287
+ jsxDev: true,
288
+ sourcemap: 'inline',
289
+ target: 'es2020',
290
+ define: {
291
+ 'process.env.NODE_ENV': '"development"',
292
+ 'process.env.RENDER_ENV': '"design"',
293
+ 'process.env.RENDER_MODE': '"design"',
294
+ },
295
+ })
296
+
297
+ return rewriteModuleSpecifiers(result.code, resolvedProjectPath)
298
+ }
299
+
300
+ function renderRuntimeScript(entryFile, params, options) {
301
+ const projectId = options.projectId || ''
302
+ const authHeaders = options.token ? { Authorization: `Bearer ${options.token}` } : {}
303
+
304
+ return `
305
+ import React from 'react';
306
+ import { createRoot } from 'react-dom/client';
307
+ import { createHotContext } from '/@vite/client';
308
+
309
+ const creghtRuntimeHeaders = ${jsonScript(authHeaders)};
310
+ const creghtRuntimeFetch = window.fetch.bind(window);
311
+
312
+ window.TalizenConfig = {
313
+ baseUrl: window.location.origin + '/api/u/v2/project/' + ${JSON.stringify(projectId)},
314
+ headers: creghtRuntimeHeaders,
315
+ fetch(input, init = {}) {
316
+ const headers = new Headers(init.headers || {});
317
+ for (const [key, value] of Object.entries(creghtRuntimeHeaders)) {
318
+ if (!headers.has(key)) headers.set(key, value);
319
+ }
320
+ return creghtRuntimeFetch(input, { ...init, headers });
321
+ },
322
+ };
323
+
324
+ const rootEl = document.getElementById('root');
325
+ const root = createRoot(rootEl);
326
+ let renderVersion = 0;
327
+
328
+ async function loadPageModule() {
329
+ return import(${JSON.stringify(`${modulePrefix}?path=${encodeURIComponent(entryFile)}`)} + '&t=' + Date.now());
330
+ }
331
+
332
+ async function renderPage() {
333
+ const version = ++renderVersion;
334
+ const pageModule = await loadPageModule();
335
+ if (version !== renderVersion) return;
336
+
337
+ const Page = pageModule.default || pageModule.App;
338
+ let props = {};
339
+
340
+ if (!Page) {
341
+ throw new Error('Cregh page has no default export: ${entryFile}');
342
+ }
343
+
344
+ if (typeof pageModule.getServerSideProps === 'function') {
345
+ const result = await pageModule.getServerSideProps({
346
+ query: Object.fromEntries(new URLSearchParams(window.location.search)),
347
+ params: ${jsonScript(params)},
348
+ });
349
+ props = result && result.props ? result.props : {};
350
+ }
351
+
352
+ root.render(React.createElement(Page, props));
353
+ }
354
+
355
+ const hot = createHotContext(${JSON.stringify(`${runtimePrefix}?entry=${encodeURIComponent(entryFile)}`)});
356
+
357
+ await renderPage();
358
+
359
+ hot.on('creght:update', () => {
360
+ renderPage().catch((err) => {
361
+ console.error('[creght vite] hot update failed', err);
362
+ });
363
+ });
364
+ `
365
+ }
366
+
367
+ async function renderHtml(projectRoot, pathname, options) {
368
+ const routes = await listPages(projectRoot)
369
+ if (routes.length === 0) {
370
+ return `<!doctype html><div style="font:14px system-ui;padding:24px">No Cregh pages found in <code>/page</code>.</div>`
371
+ }
372
+
373
+ const matched = matchRoute(routes, pathname)
374
+ const { config, imports } = await buildImportMap(projectRoot, options)
375
+ const indexCss = await readIndexCss(projectRoot, config)
376
+ const customCode = config.customCode || {}
377
+
378
+ return `<!doctype html>
379
+ <html lang="en">
380
+ <head>
381
+ <meta charset="UTF-8">
382
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
383
+ <title>${escapeHtml(matched.route === '/' ? 'Cregh' : matched.route)}</title>
384
+ <script type="importmap">${jsonScript({ imports })}</script>
385
+ <script type="module" src="/@vite/client"></script>
386
+ <style type="text/tailwindcss">${indexCss}</style>
387
+ <script type="module" src="https://esm.talizen.com/@tailwindcss/browser@4"></script>
388
+ ${customCode.head || ''}
389
+ </head>
390
+ <body>
391
+ ${customCode.bodyStart || ''}
392
+ <div id="root"></div>
393
+ <script type="module" src="${runtimePrefix}?entry=${encodeURIComponent(matched.file)}&params=${encodeURIComponent(JSON.stringify(matched.params))}&t=${Date.now()}"></script>
394
+ ${customCode.bodyEnd || customCode.body || ''}
395
+ </body>
396
+ </html>`
397
+ }
398
+
399
+ export function creght(options = {}) {
400
+ let projectRoot = ''
401
+ const apiHost = (options.apiHost || 'https://creght.cn').replace(/\/+$/, '')
402
+
403
+ return {
404
+ name: 'creght-local-preview',
405
+ enforce: 'pre',
406
+ configResolved(config) {
407
+ projectRoot = path.resolve(options.root || config.root)
408
+ },
409
+ configureServer(server) {
410
+ const hmrTimers = new Map()
411
+ const sendCreghUpdate = (file) => {
412
+ if (!file.startsWith(projectRoot)) return
413
+
414
+ const projectPath = normalizeProjectPath(path.relative(projectRoot, file))
415
+ const existing = hmrTimers.get(projectPath)
416
+ if (existing) clearTimeout(existing)
417
+
418
+ hmrTimers.set(projectPath, setTimeout(() => {
419
+ hmrTimers.delete(projectPath)
420
+ server.ws.send({
421
+ type: 'custom',
422
+ event: 'creght:update',
423
+ data: {
424
+ file: projectPath,
425
+ time: Date.now(),
426
+ },
427
+ })
428
+ }, 80))
429
+ }
430
+
431
+ server.watcher.add(path.join(projectRoot, '**/*'))
432
+ server.watcher.on('change', sendCreghUpdate)
433
+ server.watcher.on('add', sendCreghUpdate)
434
+ server.watcher.on('unlink', sendCreghUpdate)
435
+
436
+ server.middlewares.use(async (req, res, next) => {
437
+ try {
438
+ const url = new URL(req.url || '/', 'http://localhost')
439
+
440
+ if (url.pathname.startsWith('/api/')) {
441
+ const headers = new Headers()
442
+ for (const [key, value] of Object.entries(req.headers)) {
443
+ if (Array.isArray(value)) {
444
+ headers.set(key, value.join(', '))
445
+ } else if (value != null) {
446
+ headers.set(key, value)
447
+ }
448
+ }
449
+ headers.set('host', new URL(apiHost).host)
450
+ if (options.token) {
451
+ headers.delete('cookie')
452
+ headers.set('authorization', `Bearer ${options.token}`)
453
+ }
454
+
455
+ const method = req.method || 'GET'
456
+ const body = method === 'GET' || method === 'HEAD' ? undefined : req
457
+ const upstream = await fetch(apiHost + url.pathname + url.search, {
458
+ method,
459
+ headers,
460
+ body,
461
+ // Required by Node fetch when the request body is a stream.
462
+ duplex: body ? 'half' : undefined,
463
+ })
464
+
465
+ res.statusCode = upstream.status
466
+ upstream.headers.forEach((value, key) => {
467
+ const lowerKey = key.toLowerCase()
468
+ if (lowerKey === 'content-encoding') return
469
+ if (lowerKey === 'content-length') return
470
+ if (lowerKey === 'transfer-encoding') return
471
+ if (lowerKey === 'connection') return
472
+ res.setHeader(key, value)
473
+ })
474
+ const data = Buffer.from(await upstream.arrayBuffer())
475
+ res.end(data)
476
+ return
477
+ }
478
+
479
+ if (url.pathname === modulePrefix) {
480
+ const projectPath = url.searchParams.get('path')
481
+ if (!projectPath) {
482
+ res.statusCode = 400
483
+ res.end('missing path')
484
+ return
485
+ }
486
+ const code = await transformProjectModule(projectRoot, projectPath)
487
+ res.setHeader('Content-Type', 'text/javascript; charset=utf-8')
488
+ res.end(code)
489
+ return
490
+ }
491
+
492
+ if (url.pathname === runtimePrefix) {
493
+ const entry = url.searchParams.get('entry')
494
+ const rawParams = url.searchParams.get('params') || '{}'
495
+ if (!entry) {
496
+ res.statusCode = 400
497
+ res.end('missing entry')
498
+ return
499
+ }
500
+ let params = {}
501
+ try {
502
+ params = JSON.parse(rawParams)
503
+ } catch {
504
+ params = {}
505
+ }
506
+ res.setHeader('Content-Type', 'text/javascript; charset=utf-8')
507
+ res.end(renderRuntimeScript(entry, params, options))
508
+ return
509
+ }
510
+
511
+ if (url.pathname === assetPrefix) {
512
+ const projectPath = url.searchParams.get('path')
513
+ if (!projectPath) {
514
+ res.statusCode = 400
515
+ res.end('missing path')
516
+ return
517
+ }
518
+ const { abs } = await resolveFile(projectRoot, projectPath)
519
+ res.setHeader('Content-Type', contentTypeForPath(abs))
520
+ res.end(await fs.readFile(abs))
521
+ return
522
+ }
523
+
524
+ if (req.method !== 'GET') {
525
+ next()
526
+ return
527
+ }
528
+
529
+ const accept = req.headers.accept || ''
530
+ if (!accept.includes('text/html')) {
531
+ next()
532
+ return
533
+ }
534
+
535
+ const html = await renderHtml(projectRoot, url.pathname, options)
536
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
537
+ res.end(html)
538
+ } catch (err) {
539
+ next(err)
540
+ }
541
+ })
542
+ },
543
+ }
544
+ }
545
+
546
+ export default creght
@@ -0,0 +1,47 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { normalizeImportMapExternal } from './import-map.js'
5
+
6
+ describe('normalizeImportMapExternal', () => {
7
+ it('adds react and react-dom externals to regular import map entries', () => {
8
+ assert.equal(
9
+ normalizeImportMapExternal('framer-motion', 'https://esm.talizen.com/framer-motion'),
10
+ 'https://esm.talizen.com/framer-motion?external=react,react-dom',
11
+ )
12
+ })
13
+
14
+ it('merges missing externals without dropping existing entries', () => {
15
+ assert.equal(
16
+ normalizeImportMapExternal(
17
+ '@react-three/drei',
18
+ 'https://esm.talizen.com/@react-three/drei?external=three,react,@react-three/fiber',
19
+ ),
20
+ 'https://esm.talizen.com/@react-three/drei?external=three,react,@react-three/fiber,react-dom',
21
+ )
22
+ })
23
+
24
+ it('inserts externals before the trailing slash for prefix import map entries', () => {
25
+ assert.equal(
26
+ normalizeImportMapExternal('talizen/', 'https://esm.talizen.com/talizen@0.1.4/'),
27
+ 'https://esm.talizen.com/talizen@0.1.4&external=react,react-dom/',
28
+ )
29
+ })
30
+
31
+ it('merges externals before the trailing slash for prefix entries', () => {
32
+ assert.equal(
33
+ normalizeImportMapExternal(
34
+ '@kobalte/core/',
35
+ 'https://esm.talizen.com/@kobalte/core&external=react/',
36
+ ),
37
+ 'https://esm.talizen.com/@kobalte/core&external=react,react-dom/',
38
+ )
39
+ })
40
+
41
+ it('does not modify host React runtime specifiers', () => {
42
+ assert.equal(
43
+ normalizeImportMapExternal('react-dom/client', 'https://esm.talizen.com/react-dom@19/client?dev'),
44
+ 'https://esm.talizen.com/react-dom@19/client?dev',
45
+ )
46
+ })
47
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }