Skip to content

Commit 751c8df

Browse files
committed
feat(cli): init --tailwind
1 parent 0d050ca commit 751c8df

File tree

10 files changed

+528
-10
lines changed

10 files changed

+528
-10
lines changed

.depcheckrc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ ignores:
2424
- 'surge'
2525
- 'bun'
2626
- 'bun:sqlite'
27+
- 'tailwindcss'
28+
- '@tailwindcss/vite'
2729
ignore-patterns:
2830
- 'dist'
2931
- 'assets'

apps/blog/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
"@mantine/hooks": "^8.3.14",
99
"@mantine/tiptap": "^8.3.14",
1010
"@tabler/icons-react": "^3.36.1",
11-
"alepha": "^0.16.2",
1211
"@tiptap/core": "^3.19.0",
1312
"@tiptap/extension-highlight": "^3.19.0",
1413
"@tiptap/extension-link": "^3.19.0",
1514
"@tiptap/extension-placeholder": "^3.19.0",
1615
"@tiptap/extension-underline": "^3.19.0",
16+
"@tiptap/extensions": "^3.19.0",
1717
"@tiptap/pm": "^3.19.0",
1818
"@tiptap/react": "^3.19.0",
1919
"@tiptap/starter-kit": "^3.19.0",
20+
"alepha": "^0.16.2",
2021
"react": "^19.2.4",
2122
"react-dom": "^19.2.4"
2223
},

packages/alepha/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@biomejs/biome": "^2.3.14",
3636
"@electric-sql/pglite": "^0.3.15",
3737
"@faker-js/faker": "^10.3.0",
38+
"@tailwindcss/vite": "^4.1.18",
3839
"@testing-library/dom": "^10.4.1",
3940
"@testing-library/react": "^16.3.2",
4041
"@types/node": "^25.2.1",
@@ -52,6 +53,7 @@
5253
"react": "^19.2.4",
5354
"react-dom": "^19.2.4",
5455
"swagger-ui-dist": "^5.31.0",
56+
"tailwindcss": "^4.1.18",
5557
"tsdown": "^0.20.3",
5658
"vite": "^7.3.1",
5759
"vitest": "^4.0.18",

packages/alepha/src/cli/commands/init.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,142 @@ describe("alepha init", () => {
427427
});
428428
});
429429

