@jxsuite/create 0.35.0 → 1.0.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.
- package/generate.ts +15 -28
- package/package.json +5 -3
- package/scaffold.ts +162 -0
- package/template/gitignore +2 -0
package/generate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Shared project generation logic. Used by both the CLI scaffolder and the Studio server endpoint. */
|
|
2
2
|
|
|
3
3
|
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { adapterNeedsWrangler, buildWranglerJsonc } from "./scaffold";
|
|
4
5
|
import { basename, join, relative, sep } from "node:path";
|
|
5
6
|
|
|
6
7
|
import { Buffer } from "node:buffer";
|
|
@@ -49,8 +50,6 @@ export interface ProjectOptions {
|
|
|
49
50
|
design?: DesignOptions;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const CF_ADAPTERS = new Set(["cloudflare-pages", "cloudflare-workers"]);
|
|
53
|
-
|
|
54
53
|
// Paths never copied out of a starter tree into a fresh project: build artifacts and the
|
|
55
54
|
// Authoring-only Pexels fetch manifest.
|
|
56
55
|
const STARTER_EXCLUDE = new Set([
|
|
@@ -117,7 +116,7 @@ export async function generateProject(destPath: string, opts: ProjectOptions) {
|
|
|
117
116
|
cp(join(TEMPLATE_DIR, "pages"), join(destPath, "pages"), { recursive: true }),
|
|
118
117
|
];
|
|
119
118
|
|
|
120
|
-
if (
|
|
119
|
+
if (adapterNeedsWrangler(adapter)) {
|
|
121
120
|
const wranglerJsonc = buildWranglerJsonc({ adapter, slug: packageJson.name });
|
|
122
121
|
writes.push(writeFile(join(destPath, "wrangler.jsonc"), wranglerJsonc));
|
|
123
122
|
}
|
|
@@ -198,7 +197,7 @@ async function scaffoldFromStarter(
|
|
|
198
197
|
const packageJson = buildPackageJson({ adapter, description, name });
|
|
199
198
|
await writeFile(join(destPath, "package.json"), `${JSON.stringify(packageJson, null, " ")}\n`);
|
|
200
199
|
|
|
201
|
-
if (
|
|
200
|
+
if (adapterNeedsWrangler(adapter)) {
|
|
202
201
|
const wranglerJsonc = buildWranglerJsonc({ adapter, slug: packageJson.name });
|
|
203
202
|
await writeFile(join(destPath, "wrangler.jsonc"), wranglerJsonc);
|
|
204
203
|
}
|
|
@@ -311,28 +310,6 @@ function applyDescription(project: Record<string, unknown>, description: string)
|
|
|
311
310
|
*
|
|
312
311
|
* @param {{ slug: string; adapter: string }} opts
|
|
313
312
|
*/
|
|
314
|
-
function buildWranglerJsonc({ slug, adapter }: { slug: string; adapter: string }) {
|
|
315
|
-
const compatibilityDate = new Date().toISOString().slice(0, 10);
|
|
316
|
-
|
|
317
|
-
// Nodejs_compat: server functions routinely pull in Node-flavored npm packages
|
|
318
|
-
const config =
|
|
319
|
-
adapter === "cloudflare-workers"
|
|
320
|
-
? {
|
|
321
|
-
assets: { binding: "ASSETS", directory: "./dist" },
|
|
322
|
-
compatibility_date: compatibilityDate,
|
|
323
|
-
compatibility_flags: ["nodejs_compat"],
|
|
324
|
-
main: "./dist/worker.js",
|
|
325
|
-
name: slug,
|
|
326
|
-
}
|
|
327
|
-
: {
|
|
328
|
-
compatibility_date: compatibilityDate,
|
|
329
|
-
compatibility_flags: ["nodejs_compat"],
|
|
330
|
-
name: slug,
|
|
331
|
-
pages_build_output_dir: "./dist",
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
335
|
-
}
|
|
336
313
|
|
|
337
314
|
/** @param {ProjectOptions} opts */
|
|
338
315
|
function buildProjectJson({ name, description, url, adapter, template = "blank" }: ProjectOptions) {
|
|
@@ -375,13 +352,19 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
|
|
|
375
352
|
}
|
|
376
353
|
|
|
377
354
|
return {
|
|
355
|
+
// The generated per-project schema binding; written by `jx schema` (specs/extensions.md §5.2).
|
|
356
|
+
$schema: "./project.schema.json",
|
|
378
357
|
$head,
|
|
379
358
|
$media: mediaForTemplate(template),
|
|
380
359
|
build,
|
|
360
|
+
// The scaffolded pages/index.md needs the parser extension's Markdown format; `content` is the
|
|
361
|
+
// Parser-owned section ready for the first collection.
|
|
362
|
+
content: {},
|
|
381
363
|
defaults: {
|
|
382
364
|
lang: "en",
|
|
383
365
|
layout: "./layouts/base.json",
|
|
384
366
|
},
|
|
367
|
+
extensions: ["@jxsuite/parser"],
|
|
385
368
|
name,
|
|
386
369
|
style: {
|
|
387
370
|
color: "#1a1a1a",
|
|
@@ -409,7 +392,11 @@ function buildPackageJson({
|
|
|
409
392
|
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
410
393
|
.replaceAll(/^-|-$/g, "");
|
|
411
394
|
|
|
412
|
-
|
|
395
|
+
// Projects own their extension dependencies (specs/extensions.md §3) — the parser extension is
|
|
396
|
+
// A runtime dependency, resolved project-first by every host.
|
|
397
|
+
const dependencies: Record<string, string> = {
|
|
398
|
+
"@jxsuite/parser": "^0.35.1",
|
|
399
|
+
};
|
|
413
400
|
const devDependencies: Record<string, string> = {
|
|
414
401
|
"@jxsuite/compiler": "^0.19.0",
|
|
415
402
|
"@jxsuite/runtime": "^0.19.0",
|
|
@@ -425,7 +412,7 @@ function buildPackageJson({
|
|
|
425
412
|
dependencies["hono"] = "^4";
|
|
426
413
|
}
|
|
427
414
|
|
|
428
|
-
if (
|
|
415
|
+
if (adapterNeedsWrangler(adapter)) {
|
|
429
416
|
devDependencies["wrangler"] = "^4";
|
|
430
417
|
scripts.deploy =
|
|
431
418
|
adapter === "cloudflare-workers" ? "wrangler deploy" : "wrangler pages deploy dist";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Scaffold a new Jx project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"generate.ts",
|
|
17
17
|
"cli-args.ts",
|
|
18
18
|
"templates.ts",
|
|
19
|
+
"scaffold.ts",
|
|
19
20
|
"template/",
|
|
20
21
|
"templates/"
|
|
21
22
|
],
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
"exports": {
|
|
24
25
|
".": "./index.ts",
|
|
25
26
|
"./generate": "./generate.ts",
|
|
26
|
-
"./templates": "./templates.ts"
|
|
27
|
+
"./templates": "./templates.ts",
|
|
28
|
+
"./scaffold": "./scaffold.ts"
|
|
27
29
|
},
|
|
28
30
|
"publishConfig": {
|
|
29
31
|
"provenance": true
|
|
@@ -33,6 +35,6 @@
|
|
|
33
35
|
"test:coverage": "bun test --isolate --coverage"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@jxsuite/starters": "^0.
|
|
38
|
+
"@jxsuite/starters": "^1.0.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/scaffold.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scaffolding helpers — zero node imports so Studio can run them in the browser (the publish
|
|
3
|
+
* flow writes wrangler.jsonc through the PAL) and cloud platforms can seed repos through the Git
|
|
4
|
+
* Data API. generate.ts wraps these with filesystem I/O.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const CF_ADAPTERS = new Set(["cloudflare-pages", "cloudflare-workers"]);
|
|
8
|
+
|
|
9
|
+
/** True when the adapter deploys through wrangler (needs a wrangler.jsonc). */
|
|
10
|
+
export function adapterNeedsWrangler(adapter: string | undefined): boolean {
|
|
11
|
+
return Boolean(adapter && CF_ADAPTERS.has(adapter));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A fresh wrangler.jsonc for the adapter. `name` becomes the Cloudflare project name — when the
|
|
16
|
+
* project has a `build.deploy` block, pass its projectName so the config and the connected Pages
|
|
17
|
+
* project agree.
|
|
18
|
+
*/
|
|
19
|
+
export function buildWranglerJsonc({ slug, adapter }: { slug: string; adapter: string }): string {
|
|
20
|
+
const compatibilityDate = new Date().toISOString().slice(0, 10);
|
|
21
|
+
|
|
22
|
+
// Nodejs_compat: server functions routinely pull in Node-flavored npm packages
|
|
23
|
+
const config =
|
|
24
|
+
adapter === "cloudflare-workers"
|
|
25
|
+
? {
|
|
26
|
+
assets: { binding: "ASSETS", directory: "./dist" },
|
|
27
|
+
compatibility_date: compatibilityDate,
|
|
28
|
+
compatibility_flags: ["nodejs_compat"],
|
|
29
|
+
main: "./dist/worker.js",
|
|
30
|
+
name: slug,
|
|
31
|
+
}
|
|
32
|
+
: {
|
|
33
|
+
compatibility_date: compatibilityDate,
|
|
34
|
+
compatibility_flags: ["nodejs_compat"],
|
|
35
|
+
name: slug,
|
|
36
|
+
pages_build_output_dir: "./dist",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return `${JSON.stringify(config, null, "\t")}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deep-merge connector binding fragments into a wrangler.jsonc (plan Part 4a; consumed by `jx db
|
|
44
|
+
* push`). Fragments are the `bindings` connector-capability results — e.g. `{ d1_databases: [{
|
|
45
|
+
* binding, database_id }] }` or `{ hyperdrive: [{ binding, id }] }`.
|
|
46
|
+
*
|
|
47
|
+
* Merge rules: objects merge recursively; arrays whose elements carry a `binding` key are keyed by
|
|
48
|
+
* it (same-binding entries merge, new ones append — user-added keys on an entry survive); other
|
|
49
|
+
* values only land when the user has not set them (user keys are preserved). Unparseable
|
|
50
|
+
* (comment-bearing) JSONC is returned unchanged with `patched: false` rather than risking data
|
|
51
|
+
* loss.
|
|
52
|
+
*
|
|
53
|
+
* @param {string | null} existing - Current wrangler.jsonc content, or null when absent
|
|
54
|
+
* @param {Record<string, unknown>[]} fragments - Binding fragments, applied in order
|
|
55
|
+
* @returns {{ content: string; patched: boolean }}
|
|
56
|
+
*/
|
|
57
|
+
export function applyBindingFragments(
|
|
58
|
+
existing: string | null,
|
|
59
|
+
fragments: Record<string, unknown>[],
|
|
60
|
+
): { content: string; patched: boolean } {
|
|
61
|
+
if (fragments.length === 0) {
|
|
62
|
+
return { content: existing ?? "", patched: false };
|
|
63
|
+
}
|
|
64
|
+
let config: Record<string, unknown> = {};
|
|
65
|
+
if (existing) {
|
|
66
|
+
try {
|
|
67
|
+
config = JSON.parse(existing) as Record<string, unknown>;
|
|
68
|
+
} catch {
|
|
69
|
+
// Comment-bearing JSONC — leave the user's file alone.
|
|
70
|
+
return { content: existing, patched: false };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const fragment of fragments) {
|
|
74
|
+
mergeBindingValue(config, fragment);
|
|
75
|
+
}
|
|
76
|
+
return { content: `${JSON.stringify(config, null, "\t")}\n`, patched: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Recursively merge one fragment object into the target, per the applyBindingFragments rules. */
|
|
80
|
+
function mergeBindingValue(target: Record<string, unknown>, fragment: Record<string, unknown>) {
|
|
81
|
+
for (const [key, incoming] of Object.entries(fragment)) {
|
|
82
|
+
const current = target[key];
|
|
83
|
+
if (Array.isArray(incoming)) {
|
|
84
|
+
target[key] = mergeBindingArrays(Array.isArray(current) ? current : [], incoming);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isPlainObject(incoming)) {
|
|
88
|
+
if (isPlainObject(current)) {
|
|
89
|
+
mergeBindingValue(current, incoming);
|
|
90
|
+
} else if (current === undefined) {
|
|
91
|
+
target[key] = structuredClone(incoming);
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (current === undefined) {
|
|
96
|
+
target[key] = incoming;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Merge fragment array entries into an existing array, keyed by each entry's `binding`. */
|
|
102
|
+
function mergeBindingArrays(current: unknown[], incoming: unknown[]): unknown[] {
|
|
103
|
+
const merged = [...current];
|
|
104
|
+
for (const entry of incoming) {
|
|
105
|
+
if (!isPlainObject(entry) || typeof entry.binding !== "string") {
|
|
106
|
+
// Non-binding-keyed values: append when missing (dedupe by deep equality).
|
|
107
|
+
const exists = merged.some((m) => JSON.stringify(m) === JSON.stringify(entry));
|
|
108
|
+
if (!exists) {
|
|
109
|
+
merged.push(structuredClone(entry));
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const index = merged.findIndex(
|
|
114
|
+
(m) => isPlainObject(m) && (m as Record<string, unknown>).binding === entry.binding,
|
|
115
|
+
);
|
|
116
|
+
if (index === -1) {
|
|
117
|
+
merged.push(structuredClone(entry));
|
|
118
|
+
} else {
|
|
119
|
+
// Fragment values win for connector-owned keys; user-added extra keys survive.
|
|
120
|
+
merged[index] = { ...(merged[index] as Record<string, unknown>), ...entry };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
127
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Patch an existing wrangler.jsonc for an adapter change (or a Pages project rename), touching only
|
|
132
|
+
* the adapter-shape keys and preserving everything else the user added. Regenerates from scratch
|
|
133
|
+
* when the input is missing or unparseable (wrangler.jsonc supports comments, which JSON.parse does
|
|
134
|
+
* not — a commented config falls back to regeneration rather than data loss... it is returned
|
|
135
|
+
* unchanged with `patched: false` so callers can warn).
|
|
136
|
+
*/
|
|
137
|
+
export function updateWranglerConfig(
|
|
138
|
+
existing: string | null,
|
|
139
|
+
{ slug, adapter }: { slug: string; adapter: string },
|
|
140
|
+
): { content: string; patched: boolean } {
|
|
141
|
+
if (!existing) {
|
|
142
|
+
return { content: buildWranglerJsonc({ adapter, slug }), patched: true };
|
|
143
|
+
}
|
|
144
|
+
let config: Record<string, unknown>;
|
|
145
|
+
try {
|
|
146
|
+
config = JSON.parse(existing) as Record<string, unknown>;
|
|
147
|
+
} catch {
|
|
148
|
+
// Comment-bearing JSONC — leave the user's file alone.
|
|
149
|
+
return { content: existing, patched: false };
|
|
150
|
+
}
|
|
151
|
+
config["name"] = slug;
|
|
152
|
+
if (adapter === "cloudflare-workers") {
|
|
153
|
+
delete config["pages_build_output_dir"];
|
|
154
|
+
config["main"] = "./dist/worker.js";
|
|
155
|
+
config["assets"] = { binding: "ASSETS", directory: "./dist" };
|
|
156
|
+
} else {
|
|
157
|
+
delete config["main"];
|
|
158
|
+
delete config["assets"];
|
|
159
|
+
config["pages_build_output_dir"] = "./dist";
|
|
160
|
+
}
|
|
161
|
+
return { content: `${JSON.stringify(config, null, "\t")}\n`, patched: true };
|
|
162
|
+
}
|