@beatzball/create-litro 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/CHANGELOG.md +19 -0
- package/LICENSE +185 -0
- package/README.md +89 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts +4 -0
- package/dist/recipes/11ty-blog/recipe.config.d.ts.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.js +9 -0
- package/dist/recipes/11ty-blog/recipe.config.js.map +1 -0
- package/dist/recipes/11ty-blog/recipe.config.ts +11 -0
- package/dist/recipes/11ty-blog/template/app.ts +18 -0
- package/dist/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/dist/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/dist/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/dist/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/dist/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/dist/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/dist/recipes/11ty-blog/template/package.json +26 -0
- package/dist/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/dist/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/dist/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/dist/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/dist/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/dist/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/dist/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/dist/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/dist/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/dist/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/dist/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/dist/recipes/fullstack/recipe.config.d.ts +4 -0
- package/dist/recipes/fullstack/recipe.config.d.ts.map +1 -0
- package/dist/recipes/fullstack/recipe.config.js +8 -0
- package/dist/recipes/fullstack/recipe.config.js.map +1 -0
- package/dist/recipes/fullstack/recipe.config.ts +10 -0
- package/dist/recipes/fullstack/template/app.ts +20 -0
- package/dist/recipes/fullstack/template/nitro.config.ts +67 -0
- package/dist/recipes/fullstack/template/package.json +26 -0
- package/dist/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/dist/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/dist/recipes/fullstack/template/pages/index.ts +43 -0
- package/dist/recipes/fullstack/template/public/.gitkeep +0 -0
- package/dist/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/dist/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/dist/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/dist/recipes/fullstack/template/tsconfig.json +14 -0
- package/dist/recipes/fullstack/template/vite.config.ts +15 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +200 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/scaffold.d.ts +35 -0
- package/dist/src/scaffold.d.ts.map +1 -0
- package/dist/src/scaffold.js +166 -0
- package/dist/src/scaffold.js.map +1 -0
- package/dist/src/scaffold.test.d.ts +2 -0
- package/dist/src/scaffold.test.d.ts.map +1 -0
- package/dist/src/scaffold.test.js +204 -0
- package/dist/src/scaffold.test.js.map +1 -0
- package/dist/src/types.d.ts +23 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +28 -0
- package/recipes/11ty-blog/recipe.config.ts +11 -0
- package/recipes/11ty-blog/template/app.ts +18 -0
- package/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
- package/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
- package/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
- package/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
- package/recipes/11ty-blog/template/litro.recipe.json +7 -0
- package/recipes/11ty-blog/template/nitro.config.ts +60 -0
- package/recipes/11ty-blog/template/package.json +26 -0
- package/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
- package/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
- package/recipes/11ty-blog/template/pages/index.ts +58 -0
- package/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
- package/recipes/11ty-blog/template/public/.gitkeep +0 -0
- package/recipes/11ty-blog/template/server/api/hello.ts +6 -0
- package/recipes/11ty-blog/template/server/api/posts.ts +8 -0
- package/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
- package/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
- package/recipes/11ty-blog/template/tsconfig.json +14 -0
- package/recipes/11ty-blog/template/vite.config.ts +19 -0
- package/recipes/fullstack/recipe.config.ts +10 -0
- package/recipes/fullstack/template/app.ts +20 -0
- package/recipes/fullstack/template/nitro.config.ts +67 -0
- package/recipes/fullstack/template/package.json +26 -0
- package/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
- package/recipes/fullstack/template/pages/blog/index.ts +22 -0
- package/recipes/fullstack/template/pages/index.ts +43 -0
- package/recipes/fullstack/template/public/.gitkeep +0 -0
- package/recipes/fullstack/template/server/api/hello.ts +6 -0
- package/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
- package/recipes/fullstack/template/server/routes/[...].ts +59 -0
- package/recipes/fullstack/template/tsconfig.json +14 -0
- package/recipes/fullstack/template/vite.config.ts +15 -0
- package/src/index.ts +229 -0
- package/src/scaffold.test.ts +228 -0
- package/src/scaffold.ts +202 -0
- package/src/types.ts +26 -0
- package/tsconfig.json +10 -0
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scaffold.ts — Recipe-driven project scaffolding for create-litro.
|
|
3
|
+
*
|
|
4
|
+
* At runtime the compiled bin is dist/src/index.js and recipes live at
|
|
5
|
+
* dist/recipes/<name>/. This module resolves recipe directories relative to
|
|
6
|
+
* import.meta.url so it works regardless of CWD.
|
|
7
|
+
*
|
|
8
|
+
* No external dependencies — uses Node.js built-ins only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readdir, readFile, writeFile, mkdir, copyFile, stat } from 'node:fs/promises';
|
|
12
|
+
import { join, dirname, extname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import type { LitroRecipe } from './types.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface ScaffoldOptions {
|
|
21
|
+
projectName: string;
|
|
22
|
+
mode: 'ssg' | 'ssr';
|
|
23
|
+
recipeOptions?: Record<string, unknown>;
|
|
24
|
+
recipeVersion?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Internal helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the absolute path to the dist/recipes/ directory.
|
|
33
|
+
*
|
|
34
|
+
* At runtime the compiled layout is:
|
|
35
|
+
* dist/
|
|
36
|
+
* src/scaffold.js ← this file
|
|
37
|
+
* recipes/<name>/ ← recipe configs + templates
|
|
38
|
+
*
|
|
39
|
+
* So we go one level up from the src/ output dir to find recipes/.
|
|
40
|
+
*/
|
|
41
|
+
function recipesDir(): string {
|
|
42
|
+
// import.meta.url points to the current compiled file (dist/src/scaffold.js).
|
|
43
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
44
|
+
return join(dirname(thisFile), '..', 'recipes');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** File extensions treated as binary — copied byte-for-byte, no interpolation. */
|
|
48
|
+
const BINARY_EXTENSIONS = new Set([
|
|
49
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp',
|
|
50
|
+
'.svg', '.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
51
|
+
'.pdf', '.zip', '.gz', '.tar',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
function isBinary(filePath: string): boolean {
|
|
55
|
+
return BINARY_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Replace `{{key}}` placeholders in `text` with values from `vars`.
|
|
60
|
+
* Unknown keys are left unchanged (the `{{key}}` literal remains).
|
|
61
|
+
*/
|
|
62
|
+
function interpolate(text: string, vars: Record<string, string>): string {
|
|
63
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => {
|
|
64
|
+
return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : `{{${key}}}`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the interpolation variable map from ScaffoldOptions.
|
|
70
|
+
*/
|
|
71
|
+
function buildVars(options: ScaffoldOptions): Record<string, string> {
|
|
72
|
+
const vars: Record<string, string> = {
|
|
73
|
+
projectName: options.projectName,
|
|
74
|
+
mode: options.mode,
|
|
75
|
+
recipeVersion: options.recipeVersion ?? '0.0.0',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (options.recipeOptions) {
|
|
79
|
+
for (const [k, v] of Object.entries(options.recipeOptions)) {
|
|
80
|
+
vars[k] = String(v);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return vars;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively copy all files from `srcDir` to `destDir`, applying
|
|
89
|
+
* `{{placeholder}}` interpolation to text files.
|
|
90
|
+
*/
|
|
91
|
+
async function copyTemplate(
|
|
92
|
+
srcDir: string,
|
|
93
|
+
destDir: string,
|
|
94
|
+
vars: Record<string, string>,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const srcPath = join(srcDir, entry.name);
|
|
100
|
+
const destPath = join(destDir, entry.name);
|
|
101
|
+
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
await mkdir(destPath, { recursive: true });
|
|
104
|
+
await copyTemplate(srcPath, destPath, vars);
|
|
105
|
+
} else {
|
|
106
|
+
if (isBinary(entry.name)) {
|
|
107
|
+
await copyFile(srcPath, destPath);
|
|
108
|
+
} else {
|
|
109
|
+
const raw = await readFile(srcPath, 'utf8');
|
|
110
|
+
const interpolated = interpolate(raw, vars);
|
|
111
|
+
await writeFile(destPath, interpolated, 'utf8');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Public API
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Return `LitroRecipe` objects for all recipe directories found under
|
|
123
|
+
* dist/recipes/. Each recipe must have a `recipe.config.js` file that
|
|
124
|
+
* exports a default `LitroRecipe`.
|
|
125
|
+
*/
|
|
126
|
+
export async function listRecipes(): Promise<LitroRecipe[]> {
|
|
127
|
+
const dir = recipesDir();
|
|
128
|
+
let entries: { name: string; isDirectory(): boolean }[];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
132
|
+
} catch {
|
|
133
|
+
// No recipes directory — return empty list.
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const recipes: LitroRecipe[] = [];
|
|
138
|
+
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isDirectory()) continue;
|
|
141
|
+
|
|
142
|
+
const configPath = join(dir, entry.name, 'recipe.config.js');
|
|
143
|
+
try {
|
|
144
|
+
// Dynamic import resolves relative to CWD when given an absolute path.
|
|
145
|
+
const mod = await import(configPath) as { default: LitroRecipe };
|
|
146
|
+
recipes.push(mod.default);
|
|
147
|
+
} catch {
|
|
148
|
+
// Skip invalid/missing recipe configs silently.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return recipes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load a single recipe by name. Returns null if not found.
|
|
157
|
+
*/
|
|
158
|
+
export async function loadRecipe(name: string): Promise<LitroRecipe | null> {
|
|
159
|
+
const configPath = join(recipesDir(), name, 'recipe.config.js');
|
|
160
|
+
try {
|
|
161
|
+
const mod = await import(configPath) as { default: LitroRecipe };
|
|
162
|
+
return mod.default;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scaffold a project from a recipe into `targetDir`.
|
|
170
|
+
*
|
|
171
|
+
* @param recipeName The recipe directory name (e.g. "fullstack").
|
|
172
|
+
* @param options Scaffold options (projectName, mode, etc.).
|
|
173
|
+
* @param targetDir Absolute path to the target project directory.
|
|
174
|
+
*/
|
|
175
|
+
export async function scaffold(
|
|
176
|
+
recipeName: string,
|
|
177
|
+
options: ScaffoldOptions,
|
|
178
|
+
targetDir: string,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const templateDir = join(recipesDir(), recipeName, 'template');
|
|
181
|
+
|
|
182
|
+
// Verify the template directory exists.
|
|
183
|
+
try {
|
|
184
|
+
const s = await stat(templateDir);
|
|
185
|
+
if (!s.isDirectory()) {
|
|
186
|
+
throw new Error(`Recipe template path is not a directory: ${templateDir}`);
|
|
187
|
+
}
|
|
188
|
+
} catch (err: unknown) {
|
|
189
|
+
const nodeErr = err as NodeJS.ErrnoException;
|
|
190
|
+
if (nodeErr.code === 'ENOENT') {
|
|
191
|
+
throw new Error(`Recipe "${recipeName}" not found (looked for ${templateDir})`);
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Create the target directory.
|
|
197
|
+
await mkdir(targetDir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
// Build interpolation variables and copy all files.
|
|
200
|
+
const vars = buildVars(options);
|
|
201
|
+
await copyTemplate(templateDir, targetDir, vars);
|
|
202
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The shape of a recipe definition
|
|
2
|
+
export interface LitroRecipe {
|
|
3
|
+
name: string; // CLI identifier, e.g. "11ty-blog"
|
|
4
|
+
displayName: string; // Shown in prompt list
|
|
5
|
+
description: string;
|
|
6
|
+
mode: 'ssg' | 'ssr' | 'both'; // "both" = user is prompted to choose
|
|
7
|
+
options?: RecipeOption[];
|
|
8
|
+
contentLayer?: string; // Relative path to content-layer entry, if any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RecipeOption {
|
|
12
|
+
key: string;
|
|
13
|
+
prompt: string;
|
|
14
|
+
type: 'select' | 'confirm' | 'text';
|
|
15
|
+
choices?: string[];
|
|
16
|
+
default?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Written to the root of every scaffolded project
|
|
20
|
+
export interface LitroRecipeManifest {
|
|
21
|
+
recipe: string; // e.g. "11ty-blog"
|
|
22
|
+
version: string; // recipe semver at time of scaffold
|
|
23
|
+
mode: 'ssg' | 'ssr';
|
|
24
|
+
contentDir?: string; // e.g. "content/blog" — configurable
|
|
25
|
+
options: Record<string, unknown>; // resolved recipe option values
|
|
26
|
+
}
|
package/tsconfig.json
ADDED