430+
// ─────────────────────────────────────────────────────────────────────────────
431+
// Tailwind CSS (--tailwind flag)
432+
// ─────────────────────────────────────────────────────────────────────────────
433+
434+
describe("--tailwind flag", () => {
435+
it("should add tailwindcss devDependencies", async () => {
436+
const { fs, cli, cmd, json } = createTestEnv();
437+
await setupProject(fs, json);
438+
439+
await cli.run(cmd.init, { argv: "--tailwind", root: "/project" });
440+
441+
const pkg = await fs.readJsonFile<{
442+
devDependencies?: Record<string, string>;
443+
}>("/project/package.json");
444+
expect(pkg.devDependencies?.tailwindcss).toBeDefined();
445+
expect(pkg.devDependencies?.["@tailwindcss/vite"]).toBeDefined();
446+
});
447+
448+
it("should create vite.config.ts with tailwind plugin", async () => {
449+
const { fs, cli, cmd, json } = createTestEnv();
450+
await setupProject(fs, json);
451+
452+
await cli.run(cmd.init, { argv: "--tailwind", root: "/project" });
453+
454+
expect(fs.wasWritten("/project/vite.config.ts")).toBe(true);
455+
expect(
456+
fs.wasWrittenMatching("/project/vite.config.ts", /tailwindcss/),
457+
).toBe(true);
458+
expect(
459+
fs.wasWrittenMatching("/project/vite.config.ts", /@tailwindcss\/vite/),
460+
).toBe(true);
461+
expect(
462+
fs.wasWrittenMatching("/project/vite.config.ts", /defineConfig/),
463+
).toBe(true);
464+
});
465+
466+
it("should add @import tailwindcss to main.css", async () => {
467+
const { fs, cli, cmd, json } = createTestEnv();
468+
await setupProject(fs, json);
469+
470+
await cli.run(cmd.init, { argv: "--tailwind", root: "/project" });
471+
472+
expect(fs.wasWritten("/project/src/main.css")).toBe(true);
473+
expect(
474+
fs.wasWrittenMatching("/project/src/main.css", /@import "tailwindcss"/),
475+
).toBe(true);
476+
});
477+
478+
it("should imply --react", async () => {
479+
const { fs, cli, cmd, json } = createTestEnv();
480+
await setupProject(fs, json);
481+
482+
await cli.run(cmd.init, { argv: "--tailwind", root: "/project" });
483+
484+
// React web structure should be created
485+
expect(fs.wasWritten("/project/src/web/index.ts")).toBe(true);
486+
expect(fs.wasWritten("/project/src/main.browser.ts")).toBe(true);
487+
});
488+
489+
it("should not create vite.config.ts without --tailwind", async () => {
490+
const { fs, cli, cmd, json } = createTestEnv();
491+
await setupProject(fs, json);
492+
493+
await cli.run(cmd.init, { argv: "--react", root: "/project" });
494+
495+
expect(fs.wasWritten("/project/vite.config.ts")).toBe(false);
496+
});
497+
});
498+
499+
// ─────────────────────────────────────────────────────────────────────────────
500+
// Non-empty directory guard (codegen flags)
501+
// ─────────────────────────────────────────────────────────────────────────────
502+
503+
describe("non-empty directory guard", () => {
504+
it("should reject codegen into non-empty directory", async () => {
505+
const { fs, cli, cmd, json } = createTestEnv();
506+
await setupProject(fs, json);
507+
await fs.writeFile("/project/src/existing.ts", "export {}");
508+
509+
await expect(
510+
cli.run(cmd.init, { argv: "--api", root: "/project" }),
511+
).rejects.toThrowError(/Target directory is not empty/);
512+
});
513+
514+
it("should allow codegen when only package.json exists", async () => {
515+
const { fs, cli, cmd, json } = createTestEnv();
516+
await setupProject(fs, json);
517+
518+
await cli.run(cmd.init, { argv: "--api", root: "/project" });
519+
520+
expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true);
521+
});
522+
523+
it("should allow codegen into non-empty directory with --force", async () => {
524+
const { fs, cli, cmd, json } = createTestEnv();
525+
await setupProject(fs, json);
526+
await fs.writeFile("/project/src/existing.ts", "export {}");
527+
528+
await cli.run(cmd.init, {
529+
argv: "--api --force",
530+
root: "/project",
531+
});
532+
533+
expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true);
534+
});
535+
536+
it("should allow non-codegen init in non-empty directory", async () => {
537+
const { fs, cli, cmd, json } = createTestEnv();
538+
await setupProject(fs, json);
539+
await fs.writeFile("/project/src/existing.ts", "export {}");
540+
541+
// No codegen flags — should not throw
542+
await cli.run(cmd.init, { root: "/project" });
543+
544+
expect(fs.wasWritten("/project/tsconfig.json")).toBe(true);
545+
});
546+
547+
it("should check each codegen flag independently", async () => {
548+
for (const flag of [
549+
"--react",
550+
"--ui",
551+
"--auth",
552+
"--admin",
553+
"--tailwind",
554+
]) {
555+
const { fs, cli, cmd, json } = createTestEnv();
556+
await setupProject(fs, json);
557+
await fs.writeFile("/project/src/existing.ts", "export {}");
558+
559+
await expect(
560+
cli.run(cmd.init, { argv: flag, root: "/project" }),
561+
).rejects.toThrowError(/Target directory is not empty/);
562+
}
563+
});
564+
});
565+
430566
// ─────────────────────────────────────────────────────────────────────────────
431567
// Path Argument
432568
// ─────────────────────────────────────────────────────────────────────────────

packages/alepha/src/cli/commands/init.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $inject, t } from "alepha";
1+
import { $inject, AlephaError, t } from "alepha";
22
import { $command } from "alepha/command";
33
import { $logger, ConsoleColorProvider } from "alepha/logger";
44
import { FileSystemProvider } from "alepha/system";
@@ -63,6 +63,11 @@ export class InitCommand {
6363
description: "Include admin portal ($uiAdmin). Implies --auth",
6464
}),
6565
),
66+
tailwind: t.optional(
67+
t.boolean({
68+
description: "Include Tailwind CSS with Vite plugin. Implies --react",
69+
}),
70+
),
6671
test: t.optional(
6772
t.boolean({ description: "Include Vitest and create test directory" }),
6873
),
@@ -90,6 +95,28 @@ export class InitCommand {
9095
if (flags.ui) {
9196
flags.react = true;
9297
}
98+
if (flags.tailwind) {
99+
flags.react = true;
100+
}
101+
102+
// When codegen flags are set, target directory must be empty (unless --force)
103+
const hasCodegenFlags =
104+
flags.admin ||
105+
flags.auth ||
106+
flags.api ||
107+
flags.ui ||
108+
flags.react ||
109+
flags.tailwind;
110+
if (hasCodegenFlags && !flags.force) {
111+
const files = await this.fs.ls(root);
112+
// Allow a directory that only has package.json (common for monorepo packages)
113+
const meaningful = files.filter((f) => f !== "package.json");
114+
if (meaningful.length > 0) {
115+
throw new AlephaError(
116+
`Target directory is not empty (${root}). Use --force to overwrite existing files.`,
117+
);
118+
}
119+
}
93120

94121
// Detect workspace context (are we inside packages/ or apps/ of a monorepo?)
95122
const workspace = await this.pm.getWorkspaceContext(root);
@@ -145,6 +172,7 @@ export class InitCommand {
145172
ui: !!flags.ui,
146173
auth: !!flags.auth,
147174
admin: !!flags.admin,
175+
tailwind: !!flags.tailwind,
148176
force,
149177
});
150178
}

