Skip to content

Commit 190e09c

Browse files
os-zhuangclaude
andcommitted
test(create-objectstack): widen the packing ratchet to every bundled template
The file-set check only walked src/templates/blank, so a second bundled template committing a raw .gitignore — or a stripped dotfile added beside src/templates/AGENTS.md — would reintroduce the bug silently, which is the one thing this ratchet exists to prevent. Assert the tarball carries src/templates/** verbatim instead (aliases are a scaffold-time concern, so no alias mapping on either side), and keep the scaffold-based check for the alias restoration. Verified red for all three reverts: the original rename, an emptied alias map, and a new bundled template with a raw .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d7e1548 commit 190e09c

1 file changed

Lines changed: 24 additions & 18 deletions

File tree

packages/create-objectstack/src/template-consistency.test.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,12 @@ describe('blank template manifest engines.protocol (ADR-0087 D1)', () => {
7474
});
7575
});
7676

77-
// Packing ratchet (#3120): every file the blank template ships must survive a
78-
// real `npm pack` and land in a scaffold under its intended name. `.gitignore`
79-
// did not — npm strips it from the tarball at every depth, so the file the
80-
// build had faithfully copied to dist/templates/blank/ was dropped at publish
81-
// and every scaffolded project came out with no `.gitignore`, leaving
82-
// node_modules/ and the secret-bearing .env from the template README
83-
// un-ignored for every new user.
77+
// Packing ratchet (#3120): every template file must survive a real `npm pack`
78+
// and land in a scaffold under its intended name. `.gitignore` did not — npm
79+
// strips it from the tarball at every depth, so the file the build had
80+
// faithfully copied to dist/templates/blank/ was dropped at publish and every
81+
// scaffolded project came out with no `.gitignore`, leaving node_modules/ and
82+
// the secret-bearing .env from the template README un-ignored for every user.
8483
//
8584
// This bug is invisible to source-level assertions: the file is present in
8685
// src/templates/, present in a local build, and only vanishes at publish. So
@@ -89,8 +88,9 @@ describe('blank template manifest engines.protocol (ADR-0087 D1)', () => {
8988
// `test` task only dependsOn `^build` (not its own build) and excludes dist/**
9089
// from its inputs, so dist/ here is routinely absent or stale, and a pass on it
9190
// would be cached.
92-
describe('blank template survives npm packing', () => {
93-
const blankSrc = path.join(pkgRoot, 'src', 'templates', 'blank');
91+
describe('templates survive npm packing', () => {
92+
const templatesSrc = path.join(pkgRoot, 'src', 'templates');
93+
const blankSrc = path.join(templatesSrc, 'blank');
9494

9595
const walkRel = (dir: string, rel = ''): string[] =>
9696
fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
@@ -109,6 +109,7 @@ describe('blank template survives npm packing', () => {
109109
};
110110

111111
let tmp: string;
112+
let packed: string[];
112113
let scaffolded: string[];
113114
let collected: string[];
114115

@@ -120,14 +121,13 @@ describe('blank template survives npm packing', () => {
120121
// dist/templates, which is what tsup.config.ts's onSuccess hook does. The
121122
// test asserts that mirror below rather than assuming it.
122123
fs.cpSync(path.join(pkgRoot, 'package.json'), path.join(tmp, 'package.json'));
123-
fs.cpSync(path.join(pkgRoot, 'src', 'templates'), path.join(tmp, 'dist', 'templates'), {
124-
recursive: true,
125-
});
124+
fs.cpSync(templatesSrc, path.join(tmp, 'dist', 'templates'), { recursive: true });
126125

127126
execFileSync('npm', ['pack', '--ignore-scripts'], { cwd: tmp, stdio: 'pipe' });
128127
const tgz = fs.readdirSync(tmp).find((f) => f.endsWith('.tgz'));
129128
if (!tgz) throw new Error(`npm pack produced no tarball in ${tmp}`);
130129
execFileSync('tar', ['xzf', tgz], { cwd: tmp });
130+
packed = walkRel(path.join(tmp, 'package', 'dist', 'templates'));
131131

132132
// Scaffold from the *packed* template with the real copy logic.
133133
const out = path.join(tmp, 'scaffold');
@@ -150,15 +150,21 @@ describe('blank template survives npm packing', () => {
150150
).toContain("cpSync('src/templates', 'dist/templates', { recursive: true })");
151151
});
152152

153-
it('lands every template file — dotfiles included — in the scaffold', () => {
154-
const expected = walkRel(blankSrc).map(scaffoldedAs).sort();
153+
// Covers every bundled template, not just blank: the tarball must carry
154+
// src/templates/** verbatim (aliases are a scaffold-time concern).
155+
it('carries every src/templates file into the tarball', () => {
155156
expect(
156-
scaffolded.sort(),
157-
'a file in src/templates/blank/ did not survive `npm pack` (npm strips ' +
158-
'.gitignore and .npmrc from tarballs at every depth). Commit it under a ' +
157+
packed.sort(),
158+
'a file under src/templates/ did not survive `npm pack`. npm strips ' +
159+
'.gitignore and .npmrc from tarballs at every depth — commit it under a ' +
159160
'placeholder name and map it back in TEMPLATE_FILE_ALIASES, the way ' +
160161
'_gitignore → .gitignore works.',
161-
).toEqual(expected);
162+
).toEqual(walkRel(templatesSrc).sort());
163+
});
164+
165+
it('restores the aliased names when scaffolding', () => {
166+
const expected = walkRel(blankSrc).map(scaffoldedAs).sort();
167+
expect(scaffolded.sort()).toEqual(expected);
162168
// What the CLI prints as "Created files:" must match what it actually wrote.
163169
expect(collected.sort()).toEqual(expected);
164170
});

0 commit comments

Comments
 (0)