@emberkit/cli 0.6.0 → 0.6.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,8 +2,9 @@ import inquirer from "inquirer";
2
2
  import { dev } from "./commands/dev.js";
3
3
  import { build } from "./commands/build.js";
4
4
  import { preview } from "./commands/preview.js";
5
- import { create } from "./commands/create.js";
6
- import { TEMPLATES } from "./commands/create.js";
5
+ import { create, TEMPLATES } from "./commands/create.js";
6
+ import { generate } from "./utils/generator.js";
7
+ import { toKebabCase } from "./templates/index.js";
7
8
  export async function runCLI(args) {
8
9
  const [command, ...restArgs] = args.slice(2);
9
10
  if (!command) {
@@ -28,7 +29,7 @@ export async function runCLI(args) {
28
29
  break;
29
30
  case "--version":
30
31
  case "-v":
31
- console.log("EmberKit CLI v0.1.0");
32
+ console.log("EmberKit CLI v0.6.0");
32
33
  break;
33
34
  case "--help":
34
35
  case "-h":
@@ -42,7 +43,7 @@ export async function runCLI(args) {
42
43
  }
43
44
  function showHelp() {
44
45
  console.log(`
45
- 🔥 EmberKit CLI v0.1.0
46
+ 🔥 EmberKit CLI v0.6.0
46
47
 
47
48
  Usage: emberkit <command> [options]
48
49
 
@@ -51,15 +52,24 @@ Commands:
51
52
  dev Start development server
52
53
  build Build for production
53
54
  preview Preview production build
54
- generate <type> Generate code (routes, components, etc.)
55
+ generate <type> <name> Generate a file from a template
55
56
 
56
57
  Options:
57
58
  --template, -t <id> Project template to use
58
59
  --no-install Skip dependency installation
60
+ --path, -p <path> Output path for generate (overrides default)
59
61
  --help, -h Show this help message
60
62
  --version, -v Show version number
61
63
 
62
- Templates:
64
+ Generate types:
65
+ route Route component (src/routes/)
66
+ component UI component (src/components/)
67
+ layout Layout component (src/layouts/)
68
+ loader Data loader (src/loaders/)
69
+ action Form action (src/actions/)
70
+ api API route handler (src/routes/_api/)
71
+
72
+ Project templates:
63
73
  basic Simple starter with Tailwind CSS (default)
64
74
  with-ui Starter with EmberKit UI components
65
75
  minimal Barebones project, no CSS framework
@@ -163,12 +173,36 @@ function extractFlagValue(args, longFlag, shortFlag) {
163
173
  }
164
174
  return undefined;
165
175
  }
176
+ const GENERATE_TYPES = {
177
+ route: { dir: "src/routes", ext: ".tsx" },
178
+ component: { dir: "src/components", ext: ".tsx" },
179
+ layout: { dir: "src/layouts", ext: ".tsx" },
180
+ error: { dir: "src/routes", ext: ".tsx" },
181
+ loader: { dir: "src/loaders", ext: ".ts" },
182
+ action: { dir: "src/actions", ext: ".ts" },
183
+ api: { dir: "src/routes/_api", ext: ".ts" },
184
+ };
166
185
  async function runGenerate(args) {
167
- const [type, name] = args;
186
+ const nonFlagArgs = args.filter((a) => !a.startsWith("-"));
187
+ const [type, name] = nonFlagArgs;
168
188
  if (!type || !name) {
169
- console.error("Usage: emberkit generate <type> <name>");
189
+ console.error(`Usage: emberkit generate <type> <name> [--path <file-path>]\n\nTypes: ${Object.keys(GENERATE_TYPES).join(", ")}`);
190
+ process.exit(1);
191
+ }
192
+ const explicitPath = extractFlagValue(args, "--path", "-p");
193
+ const typeConfig = GENERATE_TYPES[type];
194
+ if (!typeConfig && !explicitPath) {
195
+ console.error(`Unknown type "${type}". Valid types: ${Object.keys(GENERATE_TYPES).join(", ")}`);
196
+ process.exit(1);
197
+ }
198
+ const filePath = explicitPath ??
199
+ `${typeConfig.dir}/${toKebabCase(name)}${typeConfig.ext}`;
200
+ const result = await generate({ name, path: filePath, template: type });
201
+ if (result.success) {
202
+ console.log(`✓ Generated ${type}: ${result.path}`);
203
+ }
204
+ else {
205
+ console.error(`✗ ${result.error}`);
170
206
  process.exit(1);
171
207
  }
172
- console.log(`🎨 Generating ${type}: ${name}`);
173
- console.log("(Not yet implemented)");
174
208
  }
@@ -1,14 +1,15 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "fs";
2
- import { resolve, join } from "path";
2
+ import { resolve, join, dirname } from "path";
3
3
  import { execSync } from "child_process";
4
4
  import { getPackageManager, getInstallCommand } from "../utils/filesystem.js";
5
- import { starterFiles } from "../templates/projects/starter.js";
6
- import { withUiTemplate } from "../templates/projects/with-ui.js";
7
- import { minimalTemplate } from "../templates/minimal/minimal.js";
8
- import { blogTemplate } from "../templates/blog/blog.js";
9
- import { saasTemplate } from "../templates/saas/saas.js";
10
- import { dashboardTemplate } from "../templates/dashboard/dashboard.js";
11
- import { apiTemplate } from "../templates/api/api.js";
5
+ import { formatTemplate, toKebabCase } from "../templates/index.js";
6
+ import { starterFiles } from "../templates/project-templates/starter-kit/starter.js";
7
+ import { withUiTemplate } from "../templates/project-templates/starter-kit/with-ui.js";
8
+ import { minimalTemplate } from "../templates/project-templates/minimal/minimal.js";
9
+ import { blogTemplate } from "../templates/project-templates/blog/blog.js";
10
+ import { saasTemplate } from "../templates/project-templates/saas/saas.js";
11
+ import { dashboardTemplate } from "../templates/project-templates/dashboard/dashboard.js";
12
+ import { apiTemplate } from "../templates/project-templates/api/api.js";
12
13
  const RESET = "\x1b[0m";
13
14
  const BOLD = "\x1b[1m";
14
15
  const DIM = "\x1b[2m";
@@ -68,19 +69,6 @@ function printInfo(message) {
68
69
  const info = BRIGHT_BLUE + "›" + RESET;
69
70
  console.log(` ${info} ${DIM + message + RESET}`);
70
71
  }
71
- function formatTemplate(template, vars) {
72
- let result = template;
73
- for (const [key, value] of Object.entries(vars)) {
74
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
75
- }
76
- return result;
77
- }
78
- function toKebabCase(str) {
79
- return str
80
- .replace(/([a-z])([A-Z])/g, "$1-$2")
81
- .replace(/[\s_]+/g, "-")
82
- .toLowerCase();
83
- }
84
72
  function getNpmPackageName(name) {
85
73
  const kebab = toKebabCase(name);
86
74
  return kebab.startsWith("@") ? kebab : kebab.replace(/^emberkit-/, "");
@@ -116,7 +104,7 @@ export async function create(options) {
116
104
  const templateFiles = template.files;
117
105
  for (const [filePath, content] of Object.entries(templateFiles)) {
118
106
  const fullPath = join(targetDir, filePath);
119
- const dir = join(targetDir, filePath.split("/").slice(0, -1).join("/"));
107
+ const dir = dirname(fullPath);
120
108
  if (!existsSync(dir)) {
121
109
  mkdirSync(dir, { recursive: true });
122
110
  }
@@ -0,0 +1,12 @@
1
+ export const actionTemplate = `import type { ActionFunction, LoaderResult } from '@emberkit/core';
2
+
3
+ export const action: ActionFunction = async ({ params, request }) => {
4
+ const formData = await request.formData();
5
+
6
+ return {
7
+ data: {
8
+ success: true,
9
+ },
10
+ } as LoaderResult<unknown>;
11
+ };
12
+ `;
@@ -0,0 +1,20 @@
1
+ export const apiRouteTemplate = `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
2
+
3
+ export const GET: LoaderFunction = async ({ params, query, request }) => {
4
+ return {
5
+ data: {
6
+ message: 'Hello from API',
7
+ },
8
+ } as LoaderResult<unknown>;
9
+ };
10
+
11
+ export const POST: LoaderFunction = async ({ request }) => {
12
+ const body = await request.json();
13
+
14
+ return {
15
+ data: {
16
+ received: body,
17
+ },
18
+ } as LoaderResult<unknown>;
19
+ };
20
+ `;
@@ -0,0 +1,14 @@
1
+ export const componentTemplate = `interface {{name}}Props {
2
+ className?: string;
3
+ }
4
+
5
+ const {{name}} = ({ className = '' }: {{name}}Props) => {
6
+ return (
7
+ <div className={className}>
8
+ {{name}} component
9
+ </div>
10
+ );
11
+ };
12
+
13
+ export default {{name}};
14
+ `;
@@ -0,0 +1,13 @@
1
+ export const configTemplate = `import { defineConfig } from '@emberkit/core';
2
+
3
+ export default defineConfig({
4
+ mode: 'hybrid',
5
+ server: {
6
+ port: 3000,
7
+ },
8
+ build: {
9
+ outDir: 'dist',
10
+ target: 'esnext',
11
+ },
12
+ });
13
+ `;
@@ -0,0 +1,21 @@
1
+ export const contextTemplate = `import { createContext, useContext } from '@emberkit/core';
2
+
3
+ interface {{name}}Context {
4
+ // Define your context shape
5
+ value: string;
6
+ }
7
+
8
+ const {{name}}Context = createContext<{{name}}Context>({
9
+ value: 'default',
10
+ });
11
+
12
+ // Provider usage:
13
+ // <{{name}}Context.Provider value={{ value: 'hello' }}>
14
+ // {children}
15
+ // </{{name}}Context.Provider>
16
+
17
+ // Consumer usage:
18
+ // const ctx = useContext({{name}}Context);
19
+
20
+ export { {{name}}Context };
21
+ `;
@@ -0,0 +1,15 @@
1
+ export const indexTemplate = `import { render } from '@emberkit/core';
2
+ import { routes } from 'virtual:emberkit-routes';
3
+ import App from './routes/_layout';
4
+ import './styles.css';
5
+
6
+ const root = document.getElementById('app');
7
+
8
+ if (root) {
9
+ try {
10
+ render(App, root, { routes });
11
+ } catch (error) {
12
+ console.error('[entry] Render error:', error);
13
+ }
14
+ }
15
+ `;
@@ -0,0 +1,17 @@
1
+ export const errorBoundaryTemplate = `import type { RouteComponent } from '@emberkit/core';
2
+
3
+ interface {{name}}ErrorProps {
4
+ error: Error;
5
+ }
6
+
7
+ const {{name}}Error: RouteComponent<{{name}}ErrorProps> = ({ error }) => {
8
+ return (
9
+ <div className="error-boundary">
10
+ <h2>Something went wrong</h2>
11
+ <p>{error.message}</p>
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export default {{name}}Error;
17
+ `;
@@ -0,0 +1,59 @@
1
+ export const formTemplate = `import { signal } from '@emberkit/core';
2
+
3
+ const {{name}}Form = () => {
4
+ const email = signal('');
5
+ const password = signal('');
6
+ const error = signal<string | null>(null);
7
+ const loading = signal(false);
8
+
9
+ const handleSubmit = async (e: Event) => {
10
+ e.preventDefault();
11
+ error.value = null;
12
+ loading.value = true;
13
+
14
+ try {
15
+ const response = await fetch('/api/auth/login', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({
19
+ email: email.value,
20
+ password: password.value,
21
+ }),
22
+ });
23
+
24
+ if (!response.ok) {
25
+ throw new Error('Login failed');
26
+ }
27
+
28
+ // Handle success
29
+ } catch (err) {
30
+ error.value = err instanceof Error ? err.message : 'Unknown error';
31
+ } finally {
32
+ loading.value = false;
33
+ }
34
+ };
35
+
36
+ return (
37
+ <form onSubmit={handleSubmit}>
38
+ <input
39
+ type="email"
40
+ value={email.value}
41
+ onInput={(e) => { email.value = e.currentTarget.value; }}
42
+ placeholder="Email"
43
+ />
44
+ <input
45
+ type="password"
46
+ value={password.value}
47
+ onInput={(e) => { password.value = e.currentTarget.value; }}
48
+ placeholder="Password"
49
+ />
50
+ {error.value && <p className="text-red-500">{error.value}</p>}
51
+ <button type="submit" disabled={loading.value}>
52
+ {loading.value ? 'Loading...' : 'Submit'}
53
+ </button>
54
+ </form>
55
+ );
56
+ };
57
+
58
+ export default {{name}}Form;
59
+ `;
@@ -0,0 +1,16 @@
1
+ export const layoutTemplate = `import type { RouteComponent } from '@emberkit/core';
2
+
3
+ const {{name}}Layout: RouteComponent = ({ children }) => {
4
+ return (
5
+ <div>
6
+ <header>
7
+ <nav>{{name}} Navigation</nav>
8
+ </header>
9
+ <main>{children}</main>
10
+ <footer>Footer</footer>
11
+ </div>
12
+ );
13
+ };
14
+
15
+ export default {{name}}Layout;
16
+ `;
@@ -0,0 +1,9 @@
1
+ export const layoutRoutesTemplate = `// EmberKit uses file-based routing.
2
+ // Routes are automatically discovered from the src/routes directory.
3
+ // - src/routes/index.tsx → /
4
+ // - src/routes/about.tsx → /about
5
+ // - src/routes/[slug].tsx → /:slug
6
+ // - src/routes/[...rest].tsx → catch-all
7
+
8
+ export {};
9
+ `;
@@ -0,0 +1,10 @@
1
+ export const loaderTemplate = `import type { LoaderFunction, LoaderResult } from '@emberkit/core';
2
+
3
+ export const loader: LoaderFunction = async ({ params, query, request }) => {
4
+ return {
5
+ data: {
6
+ // Add your data here
7
+ },
8
+ } as LoaderResult<unknown>;
9
+ };
10
+ `;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Shared builders for project template boilerplate.
3
+ * Each template composes from these instead of duplicating identical config files.
4
+ */
5
+ export function buildPackageJson(options = {}) {
6
+ const { hasTailwind = false, hasUI = false } = options;
7
+ const deps = { "@emberkit/core": "^0.2.4" };
8
+ if (hasUI)
9
+ deps["@emberkit/ui"] = "^0.2.3";
10
+ const devDeps = {
11
+ "@emberkit/cli": "^0.2.4",
12
+ typescript: "^5.7.0",
13
+ vite: "^6.0.0",
14
+ };
15
+ if (hasTailwind) {
16
+ devDeps["tailwindcss"] = "^4.0.0";
17
+ devDeps["@tailwindcss/vite"] = "^4.0.0";
18
+ }
19
+ return JSON.stringify({
20
+ name: "{{name}}",
21
+ version: "0.1.0",
22
+ private: true,
23
+ type: "module",
24
+ scripts: {
25
+ dev: "emberkit dev",
26
+ build: "emberkit build",
27
+ preview: "emberkit preview",
28
+ lint: "eslint src --ext .ts,.tsx",
29
+ format: 'prettier --write "src/**/*.{ts,tsx}"',
30
+ },
31
+ dependencies: deps,
32
+ devDependencies: devDeps,
33
+ }, null, 2);
34
+ }
35
+ export function buildTsConfig(hasPaths = true) {
36
+ const pathsEntry = hasPaths
37
+ ? `,
38
+ "paths": {
39
+ "@/*": ["./src/*"]
40
+ }`
41
+ : "";
42
+ return `{
43
+ "compilerOptions": {
44
+ "target": "ES2022",
45
+ "module": "ESNext",
46
+ "moduleResolution": "bundler",
47
+ "jsx": "react-jsx",
48
+ "jsxImportSource": "@emberkit/core",
49
+ "strict": true,
50
+ "esModuleInterop": true,
51
+ "skipLibCheck": true,
52
+ "forceConsistentCasingInFileNames": true,
53
+ "resolveJsonModule": true,
54
+ "isolatedModules": true,
55
+ "noEmit": true,
56
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]${pathsEntry}
57
+ },
58
+ "include": ["src"],
59
+ "exclude": ["node_modules", "dist"]
60
+ }`;
61
+ }
62
+ export function buildViteConfig(hasTailwind = false) {
63
+ if (hasTailwind) {
64
+ return `import { defineConfig } from 'vite';
65
+ import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
66
+ import tailwindcss from '@tailwindcss/vite';
67
+
68
+ export default defineConfig({
69
+ plugins: [emberkitVitePlugin(), tailwindcss()],
70
+ server: {
71
+ port: 3000,
72
+ host: 'localhost',
73
+ },
74
+ esbuild: {
75
+ jsxImportSource: '@emberkit/core',
76
+ },
77
+ });`;
78
+ }
79
+ return `import { defineConfig } from 'vite';
80
+ import { emberkitVitePlugin } from '@emberkit/core/vite-plugin';
81
+
82
+ export default defineConfig({
83
+ plugins: [emberkitVitePlugin()],
84
+ server: {
85
+ port: 3000,
86
+ host: 'localhost',
87
+ },
88
+ esbuild: {
89
+ jsxImportSource: '@emberkit/core',
90
+ },
91
+ });`;
92
+ }
93
+ export function buildIndexHtml(options = {}) {
94
+ const { title = "{{name}}", fonts = [] } = options;
95
+ const fontLinks = fonts.length > 0
96
+ ? `\n <link rel="preconnect" href="https://fonts.googleapis.com">
97
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
98
+ ${fonts.map((href) => ` <link href="${href}" rel="stylesheet">`).join("\n")}`
99
+ : "";
100
+ return `<!DOCTYPE html>
101
+ <html lang="en">
102
+ <head>
103
+ <meta charset="UTF-8">
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
+ <title>${title}</title>${fontLinks}
106
+ </head>
107
+ <body id="app">
108
+ <script type="module" src="/src/index.tsx"></script>
109
+ </body>
110
+ </html>`;
111
+ }
112
+ export function buildEntryFile(options = {}) {
113
+ const { hasLayout = false, hasCss = false } = options;
114
+ const appImport = hasLayout
115
+ ? `import App from './routes/_layout';`
116
+ : `import App from './routes/index';`;
117
+ const cssImport = hasCss ? `\nimport './styles.css';` : "";
118
+ return `import { render } from '@emberkit/core';
119
+ import { routes } from 'virtual:emberkit-routes';
120
+ ${appImport}${cssImport}
121
+
122
+ const root = document.getElementById('app');
123
+
124
+ if (root) {
125
+ try {
126
+ render(App, root, { routes });
127
+ } catch (error) {
128
+ console.error('[entry] Render error:', error);
129
+ }
130
+ }`;
131
+ }
132
+ export const GITIGNORE = `node_modules/
133
+ dist/
134
+ .env
135
+ .env.local
136
+ *.local
137
+ .DS_Store
138
+ *.tsbuildinfo
139
+ `;