packages/alepha/src/cli/services/PackageManagerUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,11 @@ export class PackageManagerUtils {
399399
modes.react = true;
400400
}
401401

402+
if (modes.tailwind) {
403+
devDependencies.tailwindcss = alephaDeps.tailwindcss;
404+
devDependencies["@tailwindcss/vite"] = alephaDeps["@tailwindcss/vite"];
405+
}
406+
402407
if (modes.react) {
403408
dependencies.react = alephaDeps.react;
404409
dependencies["react-dom"] = alephaDeps["react-dom"];
@@ -432,6 +437,7 @@ export interface DependencyModes {
432437
react?: boolean;
433438
ui?: boolean;
434439
expo?: boolean;
440+
tailwind?: boolean;
435441
test?: boolean;
436442
/**
437443
* Skip biome/vitest when inside a workspace package (they're at root).

packages/alepha/src/cli/services/ProjectScaffolder.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { mainBrowserTs } from "../templates/mainBrowserTs.ts";
1616
import { mainCss } from "../templates/mainCss.ts";
1717
import { mainServerTs } from "../templates/mainServerTs.ts";
1818
import { tsconfigJson } from "../templates/tsconfigJson.ts";
19+
import { viteConfigTs } from "../templates/viteConfigTs.ts";
1920
import { webAppRouterTs } from "../templates/webAppRouterTs.ts";
2021
import { webHomeComponentTsx } from "../templates/webHomeComponentTsx.ts";
2122
import { webIndexTs } from "../templates/webIndexTs.ts";
@@ -298,6 +299,7 @@ export class ProjectScaffolder {
298299
ui?: boolean;
299300
auth?: boolean;
300301
admin?: boolean;
302+
tailwind?: boolean;
301303
force?: boolean;
302304
} = {},
303305
): Promise<void> {
@@ -312,10 +314,15 @@ export class ProjectScaffolder {
312314
await this.ensureFile(
313315
root,
314316
"src/main.css",
315-
mainCss({ ui: opts.ui }),
317+
mainCss({ ui: opts.ui, tailwind: opts.tailwind }),
316318
opts.force,
317319
);
318320

321+
// vite.config.ts (Tailwind CSS plugin)
322+
if (opts.tailwind) {
323+
await this.ensureFile(root, "vite.config.ts", viteConfigTs(), opts.force);
324+
}
325+
319326
// Web structure
320327
await this.ensureFile(
321328
root,

packages/alepha/src/cli/templates/mainCss.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const mainCss = (opts: { ui?: boolean } = {}) => {
1+
export const mainCss = (opts: { ui?: boolean; tailwind?: boolean } = {}) => {
22
if (opts.ui) {
33
return `/**
44
* Alepha UI - Based on Mantine component library
@@ -33,12 +33,19 @@ export const mainCss = (opts: { ui?: boolean } = {}) => {
3333
@import "@alepha/ui/styles";`;
3434
}
3535

36+
if (opts.tailwind) {
37+
return `@import "tailwindcss";
38+
39+
/* Add your styles here */
40+
`;
41+
}
42+
3643
return `/**
3744
* Global styles for your application.
3845
*
3946
* Options:
4047
* - @alepha/ui: Use \`alepha init --ui\` to add Mantine-based components
41-
* - Tailwind CSS: https://tailwindcss.com/docs/installation/using-vite
48+
* - Tailwind CSS: Use \`alepha init --tailwind\` to add Tailwind CSS
4249
* - Raw CSS: Write your own styles below
4350
*/
4451
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const viteConfigTs = () => {
2+
return `import tailwindcss from "@tailwindcss/vite";
3+
import { defineConfig } from "vite";
4+
5+
export default defineConfig({
6+
plugins: [tailwindcss()],
7+
});
8+
`;
9+
};

0 commit comments

Comments
 (0)