create-jjlabs-app 0.1.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/index.js +427 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
|
|
6
|
+
// src/prompts.ts
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
|
|
9
|
+
// src/utils/validate.ts
|
|
10
|
+
import fs from "fs-extra";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var VALID_NAME_REGEX = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
|
|
13
|
+
function validateProjectName(name) {
|
|
14
|
+
if (!name || name.trim().length === 0) {
|
|
15
|
+
return { valid: false, message: "Project name is required." };
|
|
16
|
+
}
|
|
17
|
+
if (!VALID_NAME_REGEX.test(name)) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
message: "Project name must start and end with a lowercase letter or number, and can only contain lowercase letters, numbers, hyphens, dots, and underscores."
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
}
|
|
25
|
+
async function checkDirectoryAvailable(targetDir) {
|
|
26
|
+
const resolved = path.resolve(targetDir);
|
|
27
|
+
if (await fs.pathExists(resolved)) {
|
|
28
|
+
return {
|
|
29
|
+
available: false,
|
|
30
|
+
message: `Directory "${resolved}" already exists.`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { available: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/prompts.ts
|
|
37
|
+
async function getUserChoices(argProjectName) {
|
|
38
|
+
const onCancel = () => {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
};
|
|
41
|
+
const response = await prompts(
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
type: argProjectName ? null : "text",
|
|
45
|
+
name: "projectName",
|
|
46
|
+
message: "Project name:",
|
|
47
|
+
initial: "my-app",
|
|
48
|
+
validate: (value) => {
|
|
49
|
+
const result = validateProjectName(value);
|
|
50
|
+
return result.valid ? true : result.message ?? "Invalid name";
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "select",
|
|
55
|
+
name: "layout",
|
|
56
|
+
message: "Choose a layout:",
|
|
57
|
+
choices: [
|
|
58
|
+
{
|
|
59
|
+
title: "Sidebar",
|
|
60
|
+
value: "sidebar",
|
|
61
|
+
description: "Dashboard-style sidebar navigation"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: "Standard",
|
|
65
|
+
value: "standard",
|
|
66
|
+
description: "Header + footer layout"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
{ onCancel }
|
|
72
|
+
);
|
|
73
|
+
const projectName = argProjectName ?? response.projectName;
|
|
74
|
+
if (!projectName || !response.layout) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
projectName,
|
|
79
|
+
layout: response.layout
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/scaffold.ts
|
|
84
|
+
import path7 from "path";
|
|
85
|
+
|
|
86
|
+
// src/steps/clone.ts
|
|
87
|
+
import { execFileSync } from "child_process";
|
|
88
|
+
|
|
89
|
+
// src/config/constants.ts
|
|
90
|
+
var TEMPLATE_REPO = "jjlabsio/starter";
|
|
91
|
+
var APP_DIR = "apps/app";
|
|
92
|
+
var SRC_DIR = `${APP_DIR}/src`;
|
|
93
|
+
var COMPONENTS_DIR = `${SRC_DIR}/components`;
|
|
94
|
+
var AUTHENTICATED_DIR = `${SRC_DIR}/app/(authenticated)`;
|
|
95
|
+
var SIDEBAR_GROUP_DIR = `${AUTHENTICATED_DIR}/(sidebar)`;
|
|
96
|
+
var STANDARD_GROUP_DIR = `${AUTHENTICATED_DIR}/(standard)`;
|
|
97
|
+
var ROOT_PAGE = `${SRC_DIR}/app/page.tsx`;
|
|
98
|
+
var SIGN_IN_PAGE = `${SRC_DIR}/app/(public)/sign-in/page.tsx`;
|
|
99
|
+
|
|
100
|
+
// src/utils/logger.ts
|
|
101
|
+
import pc from "picocolors";
|
|
102
|
+
var logger = {
|
|
103
|
+
info(message) {
|
|
104
|
+
console.log(pc.cyan(message));
|
|
105
|
+
},
|
|
106
|
+
success(message) {
|
|
107
|
+
console.log(pc.green(message));
|
|
108
|
+
},
|
|
109
|
+
warn(message) {
|
|
110
|
+
console.log(pc.yellow(message));
|
|
111
|
+
},
|
|
112
|
+
error(message) {
|
|
113
|
+
console.error(pc.red(message));
|
|
114
|
+
},
|
|
115
|
+
step(message) {
|
|
116
|
+
console.log(pc.bold(pc.blue(`> ${message}`)));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/steps/clone.ts
|
|
121
|
+
async function cloneTemplate(targetDir) {
|
|
122
|
+
logger.step("Cloning template...");
|
|
123
|
+
try {
|
|
124
|
+
await cloneWithDegit(targetDir);
|
|
125
|
+
} catch {
|
|
126
|
+
logger.warn("degit failed, falling back to git clone...");
|
|
127
|
+
cloneWithGit(targetDir);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function cloneWithDegit(targetDir) {
|
|
131
|
+
const degit = (await import("degit")).default;
|
|
132
|
+
const emitter = degit(TEMPLATE_REPO, { cache: false, force: true });
|
|
133
|
+
await emitter.clone(targetDir);
|
|
134
|
+
}
|
|
135
|
+
function cloneWithGit(targetDir) {
|
|
136
|
+
execFileSync(
|
|
137
|
+
"git",
|
|
138
|
+
["clone", "--depth", "1", `https://github.com/${TEMPLATE_REPO}.git`, targetDir],
|
|
139
|
+
{ stdio: "pipe" }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/steps/clean-layout.ts
|
|
144
|
+
import path2 from "path";
|
|
145
|
+
|
|
146
|
+
// src/config/layout-files.ts
|
|
147
|
+
var SIDEBAR_FILES = {
|
|
148
|
+
routeGroupDir: SIDEBAR_GROUP_DIR,
|
|
149
|
+
components: [
|
|
150
|
+
`${COMPONENTS_DIR}/app-sidebar.tsx`,
|
|
151
|
+
`${COMPONENTS_DIR}/nav-main.tsx`,
|
|
152
|
+
`${COMPONENTS_DIR}/nav-secondary.tsx`,
|
|
153
|
+
`${COMPONENTS_DIR}/nav-user.tsx`,
|
|
154
|
+
`${COMPONENTS_DIR}/nav-documents.tsx`,
|
|
155
|
+
`${COMPONENTS_DIR}/site-header.tsx`,
|
|
156
|
+
`${COMPONENTS_DIR}/page-container.tsx`,
|
|
157
|
+
`${COMPONENTS_DIR}/section-cards.tsx`,
|
|
158
|
+
`${COMPONENTS_DIR}/chart-area-interactive.tsx`
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
var STANDARD_FILES = {
|
|
162
|
+
routeGroupDir: STANDARD_GROUP_DIR,
|
|
163
|
+
components: [
|
|
164
|
+
`${COMPONENTS_DIR}/app-header.tsx`,
|
|
165
|
+
`${COMPONENTS_DIR}/app-footer.tsx`,
|
|
166
|
+
`${COMPONENTS_DIR}/mobile-nav.tsx`,
|
|
167
|
+
`${COMPONENTS_DIR}/user-menu.tsx`
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
var FILE_MAP = {
|
|
171
|
+
sidebar: STANDARD_FILES,
|
|
172
|
+
standard: SIDEBAR_FILES
|
|
173
|
+
};
|
|
174
|
+
function getFilesToRemove(layout) {
|
|
175
|
+
return FILE_MAP[layout];
|
|
176
|
+
}
|
|
177
|
+
function getSelectedLayoutDir(layout) {
|
|
178
|
+
return layout === "sidebar" ? SIDEBAR_GROUP_DIR : STANDARD_GROUP_DIR;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/utils/fs.ts
|
|
182
|
+
import fs2 from "fs-extra";
|
|
183
|
+
async function removeDir(dirPath) {
|
|
184
|
+
if (await fs2.pathExists(dirPath)) {
|
|
185
|
+
await fs2.remove(dirPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function removeFile(filePath) {
|
|
189
|
+
if (await fs2.pathExists(filePath)) {
|
|
190
|
+
await fs2.remove(filePath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function readFile(filePath) {
|
|
194
|
+
return fs2.readFile(filePath, "utf-8");
|
|
195
|
+
}
|
|
196
|
+
async function writeFile(filePath, content) {
|
|
197
|
+
await fs2.writeFile(filePath, content, "utf-8");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/steps/clean-layout.ts
|
|
201
|
+
async function cleanLayout(projectDir, layout) {
|
|
202
|
+
const filesToRemove = getFilesToRemove(layout);
|
|
203
|
+
logger.step(`Removing unused ${layout === "sidebar" ? "standard" : "sidebar"} layout files...`);
|
|
204
|
+
await removeDir(path2.join(projectDir, filesToRemove.routeGroupDir));
|
|
205
|
+
await Promise.all(
|
|
206
|
+
filesToRemove.components.map(
|
|
207
|
+
(component) => removeFile(path2.join(projectDir, component))
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/steps/clean-auth-duplication.ts
|
|
213
|
+
import path3 from "path";
|
|
214
|
+
async function cleanAuthDuplication(projectDir, layout) {
|
|
215
|
+
const layoutDir = getSelectedLayoutDir(layout);
|
|
216
|
+
const layoutFile = path3.join(projectDir, layoutDir, "layout.tsx");
|
|
217
|
+
logger.step("Removing duplicate auth check from selected layout...");
|
|
218
|
+
const content = await readFile(layoutFile);
|
|
219
|
+
const transformed = transformLayout(content);
|
|
220
|
+
await writeFile(layoutFile, transformed);
|
|
221
|
+
}
|
|
222
|
+
function transformLayout(content) {
|
|
223
|
+
let result = content;
|
|
224
|
+
result = result.replace(
|
|
225
|
+
/\s*if\s*\(!session\)\s*\{[\s\S]*?redirect\([^)]*\);\s*\}\s*/,
|
|
226
|
+
"\n\n"
|
|
227
|
+
);
|
|
228
|
+
result = result.replace(
|
|
229
|
+
/import\s*\{([^}]*)\}\s*from\s*["']next\/navigation["'];\s*\n/,
|
|
230
|
+
(_match, imports) => {
|
|
231
|
+
const remaining = imports.split(",").map((s) => s.trim()).filter((s) => s !== "" && s !== "redirect");
|
|
232
|
+
if (remaining.length === 0) return "";
|
|
233
|
+
return `import { ${remaining.join(", ")} } from "next/navigation";
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
result = result.replace(/\bsession\.user\b/g, "session!.user");
|
|
238
|
+
result = result.replace(
|
|
239
|
+
/(const session = await auth\.api\.getSession\(\{[\s\S]*?\}\);)\n/,
|
|
240
|
+
"$1\n // session is already verified by (authenticated)/layout.tsx\n"
|
|
241
|
+
);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/steps/update-redirects.ts
|
|
246
|
+
import path4 from "path";
|
|
247
|
+
var REDIRECT_MAP = {
|
|
248
|
+
sidebar: "/dashboard",
|
|
249
|
+
standard: "/home"
|
|
250
|
+
};
|
|
251
|
+
async function updateRedirects(projectDir, layout) {
|
|
252
|
+
const targetPath = REDIRECT_MAP[layout];
|
|
253
|
+
if (layout === "sidebar") {
|
|
254
|
+
logger.step("Redirect paths already correct for sidebar layout.");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
logger.step(`Updating redirect paths to "${targetPath}"...`);
|
|
258
|
+
await Promise.all([
|
|
259
|
+
updateRootPage(projectDir, targetPath),
|
|
260
|
+
updateSignInPage(projectDir, targetPath)
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
async function updateRootPage(projectDir, targetPath) {
|
|
264
|
+
const filePath = path4.join(projectDir, ROOT_PAGE);
|
|
265
|
+
const content = await readFile(filePath);
|
|
266
|
+
const updated = content.replace(
|
|
267
|
+
/redirect\(["']\/dashboard["']\)/,
|
|
268
|
+
`redirect("${targetPath}")`
|
|
269
|
+
);
|
|
270
|
+
await writeFile(filePath, updated);
|
|
271
|
+
}
|
|
272
|
+
async function updateSignInPage(projectDir, targetPath) {
|
|
273
|
+
const filePath = path4.join(projectDir, SIGN_IN_PAGE);
|
|
274
|
+
const content = await readFile(filePath);
|
|
275
|
+
const updated = content.replace(
|
|
276
|
+
/callbackURL:\s*["']\/dashboard["']/,
|
|
277
|
+
`callbackURL: "${targetPath}"`
|
|
278
|
+
);
|
|
279
|
+
await writeFile(filePath, updated);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/steps/update-package-names.ts
|
|
283
|
+
import path5 from "path";
|
|
284
|
+
import fs3 from "fs-extra";
|
|
285
|
+
var PACKAGE_JSON_PATHS = [
|
|
286
|
+
"package.json",
|
|
287
|
+
"apps/app/package.json"
|
|
288
|
+
];
|
|
289
|
+
async function updatePackageNames(projectDir, projectName) {
|
|
290
|
+
logger.step("Updating package names...");
|
|
291
|
+
await Promise.all(
|
|
292
|
+
PACKAGE_JSON_PATHS.map(async (relativePath) => {
|
|
293
|
+
const filePath = path5.join(projectDir, relativePath);
|
|
294
|
+
if (!await fs3.pathExists(filePath)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const content = await fs3.readJson(filePath);
|
|
298
|
+
const updatedName = relativePath === "package.json" ? projectName : `@${projectName}/app`;
|
|
299
|
+
const { name: _, ...rest } = content;
|
|
300
|
+
await fs3.writeJson(
|
|
301
|
+
filePath,
|
|
302
|
+
{ name: updatedName, ...rest },
|
|
303
|
+
{ spaces: 2 }
|
|
304
|
+
);
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/steps/finalize.ts
|
|
310
|
+
import path6 from "path";
|
|
311
|
+
import { execSync } from "child_process";
|
|
312
|
+
import fs4 from "fs-extra";
|
|
313
|
+
async function finalize(projectDir) {
|
|
314
|
+
await removeGitDir(projectDir);
|
|
315
|
+
await copyEnvFile(projectDir);
|
|
316
|
+
await installDependencies(projectDir);
|
|
317
|
+
}
|
|
318
|
+
async function removeGitDir(projectDir) {
|
|
319
|
+
const gitDir = path6.join(projectDir, ".git");
|
|
320
|
+
if (await fs4.pathExists(gitDir)) {
|
|
321
|
+
logger.step("Removing .git directory...");
|
|
322
|
+
await fs4.remove(gitDir);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function copyEnvFile(projectDir) {
|
|
326
|
+
const envExample = path6.join(projectDir, ".env.example");
|
|
327
|
+
const envLocal = path6.join(projectDir, ".env");
|
|
328
|
+
if (await fs4.pathExists(envExample)) {
|
|
329
|
+
logger.step("Creating .env from .env.example...");
|
|
330
|
+
await fs4.copy(envExample, envLocal);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function installDependencies(projectDir) {
|
|
334
|
+
logger.step("Installing dependencies...");
|
|
335
|
+
try {
|
|
336
|
+
execSync("pnpm install", {
|
|
337
|
+
cwd: projectDir,
|
|
338
|
+
stdio: "inherit"
|
|
339
|
+
});
|
|
340
|
+
} catch {
|
|
341
|
+
logger.warn(
|
|
342
|
+
"Failed to install dependencies. Run 'pnpm install' manually."
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/scaffold.ts
|
|
348
|
+
async function scaffold(options) {
|
|
349
|
+
const { projectName, layout } = options;
|
|
350
|
+
const projectDir = path7.resolve(projectName);
|
|
351
|
+
logger.info(`
|
|
352
|
+
Creating ${projectName} with ${layout} layout...
|
|
353
|
+
`);
|
|
354
|
+
await cloneTemplate(projectDir);
|
|
355
|
+
await cleanLayout(projectDir, layout);
|
|
356
|
+
await cleanAuthDuplication(projectDir, layout);
|
|
357
|
+
await updateRedirects(projectDir, layout);
|
|
358
|
+
await updatePackageNames(projectDir, projectName);
|
|
359
|
+
await finalize(projectDir);
|
|
360
|
+
logger.success(`
|
|
361
|
+
Project "${projectName}" created successfully!
|
|
362
|
+
`);
|
|
363
|
+
logger.info("Next steps:");
|
|
364
|
+
logger.info(` cd ${projectName}`);
|
|
365
|
+
logger.info(" # Update .env with your credentials");
|
|
366
|
+
logger.info(" pnpm dev\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/index.ts
|
|
370
|
+
var require2 = createRequire(import.meta.url);
|
|
371
|
+
var { version: VERSION } = require2("../package.json");
|
|
372
|
+
async function main() {
|
|
373
|
+
const args = process.argv.slice(2);
|
|
374
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
375
|
+
console.log(VERSION);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
379
|
+
printHelp();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const argProjectName = args.find((arg) => !arg.startsWith("-"));
|
|
383
|
+
if (argProjectName) {
|
|
384
|
+
const validation = validateProjectName(argProjectName);
|
|
385
|
+
if (!validation.valid) {
|
|
386
|
+
logger.error(validation.message);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
const dirCheck = await checkDirectoryAvailable(argProjectName);
|
|
390
|
+
if (!dirCheck.available) {
|
|
391
|
+
logger.error(dirCheck.message);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const choices = await getUserChoices(argProjectName);
|
|
396
|
+
if (!choices) {
|
|
397
|
+
logger.error("Setup cancelled.");
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
if (!argProjectName) {
|
|
401
|
+
const dirCheck = await checkDirectoryAvailable(choices.projectName);
|
|
402
|
+
if (!dirCheck.available) {
|
|
403
|
+
logger.error(dirCheck.message);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
await scaffold(choices);
|
|
408
|
+
}
|
|
409
|
+
function printHelp() {
|
|
410
|
+
console.log(`
|
|
411
|
+
create-jjlabs-app [project-name]
|
|
412
|
+
|
|
413
|
+
Scaffold a new JJLabs app from the starter template.
|
|
414
|
+
|
|
415
|
+
Options:
|
|
416
|
+
-h, --help Show this help message
|
|
417
|
+
-v, --version Show version number
|
|
418
|
+
|
|
419
|
+
Examples:
|
|
420
|
+
npx create-jjlabs-app my-app
|
|
421
|
+
npx create-jjlabs-app
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
main().catch((error) => {
|
|
425
|
+
logger.error(error.message);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-jjlabs-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold a new JJLabs app from the starter template",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-jjlabs-app": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"degit": "^2.8.4",
|
|
21
|
+
"fs-extra": "^11.2.0",
|
|
22
|
+
"picocolors": "^1.1.1",
|
|
23
|
+
"prompts": "^2.4.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/fs-extra": "^11.0.4",
|
|
27
|
+
"@types/prompts": "^2.4.9",
|
|
28
|
+
"tsup": "^8.3.0",
|
|
29
|
+
"tsx": "^4.19.0",
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"vitest": "^2.1.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
}
|
|
36
|
+
}
|