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
89 changes: 25 additions & 64 deletions init/src/init.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// deno-lint-ignore-file no-console
import * as colors from "@std/fmt/colors";
import * as path from "@std/path";

Expand All @@ -7,14 +8,6 @@ const FRESH_TAILWIND_VERSION = "0.0.1-alpha.7";
const PREACT_VERSION = "10.26.6";
const PREACT_SIGNALS_VERSION = "2.0.4";

export const enum InitStep {
ProjectName = "ProjectName",
Force = "Force",
Tailwind = "Tailwind",
VSCode = "VSCode",
Docker = "Docker",
}

function css(strs: TemplateStringsArray, ...exprs: string[]): string {
let out = "";

Expand All @@ -29,8 +22,8 @@ function css(strs: TemplateStringsArray, ...exprs: string[]): string {

export class InitError extends Error {}

function error(tty: MockTTY, message: string): never {
tty.logError(`%cerror%c: ${message}`, "color: red; font-weight: bold", "");
function error(message: string): never {
console.error(`%cerror%c: ${message}`, "color: red; font-weight: bold", "");
throw new InitError();
}

Expand All @@ -55,33 +48,12 @@ OPTIONS:
--docker Setup Project to use Docker
`;

export interface MockTTY {
prompt(
step: InitStep,
message?: string | undefined,
_default?: string | undefined,
): string | null;
confirm(step: InitStep, message?: string | undefined): boolean;
log(...args: unknown[]): void;
logError(...args: unknown[]): void;
}

const realTTY: MockTTY = {
prompt(_step, message, _default) {
return prompt(message, _default);
},
confirm(_step, message) {
return confirm(message);
},
log(...args) {
// deno-lint-ignore no-console
console.log(...args);
},
logError(...args) {
// deno-lint-ignore no-console
console.error(...args);
},
};
export const CONFIRM_EMPTY_MESSAGE =
"The target directory is not empty (files could get overwritten). Do you want to continue anyway?";
export const CONFIRM_TAILWIND_MESSAGE = `Set up ${
colors.cyan("Tailwind CSS")
} for styling?`;
export const CONFIRM_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`;

export async function initProject(
cwd = Deno.cwd(),
Expand All @@ -92,34 +64,29 @@ export async function initProject(
tailwind?: boolean | null;
vscode?: boolean | null;
} = {},
tty: MockTTY = realTTY,
): Promise<void> {
tty.log();
tty.log(
console.log();
console.log(
colors.bgRgb8(
colors.rgb8(" 🍋 Fresh: The next-gen web framework. ", 0),
121,
),
);
tty.log();
console.log();

let unresolvedDirectory = Deno.args[0];
if (input.length !== 1) {
const userInput = tty.prompt(
InitStep.ProjectName,
const userInput = prompt(
"Project Name:",
"fresh-project",
);
if (!userInput) {
error(tty, HELP_TEXT);
error(HELP_TEXT);
}

unresolvedDirectory = userInput;
}

const CONFIRM_EMPTY_MESSAGE =
"The target directory is not empty (files could get overwritten). Do you want to continue anyway?";

const projectDir = path.resolve(cwd, unresolvedDirectory);

try {
Expand All @@ -128,11 +95,9 @@ export async function initProject(
dir.length === 1 && dir[0].name === ".git";
if (
!isEmpty &&
!(flags.force === null
? tty.confirm(InitStep.Force, CONFIRM_EMPTY_MESSAGE)
: flags.force)
!(flags.force === null ? confirm(CONFIRM_EMPTY_MESSAGE) : flags.force)
) {
error(tty, "Directory is not empty.");
error("Directory is not empty.");
}
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) {
Expand All @@ -144,18 +109,14 @@ export async function initProject(
let useTailwind = flags.tailwind || false;
if (flags.tailwind == null) {
if (
tty.confirm(
InitStep.Tailwind,
`Set up ${colors.cyan("Tailwind CSS")} for styling?`,
)
confirm(CONFIRM_TAILWIND_MESSAGE)
) {
useTailwind = true;
}
}

const USE_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`;
const useVSCode = flags.vscode == null
? tty.confirm(InitStep.VSCode, USE_VSCODE_MESSAGE)
? confirm(CONFIRM_VSCODE_MESSAGE)
: flags.vscode;

const writeFile = async (
Expand Down Expand Up @@ -715,30 +676,30 @@ This will watch the project directory and restart as necessary.`;

// Specifically print unresolvedDirectory, rather than resolvedDirectory in order to
// not leak personal info (e.g. `/Users/MyName`)
tty.log("\n%cProject initialized!\n", "color: green; font-weight: bold");
console.log("\n%cProject initialized!\n", "color: green; font-weight: bold");

if (unresolvedDirectory !== ".") {
tty.log(
console.log(
`Enter your project directory using %ccd ${unresolvedDirectory}%c.`,
"color: cyan",
"",
);
}
tty.log(
console.log(
"Run %cdeno task start%c to start the project. %cCTRL-C%c to stop.",
"color: cyan",
"",
"color: cyan",
"",
);
tty.log();
tty.log(
console.log();
console.log(
"Stuck? Join our Discord %chttps://discord.gg/deno",
"color: cyan",
"",
);
tty.log();
tty.log(
console.log();
console.log(
"%cHappy hacking! 🦕",
"color: gray",
);
Expand Down
105 changes: 49 additions & 56 deletions init/src/init_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { expect } from "@std/expect";
import { initProject, InitStep, type MockTTY } from "./init.ts";
import {
CONFIRM_TAILWIND_MESSAGE,
CONFIRM_VSCODE_MESSAGE,
initProject,
} from "./init.ts";
import * as path from "@std/path";
import { getStdOutput, withBrowser } from "../../tests/test_utils.tsx";
import { waitForText } from "../../tests/test_utils.tsx";
import { withChildProcessServer } from "../../tests/test_utils.tsx";
import { stub } from "@std/testing/mock";

function stubPrompt(result: string) {
return stub(globalThis, "prompt", () => result);
}

function stubConfirm(steps: Record<string, boolean> = {}) {
return stub(
globalThis,
"confirm",
(message) => message ? steps[message] : false,
);
}

async function withTmpDir(fn: (dir: string) => void | Promise<void>) {
const hash = crypto.randomUUID().replaceAll(/-/g, "");
Expand Down Expand Up @@ -32,29 +49,6 @@ async function patchProject(dir: string): Promise<void> {
await Deno.writeTextFile(jsonPath, JSON.stringify(json, null, 2) + "\n");
}

function mockUserInput(steps: Record<string, unknown>) {
const errorOutput: unknown[][] = [];
const tty: MockTTY = {
confirm(step, _msg) {
return Boolean(steps[step]);
},
prompt(step, _msg, def) {
const setting = typeof steps[step] === "string"
? steps[step] as string
: null;
return setting ?? def ?? null;
},
log: () => {},
logError: (...args) => {
errorOutput.push(args);
},
};
return {
errorOutput,
tty,
};
}

async function expectProjectFile(dir: string, pathname: string) {
const filePath = path.join(dir, ...pathname.split("/").filter(Boolean));
const stat = await Deno.stat(filePath);
Expand All @@ -71,15 +65,17 @@ async function readProjectFile(dir: string, pathname: string): Promise<string> {

Deno.test("init - new project", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({});
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt("fresh-init");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});
});
});

Deno.test("init - create project dir", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({ [InitStep.ProjectName]: "fresh-init" });
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt("fresh-init");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});

const root = path.join(dir, "fresh-init");
await expectProjectFile(root, "deno.json");
Expand All @@ -92,11 +88,11 @@ Deno.test("init - create project dir", async () => {

Deno.test("init - with tailwind", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
[InitStep.Tailwind]: true,
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm({
[CONFIRM_TAILWIND_MESSAGE]: true,
});
await initProject(dir, [], {}, mock.tty);
await initProject(dir, [], {});

const css = await readProjectFile(dir, "static/styles.css");
expect(css).toMatch(/@tailwind/);
Expand All @@ -110,11 +106,11 @@ Deno.test("init - with tailwind", async () => {

Deno.test("init - with vscode", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
[InitStep.VSCode]: true,
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm({
[CONFIRM_VSCODE_MESSAGE]: true,
});
await initProject(dir, [], {}, mock.tty);
await initProject(dir, [], {});

await expectProjectFile(dir, ".vscode/settings.json");
await expectProjectFile(dir, ".vscode/extensions.json");
Expand All @@ -128,10 +124,9 @@ Deno.test({
ignore: Deno.version.deno.includes("+"),
fn: async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
});
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});
await expectProjectFile(dir, "main.ts");
await expectProjectFile(dir, "dev.ts");

Expand All @@ -150,11 +145,12 @@ Deno.test({

Deno.test("init with tailwind - fmt, lint, and type check project", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
[InitStep.Tailwind]: true,
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm({
[CONFIRM_TAILWIND_MESSAGE]: true,
});
await initProject(dir, [], {}, mock.tty);

await initProject(dir, [], {});
await expectProjectFile(dir, "main.ts");
await expectProjectFile(dir, "dev.ts");

Expand All @@ -172,10 +168,9 @@ Deno.test("init with tailwind - fmt, lint, and type check project", async () =>

Deno.test("init - can start dev server", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
});
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});
await expectProjectFile(dir, "main.ts");
await expectProjectFile(dir, "dev.ts");

Expand All @@ -196,10 +191,9 @@ Deno.test("init - can start dev server", async () => {

Deno.test("init - can start built project", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
});
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});
await expectProjectFile(dir, "main.ts");
await expectProjectFile(dir, "dev.ts");

Expand Down Expand Up @@ -230,10 +224,9 @@ Deno.test("init - can start built project", async () => {

Deno.test("init - errors on missing build cache in prod", async () => {
await withTmpDir(async (dir) => {
const mock = mockUserInput({
[InitStep.ProjectName]: ".",
});
await initProject(dir, [], {}, mock.tty);
using _promptStub = stubPrompt(".");
using _confirmStub = stubConfirm();
await initProject(dir, [], {});
await expectProjectFile(dir, "main.ts");
await expectProjectFile(dir, "dev.ts");

Expand Down
Loading