@wolfstar/create-http-framework 2.2.0-next-20260831123711 → 2.2.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/dist/index.js CHANGED
@@ -418,6 +418,39 @@ function writeProjectFiles(targetDir, ctx) {
418
418
  const templateDir = join(fileURLToPath(import.meta.url), "../..", "template");
419
419
  const baseDir = join(templateDir, "base");
420
420
  const featuresDir = join(templateDir, "features");
421
+ /**
422
+ * Records the `TemplateContext` used for the last successful {@link processTemplate} run. Reruns
423
+ * use it to compare a stale file against an *exact* rendering of whatever produced it, so a
424
+ * genuinely pristine file is recognized regardless of what `--port`/name/`--language` changed this
425
+ * run, while a hand-edit — however small, even a single digit inside what would otherwise be a
426
+ * `{{port}}` slot — still fails the comparison and is preserved. See {@link removeStaleGeneratedFiles}
427
+ * for the (deliberately less precise) fallback used when this manifest is missing.
428
+ */
429
+ const manifestFilename = ".create-http-framework.json";
430
+ function readManifest(outputDir) {
431
+ try {
432
+ return JSON.parse(readFileSync(join(outputDir, manifestFilename), "utf-8"));
433
+ } catch {
434
+ return;
435
+ }
436
+ }
437
+ function writeManifest(outputDir, context) {
438
+ writeFile(join(outputDir, manifestFilename), `${JSON.stringify(context, null, " ")}\n`);
439
+ }
440
+ /**
441
+ * `@wolfstar/i18next-type-generator` writes this file when the CLI runs it after `processTemplate`
442
+ * (see `index.ts`). It isn't part of the Handlebars template set — its content is derived from the
443
+ * project's own locale keys, not from a static source file — so {@link removeStaleGeneratedFiles}
444
+ * has no candidate to diff it against and would never touch it, manifest or not. Delete it directly
445
+ * whenever i18n is disabled: it's pure `generate:i18n` output, never meant to be hand-edited.
446
+ */
447
+ function removeI18nDeclaration(outputDir) {
448
+ const target = join(outputDir, "src", "@types", "i18next.d.ts");
449
+ if (!existsSync(target)) return;
450
+ rmSync(target);
451
+ const typesDir = dirname(target);
452
+ if (existsSync(typesDir) && readdirSync(typesDir).length === 0) rmSync(typesDir, { recursive: true });
453
+ }
421
454
  function walkDir(dir) {
422
455
  const entries = readdirSync(dir);
423
456
  const files = [];
@@ -463,6 +496,33 @@ function renderSource(absoluteSource, context) {
463
496
  const rawContent = readFileSync(absoluteSource, "utf-8");
464
497
  return absoluteSource.endsWith(".hbs") ? Handlebars.compile(rawContent)(context) : rawContent;
465
498
  }
499
+ function escapeRegExp(text) {
500
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
501
+ }
502
+ /** True unless `rawSource` uses a Handlebars block helper (`{{#if}}`, `{{#each}}`, …) — see {@link buildStaleFileMatcher}. */
503
+ function isSimpleInterpolationOnly(rawSource) {
504
+ return !/\{\{[#/]/.test(rawSource);
505
+ }
506
+ /**
507
+ * Builds a *legacy-migration-only* fallback matcher for what `absoluteSource` could have rendered
508
+ * to, treating each `{{variable}}` interpolation as an unrestricted wildcard. This is deliberately
509
+ * imprecise — a hand-edit confined to what would otherwise be a `{{port}}` slot (e.g. the user
510
+ * changing just the fallback port number) is indistinguishable from a stale-but-pristine file and
511
+ * gets deleted. It only runs when there's no manifest to compare against exactly (see
512
+ * {@link removeStaleGeneratedFiles}), which is limited to projects generated before the manifest
513
+ * existed — every run after this one writes a manifest, closing the gap for good.
514
+ *
515
+ * `null` for files with block helpers (`{{#if}}`, …), which can't be turned into a simple wildcard
516
+ * pattern this way; none currently end up in "stale candidate" position, but this keeps the caller's
517
+ * exact-render fallback available if that changes.
518
+ */
519
+ function buildStaleFileMatcher(absoluteSource) {
520
+ const rawContent = readFileSync(absoluteSource, "utf-8");
521
+ if (!absoluteSource.endsWith(".hbs")) return new RegExp(`^${escapeRegExp(rawContent)}$`);
522
+ if (!isSimpleInterpolationOnly(rawContent)) return null;
523
+ const literalChunks = rawContent.split(/\{\{\s*[\w.]+\s*\}\}/g).map(escapeRegExp);
524
+ return new RegExp(`^${literalChunks.join("[\\s\\S]*?")}$`);
525
+ }
466
526
  /**
467
527
  * Maps every output-relative path `root` could ever produce — for either language — to the source
468
528
  * file(s) that render it. Unlike {@link collectOutputPaths} this ignores `context.language`, so it
@@ -488,11 +548,20 @@ function collectCandidateSources(root) {
488
548
  * dropped from `package.json` (e.g. `@wolfstar/plugin-i18next`), breaking install/build.
489
549
  *
490
550
  * Deletes any known generator output path that isn't produced by `base/` or an active feature this
491
- * run, but only when the file on disk still matches byte-for-byte what the generator would have
492
- * written for it — i.e. it wasn't hand-edited since. Hand-edited files are left in place (and their
493
- * paths returned) so the caller can warn instead of silently discarding user work.
551
+ * run, but only when the file on disk still matches what the generator would have written for it —
552
+ * i.e. it wasn't hand-edited since. Hand-edited files are left in place (and their paths returned)
553
+ * so the caller can warn instead of silently discarding user work.
554
+ *
555
+ * When a manifest is present, the comparison is an *exact* render of the candidate against the
556
+ * context recorded there — the one that actually produced the file — so it's precise even after
557
+ * `--port`/name/`--language` changed this run, and still catches a hand-edit as small as one digit.
558
+ * When it's missing (a project generated before the manifest existed), falls back to
559
+ * {@link buildStaleFileMatcher}'s wildcard comparison: less precise (a hand-edit confined to a
560
+ * `{{variable}}` slot won't be caught), but still correctly cleans up the common case instead of
561
+ * leaving a dangling import forever. That gap closes after this run, which always writes a manifest.
494
562
  */
495
563
  function removeStaleGeneratedFiles(outputDir, context) {
564
+ const manifestContext = readManifest(outputDir);
496
565
  const keepPaths = collectOutputPaths(baseDir, context.language);
497
566
  for (const feature of resolveFeatureDirs(context)) for (const path of collectOutputPaths(join(featuresDir, feature), context.language)) keepPaths.add(path);
498
567
  const candidates = collectCandidateSources(baseDir);
@@ -507,7 +576,10 @@ function removeStaleGeneratedFiles(outputDir, context) {
507
576
  const target = join(outputDir, path);
508
577
  if (!existsSync(target)) continue;
509
578
  const actual = readFileSync(target, "utf-8");
510
- if (sources.some((source) => renderSource(source, context) === actual)) rmSync(target);
579
+ if (manifestContext ? sources.some((source) => renderSource(source, manifestContext) === actual) : sources.some((source) => {
580
+ const matcher = buildStaleFileMatcher(source);
581
+ return matcher ? matcher.test(actual) : renderSource(source, context) === actual;
582
+ })) rmSync(target);
511
583
  else preserved.push(path);
512
584
  }
513
585
  return preserved;
@@ -541,8 +613,10 @@ function processDir(root, outputDir, context) {
541
613
  */
542
614
  async function processTemplate(outputDir, context) {
543
615
  const preserved = removeStaleGeneratedFiles(outputDir, context);
616
+ if (!context.i18n) removeI18nDeclaration(outputDir);
544
617
  processDir(baseDir, outputDir, context);
545
618
  for (const feature of resolveFeatureDirs(context)) processDir(join(featuresDir, feature), outputDir, context);
619
+ writeManifest(outputDir, context);
546
620
  return preserved;
547
621
  }
548
622
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolfstar/create-http-framework",
3
- "version": "2.2.0-next-20260831123711",
3
+ "version": "2.2.0",
4
4
  "description": "Create a new WolfStar HTTP Framework bot project",
5
5
  "keywords": [
6
6
  "cli",
@@ -2,3 +2,4 @@ node_modules/
2
2
  dist/
3
3
  .env
4
4
  *.js.map
5
+ .create-http-framework.json