Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion packages/cli/src/capture/scaffolding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("generateProjectScaffold metadata", () => {

afterEach(() => {
if (fs.existsSync(dir)) {
expect(fs.readdirSync(dir).filter((name) => name.startsWith(".hf-meta-"))).toEqual([]);
expect(fs.readdirSync(dir).filter((name) => name.startsWith(".hf-create-"))).toEqual([]);
}
fs.rmSync(dir, { recursive: true, force: true });
});
Expand Down
24 changes: 6 additions & 18 deletions packages/cli/src/capture/scaffolding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* (index.html, meta.json, AGENTS.md, CLAUDE.md).
*/

import { existsSync, writeFileSync, readFileSync, mkdtempSync, linkSync, rmSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { writeNewFileSync } from "../utils/writeNewFile.js";
import type { CatalogedAsset } from "./assetCataloger.js";
import type { CaptureResult, DesignTokens } from "./types.js";

Expand Down Expand Up @@ -71,23 +72,10 @@ export async function generateProjectScaffold(
const metaPath = join(outputDir, "meta.json");
if (!existsSync(metaPath)) {
const hostname = new URL(url).hostname.replace(/^www\./, "");
const stagingDir = mkdtempSync(join(outputDir, ".hf-meta-"));
try {
const stagedPath = join(stagingDir, "meta.json");
writeFileSync(
stagedPath,
JSON.stringify({ id: hostname + "-video", name: tokens.title || hostname }, null, 2),
{ encoding: "utf-8", flag: "wx" },
);
// Linking publishes without following or replacing an existing destination entry.
try {
linkSync(stagedPath, metaPath);
} catch (err) {
if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
}
} finally {
rmSync(stagingDir, { recursive: true, force: true });
}
writeNewFileSync(
metaPath,
JSON.stringify({ id: hostname + "-video", name: tokens.title || hostname }, null, 2),
);
}

// Generate AGENTS.md + CLAUDE.md (AI agent instructions — always, regardless of API keys)
Expand Down
62 changes: 62 additions & 0 deletions packages/cli/src/commands/init.package-race.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCommand } from "citty";
import init from "./init.js";

vi.mock("node:fs", async (importOriginal) => {
const original = await importOriginal<typeof fs>();
return { ...original, existsSync: vi.fn(original.existsSync) };
});
vi.mock("../telemetry/events.js", () => ({ trackInitTemplate: vi.fn() }));

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});

describe("init package.json creation", () => {
it.each(["existing", "concurrent", "dangling symlink"])(
"preserves a %s entry while completing initialization",
async (kind) => {
const dir = fs.mkdtempSync(join(tmpdir(), "hf-init-package-"));
const project = join(dir, "project");
const packagePath = join(project, "package.json");
const target = join(dir, "missing-target.json");
const original = await vi.importActual<typeof fs>("node:fs");
let injected = false;
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === packagePath && !injected) {
injected = true;
if (kind === "dangling symlink") fs.symlinkSync(target, packagePath);
else fs.writeFileSync(packagePath, '{"name":"preserve-me"}\n');
return kind === "existing";
}
return original.existsSync(path);
});
vi.stubEnv("HYPERFRAMES_SKIP_SKILLS", "1");
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
await runCommand(init, {
rawArgs: [project, "--example", "blank", "--non-interactive"],
});
expect(injected).toBe(true);
if (kind === "dangling symlink") {
expect(fs.lstatSync(packagePath).isSymbolicLink()).toBe(true);
expect(fs.existsSync(target)).toBe(false);
} else {
expect(fs.readFileSync(packagePath, "utf-8")).toBe('{"name":"preserve-me"}\n');
}
expect(fs.existsSync(join(project, "index.html"))).toBe(true);
expect(log.mock.calls.flat().join("\n")).toContain("npm run dev");
expect(fs.readdirSync(project).filter((name) => name.startsWith(".hf-create-"))).toEqual(
[],
);
} finally {
vi.mocked(fs.existsSync).mockImplementation(original.existsSync);
fs.rmSync(dir, { recursive: true, force: true });
}
},
);
});
4 changes: 2 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// own task.
// fallow-ignore-file complexity
import { failCommand, finishCommand } from "../utils/commandResult.js";
import { writeNewFileSync } from "../utils/writeNewFile.js";
import { defineCommand, runCommand } from "citty";
import type { Example } from "./_examples.js";

Expand Down Expand Up @@ -269,7 +270,7 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void {
const packageJsonPath = resolve(destDir, "package.json");
if (existsSync(packageJsonPath)) return;

writeFileSync(
writeNewFileSync(
packageJsonPath,
`${JSON.stringify(
{
Expand All @@ -281,7 +282,6 @@ function writeDefaultPackageJson(destDir: string, projectName: string): void {
null,
2,
)}\n`,
"utf-8",
);
}

Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/utils/writeNewFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { linkSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";

/** Publish complete content without replacing or following an existing destination entry. */
export function writeNewFileSync(filePath: string, content: string): void {
const stagingDir = mkdtempSync(join(dirname(filePath), ".hf-create-"));
try {
const stagedPath = join(stagingDir, "content");
writeFileSync(stagedPath, content, { encoding: "utf-8", flag: "wx" });
try {
linkSync(stagedPath, filePath);
} catch (err) {
if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
}
} finally {
rmSync(stagingDir, { recursive: true, force: true });
}
}
Loading