Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions __tests__/fullstack-init-next-rename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const fs = require('fs');
const path = require('path');
// `filesystem` / `patching` are required lazily inside the test that
// uses them so this file doesn't collide with the global-scope
// declarations in other test files (e.g.
// `fullstack-claude-md-patching.test.ts`). ts-jest treats every test
// file as part of one TypeScript program, so top-level `const
// { filesystem } = require('gluegun')` here would trigger TS2451.

/**
* Auto-rename behaviour for `lt fullstack init --next`.
*
* The experimental nest-base template ships with hard-coded `nest-base`
* references in four files. After cloning, the CLI runs
* `bun run rename <projectDir>` for the user.
*
* Two things need to hold for that to work end-to-end:
*
* 1. The init.ts code path actually invokes the rename script when
* `--next` is set, and only then. We verify this by reading the
* command source directly — the rename is a single, deterministic
* `system.run` call inside the `experimental` block.
*
* 2. Before invoking the rename, init.ts restores `package.json`'s
* `name` to `"nest-base"` so the planner has a coherent starting
* state across all four files. Otherwise the planner would treat
* `projectDir` as the "old" name, fail to match `# nest-base` in
* the README, and leave the rename half-done. This regression is
* easy to reintroduce, so we cover the package.json reset as a
* black-box test against a fixture.
*/
describe('Fullstack init --next auto-rename', () => {
const initSource = fs.readFileSync(
path.join(__dirname, '..', 'src', 'commands', 'fullstack', 'init.ts'),
'utf8',
);

test('init.ts invokes `bun run rename` when --next is set', () => {
expect(initSource).toMatch(/bun run rename \$\{projectDir\}/);
});

test('rename invocation is gated on the experimental flag', () => {
// Match the entire `if (experimental && apiResult.method !== 'link') {
// ... }` block so a refactor that drops the gate without re-adding it
// breaks the test.
expect(initSource).toMatch(/if \(experimental && apiResult\.method !== 'link'\) \{[\s\S]*?bun run rename/);
});

test('init.ts no longer prints a manual rename hint in the Next section', () => {
// The Next: section for the experimental branch must not tell the
// user to run rename themselves — the CLI does it now.
const nextSection = initSource.match(/info\('Next:'\);[\s\S]*?info\(''\);/);
expect(nextSection).not.toBeNull();
expect(nextSection![0]).not.toMatch(/bun run rename/);
});

describe('package.json name reset', () => {
let tempDir: string;
// Lazy require to avoid a top-level `filesystem` declaration that
// would clash with other test files' globals (see header comment).
const { filesystem, patching } = require('gluegun');

beforeEach(() => {
tempDir = filesystem.path('__tests__', `temp-fullstack-rename-${Date.now()}`);
filesystem.dir(tempDir);
});

afterEach(() => {
filesystem.remove(tempDir);
});

test('reverts package.json name back to "nest-base" so the planner can detect the canonical old slug', async () => {
const pkgPath = filesystem.path(tempDir, 'package.json');
// Simulate state after setupServerForFullstack: the experimental
// patch has already overwritten the template's `name` field with
// the project's kebab-cased directory name.
filesystem.write(pkgPath, {
name: 'my-next-fs',
description: 'API for my-next-fs app',
version: '0.0.0',
});

// This is the exact patch init.ts performs before invoking rename.
await patching.update(pkgPath, (config: Record<string, unknown>) => {
config.name = 'nest-base';
return config;
});

const result = filesystem.read(pkgPath, 'json');
expect(result.name).toBe('nest-base');
// Other fields must survive untouched so the rename is the only
// thing that changes the surrounding state.
expect(result.description).toBe('API for my-next-fs app');
expect(result.version).toBe('0.0.0');
});
});
});
36 changes: 36 additions & 0 deletions src/commands/fullstack/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,42 @@ const NewCommand: GluegunCommand = {
return;
}

// Auto-run `bun run rename <projectDir>` for the experimental nest-base
// template. The template ships with hard-coded `nest-base` references in
// four files (package.json, README.md, portless.yml, docker-compose.yml).
// The rename script patches all four idempotently. Since the consumer
// already gave us --name, doing this for them is strictly less
// friction-prone than relying on a manual follow-up step (which agents
// and humans both forget). Failure is non-fatal: the workspace is still
// usable, the user can re-run `bun run rename <name>` manually.
//
// Note: setupServerForFullstack already patched projects/api/package.json
// to set `name = projectDir`. The rename planner reads that name as the
// "old" slug, which would short-circuit the README/portless/compose
// rewrites because they still say `nest-base`. We restore the canonical
// `name = "nest-base"` first so the planner has a coherent starting
// state across all four files; the rename then writes the project name
// into every spot consistently.
if (experimental && apiResult.method !== 'link') {
const apiPackageJsonPath = `${apiDest}/package.json`;
if (filesystem.exists(apiPackageJsonPath)) {
await patching.update(apiPackageJsonPath, (config: Record<string, unknown>) => {
config.name = 'nest-base';
return config;
});
}

const renameSpinner = spin(`Rename nest-base → ${projectDir}`);
try {
await system.run(`cd ${apiDest} && bun run rename ${projectDir}`);
renameSpinner.succeed(`Renamed nest-base → ${projectDir} in projects/api`);
} catch (err) {
renameSpinner.warn(
`Auto-rename failed (${(err as Error).message}). Run \`bun run rename ${projectDir}\` manually inside projects/api.`,
);
}
}

// Create lt.config.json for API
// Note: frameworkMode is persisted under meta so that subsequent `lt
// server module` / `addProp` / `permissions` calls can detect the mode
Expand Down
Loading