create-lumen 1.2.0 → 2.0.0-alpha.1
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/README.md +71 -9
- package/bin/cli.js +18 -4
- package/package.json +15 -4
- package/schema/fixtures/index.js +62 -0
- package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
- package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
- package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
- package/schema/fixtures/v2/invalid/invalid-manifest-version.json +33 -0
- package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
- package/schema/fixtures/v2/invalid/malformed-paths.json +33 -0
- package/schema/fixtures/v2/invalid/missing-required-fields.json +17 -0
- package/schema/fixtures/v2/invalid/next-type-based.json +33 -0
- package/schema/fixtures/v2/invalid/next-vite.json +33 -0
- package/schema/fixtures/v2/invalid/react-bundler.json +34 -0
- package/schema/fixtures/v2/invalid/react-hybrid.json +33 -0
- package/schema/fixtures/v2/invalid/react-variant-mismatch.json +33 -0
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +33 -0
- package/schema/fixtures/v2/invalid/v1-flat-config.json +11 -0
- package/schema/fixtures/v2/valid/full-kitchen-sink.json +35 -0
- package/schema/fixtures/v2/valid/harness-full.json +44 -0
- package/schema/fixtures/v2/valid/next-app-router-feature-based.json +35 -0
- package/schema/fixtures/v2/valid/next-app-router-hybrid.json +35 -0
- package/schema/fixtures/v2/valid/next-app-router-none.json +33 -0
- package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +35 -0
- package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +35 -0
- package/schema/fixtures/v2/valid/next-pages-router-none.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-feature-based.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-none.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-type-based.json +33 -0
- package/schema/fixtures/v2/valid/validation-none.json +41 -0
- package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
- package/schema/lumen.config.v2.json +269 -0
- package/skills/create-lumen/SKILL.md +110 -0
- package/skills/create-lumen/references/manifest-v2.md +9 -0
- package/src/agent-context.js +114 -0
- package/src/cli-args.js +294 -0
- package/src/css.js +20 -9
- package/src/dependencies.js +12 -6
- package/src/engine/capabilities.js +218 -0
- package/src/engine/index.js +2 -0
- package/src/engine/providers/base-provider.js +219 -0
- package/src/injector.js +26 -13
- package/src/main.js +66 -10
- package/src/manifest/emit.js +166 -0
- package/src/manifest/index.js +11 -0
- package/src/manifest/paths.js +128 -0
- package/src/manifest/schema.js +270 -0
- package/src/prompts.js +26 -6
- package/templates/conditional/linting/eslint/next/eslint.config.mjs +16 -0
- package/templates/css/tailwind/postcss.config.mjs +5 -0
- /package/templates/architectures/{component-based → type-based}/main.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/main.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/App.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/App.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/form/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/config/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/services/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/store/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/styles/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/types/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/types/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/utils/cn.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/utils/cn.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.ts +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/App.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/App.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/index.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/index.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.jsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.ts +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.jsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.tsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.js +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.ts +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.js +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.ts +0 -0
- /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.jsx +0 -0
- /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.ts +0 -0
- /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
- /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
- /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
- /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path mapping for lumen.config.json — #15
|
|
3
|
+
* Resolves the 6 generator target directories from framework + architecture.
|
|
4
|
+
* Paths are relative to project root and use posix separators.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const PATH_KEYS = ["features", "components", "services", "hooks", "pages", "ui"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ architecture: string, framework?: { name?: string, variant?: string } }} opts
|
|
11
|
+
* @returns {{ features: string, components: string, services: string, hooks: string, pages: string, ui: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePaths({ architecture, framework = {} } = {}) {
|
|
14
|
+
const arch = architecture || "feature-based";
|
|
15
|
+
const fwName = framework.name || "react";
|
|
16
|
+
const variant = framework.variant || (fwName === "next" ? "app-router" : "vite");
|
|
17
|
+
|
|
18
|
+
// Helper to pick Next.js pages root based on variant
|
|
19
|
+
const nextPagesForVariant = (v) => (v === "pages-router" ? "pages" : "app");
|
|
20
|
+
const isNext = fwName === "next";
|
|
21
|
+
|
|
22
|
+
// -- feature-based (React + Vite canonical, also Next feature-based)
|
|
23
|
+
if (arch === "feature-based") {
|
|
24
|
+
if (isNext) {
|
|
25
|
+
const nextRoot = nextPagesForVariant(variant);
|
|
26
|
+
// Next keeps features in src/features, shared code in src/shared, routing in app/ or pages/
|
|
27
|
+
// Use posix paths regardless of OS
|
|
28
|
+
return {
|
|
29
|
+
features: "src/features",
|
|
30
|
+
components: "src/shared/components",
|
|
31
|
+
services: "src/shared/services",
|
|
32
|
+
hooks: "src/shared/hooks",
|
|
33
|
+
pages: nextRoot, // app/ or pages/ at project root for Next
|
|
34
|
+
ui: "src/shared/components/ui",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
features: "src/features",
|
|
39
|
+
components: "src/shared/components",
|
|
40
|
+
services: "src/shared/services",
|
|
41
|
+
hooks: "src/shared/hooks",
|
|
42
|
+
pages: "src/app/router",
|
|
43
|
+
ui: "src/shared/components/ui",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// -- type-based (React + Vite only; component-based supported as legacy alias)
|
|
48
|
+
if (arch === "type-based" || arch === "component-based") {
|
|
49
|
+
if (isNext) {
|
|
50
|
+
// Not a valid combo per schema, but provide a deterministic fallback
|
|
51
|
+
// to keep resolver total (hybrid should be used for Next)
|
|
52
|
+
const nextRoot = nextPagesForVariant(variant);
|
|
53
|
+
return {
|
|
54
|
+
features: "src/components",
|
|
55
|
+
components: "src/components",
|
|
56
|
+
services: "src/services",
|
|
57
|
+
hooks: "src/hooks",
|
|
58
|
+
pages: nextRoot,
|
|
59
|
+
ui: "src/components/ui",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
features: "src/features",
|
|
64
|
+
components: "src/components",
|
|
65
|
+
services: "src/services",
|
|
66
|
+
hooks: "src/hooks",
|
|
67
|
+
pages: "src/pages",
|
|
68
|
+
ui: "src/ui",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -- hybrid (Next.js: feature + shared). For React, fallback to feature-based
|
|
73
|
+
if (arch === "hybrid") {
|
|
74
|
+
if (isNext) {
|
|
75
|
+
const nextRoot = nextPagesForVariant(variant);
|
|
76
|
+
return {
|
|
77
|
+
features: "src/features",
|
|
78
|
+
components: "src/shared/components",
|
|
79
|
+
services: "src/shared/services",
|
|
80
|
+
hooks: "src/shared/hooks",
|
|
81
|
+
pages: nextRoot,
|
|
82
|
+
ui: "src/shared/components/ui",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// React hybrid = hybrid not valid, but map to feature-based for determinism
|
|
86
|
+
return {
|
|
87
|
+
features: "src/features",
|
|
88
|
+
components: "src/shared/components",
|
|
89
|
+
services: "src/shared/services",
|
|
90
|
+
hooks: "src/shared/hooks",
|
|
91
|
+
pages: "src/app/router",
|
|
92
|
+
ui: "src/shared/components/ui",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -- none (minimal flat src/ — no imposed convention)
|
|
97
|
+
if (arch === "none") {
|
|
98
|
+
if (isNext) {
|
|
99
|
+
const nextRoot = nextPagesForVariant(variant);
|
|
100
|
+
return {
|
|
101
|
+
features: "src",
|
|
102
|
+
components: "src/components",
|
|
103
|
+
services: "src/services",
|
|
104
|
+
hooks: "src/hooks",
|
|
105
|
+
pages: nextRoot,
|
|
106
|
+
ui: "src/components/ui",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
features: "src",
|
|
111
|
+
components: "src/components",
|
|
112
|
+
services: "src/services",
|
|
113
|
+
hooks: "src/hooks",
|
|
114
|
+
pages: "src/pages",
|
|
115
|
+
ui: "src/components/ui",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fallback: treat unknown as feature-based
|
|
120
|
+
return {
|
|
121
|
+
features: "src/features",
|
|
122
|
+
components: "src/shared/components",
|
|
123
|
+
services: "src/shared/services",
|
|
124
|
+
hooks: "src/shared/hooks",
|
|
125
|
+
pages: isNext ? nextPagesForVariant(variant) : "src/app/router",
|
|
126
|
+
ui: "src/shared/components/ui",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fromZodError } from "zod-validation-error";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Constants - keep in sync with docs/manifest-v2.md and ROADMAP
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const MANIFEST_VERSION = 2;
|
|
8
|
+
export const SCHEMA_URL = "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json";
|
|
9
|
+
|
|
10
|
+
export const FRAMEWORK_NAMES = ["react", "next"];
|
|
11
|
+
export const FRAMEWORK_VARIANTS = ["vite", "app-router", "pages-router"];
|
|
12
|
+
export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
|
|
13
|
+
export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
|
|
14
|
+
|
|
15
|
+
export const STYLING_ENGINES = ["tailwind", "bootstrap", "none"];
|
|
16
|
+
export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
|
|
17
|
+
// Legacy alias: docs used architecture.type, canonical is architecture.preset.
|
|
18
|
+
// Keep both in sync; parseManifest normalizes to expose both keys.
|
|
19
|
+
export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
|
|
20
|
+
export const ARCHITECTURE_VALIDATIONS = ["strict", "relaxed", "none"];
|
|
21
|
+
export const UI_KITS = ["shadcn", "none"];
|
|
22
|
+
export const DOCS_LANGUAGES = ["en", "es"];
|
|
23
|
+
export const TOOLING_LANGUAGES = ["ts", "js"];
|
|
24
|
+
export const TOOLING_LINTERS = ["eslint", "oxlint", "biome", "none"];
|
|
25
|
+
export const TOOLING_FORMATTERS = ["prettier", "oxfmt", "none"];
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Sub-schemas
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const frameworkSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
name: z.enum(FRAMEWORK_NAMES),
|
|
33
|
+
variant: z.enum(FRAMEWORK_VARIANTS),
|
|
34
|
+
bundler: z.enum(FRAMEWORK_BUNDLERS).optional(),
|
|
35
|
+
adapter: z.enum(FRAMEWORK_ADAPTERS).optional(),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
|
|
39
|
+
const stylingSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
engine: z.enum(STYLING_ENGINES),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
|
|
45
|
+
const architectureSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
// Canonical key per docs/manifest-v2.md; legacy `type` kept as alias.
|
|
48
|
+
preset: z.enum(ARCHITECTURE_PRESETS).optional(),
|
|
49
|
+
type: z.enum(ARCHITECTURE_TYPES).optional(),
|
|
50
|
+
validation: z.enum(ARCHITECTURE_VALIDATIONS).optional().default("strict"),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
.superRefine((val, ctx) => {
|
|
54
|
+
if (!val.preset && !val.type) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: "custom",
|
|
57
|
+
path: ["preset"],
|
|
58
|
+
message: 'architecture must define "preset" or legacy "type"',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (val.preset && val.type && val.preset !== val.type) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["preset"],
|
|
65
|
+
message: `architecture.preset "${val.preset}" must match architecture.type "${val.type}"`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const HARNESS_DEFAULT_COMMANDS = [
|
|
71
|
+
{ name: "lint", command: "oxlint .", required: true },
|
|
72
|
+
{ name: "format", command: "oxfmt --check .", required: true },
|
|
73
|
+
{ name: "types", command: "tsc -b", required: true },
|
|
74
|
+
{ name: "test", command: "vitest run", required: true },
|
|
75
|
+
{ name: "build", command: "npm run build", required: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const harnessCommandSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
name: z.string().min(1),
|
|
81
|
+
command: z.string().min(1),
|
|
82
|
+
required: z.boolean().optional().default(true),
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
|
|
87
|
+
const harnessSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
commands: z.array(harnessCommandSchema).min(1),
|
|
90
|
+
})
|
|
91
|
+
.strict();
|
|
92
|
+
|
|
93
|
+
const uiSchema = z
|
|
94
|
+
.object({
|
|
95
|
+
kit: z.enum(UI_KITS),
|
|
96
|
+
})
|
|
97
|
+
.strict();
|
|
98
|
+
|
|
99
|
+
const docsSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
language: z.enum(DOCS_LANGUAGES),
|
|
102
|
+
})
|
|
103
|
+
.strict();
|
|
104
|
+
|
|
105
|
+
const pathsSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
features: z.string().min(1),
|
|
108
|
+
components: z.string().min(1),
|
|
109
|
+
services: z.string().min(1),
|
|
110
|
+
hooks: z.string().min(1),
|
|
111
|
+
pages: z.string().min(1),
|
|
112
|
+
ui: z.string().min(1),
|
|
113
|
+
})
|
|
114
|
+
.strict();
|
|
115
|
+
|
|
116
|
+
const toolingSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
language: z.enum(TOOLING_LANGUAGES),
|
|
119
|
+
linter: z.enum(TOOLING_LINTERS),
|
|
120
|
+
formatter: z.enum(TOOLING_FORMATTERS),
|
|
121
|
+
})
|
|
122
|
+
.strict();
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Top-level manifest v2 - strict, no extra keys except $schema
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
export const manifestSchemaV2 = z
|
|
128
|
+
.object({
|
|
129
|
+
$schema: z.string().url().optional().default(SCHEMA_URL),
|
|
130
|
+
manifestVersion: z.literal(MANIFEST_VERSION),
|
|
131
|
+
framework: frameworkSchema,
|
|
132
|
+
styling: stylingSchema,
|
|
133
|
+
architecture: architectureSchema,
|
|
134
|
+
harness: harnessSchema.optional(),
|
|
135
|
+
ui: uiSchema,
|
|
136
|
+
docs: docsSchema,
|
|
137
|
+
paths: pathsSchema,
|
|
138
|
+
tooling: toolingSchema,
|
|
139
|
+
reactCompiler: z.boolean().optional(),
|
|
140
|
+
agentDocs: z.boolean().optional(),
|
|
141
|
+
})
|
|
142
|
+
.strict()
|
|
143
|
+
.superRefine((val, ctx) => {
|
|
144
|
+
const { framework, architecture, styling, ui } = val;
|
|
145
|
+
|
|
146
|
+
// framework.variant must match framework.name
|
|
147
|
+
if (framework.name === "react" && framework.variant !== "vite") {
|
|
148
|
+
ctx.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["framework", "variant"],
|
|
151
|
+
message: `framework.variant must be "vite" when framework.name is "react" (got "${framework.variant}")`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (framework.name === "next" && framework.variant === "vite") {
|
|
155
|
+
ctx.addIssue({
|
|
156
|
+
code: "custom",
|
|
157
|
+
path: ["framework", "variant"],
|
|
158
|
+
message: `framework.variant must be "app-router" or "pages-router" when framework.name is "next" (got "vite")`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// bundler only for Next.js
|
|
163
|
+
if (framework.bundler !== undefined && framework.name !== "next") {
|
|
164
|
+
ctx.addIssue({
|
|
165
|
+
code: "custom",
|
|
166
|
+
path: ["framework", "bundler"],
|
|
167
|
+
message: `framework.bundler is only valid when framework.name is "next"`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// adapter only for Next.js (behavior in #22)
|
|
172
|
+
if (framework.adapter !== undefined && framework.name !== "next") {
|
|
173
|
+
ctx.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["framework", "adapter"],
|
|
176
|
+
message: `framework.adapter is only valid when framework.name is "next"`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
|
|
181
|
+
const archValue = architecture.preset ?? architecture.type;
|
|
182
|
+
const allowedByFramework =
|
|
183
|
+
framework.name === "react"
|
|
184
|
+
? ["feature-based", "type-based", "none"]
|
|
185
|
+
: ["feature-based", "hybrid", "none"];
|
|
186
|
+
if (!allowedByFramework.includes(archValue)) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
path: ["architecture", "type"],
|
|
190
|
+
message: `architecture.type "${archValue}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// shadcn requires tailwind (#33)
|
|
195
|
+
if (ui.kit === "shadcn" && styling.engine !== "tailwind") {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
path: ["ui", "kit"],
|
|
199
|
+
message: `ui.kit "shadcn" requires styling.engine "tailwind" (got "${styling.engine}")`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// v1 detection - flat shape from create-lumen 1.x
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
const V1_FLAT_KEYS = ["css", "cssFramework", "architecture", "language", "router", "stateManagement", "iconLibrary", "apiClient", "testing"];
|
|
208
|
+
|
|
209
|
+
export function isV1Manifest(raw) {
|
|
210
|
+
if (!raw || typeof raw !== "object") return false;
|
|
211
|
+
// v1 had framework as string or missing manifestVersion
|
|
212
|
+
if (raw.manifestVersion === 1) return true;
|
|
213
|
+
if (typeof raw.framework === "string") return true;
|
|
214
|
+
if (raw.framework === undefined && V1_FLAT_KEYS.some((k) => k in raw)) return true;
|
|
215
|
+
// flat config without nested framework object but with css/architecture as string
|
|
216
|
+
if (typeof raw.architecture === "string" && typeof raw.framework !== "object") return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function formatZodError(error) {
|
|
221
|
+
try {
|
|
222
|
+
const ve = fromZodError(error, {
|
|
223
|
+
prefix: "Manifest validation failed",
|
|
224
|
+
prefixSeparator: ": ",
|
|
225
|
+
issueSeparator: "; ",
|
|
226
|
+
});
|
|
227
|
+
return ve.message;
|
|
228
|
+
} catch {
|
|
229
|
+
return error.message;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse and validate a manifest v2 object.
|
|
235
|
+
* - Throws with migration-oriented message for v1 manifests (#14 AC)
|
|
236
|
+
* - Throws with user-friendly field path + expected shape for malformed v2
|
|
237
|
+
* @param {unknown} raw
|
|
238
|
+
* @returns {import("zod").infer<typeof manifestSchemaV2>}
|
|
239
|
+
*/
|
|
240
|
+
export function parseManifest(raw) {
|
|
241
|
+
if (isV1Manifest(raw)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Manifest v1 detected (flat config). create-lumen v2 uses a nested manifest (manifestVersion: 2).\n` +
|
|
244
|
+
`Please migrate: delete the old config and re-run create-lumen, or see the migration guide (docs/migration/v1-to-v2.md).\n` +
|
|
245
|
+
`Raw keys: ${Object.keys(raw || {}).join(", ")}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = manifestSchemaV2.safeParse(raw);
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
const msg = formatZodError(result.error);
|
|
252
|
+
const err = new Error(msg);
|
|
253
|
+
err.cause = result.error;
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
// Normalize: always expose architecture.preset + architecture.type (synced)
|
|
257
|
+
// + architecture.validation default, for backward/forward compat.
|
|
258
|
+
const data = result.data;
|
|
259
|
+
const preset = data.architecture.preset ?? data.architecture.type;
|
|
260
|
+
const type = data.architecture.type ?? data.architecture.preset;
|
|
261
|
+
data.architecture = {
|
|
262
|
+
preset,
|
|
263
|
+
type,
|
|
264
|
+
validation: data.architecture.validation ?? "strict",
|
|
265
|
+
};
|
|
266
|
+
return data;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Re-export type helper for JSDoc (z.infer)
|
|
270
|
+
// /** @typedef {z.infer<typeof manifestSchemaV2>} ManifestV2 */
|
package/src/prompts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirm, isCancel, note, select } from "@clack/prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
|
|
4
4
|
|
|
@@ -12,7 +12,9 @@ function formatConfig(responses) {
|
|
|
12
12
|
const arch =
|
|
13
13
|
responses.architecture === "feature-based"
|
|
14
14
|
? "Feature-based"
|
|
15
|
-
: "
|
|
15
|
+
: responses.architecture === "type-based" || responses.architecture === "component-based"
|
|
16
|
+
? "Type-based"
|
|
17
|
+
: responses.architecture;
|
|
16
18
|
const css =
|
|
17
19
|
responses.cssFramework === "none"
|
|
18
20
|
? "None"
|
|
@@ -32,6 +34,7 @@ function formatConfig(responses) {
|
|
|
32
34
|
? "Redux Toolkit"
|
|
33
35
|
: "Zustand";
|
|
34
36
|
|
|
37
|
+
const docsLang = responses.docsLanguage === "es" ? "ES" : responses.docsLanguage === "en" ? "EN" : "—";
|
|
35
38
|
return [
|
|
36
39
|
`• ${chalk.bold("Architecture:")} ${chalk.green(arch)}`,
|
|
37
40
|
`• ${chalk.bold("Language:")} ${chalk.green(lang)}`,
|
|
@@ -43,14 +46,15 @@ function formatConfig(responses) {
|
|
|
43
46
|
`• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
|
|
44
47
|
`• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
|
|
45
48
|
`• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
|
|
49
|
+
`• ${chalk.bold("Docs:")} ${chalk.cyan(docsLang)}`,
|
|
46
50
|
`• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
|
|
47
51
|
`• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`,
|
|
48
52
|
].join("\n");
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
52
|
-
let oldConfig
|
|
53
|
-
let useOldConfig
|
|
56
|
+
let oldConfig;
|
|
57
|
+
let useOldConfig;
|
|
54
58
|
|
|
55
59
|
// Non-interactive quick setup: apply defaults without prompting.
|
|
56
60
|
if (quickSetup) {
|
|
@@ -66,6 +70,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
66
70
|
apiClient: "none",
|
|
67
71
|
linter: "eslint",
|
|
68
72
|
formatter: "prettier",
|
|
73
|
+
docsLanguage: "en",
|
|
69
74
|
gitInit: true,
|
|
70
75
|
readme: true,
|
|
71
76
|
};
|
|
@@ -101,6 +106,9 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
101
106
|
if (oldConfig.formatter === undefined) {
|
|
102
107
|
oldConfig.formatter = "none";
|
|
103
108
|
}
|
|
109
|
+
if (oldConfig.docsLanguage === undefined) {
|
|
110
|
+
oldConfig.docsLanguage = "en";
|
|
111
|
+
}
|
|
104
112
|
return { ...oldConfig, projectName };
|
|
105
113
|
}
|
|
106
114
|
}
|
|
@@ -126,6 +134,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
126
134
|
apiClient: "none",
|
|
127
135
|
linter: "eslint",
|
|
128
136
|
formatter: "prettier",
|
|
137
|
+
docsLanguage: "en",
|
|
129
138
|
gitInit: true,
|
|
130
139
|
readme: true,
|
|
131
140
|
};
|
|
@@ -143,8 +152,8 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
143
152
|
hint: "Scales to large apps — code grouped by business domain",
|
|
144
153
|
},
|
|
145
154
|
{
|
|
146
|
-
label: "
|
|
147
|
-
value: "
|
|
155
|
+
label: "Type-based",
|
|
156
|
+
value: "type-based",
|
|
148
157
|
hint: "Small apps / component libraries — code grouped by UI type",
|
|
149
158
|
},
|
|
150
159
|
],
|
|
@@ -261,6 +270,16 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
261
270
|
if (isCancel(formatter)) onCancel();
|
|
262
271
|
}
|
|
263
272
|
|
|
273
|
+
const docsLanguage = await select({
|
|
274
|
+
message: "Which docs language do you want?",
|
|
275
|
+
options: [
|
|
276
|
+
{ label: "English", value: "en" },
|
|
277
|
+
{ label: "Español", value: "es" },
|
|
278
|
+
],
|
|
279
|
+
initialValue: "en",
|
|
280
|
+
});
|
|
281
|
+
if (isCancel(docsLanguage)) onCancel();
|
|
282
|
+
|
|
264
283
|
const readme = await confirm({
|
|
265
284
|
message: "Would you like to generate a README.md and LICENSE?",
|
|
266
285
|
initialValue: true,
|
|
@@ -279,6 +298,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
|
|
|
279
298
|
apiClient,
|
|
280
299
|
linter,
|
|
281
300
|
formatter,
|
|
301
|
+
docsLanguage,
|
|
282
302
|
gitInit,
|
|
283
303
|
readme,
|
|
284
304
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const compat = new FlatCompat({
|
|
9
|
+
baseDirectory: __dirname,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const eslintConfig = [
|
|
13
|
+
...compat.extends("next/core-web-vitals"),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default eslintConfig;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/components/common/Button.jsx
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/components/common/Button.tsx
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/components/common/index.js
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/components/common/index.ts
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/components/form/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.jsx
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.jsx
RENAMED
|
File without changes
|
/package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.js
RENAMED
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.ts
RENAMED
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.js
RENAMED
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.ts
RENAMED
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.js
RENAMED
|
File without changes
|
/package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.ts
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.js
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.ts
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.js
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.ts
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.js
RENAMED
|
File without changes
|
/package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.jsx
RENAMED
|
File without changes
|
/package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.jsx
RENAMED
|
File without changes
|
/package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.tsx
RENAMED
|
File without changes
|