create-objectstack 17.0.0-rc.3 → 17.0.0-rc.5
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/CHANGELOG.md +28 -1
- package/dist/index.js +120 -72
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# create-objectstack
|
|
2
2
|
|
|
3
|
-
## 17.0.0-rc.
|
|
3
|
+
## 17.0.0-rc.5
|
|
4
|
+
|
|
5
|
+
## 17.0.0-rc.4
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 8d41998: fix(create-objectstack): scaffolding a remote template no longer produces a project that cannot build (#4926)
|
|
10
|
+
|
|
11
|
+
`npx create-objectstack@latest my-app -t todo` (and `compliance`, `content`,
|
|
12
|
+
`contracts`, `procurement`) generated a project that failed `objectstack build`
|
|
13
|
+
immediately — 5 of the 6 offered templates. Only the bundled `blank` worked.
|
|
14
|
+
|
|
15
|
+
The scaffolder read the template's original namespace from
|
|
16
|
+
`objectstack.manifest.json`, and that filename names two different documents.
|
|
17
|
+
The bundled template's is app-shaped and carries `namespace`; a remote
|
|
18
|
+
template's is the template-registry document
|
|
19
|
+
(`$schema: …/template-manifest.json`) and carries none — its namespace lives
|
|
20
|
+
only in `objectstack.config.ts`. So the value came back `undefined` for every
|
|
21
|
+
remote template and the object-name rewrite was skipped, while the config's
|
|
22
|
+
`namespace:` was rewritten anyway. The result was `namespace: 'my_app'` sitting
|
|
23
|
+
next to `name: 'todo_task'`, which the `${namespace}_${shortName}` rule rejects.
|
|
24
|
+
Across the five templates, 74 object names were left unrewritten.
|
|
25
|
+
|
|
26
|
+
`objectstack.config.ts` is now the authority for the template namespace (it
|
|
27
|
+
holds the very literal the scaffolder overwrites, so the two cannot disagree),
|
|
28
|
+
with the manifest as fallback. The rewrite also verifies itself: any surviving
|
|
29
|
+
stale prefix throws at the scaffold, naming the files and lines, instead of
|
|
30
|
+
surfacing as a build failure on the user's first command.
|
|
4
31
|
|
|
5
32
|
## 17.0.0-rc.2
|
|
6
33
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs3 from "fs";
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
@@ -47,10 +47,77 @@ function copyDir(src, dest, collected, rel = "") {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// src/rewrite-identity.ts
|
|
51
|
+
import fs2 from "fs";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
var CONFIG_NAMESPACE_RE = /\bnamespace:\s*(['"`])([a-z0-9_]+)\1/i;
|
|
54
|
+
function readTemplateNamespace(targetDir) {
|
|
55
|
+
const configPath = path2.join(targetDir, "objectstack.config.ts");
|
|
56
|
+
if (fs2.existsSync(configPath)) {
|
|
57
|
+
const m = CONFIG_NAMESPACE_RE.exec(fs2.readFileSync(configPath, "utf8"));
|
|
58
|
+
if (m) return m[2];
|
|
59
|
+
}
|
|
60
|
+
const manifestPath = path2.join(targetDir, "objectstack.manifest.json");
|
|
61
|
+
if (fs2.existsSync(manifestPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const m = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
|
|
64
|
+
if (typeof m.namespace === "string" && m.namespace) return m.namespace;
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
function tsFiles(dir, out = []) {
|
|
71
|
+
if (!fs2.existsSync(dir)) return out;
|
|
72
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
73
|
+
const full = path2.join(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (entry.name === "node_modules") continue;
|
|
76
|
+
tsFiles(full, out);
|
|
77
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
78
|
+
out.push(full);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
var namePrefixRe = (ns, flags) => new RegExp(`(\\bname:\\s*)(['"\`])${ns}_([a-z0-9_]+)\\2`, flags);
|
|
84
|
+
function rewriteObjectNamePrefix(dir, from, to) {
|
|
85
|
+
let rewritten = 0;
|
|
86
|
+
for (const file of tsFiles(dir)) {
|
|
87
|
+
const before = fs2.readFileSync(file, "utf8");
|
|
88
|
+
const after = before.replace(
|
|
89
|
+
namePrefixRe(from, "g"),
|
|
90
|
+
(_m, prefix, q, rest) => {
|
|
91
|
+
rewritten++;
|
|
92
|
+
return `${prefix}${q}${to}_${rest}${q}`;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
if (after !== before) fs2.writeFileSync(file, after);
|
|
96
|
+
}
|
|
97
|
+
return rewritten;
|
|
98
|
+
}
|
|
99
|
+
function findStaleNamespacePrefixes(dir, oldNs) {
|
|
100
|
+
const re = namePrefixRe(oldNs, "");
|
|
101
|
+
const stale = [];
|
|
102
|
+
for (const file of tsFiles(dir)) {
|
|
103
|
+
const lines = fs2.readFileSync(file, "utf8").split("\n");
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
if (re.test(lines[i])) {
|
|
106
|
+
stale.push({
|
|
107
|
+
file: path2.relative(dir, file),
|
|
108
|
+
line: i + 1,
|
|
109
|
+
text: lines[i].trim()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return stale;
|
|
115
|
+
}
|
|
116
|
+
|
|
50
117
|
// src/index.ts
|
|
51
118
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
52
|
-
var __dirname2 =
|
|
53
|
-
var BUNDLED_TEMPLATES_DIR =
|
|
119
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
120
|
+
var BUNDLED_TEMPLATES_DIR = path3.resolve(__dirname2, "templates");
|
|
54
121
|
var REMOTE_REPO = "objectstack-ai/templates";
|
|
55
122
|
var REMOTE_BRANCH = "main";
|
|
56
123
|
var REMOTE_TARBALL_URL = `https://codeload.github.com/${REMOTE_REPO}/tar.gz/refs/heads/${REMOTE_BRANCH}`;
|
|
@@ -96,8 +163,8 @@ function sanitizeNamespace(name) {
|
|
|
96
163
|
}
|
|
97
164
|
function readCliVersion() {
|
|
98
165
|
try {
|
|
99
|
-
const pkgPath =
|
|
100
|
-
const pkg = JSON.parse(
|
|
166
|
+
const pkgPath = path3.resolve(__dirname2, "..", "package.json");
|
|
167
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
101
168
|
return String(pkg.version || "0.0.0");
|
|
102
169
|
} catch {
|
|
103
170
|
return "0.0.0";
|
|
@@ -132,8 +199,8 @@ function detectPackageManager() {
|
|
|
132
199
|
}
|
|
133
200
|
}
|
|
134
201
|
function loadBundled(templateDir, targetDir) {
|
|
135
|
-
const src =
|
|
136
|
-
if (!
|
|
202
|
+
const src = path3.join(BUNDLED_TEMPLATES_DIR, templateDir);
|
|
203
|
+
if (!fs3.existsSync(src)) {
|
|
137
204
|
throw new Error(`Bundled template missing on disk: ${src}`);
|
|
138
205
|
}
|
|
139
206
|
const collected = [];
|
|
@@ -154,12 +221,12 @@ async function downloadTarball(url, destFile) {
|
|
|
154
221
|
});
|
|
155
222
|
}
|
|
156
223
|
async function loadRemote(pkgName, targetDir) {
|
|
157
|
-
const tmp = await mkdtemp(
|
|
224
|
+
const tmp = await mkdtemp(path3.join(os.tmpdir(), "create-objectstack-"));
|
|
158
225
|
try {
|
|
159
|
-
const tarball =
|
|
226
|
+
const tarball = path3.join(tmp, "templates.tar.gz");
|
|
160
227
|
printStep(`Fetching template "${pkgName}" from ${REMOTE_REPO}@${REMOTE_BRANCH}\u2026`);
|
|
161
228
|
await downloadTarball(REMOTE_TARBALL_URL, tarball);
|
|
162
|
-
|
|
229
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
163
230
|
const collected = [];
|
|
164
231
|
await pipeline(
|
|
165
232
|
createReadStream(tarball),
|
|
@@ -190,98 +257,79 @@ async function loadRemote(pkgName, targetDir) {
|
|
|
190
257
|
await rm(tmp, { recursive: true, force: true });
|
|
191
258
|
}
|
|
192
259
|
}
|
|
193
|
-
function walkAndRewriteTs(dir, fn) {
|
|
194
|
-
if (!fs2.existsSync(dir)) return;
|
|
195
|
-
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
196
|
-
const full = path2.join(dir, entry.name);
|
|
197
|
-
if (entry.isDirectory()) {
|
|
198
|
-
walkAndRewriteTs(full, fn);
|
|
199
|
-
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
200
|
-
const before = fs2.readFileSync(full, "utf8");
|
|
201
|
-
const after = fn(before);
|
|
202
|
-
if (after !== before) fs2.writeFileSync(full, after);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
260
|
function rewriteProjectIdentity(targetDir, projectName, namespace) {
|
|
207
261
|
const title = toTitleCase(projectName);
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
try {
|
|
212
|
-
const m = JSON.parse(fs2.readFileSync(manifestPathPre, "utf8"));
|
|
213
|
-
if (typeof m.namespace === "string") templateNamespace = m.namespace;
|
|
214
|
-
} catch {
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const pkgPath = path2.join(targetDir, "package.json");
|
|
218
|
-
if (fs2.existsSync(pkgPath)) {
|
|
262
|
+
const templateNamespace = readTemplateNamespace(targetDir);
|
|
263
|
+
const pkgPath = path3.join(targetDir, "package.json");
|
|
264
|
+
if (fs3.existsSync(pkgPath)) {
|
|
219
265
|
try {
|
|
220
|
-
const pkg = JSON.parse(
|
|
266
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
|
|
221
267
|
pkg.name = projectName;
|
|
222
268
|
syncObjectStackDeps(pkg, readCliVersion());
|
|
223
|
-
|
|
269
|
+
fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
224
270
|
} catch {
|
|
225
271
|
}
|
|
226
272
|
}
|
|
227
|
-
const manifestPath =
|
|
228
|
-
if (
|
|
273
|
+
const manifestPath = path3.join(targetDir, "objectstack.manifest.json");
|
|
274
|
+
if (fs3.existsSync(manifestPath)) {
|
|
229
275
|
try {
|
|
230
|
-
const m = JSON.parse(
|
|
276
|
+
const m = JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
|
|
231
277
|
m.name = projectName;
|
|
232
278
|
m.displayName = title;
|
|
233
279
|
if ("namespace" in m) m.namespace = namespace;
|
|
234
|
-
|
|
280
|
+
fs3.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
235
281
|
} catch {
|
|
236
282
|
}
|
|
237
283
|
}
|
|
238
|
-
const configPath =
|
|
239
|
-
if (
|
|
240
|
-
let cfg =
|
|
284
|
+
const configPath = path3.join(targetDir, "objectstack.config.ts");
|
|
285
|
+
if (fs3.existsSync(configPath)) {
|
|
286
|
+
let cfg = fs3.readFileSync(configPath, "utf8");
|
|
241
287
|
cfg = cfg.replace(/(\bid:\s*)(['"`])[^'"`]*\2/, `$1$2${projectName}$2`);
|
|
242
288
|
cfg = cfg.replace(/(\bnamespace:\s*)(['"`])[^'"`]*\2/, `$1$2${namespace}$2`);
|
|
243
289
|
cfg = cfg.replace(/(\bname:\s*)(['"`])[^'"`]*\2/, `$1$2${title}$2`);
|
|
244
|
-
|
|
290
|
+
fs3.writeFileSync(configPath, cfg);
|
|
245
291
|
}
|
|
246
|
-
if (namespace !== templateNamespace
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
if (templateNamespace && namespace !== templateNamespace) {
|
|
293
|
+
const srcDir = path3.join(targetDir, "src");
|
|
294
|
+
rewriteObjectNamePrefix(srcDir, templateNamespace, namespace);
|
|
295
|
+
const stale = findStaleNamespacePrefixes(srcDir, templateNamespace);
|
|
296
|
+
if (stale.length > 0) {
|
|
297
|
+
const shown = stale.slice(0, 5).map((s) => ` src/${s.file}:${s.line} ${s.text}`).join("\n");
|
|
298
|
+
const more = stale.length > 5 ? `
|
|
299
|
+
\u2026and ${stale.length - 5} more` : "";
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Scaffolding rewrote the namespace to '${namespace}' but ${stale.length} object name(s) still carry the template's '${templateNamespace}_' prefix:
|
|
302
|
+
${shown}${more}
|
|
303
|
+
The generated project would fail 'objectstack build' on the \${namespace}_\${shortName} rule. This is a bug in the scaffolder, not in your input.`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
258
306
|
}
|
|
259
|
-
const readmePath =
|
|
260
|
-
if (
|
|
261
|
-
let md =
|
|
307
|
+
const readmePath = path3.join(targetDir, "README.md");
|
|
308
|
+
if (fs3.existsSync(readmePath)) {
|
|
309
|
+
let md = fs3.readFileSync(readmePath, "utf8");
|
|
262
310
|
md = md.replace(/^#\s+.*$/m, `# ${title}`);
|
|
263
|
-
|
|
311
|
+
fs3.writeFileSync(readmePath, md);
|
|
264
312
|
}
|
|
265
313
|
writeAgentGuides(targetDir, title, projectName);
|
|
266
314
|
}
|
|
267
315
|
function writeAgentGuides(targetDir, title, projectName) {
|
|
268
|
-
const templatePath =
|
|
316
|
+
const templatePath = path3.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
|
|
269
317
|
let template;
|
|
270
318
|
try {
|
|
271
|
-
template =
|
|
319
|
+
template = fs3.readFileSync(templatePath, "utf8");
|
|
272
320
|
} catch (err) {
|
|
273
321
|
if (err?.code === "ENOENT") return;
|
|
274
322
|
throw err;
|
|
275
323
|
}
|
|
276
324
|
const rendered = template.replace(/\{\{PROJECT_TITLE\}\}/g, title).replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
277
|
-
writeIfAbsent(
|
|
278
|
-
const copilotPath =
|
|
279
|
-
|
|
325
|
+
writeIfAbsent(path3.join(targetDir, "AGENTS.md"), rendered);
|
|
326
|
+
const copilotPath = path3.join(targetDir, ".github", "copilot-instructions.md");
|
|
327
|
+
fs3.mkdirSync(path3.dirname(copilotPath), { recursive: true });
|
|
280
328
|
writeIfAbsent(copilotPath, rendered);
|
|
281
329
|
}
|
|
282
330
|
function writeIfAbsent(filePath, contents) {
|
|
283
331
|
try {
|
|
284
|
-
|
|
332
|
+
fs3.writeFileSync(filePath, contents, { flag: "wx" });
|
|
285
333
|
} catch (err) {
|
|
286
334
|
if (err?.code !== "EEXIST") throw err;
|
|
287
335
|
}
|
|
@@ -303,24 +351,24 @@ var program = new Command().name("create-objectstack").description("Create a new
|
|
|
303
351
|
process.exit(1);
|
|
304
352
|
}
|
|
305
353
|
const cwd = process.cwd();
|
|
306
|
-
const projectName = name ||
|
|
354
|
+
const projectName = name || path3.basename(cwd);
|
|
307
355
|
const namespace = sanitizeNamespace(projectName);
|
|
308
|
-
const targetDir = name ?
|
|
356
|
+
const targetDir = name ? path3.resolve(cwd, name) : cwd;
|
|
309
357
|
const isCurrentDir = targetDir === cwd;
|
|
310
358
|
printKV("Environment", projectName);
|
|
311
359
|
printKV("Namespace", namespace);
|
|
312
360
|
printKV("Template", `${options.template} \u2014 ${template.description}`);
|
|
313
361
|
printKV("Directory", targetDir);
|
|
314
362
|
console.log("");
|
|
315
|
-
if (!isCurrentDir &&
|
|
316
|
-
const existing =
|
|
363
|
+
if (!isCurrentDir && fs3.existsSync(targetDir)) {
|
|
364
|
+
const existing = fs3.readdirSync(targetDir);
|
|
317
365
|
if (existing.length > 0) {
|
|
318
366
|
printError(`Directory already exists and is not empty: ${targetDir}`);
|
|
319
367
|
process.exit(1);
|
|
320
368
|
}
|
|
321
369
|
}
|
|
322
370
|
try {
|
|
323
|
-
|
|
371
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
324
372
|
let createdFiles;
|
|
325
373
|
if (template.source.kind === "bundled") {
|
|
326
374
|
createdFiles = loadBundled(template.source.dir, targetDir);
|