@jxsuite/create 0.36.0 → 1.1.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 -3
- package/package.json +3 -2
- package/scaffold.ts +162 -0
- package/template/gitignore +2 -0
package/generate.ts
CHANGED
|
@@ -341,8 +341,7 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
|
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
const build: { outDir: string;
|
|
345
|
-
format: "directory",
|
|
344
|
+
const build: { outDir: string; trailingSlash: string; adapter?: string } = {
|
|
346
345
|
outDir: "./dist",
|
|
347
346
|
trailingSlash: "always",
|
|
348
347
|
};
|
|
@@ -352,13 +351,19 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
|
|
|
352
351
|
}
|
|
353
352
|
|
|
354
353
|
return {
|
|
354
|
+
// The generated per-project schema binding; written by `jx schema` (specs/extensions.md §5.2).
|
|
355
|
+
$schema: "./project.schema.json",
|
|
355
356
|
$head,
|
|
356
357
|
$media: mediaForTemplate(template),
|
|
357
358
|
build,
|
|
359
|
+
// The scaffolded pages/index.md needs the parser extension's Markdown format; `content` is the
|
|
360
|
+
// Parser-owned section ready for the first collection.
|
|
361
|
+
content: {},
|
|
358
362
|
defaults: {
|
|
359
363
|
lang: "en",
|
|
360
364
|
layout: "./layouts/base.json",
|
|
361
365
|
},
|
|
366
|
+
extensions: ["@jxsuite/parser"],
|
|
362
367
|
name,
|
|
363
368
|
style: {
|
|
364
369
|
color: "#1a1a1a",
|
|
@@ -386,15 +391,22 @@ function buildPackageJson({
|
|
|
386
391
|
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
387
392
|
.replaceAll(/^-|-$/g, "");
|
|
388
393
|
|
|
389
|
-
|
|
394
|
+
// Projects own their extension dependencies (specs/extensions.md §3) — the parser extension is
|
|
395
|
+
// A runtime dependency, resolved project-first by every host.
|
|
396
|
+
const dependencies: Record<string, string> = {
|
|
397
|
+
"@jxsuite/parser": "^0.35.1",
|
|
398
|
+
};
|
|
390
399
|
const devDependencies: Record<string, string> = {
|
|
391
400
|
"@jxsuite/compiler": "^0.19.0",
|
|
392
401
|
"@jxsuite/runtime": "^0.19.0",
|
|
402
|
+
// The `dev` script spawns @jxsuite/server's dev entry (see jx dev in the compiler CLI)
|
|
403
|
+
"@jxsuite/server": "^1.0.0",
|
|
393
404
|
};
|
|
394
405
|
|
|
395
406
|
const scripts: Record<string, string> = {
|
|
396
407
|
build: "jx build",
|
|
397
408
|
dev: "jx dev",
|
|
409
|
+
preview: "jx preview",
|
|
398
410
|
};
|
|
399
411
|
|
|
400
412
|
if (adapter && adapter !== "static") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/create",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.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
|
],
|
|
@@ -34,6 +35,6 @@
|
|
|
34
35
|
"test:coverage": "bun test --isolate --coverage"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@jxsuite/starters": "^
|
|
38
|
+
"@jxsuite/starters": "^1.1.0"
|
|
38
39
|
}
|
|
39
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
|
+
}
|