@jxsuite/create 0.36.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 +11 -1
- package/package.json +3 -2
- package/scaffold.ts +162 -0
- package/template/gitignore +2 -0
package/generate.ts
CHANGED
|
@@ -352,13 +352,19 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
return {
|
|
355
|
+
// The generated per-project schema binding; written by `jx schema` (specs/extensions.md §5.2).
|
|
356
|
+
$schema: "./project.schema.json",
|
|
355
357
|
$head,
|
|
356
358
|
$media: mediaForTemplate(template),
|
|
357
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: {},
|
|
358
363
|
defaults: {
|
|
359
364
|
lang: "en",
|
|
360
365
|
layout: "./layouts/base.json",
|
|
361
366
|
},
|
|
367
|
+
extensions: ["@jxsuite/parser"],
|
|
362
368
|
name,
|
|
363
369
|
style: {
|
|
364
370
|
color: "#1a1a1a",
|
|
@@ -386,7 +392,11 @@ function buildPackageJson({
|
|
|
386
392
|
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
387
393
|
.replaceAll(/^-|-$/g, "");
|
|
388
394
|
|
|
389
|
-
|
|
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
|
+
};
|
|
390
400
|
const devDependencies: Record<string, string> = {
|
|
391
401
|
"@jxsuite/compiler": "^0.19.0",
|
|
392
402
|
"@jxsuite/runtime": "^0.19.0",
|
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
|
],
|
|
@@ -34,6 +35,6 @@
|
|
|
34
35
|
"test:coverage": "bun test --isolate --coverage"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@jxsuite/starters": "^0.
|
|
38
|
+
"@jxsuite/starters": "^1.0.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
|
+
}
|