@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 +78 -4
- package/package.json +1 -1
- package/template/base/.gitignore.hbs +1 -0
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
|
|
492
|
-
*
|
|
493
|
-
*
|
|
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,
|
|
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