create-objectstack 17.0.0-rc.2 → 17.0.0-rc.4

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.js +120 -72
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # create-objectstack
2
2
 
3
+ ## 17.0.0-rc.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 8d41998: fix(create-objectstack): scaffolding a remote template no longer produces a project that cannot build (#4926)
8
+
9
+ `npx create-objectstack@latest my-app -t todo` (and `compliance`, `content`,
10
+ `contracts`, `procurement`) generated a project that failed `objectstack build`
11
+ immediately — 5 of the 6 offered templates. Only the bundled `blank` worked.
12
+
13
+ The scaffolder read the template's original namespace from
14
+ `objectstack.manifest.json`, and that filename names two different documents.
15
+ The bundled template's is app-shaped and carries `namespace`; a remote
16
+ template's is the template-registry document
17
+ (`$schema: …/template-manifest.json`) and carries none — its namespace lives
18
+ only in `objectstack.config.ts`. So the value came back `undefined` for every
19
+ remote template and the object-name rewrite was skipped, while the config's
20
+ `namespace:` was rewritten anyway. The result was `namespace: 'my_app'` sitting
21
+ next to `name: 'todo_task'`, which the `${namespace}_${shortName}` rule rejects.
22
+ Across the five templates, 74 object names were left unrewritten.
23
+
24
+ `objectstack.config.ts` is now the authority for the template namespace (it
25
+ holds the very literal the scaffolder overwrites, so the two cannot disagree),
26
+ with the manifest as fallback. The rewrite also verifies itself: any surviving
27
+ stale prefix throws at the scaffold, naming the files and lines, instead of
28
+ surfacing as a build failure on the user's first command.
29
+
3
30
  ## 17.0.0-rc.2
4
31
 
5
32
  ## 17.0.0-rc.1
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 fs2 from "fs";
5
- import path2 from "path";
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 = path2.dirname(__filename2);
53
- var BUNDLED_TEMPLATES_DIR = path2.resolve(__dirname2, "templates");
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 = path2.resolve(__dirname2, "..", "package.json");
100
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
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 = path2.join(BUNDLED_TEMPLATES_DIR, templateDir);
136
- if (!fs2.existsSync(src)) {
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(path2.join(os.tmpdir(), "create-objectstack-"));
224
+ const tmp = await mkdtemp(path3.join(os.tmpdir(), "create-objectstack-"));
158
225
  try {
159
- const tarball = path2.join(tmp, "templates.tar.gz");
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
- fs2.mkdirSync(targetDir, { recursive: true });
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
- let templateNamespace;
209
- const manifestPathPre = path2.join(targetDir, "objectstack.manifest.json");
210
- if (fs2.existsSync(manifestPathPre)) {
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(fs2.readFileSync(pkgPath, "utf8"));
266
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
221
267
  pkg.name = projectName;
222
268
  syncObjectStackDeps(pkg, readCliVersion());
223
- fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
269
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
224
270
  } catch {
225
271
  }
226
272
  }
227
- const manifestPath = path2.join(targetDir, "objectstack.manifest.json");
228
- if (fs2.existsSync(manifestPath)) {
273
+ const manifestPath = path3.join(targetDir, "objectstack.manifest.json");
274
+ if (fs3.existsSync(manifestPath)) {
229
275
  try {
230
- const m = JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
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
- fs2.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
280
+ fs3.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
235
281
  } catch {
236
282
  }
237
283
  }
238
- const configPath = path2.join(targetDir, "objectstack.config.ts");
239
- if (fs2.existsSync(configPath)) {
240
- let cfg = fs2.readFileSync(configPath, "utf8");
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
- fs2.writeFileSync(configPath, cfg);
290
+ fs3.writeFileSync(configPath, cfg);
245
291
  }
246
- if (namespace !== templateNamespace && templateNamespace) {
247
- const prefixRe = new RegExp(
248
- `(\\bname:\\s*)(['"\`])${templateNamespace}_([a-z0-9_]+)\\2`,
249
- "g"
250
- );
251
- walkAndRewriteTs(
252
- path2.join(targetDir, "src"),
253
- (src) => src.replace(
254
- prefixRe,
255
- (_m, prefix, q, rest) => `${prefix}${q}${namespace}_${rest}${q}`
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 = path2.join(targetDir, "README.md");
260
- if (fs2.existsSync(readmePath)) {
261
- let md = fs2.readFileSync(readmePath, "utf8");
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
- fs2.writeFileSync(readmePath, md);
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 = path2.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
316
+ const templatePath = path3.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
269
317
  let template;
270
318
  try {
271
- template = fs2.readFileSync(templatePath, "utf8");
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(path2.join(targetDir, "AGENTS.md"), rendered);
278
- const copilotPath = path2.join(targetDir, ".github", "copilot-instructions.md");
279
- fs2.mkdirSync(path2.dirname(copilotPath), { recursive: true });
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
- fs2.writeFileSync(filePath, contents, { flag: "wx" });
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 || path2.basename(cwd);
354
+ const projectName = name || path3.basename(cwd);
307
355
  const namespace = sanitizeNamespace(projectName);
308
- const targetDir = name ? path2.resolve(cwd, name) : cwd;
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 && fs2.existsSync(targetDir)) {
316
- const existing = fs2.readdirSync(targetDir);
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
- fs2.mkdirSync(targetDir, { recursive: true });
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-objectstack",
3
- "version": "17.0.0-rc.2",
3
+ "version": "17.0.0-rc.4",
4
4
  "description": "Create a new ObjectStack project — npx create-objectstack",
5
5
  "bin": {
6
6
  "create-objectstack": "./bin/create-objectstack.js"