@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +185 -0
  3. package/README.md +89 -0
  4. package/dist/recipes/11ty-blog/recipe.config.d.ts +4 -0
  5. package/dist/recipes/11ty-blog/recipe.config.d.ts.map +1 -0
  6. package/dist/recipes/11ty-blog/recipe.config.js +9 -0
  7. package/dist/recipes/11ty-blog/recipe.config.js.map +1 -0
  8. package/dist/recipes/11ty-blog/recipe.config.ts +11 -0
  9. package/dist/recipes/11ty-blog/template/app.ts +18 -0
  10. package/dist/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
  11. package/dist/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
  12. package/dist/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
  13. package/dist/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
  14. package/dist/recipes/11ty-blog/template/litro.recipe.json +7 -0
  15. package/dist/recipes/11ty-blog/template/nitro.config.ts +60 -0
  16. package/dist/recipes/11ty-blog/template/package.json +26 -0
  17. package/dist/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
  18. package/dist/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
  19. package/dist/recipes/11ty-blog/template/pages/index.ts +58 -0
  20. package/dist/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
  21. package/dist/recipes/11ty-blog/template/public/.gitkeep +0 -0
  22. package/dist/recipes/11ty-blog/template/server/api/hello.ts +6 -0
  23. package/dist/recipes/11ty-blog/template/server/api/posts.ts +8 -0
  24. package/dist/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
  25. package/dist/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
  26. package/dist/recipes/11ty-blog/template/tsconfig.json +14 -0
  27. package/dist/recipes/11ty-blog/template/vite.config.ts +19 -0
  28. package/dist/recipes/fullstack/recipe.config.d.ts +4 -0
  29. package/dist/recipes/fullstack/recipe.config.d.ts.map +1 -0
  30. package/dist/recipes/fullstack/recipe.config.js +8 -0
  31. package/dist/recipes/fullstack/recipe.config.js.map +1 -0
  32. package/dist/recipes/fullstack/recipe.config.ts +10 -0
  33. package/dist/recipes/fullstack/template/app.ts +20 -0
  34. package/dist/recipes/fullstack/template/nitro.config.ts +67 -0
  35. package/dist/recipes/fullstack/template/package.json +26 -0
  36. package/dist/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
  37. package/dist/recipes/fullstack/template/pages/blog/index.ts +22 -0
  38. package/dist/recipes/fullstack/template/pages/index.ts +43 -0
  39. package/dist/recipes/fullstack/template/public/.gitkeep +0 -0
  40. package/dist/recipes/fullstack/template/server/api/hello.ts +6 -0
  41. package/dist/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
  42. package/dist/recipes/fullstack/template/server/routes/[...].ts +59 -0
  43. package/dist/recipes/fullstack/template/tsconfig.json +14 -0
  44. package/dist/recipes/fullstack/template/vite.config.ts +15 -0
  45. package/dist/src/index.d.ts +17 -0
  46. package/dist/src/index.d.ts.map +1 -0
  47. package/dist/src/index.js +200 -0
  48. package/dist/src/index.js.map +1 -0
  49. package/dist/src/scaffold.d.ts +35 -0
  50. package/dist/src/scaffold.d.ts.map +1 -0
  51. package/dist/src/scaffold.js +166 -0
  52. package/dist/src/scaffold.js.map +1 -0
  53. package/dist/src/scaffold.test.d.ts +2 -0
  54. package/dist/src/scaffold.test.d.ts.map +1 -0
  55. package/dist/src/scaffold.test.js +204 -0
  56. package/dist/src/scaffold.test.js.map +1 -0
  57. package/dist/src/types.d.ts +23 -0
  58. package/dist/src/types.d.ts.map +1 -0
  59. package/dist/src/types.js +2 -0
  60. package/dist/src/types.js.map +1 -0
  61. package/dist/tsconfig.tsbuildinfo +1 -0
  62. package/package.json +28 -0
  63. package/recipes/11ty-blog/recipe.config.ts +11 -0
  64. package/recipes/11ty-blog/template/app.ts +18 -0
  65. package/recipes/11ty-blog/template/content/_data/metadata.js +10 -0
  66. package/recipes/11ty-blog/template/content/blog/blog.11tydata.json +1 -0
  67. package/recipes/11ty-blog/template/content/blog/getting-started/index.md +66 -0
  68. package/recipes/11ty-blog/template/content/blog/hello-world.md +39 -0
  69. package/recipes/11ty-blog/template/litro.recipe.json +7 -0
  70. package/recipes/11ty-blog/template/nitro.config.ts +60 -0
  71. package/recipes/11ty-blog/template/package.json +26 -0
  72. package/recipes/11ty-blog/template/pages/blog/[slug].ts +65 -0
  73. package/recipes/11ty-blog/template/pages/blog/index.ts +43 -0
  74. package/recipes/11ty-blog/template/pages/index.ts +58 -0
  75. package/recipes/11ty-blog/template/pages/tags/[tag].ts +53 -0
  76. package/recipes/11ty-blog/template/public/.gitkeep +0 -0
  77. package/recipes/11ty-blog/template/server/api/hello.ts +6 -0
  78. package/recipes/11ty-blog/template/server/api/posts.ts +8 -0
  79. package/recipes/11ty-blog/template/server/middleware/vite-dev.ts +29 -0
  80. package/recipes/11ty-blog/template/server/routes/[...].ts +55 -0
  81. package/recipes/11ty-blog/template/tsconfig.json +14 -0
  82. package/recipes/11ty-blog/template/vite.config.ts +19 -0
  83. package/recipes/fullstack/recipe.config.ts +10 -0
  84. package/recipes/fullstack/template/app.ts +20 -0
  85. package/recipes/fullstack/template/nitro.config.ts +67 -0
  86. package/recipes/fullstack/template/package.json +26 -0
  87. package/recipes/fullstack/template/pages/blog/[slug].ts +43 -0
  88. package/recipes/fullstack/template/pages/blog/index.ts +22 -0
  89. package/recipes/fullstack/template/pages/index.ts +43 -0
  90. package/recipes/fullstack/template/public/.gitkeep +0 -0
  91. package/recipes/fullstack/template/server/api/hello.ts +6 -0
  92. package/recipes/fullstack/template/server/middleware/vite-dev.ts +31 -0
  93. package/recipes/fullstack/template/server/routes/[...].ts +59 -0
  94. package/recipes/fullstack/template/tsconfig.json +14 -0
  95. package/recipes/fullstack/template/vite.config.ts +15 -0
  96. package/src/index.ts +229 -0
  97. package/src/scaffold.test.ts +228 -0
  98. package/src/scaffold.ts +202 -0
  99. package/src/types.ts +26 -0
  100. package/tsconfig.json +10 -0
@@ -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
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "./dist",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*", "recipes/**/*.ts"],
9
+ "exclude": ["node_modules", "dist", "recipes/**/template/**"]
10
+ }