@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 CHANGED
@@ -341,8 +341,7 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
341
341
  });
342
342
  }
343
343
 
344
- const build: { outDir: string; format: string; trailingSlash: string; adapter?: 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
- const dependencies: Record<string, string> = {};
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": "0.36.0",
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": "^0.35.1"
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
+ }
@@ -1,3 +1,5 @@
1
1
  .cache
2
2
  dist
3
3
  node_modules
4
+ .dev.vars
5
+ .jx