@jxsuite/create 1.3.14 → 1.3.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/cli-args.ts +13 -9
- package/generate.ts +57 -28
- package/index.ts +8 -8
- package/package.json +2 -2
- package/scaffold.ts +78 -32
- package/template-versions.json +4 -4
package/cli-args.ts
CHANGED
|
@@ -17,20 +17,24 @@ export interface CliArgs {
|
|
|
17
17
|
export function parseCliArgs(argv: string[]): CliArgs {
|
|
18
18
|
const positional: string[] = [];
|
|
19
19
|
let template: string | undefined;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// One iterator drives the loop, so `--template` can consume its value token with next().
|
|
21
|
+
const tokens = argv.values();
|
|
22
|
+
for (const arg of tokens) {
|
|
23
23
|
if (arg === "--template") {
|
|
24
|
-
template =
|
|
25
|
-
i += 1;
|
|
24
|
+
template = tokens.next().value;
|
|
26
25
|
} else if (arg.startsWith("--template=")) {
|
|
27
26
|
template = arg.slice("--template=".length);
|
|
28
27
|
} else {
|
|
29
28
|
positional.push(arg);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const args: CliArgs = {};
|
|
32
|
+
const [dest] = positional;
|
|
33
|
+
if (dest !== undefined) {
|
|
34
|
+
args.dest = dest;
|
|
35
|
+
}
|
|
36
|
+
if (template !== undefined) {
|
|
37
|
+
args.template = template;
|
|
38
|
+
}
|
|
39
|
+
return args;
|
|
36
40
|
}
|
package/generate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Shared project generation logic. Used by both the CLI scaffolder and the Studio server endpoint. */
|
|
2
2
|
|
|
3
3
|
import { chmod, cp, lstat, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { adapterNeedsWrangler, buildWranglerJsonc } from "./scaffold";
|
|
4
|
+
import { adapterNeedsWrangler, buildWranglerJsonc, isJsonObject, parseJson } from "./scaffold";
|
|
5
5
|
import { basename, join, relative, sep } from "node:path";
|
|
6
6
|
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
@@ -9,6 +9,7 @@ import { Buffer } from "node:buffer";
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { mediaForTemplate } from "./templates";
|
|
11
11
|
import type { TemplateId } from "./templates";
|
|
12
|
+
import type { JsonObject } from "./scaffold";
|
|
12
13
|
|
|
13
14
|
import TEMPLATE_VERSIONS from "./template-versions.json";
|
|
14
15
|
|
|
@@ -183,13 +184,7 @@ async function populateProject(destPath: string, opts: ProjectOptions) {
|
|
|
183
184
|
} = opts;
|
|
184
185
|
|
|
185
186
|
if (starter && starter !== "blank") {
|
|
186
|
-
await scaffoldFromStarter(destPath, starter, {
|
|
187
|
-
adapter,
|
|
188
|
-
description,
|
|
189
|
-
name,
|
|
190
|
-
url,
|
|
191
|
-
...(design !== undefined ? { design } : {}),
|
|
192
|
-
});
|
|
187
|
+
await scaffoldFromStarter(destPath, starter, { adapter, description, design, name, url });
|
|
193
188
|
return;
|
|
194
189
|
}
|
|
195
190
|
|
|
@@ -199,7 +194,7 @@ async function populateProject(destPath: string, opts: ProjectOptions) {
|
|
|
199
194
|
|
|
200
195
|
const projectJson = buildProjectJson({ adapter, description, name, template, url });
|
|
201
196
|
if (design) {
|
|
202
|
-
applyDesign(projectJson
|
|
197
|
+
applyDesign(projectJson, design, { starter: false });
|
|
203
198
|
}
|
|
204
199
|
const packageJson = buildPackageJson({ adapter, description, name });
|
|
205
200
|
|
|
@@ -256,7 +251,7 @@ async function scaffoldFromStarter(
|
|
|
256
251
|
description?: string;
|
|
257
252
|
url?: string;
|
|
258
253
|
adapter?: ProjectOptions["adapter"];
|
|
259
|
-
design?: DesignOptions;
|
|
254
|
+
design?: DesignOptions | undefined;
|
|
260
255
|
},
|
|
261
256
|
) {
|
|
262
257
|
const { name, description = "", url = "", adapter = "static", design } = opts;
|
|
@@ -279,14 +274,17 @@ async function scaffoldFromStarter(
|
|
|
279
274
|
// Re-stamp project.json with the new project's identity, keeping the starter's design tokens,
|
|
280
275
|
// Content types, image pipeline, and head.
|
|
281
276
|
const projectPath = join(destPath, "project.json");
|
|
282
|
-
const project =
|
|
277
|
+
const project = parseJson(await readFile(projectPath, "utf8"));
|
|
278
|
+
if (!isJsonObject(project)) {
|
|
279
|
+
throw new TypeError(`Starter "${starter}" has a project.json that is not a JSON object`);
|
|
280
|
+
}
|
|
283
281
|
project.name = name;
|
|
284
282
|
if (url) {
|
|
285
283
|
project.url = url;
|
|
286
284
|
}
|
|
287
285
|
applyDescription(project, description);
|
|
288
|
-
if (adapter
|
|
289
|
-
const build = (project.build
|
|
286
|
+
if (adapter !== "static") {
|
|
287
|
+
const build = isJsonObject(project.build) ? project.build : {};
|
|
290
288
|
build.adapter = adapter;
|
|
291
289
|
project.build = build;
|
|
292
290
|
}
|
|
@@ -320,19 +318,19 @@ async function scaffoldFromStarter(
|
|
|
320
318
|
* (falling back to the plain CSS property where noted), and is otherwise skipped silently rather
|
|
321
319
|
* than fighting the starter's layered styles. `--color-primary-hover` is intentionally left alone.
|
|
322
320
|
*
|
|
323
|
-
* @param {
|
|
321
|
+
* @param {JsonObject} project — parsed project.json, mutated in place
|
|
324
322
|
* @param {DesignOptions} design
|
|
325
323
|
* @param {{ starter: boolean }} mode — starter clone vs. blank/template scaffold
|
|
326
324
|
*/
|
|
327
325
|
function applyDesign(
|
|
328
|
-
project:
|
|
326
|
+
project: JsonObject,
|
|
329
327
|
design: DesignOptions,
|
|
330
328
|
{ starter }: { starter: boolean },
|
|
331
329
|
) {
|
|
332
330
|
const { accent, background, text, bodyFont, headingFont, media } = design;
|
|
333
|
-
const style = project
|
|
331
|
+
const { style } = project;
|
|
334
332
|
|
|
335
|
-
if (style) {
|
|
333
|
+
if (isJsonObject(style)) {
|
|
336
334
|
if (accent && (!starter || "--color-primary" in style)) {
|
|
337
335
|
style["--color-primary"] = accent;
|
|
338
336
|
}
|
|
@@ -394,25 +392,56 @@ async function writeLogo(destPath: string, logo: { name: string; base64: string
|
|
|
394
392
|
* Set the `<meta name="description">` content in a project's `$head` (or leave it untouched when
|
|
395
393
|
* the starter has no such tag or no description was supplied).
|
|
396
394
|
*
|
|
397
|
-
* @param {
|
|
395
|
+
* @param {JsonObject} project
|
|
398
396
|
* @param {string} description
|
|
399
397
|
*/
|
|
400
|
-
function applyDescription(project:
|
|
398
|
+
function applyDescription(project: JsonObject, description: string) {
|
|
401
399
|
if (!description || !Array.isArray(project.$head)) {
|
|
402
400
|
return;
|
|
403
401
|
}
|
|
404
|
-
for (const tag of project.$head
|
|
405
|
-
if (
|
|
402
|
+
for (const tag of project.$head) {
|
|
403
|
+
if (
|
|
404
|
+
isJsonObject(tag) &&
|
|
405
|
+
tag.tagName === "meta" &&
|
|
406
|
+
isJsonObject(tag.attributes) &&
|
|
407
|
+
tag.attributes.name === "description"
|
|
408
|
+
) {
|
|
406
409
|
tag.attributes.content = description;
|
|
407
410
|
}
|
|
408
411
|
}
|
|
409
412
|
}
|
|
410
413
|
|
|
411
414
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
+
* The scaffolded project.json `build` section. Non-static adapters add `adapter` through the
|
|
416
|
+
* JsonObject index signature; an optional `adapter?: string` member would conflict with that index
|
|
417
|
+
* under configs without `exactOptionalPropertyTypes` (packages/desktop's typecheck).
|
|
415
418
|
*/
|
|
419
|
+
interface BuildSection extends JsonObject {
|
|
420
|
+
outDir: string;
|
|
421
|
+
trailingSlash: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Scaffolded package.json `dependencies`; `hono` joins for adapters with a server worker. */
|
|
425
|
+
interface Dependencies {
|
|
426
|
+
"@jxsuite/parser": string;
|
|
427
|
+
hono?: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Scaffolded package.json `devDependencies`; `wrangler` joins for Cloudflare adapters. */
|
|
431
|
+
interface DevDependencies {
|
|
432
|
+
"@jxsuite/compiler": string;
|
|
433
|
+
"@jxsuite/runtime": string;
|
|
434
|
+
"@jxsuite/server": string;
|
|
435
|
+
wrangler?: string;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Scaffolded package.json `scripts`; `deploy` joins for Cloudflare adapters. */
|
|
439
|
+
interface PackageScripts {
|
|
440
|
+
build: string;
|
|
441
|
+
dev: string;
|
|
442
|
+
preview: string;
|
|
443
|
+
deploy?: string;
|
|
444
|
+
}
|
|
416
445
|
|
|
417
446
|
/** @param {ProjectOptions} opts */
|
|
418
447
|
function buildProjectJson({ name, description, url, adapter, template = "blank" }: ProjectOptions) {
|
|
@@ -444,7 +473,7 @@ function buildProjectJson({ name, description, url, adapter, template = "blank"
|
|
|
444
473
|
});
|
|
445
474
|
}
|
|
446
475
|
|
|
447
|
-
const build:
|
|
476
|
+
const build: BuildSection = {
|
|
448
477
|
outDir: "./dist",
|
|
449
478
|
trailingSlash: "always",
|
|
450
479
|
};
|
|
@@ -502,17 +531,17 @@ function buildPackageJson({
|
|
|
502
531
|
// (release-please-config.json extra-files). Hardcoding them here is what left every scaffolded
|
|
503
532
|
// Project asking for a compiler version that was never published. Never hand-edit the
|
|
504
533
|
// Map; `bun run templates:sync` is the fixer.
|
|
505
|
-
const dependencies:
|
|
534
|
+
const dependencies: Dependencies = {
|
|
506
535
|
"@jxsuite/parser": TEMPLATE_VERSIONS.parser,
|
|
507
536
|
};
|
|
508
|
-
const devDependencies:
|
|
537
|
+
const devDependencies: DevDependencies = {
|
|
509
538
|
"@jxsuite/compiler": TEMPLATE_VERSIONS.compiler,
|
|
510
539
|
"@jxsuite/runtime": TEMPLATE_VERSIONS.runtime,
|
|
511
540
|
// The `dev` script spawns @jxsuite/server's dev entry (see jx dev in the compiler CLI)
|
|
512
541
|
"@jxsuite/server": TEMPLATE_VERSIONS.server,
|
|
513
542
|
};
|
|
514
543
|
|
|
515
|
-
const scripts:
|
|
544
|
+
const scripts: PackageScripts = {
|
|
516
545
|
build: "jx build",
|
|
517
546
|
dev: "jx dev",
|
|
518
547
|
preview: "jx preview",
|
package/index.ts
CHANGED
|
@@ -77,14 +77,14 @@ Deployment adapter:
|
|
|
77
77
|
const adapterChoice = await rl.question("Adapter [1]: ");
|
|
78
78
|
rl.close();
|
|
79
79
|
|
|
80
|
-
const adapterMap
|
|
81
|
-
1
|
|
82
|
-
2
|
|
83
|
-
3
|
|
84
|
-
4
|
|
85
|
-
5
|
|
86
|
-
|
|
87
|
-
const adapter = adapterMap
|
|
80
|
+
const adapterMap = new Map<string, NonNullable<ProjectOptions["adapter"]>>([
|
|
81
|
+
["1", "static"],
|
|
82
|
+
["2", "cloudflare-pages"],
|
|
83
|
+
["3", "node"],
|
|
84
|
+
["4", "bun"],
|
|
85
|
+
["5", "cloudflare-workers"],
|
|
86
|
+
]);
|
|
87
|
+
const adapter = adapterMap.get(adapterChoice) ?? "static";
|
|
88
88
|
|
|
89
89
|
await generateProject(destPath, { adapter, description, name, starter, template, url });
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/create",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.16",
|
|
4
4
|
"description": "Scaffold a new Jx project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test:coverage": "bun test --isolate --coverage"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@jxsuite/starters": "^
|
|
40
|
+
"@jxsuite/starters": "^3.0.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/scaffold.ts
CHANGED
|
@@ -6,6 +6,55 @@
|
|
|
6
6
|
|
|
7
7
|
const CF_ADAPTERS = new Set(["cloudflare-pages", "cloudflare-workers"]);
|
|
8
8
|
|
|
9
|
+
/** A JSON primitive. */
|
|
10
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
11
|
+
/** Any value `JSON.parse` can produce, and `JSON.stringify` can write back unchanged. */
|
|
12
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | JsonObject;
|
|
13
|
+
/** A JSON object: wrangler.jsonc, project.json and connector binding fragments are all one. */
|
|
14
|
+
export interface JsonObject {
|
|
15
|
+
[key: string]: JsonValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The result of patching a wrangler.jsonc: the new text, and whether it was rewritten at all. */
|
|
19
|
+
export interface WranglerPatch {
|
|
20
|
+
content: string;
|
|
21
|
+
patched: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A fragment array entry keyed by its `binding` name (`{ binding: "DB", database_id }`). */
|
|
25
|
+
interface BindingEntry extends JsonObject {
|
|
26
|
+
binding: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parse JSON text. Throws the `SyntaxError` `JSON.parse` raises for text that is not JSON. */
|
|
30
|
+
export function parseJson(text: string): JsonValue {
|
|
31
|
+
// SAFETY: JSON.parse without a reviver can only build objects, arrays, strings, finite numbers,
|
|
32
|
+
// Booleans and null out of JSON text, which is exactly the JsonValue union.
|
|
33
|
+
return JSON.parse(text) as JsonValue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True for a JSON object; false for arrays, primitives, null and a missing value. */
|
|
37
|
+
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isBindingEntry(value: JsonValue): value is BindingEntry {
|
|
42
|
+
return isJsonObject(value) && typeof value.binding === "string";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a wrangler.jsonc that can be patched: undefined when it is comment-bearing JSONC (which
|
|
47
|
+
* `JSON.parse` rejects) or its top level is not an object, so callers leave the file alone.
|
|
48
|
+
*/
|
|
49
|
+
function parseWranglerObject(text: string): JsonObject | undefined {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = parseJson(text);
|
|
52
|
+
return isJsonObject(parsed) ? parsed : undefined;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
9
58
|
/** True when the adapter deploys through wrangler (needs a wrangler.jsonc). */
|
|
10
59
|
export function adapterNeedsWrangler(adapter: string | undefined): boolean {
|
|
11
60
|
return Boolean(adapter && CF_ADAPTERS.has(adapter));
|
|
@@ -51,24 +100,24 @@ export function buildWranglerJsonc({ slug, adapter }: { slug: string; adapter: s
|
|
|
51
100
|
* loss.
|
|
52
101
|
*
|
|
53
102
|
* @param {string | null} existing - Current wrangler.jsonc content, or null when absent
|
|
54
|
-
* @param {
|
|
55
|
-
* @returns {
|
|
103
|
+
* @param {JsonObject[]} fragments - Binding fragments, applied in order
|
|
104
|
+
* @returns {WranglerPatch}
|
|
56
105
|
*/
|
|
57
106
|
export function applyBindingFragments(
|
|
58
107
|
existing: string | null,
|
|
59
|
-
fragments:
|
|
60
|
-
):
|
|
108
|
+
fragments: JsonObject[],
|
|
109
|
+
): WranglerPatch {
|
|
61
110
|
if (fragments.length === 0) {
|
|
62
111
|
return { content: existing ?? "", patched: false };
|
|
63
112
|
}
|
|
64
|
-
let config:
|
|
113
|
+
let config: JsonObject = {};
|
|
65
114
|
if (existing) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Comment-bearing JSONC — leave the user's file alone.
|
|
115
|
+
const parsed = parseWranglerObject(existing);
|
|
116
|
+
if (parsed === undefined) {
|
|
117
|
+
// Comment-bearing JSONC (or a non-object top level) — leave the user's file alone.
|
|
70
118
|
return { content: existing, patched: false };
|
|
71
119
|
}
|
|
120
|
+
config = parsed;
|
|
72
121
|
}
|
|
73
122
|
for (const fragment of fragments) {
|
|
74
123
|
mergeBindingValue(config, fragment);
|
|
@@ -77,15 +126,18 @@ export function applyBindingFragments(
|
|
|
77
126
|
}
|
|
78
127
|
|
|
79
128
|
/** Recursively merge one fragment object into the target, per the applyBindingFragments rules. */
|
|
80
|
-
function mergeBindingValue(target:
|
|
129
|
+
function mergeBindingValue(target: JsonObject, fragment: JsonObject) {
|
|
81
130
|
for (const [key, incoming] of Object.entries(fragment)) {
|
|
82
|
-
|
|
131
|
+
/* A key the target lacks reads as undefined. Spelled out rather than left to
|
|
132
|
+
noUncheckedIndexedAccess, which the inferred program this file is linted in does not set
|
|
133
|
+
(packages/create/tsconfig.json does not include it). */
|
|
134
|
+
const current = key in target ? target[key] : undefined;
|
|
83
135
|
if (Array.isArray(incoming)) {
|
|
84
136
|
target[key] = mergeBindingArrays(Array.isArray(current) ? current : [], incoming);
|
|
85
137
|
continue;
|
|
86
138
|
}
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
139
|
+
if (isJsonObject(incoming)) {
|
|
140
|
+
if (isJsonObject(current)) {
|
|
89
141
|
mergeBindingValue(current, incoming);
|
|
90
142
|
} else if (current === undefined) {
|
|
91
143
|
target[key] = structuredClone(incoming);
|
|
@@ -99,10 +151,10 @@ function mergeBindingValue(target: Record<string, unknown>, fragment: Record<str
|
|
|
99
151
|
}
|
|
100
152
|
|
|
101
153
|
/** Merge fragment array entries into an existing array, keyed by each entry's `binding`. */
|
|
102
|
-
function mergeBindingArrays(current:
|
|
154
|
+
function mergeBindingArrays(current: JsonValue[], incoming: JsonValue[]): JsonValue[] {
|
|
103
155
|
const merged = [...current];
|
|
104
156
|
for (const entry of incoming) {
|
|
105
|
-
if (!
|
|
157
|
+
if (!isBindingEntry(entry)) {
|
|
106
158
|
// Non-binding-keyed values: append when missing (dedupe by deep equality).
|
|
107
159
|
const exists = merged.some((m) => JSON.stringify(m) === JSON.stringify(entry));
|
|
108
160
|
if (!exists) {
|
|
@@ -110,23 +162,19 @@ function mergeBindingArrays(current: unknown[], incoming: unknown[]): unknown[]
|
|
|
110
162
|
}
|
|
111
163
|
continue;
|
|
112
164
|
}
|
|
113
|
-
const index = merged.findIndex(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
merged.push(structuredClone(entry));
|
|
118
|
-
} else {
|
|
165
|
+
const index = merged.findIndex((m) => isJsonObject(m) && m.binding === entry.binding);
|
|
166
|
+
// Undefined when no entry matched (index -1), so a miss appends.
|
|
167
|
+
const match = merged[index];
|
|
168
|
+
if (isJsonObject(match)) {
|
|
119
169
|
// Fragment values win for connector-owned keys; user-added extra keys survive.
|
|
120
|
-
merged[index] = { ...
|
|
170
|
+
merged[index] = { ...match, ...entry };
|
|
171
|
+
} else {
|
|
172
|
+
merged.push(structuredClone(entry));
|
|
121
173
|
}
|
|
122
174
|
}
|
|
123
175
|
return merged;
|
|
124
176
|
}
|
|
125
177
|
|
|
126
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
127
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
178
|
/**
|
|
131
179
|
* Patch an existing wrangler.jsonc for an adapter change (or a Pages project rename), touching only
|
|
132
180
|
* the adapter-shape keys and preserving everything else the user added. Regenerates from scratch
|
|
@@ -137,15 +185,13 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
137
185
|
export function updateWranglerConfig(
|
|
138
186
|
existing: string | null,
|
|
139
187
|
{ slug, adapter }: { slug: string; adapter: string },
|
|
140
|
-
):
|
|
188
|
+
): WranglerPatch {
|
|
141
189
|
if (!existing) {
|
|
142
190
|
return { content: buildWranglerJsonc({ adapter, slug }), patched: true };
|
|
143
191
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch {
|
|
148
|
-
// Comment-bearing JSONC — leave the user's file alone.
|
|
192
|
+
const config = parseWranglerObject(existing);
|
|
193
|
+
if (config === undefined) {
|
|
194
|
+
// Comment-bearing JSONC (or a non-object top level) — leave the user's file alone.
|
|
149
195
|
return { content: existing, patched: false };
|
|
150
196
|
}
|
|
151
197
|
config["name"] = slug;
|
package/template-versions.json
CHANGED