stackloom-cli 1.0.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/cli.js +306 -0
  4. package/branding.json +8 -0
  5. package/package.json +72 -0
  6. package/src/__tests__/cli-smoke.test.js +46 -0
  7. package/src/blueprint/__tests__/blueprint.test.js +116 -0
  8. package/src/blueprint/blueprint.js +181 -0
  9. package/src/blueprint/default.blueprint.json +78 -0
  10. package/src/blueprint/index.js +10 -0
  11. package/src/blueprint/loader.js +101 -0
  12. package/src/blueprint/schema-kit.js +161 -0
  13. package/src/blueprint/schema.js +78 -0
  14. package/src/branding/__tests__/branding.test.js +49 -0
  15. package/src/branding/index.js +48 -0
  16. package/src/commands/__tests__/commands.test.js +83 -0
  17. package/src/commands/check.js +71 -0
  18. package/src/commands/cleanup.js +347 -0
  19. package/src/commands/customize.js +263 -0
  20. package/src/commands/doctor.js +84 -0
  21. package/src/commands/env.js +75 -0
  22. package/src/commands/finalize.js +68 -0
  23. package/src/commands/generate/ci-cd.js +378 -0
  24. package/src/commands/generate/deploy-advanced.js +253 -0
  25. package/src/commands/generate/deploy.js +99 -0
  26. package/src/commands/generate/env.template.js +221 -0
  27. package/src/commands/generate/index.js +7 -0
  28. package/src/commands/generate/module.js +836 -0
  29. package/src/commands/generate/page.js +1415 -0
  30. package/src/commands/generate/test-scaffold.js +279 -0
  31. package/src/commands/generate/theme.js +67 -0
  32. package/src/commands/generate-resource.js +133 -0
  33. package/src/commands/index.js +9 -0
  34. package/src/commands/init.js +350 -0
  35. package/src/commands/make/resource.js +298 -0
  36. package/src/commands/preset.js +57 -0
  37. package/src/commands/remove.js +170 -0
  38. package/src/commands/rename.js +54 -0
  39. package/src/commands/rollback.js +90 -0
  40. package/src/commands/wizard.js +303 -0
  41. package/src/core/__tests__/generator.test.js +67 -0
  42. package/src/core/__tests__/marker-strategy.test.js +57 -0
  43. package/src/core/__tests__/resource-definition.test.js +32 -0
  44. package/src/core/generator.js +542 -0
  45. package/src/core/marker-strategy.js +138 -0
  46. package/src/core/resource-definition.js +346 -0
  47. package/src/core/state-tracker.js +67 -0
  48. package/src/core/template-loader.js +163 -0
  49. package/src/engine/__tests__/engine.test.js +306 -0
  50. package/src/engine/index.js +21 -0
  51. package/src/engine/injector.js +198 -0
  52. package/src/engine/pipeline.js +138 -0
  53. package/src/engine/transaction.js +105 -0
  54. package/src/engine/validator.js +190 -0
  55. package/src/index.js +4 -0
  56. package/src/recipes/__tests__/recipe.test.js +128 -0
  57. package/src/recipes/builtin/module.json +22 -0
  58. package/src/recipes/builtin/page.json +21 -0
  59. package/src/recipes/builtin/resource.json +35 -0
  60. package/src/recipes/condition.js +147 -0
  61. package/src/recipes/index.js +11 -0
  62. package/src/recipes/loader.js +95 -0
  63. package/src/recipes/recipe.js +89 -0
  64. package/src/recipes/schema.js +47 -0
  65. package/src/schemas/__tests__/schemas.test.js +67 -0
  66. package/src/schemas/index.js +18 -0
  67. package/src/schemas/options.js +38 -0
  68. package/src/schemas/resource.js +112 -0
  69. package/src/services/__tests__/reporter.test.js +98 -0
  70. package/src/services/clock.js +31 -0
  71. package/src/services/index.js +43 -0
  72. package/src/services/reporter.js +136 -0
  73. package/src/templates/resource/api.js.ejs +39 -0
  74. package/src/templates/resource/components/form.jsx.ejs +81 -0
  75. package/src/templates/resource/components/table.jsx.ejs +68 -0
  76. package/src/templates/resource/controller.js.ejs +154 -0
  77. package/src/templates/resource/hooks.js.ejs +46 -0
  78. package/src/templates/resource/model.js.ejs +64 -0
  79. package/src/templates/resource/page-detail.jsx.ejs +55 -0
  80. package/src/templates/resource/page-form.jsx.ejs +30 -0
  81. package/src/templates/resource/page-inline.jsx.ejs +74 -0
  82. package/src/templates/resource/page-modal.jsx.ejs +98 -0
  83. package/src/templates/resource/page-page.jsx.ejs +99 -0
  84. package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
  85. package/src/templates/resource/routes.js.ejs +35 -0
  86. package/src/templates/resource/service.js.ejs +132 -0
  87. package/src/templates/resource/test.ejs +71 -0
  88. package/src/templates/resource/types.ts.ejs +17 -0
  89. package/src/templates/resource/validator.js.ejs +26 -0
  90. package/src/templates/snippets/lazy-import.ejs +1 -0
  91. package/src/templates/snippets/nav-entry.ejs +1 -0
  92. package/src/templates/snippets/route-entry.ejs +5 -0
  93. package/src/templates/snippets/route-mount.ejs +1 -0
  94. package/src/utils/fieldValidators.js +371 -0
  95. package/src/utils/logging/logger.js +47 -0
  96. package/src/utils/namingUtils.js +38 -0
  97. package/src/utils/sanitize.js +200 -0
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * Generate test scaffolding for modules
8
+ */
9
+ export async function generateModuleTests(moduleName, testsDir) {
10
+ const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
11
+
12
+ return `const request = require('supertest');
13
+ const { ${pascalName}Model } = require('../${moduleName}/${moduleName}.model');
14
+ const app = require('../../app');
15
+
16
+ describe('${pascalName} Module API', () => {
17
+ let test${pascalName}Id;
18
+
19
+ beforeAll(async () => {
20
+ // Setup test database connection
21
+ await connectToTestDatabase();
22
+ });
23
+
24
+ afterAll(async () => {
25
+ // Cleanup test database
26
+ await ${pascalName}Model.deleteMany({});
27
+ await disconnectFromTestDatabase();
28
+ });
29
+
30
+ describe('POST /api/${moduleName} - Create', () => {
31
+ it('should create a new ${moduleName}', async () => {
32
+ const new${pascalName} = {
33
+ name: 'Test ${pascalName}',
34
+ // Add required fields based on schema
35
+ };
36
+
37
+ const response = await request(app)
38
+ .post('/api/${moduleName}')
39
+ .send(new${pascalName})
40
+ .expect(201);
41
+
42
+ expect(response.body).toHaveProperty('data');
43
+ expect(response.body.data).toHaveProperty('_id');
44
+ expect(response.body.data.name).toBe(new${pascalName}.name);
45
+
46
+ test${pascalName}Id = response.body.data._id;
47
+ });
48
+
49
+ it('should return 400 for invalid data', async () => {
50
+ await request(app)
51
+ .post('/api/${moduleName}')
52
+ .send({}) // Empty body
53
+ .expect(400);
54
+ });
55
+ });
56
+
57
+ describe('GET /api/${moduleName} - List', () => {
58
+ it('should list all ${moduleName}s', async () => {
59
+ const response = await request(app)
60
+ .get('/api/${moduleName}')
61
+ .expect(200);
62
+
63
+ expect(response.body).toHaveProperty('data');
64
+ expect(Array.isArray(response.body.data)).toBe(true);
65
+ });
66
+
67
+ it('should support pagination', async () => {
68
+ const response = await request(app)
69
+ .get('/api/${moduleName}?page=1&limit=10')
70
+ .expect(200);
71
+
72
+ expect(response.body).toHaveProperty('data');
73
+ });
74
+ });
75
+
76
+ describe('GET /api/${moduleName}/:id - Get One', () => {
77
+ it('should get a single ${moduleName}', async () => {
78
+ const response = await request(app)
79
+ .get(\`/api/${moduleName}/\${test${pascalName}Id}\`)
80
+ .expect(200);
81
+
82
+ expect(response.body.data._id).toBe(test${pascalName}Id);
83
+ });
84
+
85
+ it('should return 404 for non-existent ${moduleName}', async () => {
86
+ await request(app)
87
+ .get('/api/${moduleName}/507f1f77bcf86cd799439011')
88
+ .expect(404);
89
+ });
90
+ });
91
+
92
+ describe('PUT /api/${moduleName}/:id - Update', () => {
93
+ it('should update a ${moduleName}', async () => {
94
+ const updatedData = {
95
+ name: 'Updated ${pascalName} Name',
96
+ };
97
+
98
+ const response = await request(app)
99
+ .put(\`/api/${moduleName}/\${test${pascalName}Id}\`)
100
+ .send(updatedData)
101
+ .expect(200);
102
+
103
+ expect(response.body.data.name).toBe(updatedData.name);
104
+ });
105
+
106
+ it('should return 404 for updating non-existent ${moduleName}', async () => {
107
+ await request(app)
108
+ .put('/api/${moduleName}/507f1f77bcf86cd799439011')
109
+ .send({ name: 'Updated' })
110
+ .expect(404);
111
+ });
112
+ });
113
+
114
+ describe('DELETE /api/${moduleName}/:id - Delete', () => {
115
+ it('should delete a ${moduleName}', async () => {
116
+ await request(app)
117
+ .delete(\`/api/${moduleName}/\${test${pascalName}Id}\`)
118
+ .expect(200);
119
+
120
+ // Verify deletion
121
+ await request(app)
122
+ .get(\`/api/${moduleName}/\${test${pascalName}Id}\`)
123
+ .expect(404);
124
+ });
125
+
126
+ it('should return 404 when deleting non-existent ${moduleName}', async () => {
127
+ await request(app)
128
+ .delete('/api/${moduleName}/507f1f77bcf86cd799439011')
129
+ .expect(404);
130
+ });
131
+ });
132
+
133
+ describe('Validation Tests', () => {
134
+ it('should validate required fields', async () => {
135
+ const response = await request(app)
136
+ .post('/api/${moduleName}')
137
+ .send({})
138
+ .expect(400);
139
+
140
+ expect(response.body).toHaveProperty('error');
141
+ });
142
+
143
+ it('should validate field types', async () => {
144
+ const response = await request(app)
145
+ .post('/api/${moduleName}')
146
+ .send({ name: 12345 }) // Wrong type
147
+ .expect(400);
148
+
149
+ expect(response.body).toHaveProperty('error');
150
+ });
151
+ });
152
+ });
153
+ `;
154
+ }
155
+
156
+ export async function generateFrontendTests(componentName) {
157
+ return `import { render, screen, fireEvent, waitFor } from '@testing-library/react';
158
+ import { BrowserRouter } from 'react-router-dom';
159
+ import { ${componentName}Page } from './${componentName}Page';
160
+ import '@testing-library/jest-dom';
161
+
162
+ jest.mock('@/hooks/useAuth', () => ({
163
+ useAuth: () => ({
164
+ user: { name: 'Test User', email: 'test@example.com' },
165
+ }),
166
+ }));
167
+
168
+ describe('${componentName}Page', () => {
169
+ const renderComponent = () => {
170
+ return render(
171
+ <BrowserRouter>
172
+ <${componentName}Page />
173
+ </BrowserRouter>
174
+ );
175
+ };
176
+
177
+ it('should render the page title', () => {
178
+ renderComponent();
179
+ expect(screen.getByText('${componentName}')).toBeInTheDocument();
180
+ });
181
+
182
+ it('should display user name', () => {
183
+ renderComponent();
184
+ expect(screen.getByText(/Test User/i)).toBeInTheDocument();
185
+ });
186
+
187
+ describe('Page structure', () => {
188
+ it('should have main sections', () => {
189
+ renderComponent();
190
+ expect(screen.getByRole('main')).toBeInTheDocument();
191
+ });
192
+ });
193
+ });
194
+ `;
195
+ }
196
+
197
+ export async function generateApiTests(moduleName) {
198
+ const pascalName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
199
+
200
+ return `import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
201
+ import request from 'supertest';
202
+ import app from '../../app';
203
+ import { ${pascalName}Model } from '../../src/modules/${moduleName}/${moduleName}.model';
204
+
205
+ describe('${pascalName} API Integration Tests', () => {
206
+ let testId;
207
+
208
+ beforeAll(async () => {
209
+ // Clear collection before tests
210
+ await ${pascalName}Model.deleteMany({});
211
+ });
212
+
213
+ afterAll(async () => {
214
+ await ${pascalName}Model.deleteMany({});
215
+ });
216
+
217
+ describe('CRUD Operations', () => {
218
+ it('should create a ${moduleName}', async () => {
219
+ const response = await request(app)
220
+ .post('/api/${moduleName}')
221
+ .send({
222
+ name: 'Integration Test ${pascalName}',
223
+ })
224
+ .expect('Content-Type', /json/)
225
+ .expect(201);
226
+
227
+ expect(response.body.data).toBeDefined();
228
+ expect(response.body.data.name).toBe('Integration Test ${pascalName}');
229
+ testId = response.body.data._id;
230
+ });
231
+
232
+ it('should retrieve all ${moduleName}s', async () => {
233
+ const response = await request(app)
234
+ .get('/api/${moduleName}')
235
+ .expect(200);
236
+
237
+ expect(Array.isArray(response.body.data)).toBe(true);
238
+ expect(response.body.data.length).toBeGreaterThan(0);
239
+ });
240
+
241
+ it('should retrieve a single ${moduleName}', async () => {
242
+ const response = await request(app)
243
+ .get(\`/api/${moduleName}/\${testId}\`)
244
+ .expect(200);
245
+
246
+ expect(response.body.data._id).toBe(testId);
247
+ });
248
+
249
+ it('should update a ${moduleName}', async () => {
250
+ const response = await request(app)
251
+ .put(\`/api/${moduleName}/\${testId}\`)
252
+ .send({ name: 'Updated Name' })
253
+ .expect(200);
254
+
255
+ expect(response.body.data.name).toBe('Updated Name');
256
+ });
257
+
258
+ it('should delete a ${moduleName}', async () => {
259
+ await request(app)
260
+ .delete(\`/api/${moduleName}/\${testId}\`)
261
+ .expect(200);
262
+
263
+ // Verify it's gone
264
+ await request(app)
265
+ .get(\`/api/${moduleName}/\${testId}\`)
266
+ .expect(404);
267
+ });
268
+ });
269
+
270
+ describe('Error Handling', () => {
271
+ it('should return 404 for non-existent ${moduleName}', async () => {
272
+ await request(app)
273
+ .get('/api/${moduleName}/507f1f77bcf86cd799439011')
274
+ .expect(404);
275
+ });
276
+ });
277
+ });
278
+ `;
279
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import process from 'process';
8
+
9
+ export default async function generateThemeCmd(options) {
10
+ const spinner = ora();
11
+ const projectRoot = process.cwd();
12
+
13
+ // Validate project
14
+ if (!fs.existsSync(path.join(projectRoot, 'frontend/src/config/app-preset.js'))) {
15
+ console.log(chalk.red('✖ Not a MERN Starter Kit project (missing app-preset.js).'));
16
+ process.exit(1);
17
+ }
18
+
19
+ let cssContent = '';
20
+
21
+ if (options.file) {
22
+ const filePath = path.resolve(projectRoot, options.file);
23
+ if (!fs.existsSync(filePath)) {
24
+ console.log(chalk.red(`✖ File not found: ${filePath}`));
25
+ process.exit(1);
26
+ }
27
+ cssContent = await fs.readFile(filePath, 'utf-8');
28
+ } else if (options.paste) {
29
+ cssContent = options.paste;
30
+ } else {
31
+ console.log(chalk.yellow('⚠ No CSS provided. Use --file path/to/theme.css or --paste "css string".'));
32
+ process.exit(1);
33
+ }
34
+
35
+ // Basic validation
36
+ if (!cssContent.includes(':root') && !cssContent.includes('.dark')) {
37
+ console.log(chalk.yellow('⚠ CSS does not contain :root or .dark blocks.'));
38
+ }
39
+
40
+ // Persist the raw CSS in the project for reference (non-destructive, new file)
41
+ const cssOutPath = path.join(projectRoot, 'frontend/src/config/imported-shadcn-theme.css');
42
+ await fs.writeFile(cssOutPath, cssContent);
43
+ spinner.succeed(`CSS saved to ${cssOutPath}`);
44
+
45
+ // Print the code snippet to paste into app-preset.js
46
+ const fallbackTheme = options.fallback || 'designTokens.calmBlue';
47
+ const appearance = options.appearance || 'appearanceRecipes.elevated';
48
+
49
+ console.log('');
50
+ console.log(chalk.green('✓ Theme CSS saved. Add this to your app-preset.js:'));
51
+ console.log('');
52
+ console.log(chalk.white(`import { installShadcnDesignPreset } from "@/lib/shadcn-theme";
53
+ import { ${fallbackTheme} } from "./design-themes";
54
+ import { ${appearance} } from "./design-themes";
55
+
56
+ export const appPreset = {
57
+ ...presetVariants.saas, // or your chosen base
58
+ theme: installShadcnDesignPreset(\`${cssContent.replace(/`/g, '\\`')}\`, {
59
+ fallbackTheme: ${fallbackTheme},
60
+ appearance: ${appearance},
61
+ }),
62
+ // optional: override other fields (brand, navigation, landing)
63
+ };`));
64
+ console.log('');
65
+ console.log(chalk.gray('Alternatively, use designThemes object directly with parsed cssVars.'));
66
+ console.log('');
67
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `loom generate resource|module|page <name>` — the unified, engine-backed
3
+ * generation command.
4
+ *
5
+ * Replaces the divergent code paths (string-template page generator vs the EJS
6
+ * generator) with one flow: blueprint + recipe → transactional pipeline. It
7
+ * decides *nothing* about what files exist or where they go — the recipe and
8
+ * blueprint do — it only wires the pieces and reports.
9
+ */
10
+ import path from "node:path";
11
+ import { ResourceDefinition, parseFieldSpec } from "../core/resource-definition.js";
12
+ import { TemplateLoader } from "../core/template-loader.js";
13
+ import { blueprintLoader } from "../blueprint/index.js";
14
+ import { recipeLoader } from "../recipes/index.js";
15
+ import { createGenerationPipeline } from "../engine/index.js";
16
+ import { reporterFromOptions } from "../services/index.js";
17
+ import { validateGenerateOptions, validateResourceDefinition } from "../schemas/index.js";
18
+
19
+ const NAMING = {
20
+ pascal: (s) => s.charAt(0).toUpperCase() + s.slice(1),
21
+ camel: (s) => s.charAt(0).toLowerCase() + s.slice(1),
22
+ kebab: (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase(),
23
+ };
24
+
25
+ /** Build a validated ResourceDefinition from --fields / --file / a bare name. */
26
+ async function resolveResource(name, options) {
27
+ let raw;
28
+ if (options.file) {
29
+ const mod = await import(path.resolve(process.cwd(), options.file));
30
+ raw = mod.default || mod;
31
+ } else {
32
+ const fields = options.fields
33
+ ? options.fields
34
+ .split(";")
35
+ .map((spec) => parseFieldSpec(spec.trim()))
36
+ .filter(Boolean)
37
+ : [];
38
+ raw = { name: NAMING.pascal(name), fields };
39
+ }
40
+
41
+ // Schema-validate before construction — typed errors, not a thrown stack trace.
42
+ const validated = validateResourceDefinition(raw);
43
+ if (!validated.success) {
44
+ throw new Error(`Invalid resource definition:\n - ${validated.issues.join("\n - ")}`);
45
+ }
46
+ return new ResourceDefinition(validated.data);
47
+ }
48
+
49
+ /**
50
+ * @param {string} type - recipe name: "resource" | "module" | "page"
51
+ * @param {string} name - resource name
52
+ * @param {object} options - merged command + global options
53
+ */
54
+ export default async function generateResource(type, name, options = {}) {
55
+ const reporter = reporterFromOptions(options);
56
+ const projectRoot = process.cwd();
57
+
58
+ try {
59
+ if (!name) throw new Error(`A name is required: loom generate ${type} <Name>`);
60
+
61
+ const optionCheck = validateGenerateOptions(options);
62
+ if (!optionCheck.success) {
63
+ throw new Error(`Invalid options:\n - ${optionCheck.issues.join("\n - ")}`);
64
+ }
65
+
66
+ const resource = await resolveResource(name, options);
67
+ const blueprint = await blueprintLoader.load(projectRoot);
68
+ const recipe = await recipeLoader.load(options.recipe || type, blueprint);
69
+
70
+ reporter.step(`Generating ${recipe.name} "${resource.name}" (${blueprint.architecture.name})`);
71
+
72
+ // The recipe's `when` evaluation context: params + derived flags.
73
+ const recipeContext = {
74
+ withFrontend: options.frontend !== false,
75
+ withTests: Boolean(options.withTests),
76
+ architecture: options.arch || "moderate",
77
+ formMode: options.formMode || "page",
78
+ usesTypeScript: blueprint.usesTypeScript(projectRoot),
79
+ };
80
+ for (const field of resource.fields) recipeContext[`hasField:${field.name}`] = true;
81
+
82
+ // Template-path tokens ({kebab}, {Name}) used by recipe `out`/`template`.
83
+ const vars = { kebab: resource.kebabName, Name: resource.pascalName };
84
+
85
+ // EJS rendering bridged to the engine's injected-renderer contract.
86
+ const templates = new TemplateLoader();
87
+ templates.projectRoot = projectRoot;
88
+ const templateContext = {
89
+ resource,
90
+ blueprint,
91
+ options: recipeContext,
92
+ project: { root: projectRoot, usesTypeScript: recipeContext.usesTypeScript },
93
+ utils: NAMING,
94
+ };
95
+ const renderer = (templatePath) => templates.render(templatePath, templateContext, projectRoot);
96
+
97
+ const pipeline = createGenerationPipeline({ renderer });
98
+ const ctx = await pipeline.run({
99
+ projectRoot,
100
+ recipe,
101
+ blueprint,
102
+ recipeContext,
103
+ vars,
104
+ templateContext,
105
+ dryRun: Boolean(options.dryRun),
106
+ });
107
+
108
+ const { files, dryRun } = ctx.result;
109
+ for (const file of files) {
110
+ reporter.event("file", { action: file.action, path: file.relPath });
111
+ reporter.info(`${file.action === "create" ? "+" : "~"} ${file.relPath}`);
112
+ }
113
+ reporter.result({
114
+ recipe: recipe.name,
115
+ resource: resource.name,
116
+ dryRun,
117
+ files,
118
+ injections: ctx.injections || [],
119
+ });
120
+ reporter.success(
121
+ dryRun
122
+ ? `Dry run — ${files.length} file(s) would change`
123
+ : `Generated ${resource.name} — ${files.length} file(s)`,
124
+ );
125
+ } catch (err) {
126
+ reporter.error(err.message);
127
+ reporter.result({ error: err.message });
128
+ reporter.flush();
129
+ process.exitCode = err.name === "BlueprintLoadError" || err.name === "RecipeLoadError" ? 1 : 2;
130
+ return;
131
+ }
132
+ reporter.flush();
133
+ }
@@ -0,0 +1,9 @@
1
+ export { default as initCmd } from "./init.js";
2
+ export { default as generatePageCmd } from "./generate/page.js";
3
+ export { default as generateModuleCmd } from "./generate/module.js";
4
+ export { default as presetCmd } from "./preset.js";
5
+ export { default as finalizeCmd } from "./finalize.js";
6
+ export { default as rollbackCmd } from "./rollback.js";
7
+ export { default as doctorCmd } from "./doctor.js";
8
+ export { default as cleanupCmd } from "./cleanup.js";
9
+ export { getConfigPaths } from "./generate/page.js";