@lucaismyname/create-l1-stack 0.0.19 → 0.0.20
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/package.json +1 -1
- package/src/index.js +379 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -77,6 +77,73 @@ const THEME_PRESETS = {
|
|
|
77
77
|
},
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
const INTEGRATIONS = {
|
|
81
|
+
contentful: {
|
|
82
|
+
label: 'Contentful',
|
|
83
|
+
env: {
|
|
84
|
+
VITE_CONTENTFUL_SPACE_ID: '',
|
|
85
|
+
VITE_CONTENTFUL_ENVIRONMENT: 'master',
|
|
86
|
+
VITE_CONTENTFUL_ACCESS_TOKEN: '',
|
|
87
|
+
},
|
|
88
|
+
clientOnlyWarning: false,
|
|
89
|
+
},
|
|
90
|
+
notion: {
|
|
91
|
+
label: 'Notion (frontend-only template; do not ship secret token to browser)',
|
|
92
|
+
env: {
|
|
93
|
+
VITE_NOTION_TOKEN: '',
|
|
94
|
+
},
|
|
95
|
+
clientOnlyWarning: true,
|
|
96
|
+
},
|
|
97
|
+
strapi: {
|
|
98
|
+
label: 'Strapi',
|
|
99
|
+
env: {
|
|
100
|
+
VITE_STRAPI_URL: 'https://example.com',
|
|
101
|
+
VITE_STRAPI_TOKEN: '',
|
|
102
|
+
},
|
|
103
|
+
clientOnlyWarning: false,
|
|
104
|
+
},
|
|
105
|
+
sanity: {
|
|
106
|
+
label: 'Sanity',
|
|
107
|
+
env: {
|
|
108
|
+
VITE_SANITY_PROJECT_ID: '',
|
|
109
|
+
VITE_SANITY_DATASET: 'production',
|
|
110
|
+
VITE_SANITY_API_VERSION: '2024-01-01',
|
|
111
|
+
VITE_SANITY_TOKEN: '',
|
|
112
|
+
VITE_SANITY_USE_CDN: 'true',
|
|
113
|
+
},
|
|
114
|
+
clientOnlyWarning: false,
|
|
115
|
+
},
|
|
116
|
+
mysql: {
|
|
117
|
+
label: 'MySQL (backend-only; template will only include docs)',
|
|
118
|
+
env: {
|
|
119
|
+
VITE_MYSQL_HOST: 'localhost',
|
|
120
|
+
VITE_MYSQL_USER: '',
|
|
121
|
+
VITE_MYSQL_PASSWORD: '',
|
|
122
|
+
VITE_MYSQL_DATABASE: '',
|
|
123
|
+
},
|
|
124
|
+
clientOnlyWarning: true,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const LAYOUTS = {
|
|
129
|
+
topNav: { label: 'Top nav (default)' },
|
|
130
|
+
sidebar: { label: 'Sidebar (responsive + collapsible)' },
|
|
131
|
+
blank: { label: 'Blank' },
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const CONTAINER_PRESETS = {
|
|
135
|
+
narrow: { label: 'Narrow', maxW: 'max-w-3xl' },
|
|
136
|
+
default: { label: 'Default', maxW: 'max-w-5xl' },
|
|
137
|
+
wide: { label: 'Wide', maxW: 'max-w-7xl' },
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const DEPLOY_TARGETS = {
|
|
141
|
+
static: { label: 'Static (Apache/Nginx)' },
|
|
142
|
+
netlify: { label: 'Netlify' },
|
|
143
|
+
vercel: { label: 'Vercel' },
|
|
144
|
+
cloudflare: { label: 'Cloudflare Pages' },
|
|
145
|
+
}
|
|
146
|
+
|
|
80
147
|
const ICON_SETS = {
|
|
81
148
|
lucide: {
|
|
82
149
|
dependency: { name: 'lucide-react', version: '^0.563.0' },
|
|
@@ -172,6 +239,253 @@ async function applyIconSet(targetDir, language, iconSetKey) {
|
|
|
172
239
|
await fs.writeFile(iconFile, language === 'ts' ? iconSet.tsx : iconSet.jsx, 'utf8')
|
|
173
240
|
}
|
|
174
241
|
|
|
242
|
+
function renderEnvFile(envMap) {
|
|
243
|
+
const keys = Object.keys(envMap)
|
|
244
|
+
keys.sort()
|
|
245
|
+
return keys.map((k) => `${k}=${envMap[k] ?? ''}`).join('\n') + '\n'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function configModuleTs() {
|
|
249
|
+
return `type Env = ImportMetaEnv\n\nfunction required(key: keyof Env): string {\n const value = import.meta.env[key]\n if (!value) throw new Error(\`Missing env: \${String(key)}\`)\n return value\n}\n\nexport const config = {\n contentful: {\n spaceId: import.meta.env.VITE_CONTENTFUL_SPACE_ID ?? \"\",\n environment: import.meta.env.VITE_CONTENTFUL_ENVIRONMENT,\n accessToken: import.meta.env.VITE_CONTENTFUL_ACCESS_TOKEN ?? \"\",\n },\n notion: {\n token: import.meta.env.VITE_NOTION_TOKEN,\n },\n strapi: {\n url: import.meta.env.VITE_STRAPI_URL ?? \"\",\n token: import.meta.env.VITE_STRAPI_TOKEN,\n },\n sanity: {\n projectId: import.meta.env.VITE_SANITY_PROJECT_ID ?? \"\",\n dataset: import.meta.env.VITE_SANITY_DATASET ?? \"\",\n apiVersion: import.meta.env.VITE_SANITY_API_VERSION,\n token: import.meta.env.VITE_SANITY_TOKEN,\n useCdn: import.meta.env.VITE_SANITY_USE_CDN === \"true\",\n },\n}\n\nexport const env = { required }\n`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function configModuleJs() {
|
|
253
|
+
return `function required(key) {\n const value = import.meta.env[key]\n if (!value) throw new Error(\`Missing env: \${String(key)}\`)\n return value\n}\n\nexport const config = {\n contentful: {\n spaceId: import.meta.env.VITE_CONTENTFUL_SPACE_ID ?? \"\",\n environment: import.meta.env.VITE_CONTENTFUL_ENVIRONMENT,\n accessToken: import.meta.env.VITE_CONTENTFUL_ACCESS_TOKEN ?? \"\",\n },\n notion: {\n token: import.meta.env.VITE_NOTION_TOKEN,\n },\n strapi: {\n url: import.meta.env.VITE_STRAPI_URL ?? \"\",\n token: import.meta.env.VITE_STRAPI_TOKEN,\n },\n sanity: {\n projectId: import.meta.env.VITE_SANITY_PROJECT_ID ?? \"\",\n dataset: import.meta.env.VITE_SANITY_DATASET ?? \"\",\n apiVersion: import.meta.env.VITE_SANITY_API_VERSION,\n token: import.meta.env.VITE_SANITY_TOKEN,\n useCdn: import.meta.env.VITE_SANITY_USE_CDN === \"true\",\n },\n}\n\nexport const env = { required }\n`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function applyEnvAndConfig(targetDir, language, enabledIntegrations) {
|
|
257
|
+
const envExample = {}
|
|
258
|
+
const envLocal = {}
|
|
259
|
+
|
|
260
|
+
let hasClientWarning = false
|
|
261
|
+
|
|
262
|
+
for (const key of enabledIntegrations) {
|
|
263
|
+
const integration = INTEGRATIONS[key]
|
|
264
|
+
if (!integration) continue
|
|
265
|
+
if (integration.clientOnlyWarning) hasClientWarning = true
|
|
266
|
+
for (const [envKey, envValue] of Object.entries(integration.env)) {
|
|
267
|
+
envExample[envKey] = envValue
|
|
268
|
+
envLocal[envKey] = envValue
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (hasClientWarning) {
|
|
273
|
+
envExample.VITE_CLIENT_SIDE_SECRET_WARNING =
|
|
274
|
+
'DO_NOT_PUT_SECRETS_IN_VITE_ENV__MOVE_TO_SERVER'
|
|
275
|
+
envLocal.VITE_CLIENT_SIDE_SECRET_WARNING =
|
|
276
|
+
'DO_NOT_PUT_SECRETS_IN_VITE_ENV__MOVE_TO_SERVER'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const envExamplePath = path.join(targetDir, '.env.example')
|
|
280
|
+
const envLocalPath = path.join(targetDir, '.env.local')
|
|
281
|
+
await fs.writeFile(envExamplePath, renderEnvFile(envExample), 'utf8')
|
|
282
|
+
await fs.writeFile(envLocalPath, renderEnvFile(envLocal), 'utf8')
|
|
283
|
+
|
|
284
|
+
const configPath = path.join(
|
|
285
|
+
targetDir,
|
|
286
|
+
'src',
|
|
287
|
+
language === 'ts' ? 'config.ts' : 'config.js'
|
|
288
|
+
)
|
|
289
|
+
await fs.writeFile(configPath, language === 'ts' ? configModuleTs() : configModuleJs(), 'utf8')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function applyIntegrations(targetDir, language, enabledIntegrations) {
|
|
293
|
+
const libDir = path.join(targetDir, 'src', 'lib')
|
|
294
|
+
await fs.mkdir(libDir, { recursive: true })
|
|
295
|
+
|
|
296
|
+
const ext = language === 'ts' ? 'ts' : 'js'
|
|
297
|
+
const configImport = "import { config } from '@/config'"
|
|
298
|
+
|
|
299
|
+
if (!enabledIntegrations.includes('contentful')) {
|
|
300
|
+
await fse.remove(path.join(libDir, `contentful.${ext}`))
|
|
301
|
+
} else {
|
|
302
|
+
const content = `${configImport}\nimport { createContentfulClient } from '@lucaismyname/l1-utils'\n\nexport const contentful = createContentfulClient({\n spaceId: config.contentful.spaceId,\n environment: config.contentful.environment,\n accessToken: config.contentful.accessToken,\n})\n`
|
|
303
|
+
await fs.writeFile(path.join(libDir, `contentful.${ext}`), content, 'utf8')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!enabledIntegrations.includes('strapi')) {
|
|
307
|
+
await fse.remove(path.join(libDir, `strapi.${ext}`))
|
|
308
|
+
} else {
|
|
309
|
+
const content = `${configImport}\nimport { createStrapiClient } from '@lucaismyname/l1-utils'\n\nexport const strapi = createStrapiClient({\n baseUrl: config.strapi.url,\n apiToken: config.strapi.token,\n})\n`
|
|
310
|
+
await fs.writeFile(path.join(libDir, `strapi.${ext}`), content, 'utf8')
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!enabledIntegrations.includes('sanity')) {
|
|
314
|
+
await fse.remove(path.join(libDir, `sanity.${ext}`))
|
|
315
|
+
} else {
|
|
316
|
+
const content = `${configImport}\nimport { createSanityClient } from '@lucaismyname/l1-utils'\n\nexport const sanity = createSanityClient({\n projectId: config.sanity.projectId,\n dataset: config.sanity.dataset,\n apiVersion: config.sanity.apiVersion,\n token: config.sanity.token,\n useCdn: config.sanity.useCdn,\n})\n`
|
|
317
|
+
await fs.writeFile(path.join(libDir, `sanity.${ext}`), content, 'utf8')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!enabledIntegrations.includes('notion')) {
|
|
321
|
+
await fse.remove(path.join(libDir, `notion.${ext}`))
|
|
322
|
+
} else {
|
|
323
|
+
const content = `${configImport}\nimport { createNotionClient } from '@lucaismyname/l1-utils'\n\nexport async function getNotion() {\n const token = config.notion.token\n if (!token) throw new Error('Missing VITE_NOTION_TOKEN')\n return createNotionClient({ token })\n}\n`
|
|
324
|
+
await fs.writeFile(path.join(libDir, `notion.${ext}`), content, 'utf8')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!enabledIntegrations.includes('mysql')) {
|
|
328
|
+
await fse.remove(path.join(libDir, `mysql.${ext}`))
|
|
329
|
+
} else {
|
|
330
|
+
const content = `// MySQL is backend-only.\n// Do not use MySQL from a Vite frontend app.\n// Use @lucaismyname/l1-utils from a Node server (Express/Fastify/Next API route, etc.).\n// Example (Node):\n// import { createMySqlPool } from '@lucaismyname/l1-utils'\n// const db = await createMySqlPool({ host, user, password, database })\n`
|
|
331
|
+
await fs.writeFile(path.join(libDir, `mysql.${ext}`), content, 'utf8')
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function applyContainerPreset(targetDir, language, presetKey) {
|
|
336
|
+
const preset = CONTAINER_PRESETS[presetKey] ?? CONTAINER_PRESETS.default
|
|
337
|
+
const appPath = path.join(targetDir, 'src', language === 'ts' ? 'App.tsx' : 'App.jsx')
|
|
338
|
+
const topNavPath = path.join(
|
|
339
|
+
targetDir,
|
|
340
|
+
'src',
|
|
341
|
+
'components',
|
|
342
|
+
language === 'ts' ? 'TopNav.tsx' : 'TopNav.jsx'
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const patchMaxW = async (filePath) => {
|
|
346
|
+
try {
|
|
347
|
+
const src = await fs.readFile(filePath, 'utf8')
|
|
348
|
+
const next = src
|
|
349
|
+
.replace(/max-w-5xl/g, preset.maxW)
|
|
350
|
+
.replace(/max-w-7xl/g, preset.maxW)
|
|
351
|
+
.replace(/max-w-3xl/g, preset.maxW)
|
|
352
|
+
if (next !== src) await fs.writeFile(filePath, next, 'utf8')
|
|
353
|
+
} catch {
|
|
354
|
+
// ignore
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await patchMaxW(appPath)
|
|
359
|
+
await patchMaxW(topNavPath)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function notFoundPageTs() {
|
|
363
|
+
return `import { Link } from 'react-router-dom'\n\nexport default function NotFound() {\n return (\n <div className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold tracking-tight\">Page not found</h1>\n <p className=\"text-muted-foreground\">The page you’re looking for doesn’t exist.</p>\n <Link to=\"/\" className=\"text-sm font-medium underline underline-offset-4\">\n Go home\n </Link>\n </div>\n )\n}\n`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function notFoundPageJs() {
|
|
367
|
+
return `import { Link } from 'react-router-dom'\n\nexport default function NotFound() {\n return (\n <div className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold tracking-tight\">Page not found</h1>\n <p className=\"text-muted-foreground\">The page you’re looking for doesn’t exist.</p>\n <Link to=\"/\" className=\"text-sm font-medium underline underline-offset-4\">\n Go home\n </Link>\n </div>\n )\n}\n`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function errorBoundaryTs() {
|
|
371
|
+
return `import * as React from 'react'\n\ntype Props = { children: React.ReactNode }\n\ntype State = { hasError: boolean; error?: unknown }\n\nexport class ErrorBoundary extends React.Component<Props, State> {\n state: State = { hasError: false }\n\n static getDerivedStateFromError(error: unknown) {\n return { hasError: true, error }\n }\n\n render() {\n if (this.state.hasError) {\n return (\n <div className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold tracking-tight\">Something went wrong</h1>\n <p className=\"text-muted-foreground\">An unexpected error occurred.</p>\n </div>\n )\n }\n\n return this.props.children\n }\n}\n`
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function errorBoundaryJs() {
|
|
375
|
+
return `import * as React from 'react'\n\nexport class ErrorBoundary extends React.Component {\n constructor(props) {\n super(props)\n this.state = { hasError: false }\n }\n\n static getDerivedStateFromError(error) {\n return { hasError: true, error }\n }\n\n render() {\n if (this.state.hasError) {\n return (\n <div className=\"space-y-2\">\n <h1 className=\"text-2xl font-semibold tracking-tight\">Something went wrong</h1>\n <p className=\"text-muted-foreground\">An unexpected error occurred.</p>\n </div>\n )\n }\n\n return this.props.children\n }\n}\n`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sidebarLayoutTs(containerMaxW) {
|
|
379
|
+
return `import { useMemo, useState } from 'react'\nimport { Link, NavLink, useLocation } from 'react-router-dom'\n\nconst navLinkClassName = ({ isActive }: { isActive: boolean }) =>\n [\n 'flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors',\n isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent/40',\n ].join(' ')\n\nexport default function SidebarLayout({ children }: { children: React.ReactNode }) {\n const [open, setOpen] = useState(false)\n const location = useLocation()\n\n useMemo(() => {\n setOpen(false)\n }, [location.pathname])\n\n return (\n <div className=\"min-h-dvh\">\n <header className=\"border-b md:hidden\">\n <div className=\"mx-auto flex h-14 ${containerMaxW} items-center justify-between px-4\">\n <Link to=\"/\" className=\"text-sm font-semibold\">\n L1 Stack\n </Link>\n <button\n type=\"button\"\n className=\"inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm\"\n onClick={() => setOpen((v) => !v)}\n >\n Menu\n </button>\n </div>\n </header>\n\n <div className=\"mx-auto ${containerMaxW} px-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-[240px_1fr] md:gap-8\">\n <aside className=\"md:sticky md:top-6 md:h-[calc(100dvh-3rem)] md:py-6\">\n <div className={open ? 'block' : 'hidden md:block'}>\n <div className=\"py-6 md:py-0\">\n <div className=\"mb-6 hidden md:block\">\n <Link to=\"/\" className=\"text-sm font-semibold\">\n L1 Stack\n </Link>\n </div>\n <nav className=\"space-y-1\">\n <NavLink to=\"/\" end className={navLinkClassName}>\n Home\n </NavLink>\n <NavLink to=\"/integrations\" className={navLinkClassName}>\n Integrations\n </NavLink>\n </nav>\n </div>\n </div>\n </aside>\n\n <main className=\"py-10\">{children}</main>\n </div>\n </div>\n </div>\n )\n}\n`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sidebarLayoutJs(containerMaxW) {
|
|
383
|
+
return `import { useMemo, useState } from 'react'\nimport { Link, NavLink, useLocation } from 'react-router-dom'\n\nconst navLinkClassName = ({ isActive }) =>\n [\n 'flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors',\n isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent/40',\n ].join(' ')\n\nexport default function SidebarLayout({ children }) {\n const [open, setOpen] = useState(false)\n const location = useLocation()\n\n useMemo(() => {\n setOpen(false)\n }, [location.pathname])\n\n return (\n <div className=\"min-h-dvh\">\n <header className=\"border-b md:hidden\">\n <div className=\"mx-auto flex h-14 ${containerMaxW} items-center justify-between px-4\">\n <Link to=\"/\" className=\"text-sm font-semibold\">\n L1 Stack\n </Link>\n <button\n type=\"button\"\n className=\"inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm\"\n onClick={() => setOpen((v) => !v)}\n >\n Menu\n </button>\n </div>\n </header>\n\n <div className=\"mx-auto ${containerMaxW} px-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-[240px_1fr] md:gap-8\">\n <aside className=\"md:sticky md:top-6 md:h-[calc(100dvh-3rem)] md:py-6\">\n <div className={open ? 'block' : 'hidden md:block'}>\n <div className=\"py-6 md:py-0\">\n <div className=\"mb-6 hidden md:block\">\n <Link to=\"/\" className=\"text-sm font-semibold\">\n L1 Stack\n </Link>\n </div>\n <nav className=\"space-y-1\">\n <NavLink to=\"/\" end className={navLinkClassName}>\n Home\n </NavLink>\n <NavLink to=\"/integrations\" className={navLinkClassName}>\n Integrations\n </NavLink>\n </nav>\n </div>\n </div>\n </aside>\n\n <main className=\"py-10\">{children}</main>\n </div>\n </div>\n </div>\n )\n}\n`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function applyLayout(targetDir, language, layoutKey, containerPresetKey) {
|
|
387
|
+
const appPath = path.join(targetDir, 'src', language === 'ts' ? 'App.tsx' : 'App.jsx')
|
|
388
|
+
const notFoundPath = path.join(
|
|
389
|
+
targetDir,
|
|
390
|
+
'src',
|
|
391
|
+
'pages',
|
|
392
|
+
language === 'ts' ? 'NotFound.tsx' : 'NotFound.jsx'
|
|
393
|
+
)
|
|
394
|
+
const errorBoundaryPath = path.join(
|
|
395
|
+
targetDir,
|
|
396
|
+
'src',
|
|
397
|
+
'components',
|
|
398
|
+
language === 'ts' ? 'ErrorBoundary.tsx' : 'ErrorBoundary.jsx'
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
await fs.mkdir(path.dirname(notFoundPath), { recursive: true })
|
|
402
|
+
await fs.mkdir(path.dirname(errorBoundaryPath), { recursive: true })
|
|
403
|
+
await fs.writeFile(notFoundPath, language === 'ts' ? notFoundPageTs() : notFoundPageJs(), 'utf8')
|
|
404
|
+
await fs.writeFile(errorBoundaryPath, language === 'ts' ? errorBoundaryTs() : errorBoundaryJs(), 'utf8')
|
|
405
|
+
|
|
406
|
+
const preset = CONTAINER_PRESETS[containerPresetKey] ?? CONTAINER_PRESETS.default
|
|
407
|
+
const containerMaxW = preset.maxW
|
|
408
|
+
|
|
409
|
+
if (layoutKey === 'sidebar') {
|
|
410
|
+
const sidebarPath = path.join(
|
|
411
|
+
targetDir,
|
|
412
|
+
'src',
|
|
413
|
+
'components',
|
|
414
|
+
language === 'ts' ? 'SidebarLayout.tsx' : 'SidebarLayout.jsx'
|
|
415
|
+
)
|
|
416
|
+
await fs.mkdir(path.dirname(sidebarPath), { recursive: true })
|
|
417
|
+
await fs.writeFile(
|
|
418
|
+
sidebarPath,
|
|
419
|
+
language === 'ts' ? sidebarLayoutTs(containerMaxW) : sidebarLayoutJs(containerMaxW),
|
|
420
|
+
'utf8'
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
const app = `import { Route, Routes } from 'react-router-dom'\nimport SidebarLayout from './components/SidebarLayout'\nimport { ErrorBoundary } from './components/ErrorBoundary'\nimport Home from './pages/Home'\nimport Integrations from './pages/Integrations'\nimport NotFound from './pages/NotFound'\n\nfunction App() {\n return (\n <SidebarLayout>\n <ErrorBoundary>\n <Routes>\n <Route path=\"/\" element={<Home />} />\n <Route path=\"/integrations\" element={<Integrations />} />\n <Route path=\"*\" element={<NotFound />} />\n </Routes>\n </ErrorBoundary>\n </SidebarLayout>\n )\n}\n\nexport default App\n`
|
|
424
|
+
await fs.writeFile(appPath, app, 'utf8')
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (layoutKey === 'blank') {
|
|
429
|
+
const app = `import { Route, Routes } from 'react-router-dom'\nimport { ErrorBoundary } from './components/ErrorBoundary'\nimport Home from './pages/Home'\nimport Integrations from './pages/Integrations'\nimport NotFound from './pages/NotFound'\n\nfunction App() {\n return (\n <div className=\"min-h-dvh\">\n <main className=\"mx-auto ${containerMaxW} px-4 py-10\">\n <ErrorBoundary>\n <Routes>\n <Route path=\"/\" element={<Home />} />\n <Route path=\"/integrations\" element={<Integrations />} />\n <Route path=\"*\" element={<NotFound />} />\n </Routes>\n </ErrorBoundary>\n </main>\n </div>\n )\n}\n\nexport default App\n`
|
|
430
|
+
await fs.writeFile(appPath, app, 'utf8')
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// topNav: patch existing App if possible
|
|
435
|
+
try {
|
|
436
|
+
const src = await fs.readFile(appPath, 'utf8')
|
|
437
|
+
let next = src
|
|
438
|
+
next = next.replace(
|
|
439
|
+
/import \{ Navigate, Route, Routes \} from 'react-router-dom'/g,
|
|
440
|
+
"import { Route, Routes } from 'react-router-dom'"
|
|
441
|
+
)
|
|
442
|
+
if (!next.includes("./components/ErrorBoundary")) {
|
|
443
|
+
next = next.replace(
|
|
444
|
+
/import TopNav from '\.\/components\/TopNav'\n/,
|
|
445
|
+
"import TopNav from './components/TopNav'\nimport { ErrorBoundary } from './components/ErrorBoundary'\n"
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
if (!next.includes("./pages/NotFound")) {
|
|
449
|
+
next = next.replace(
|
|
450
|
+
/import Integrations from '\.\/pages\/Integrations'\n/,
|
|
451
|
+
"import Integrations from './pages/Integrations'\nimport NotFound from './pages/NotFound'\n"
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
next = next.replace(
|
|
455
|
+
/<Routes>[\s\S]*?<\/Routes>/m,
|
|
456
|
+
`<ErrorBoundary>\n <Routes>\n <Route path="/" element={<Home />} />\n <Route path="/integrations" element={<Integrations />} />\n <Route path="*" element={<NotFound />} />\n </Routes>\n </ErrorBoundary>`
|
|
457
|
+
)
|
|
458
|
+
if (next !== src) await fs.writeFile(appPath, next, 'utf8')
|
|
459
|
+
} catch {
|
|
460
|
+
// ignore
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function applyDeployTarget(targetDir, targetKey) {
|
|
465
|
+
const target = DEPLOY_TARGETS[targetKey] ? targetKey : 'static'
|
|
466
|
+
|
|
467
|
+
const publicDir = path.join(targetDir, 'public')
|
|
468
|
+
await fs.mkdir(publicDir, { recursive: true })
|
|
469
|
+
|
|
470
|
+
const redirectsPath = path.join(publicDir, '_redirects')
|
|
471
|
+
const redirects = `/* /index.html 200\n`
|
|
472
|
+
await fs.writeFile(redirectsPath, redirects, 'utf8')
|
|
473
|
+
|
|
474
|
+
const nginxSnippetPath = path.join(targetDir, 'nginx.conf.snippet')
|
|
475
|
+
const nginxSnippet = `location / {\n try_files $uri $uri/ /index.html;\n}\n`
|
|
476
|
+
await fs.writeFile(nginxSnippetPath, nginxSnippet, 'utf8')
|
|
477
|
+
|
|
478
|
+
if (target === 'netlify') {
|
|
479
|
+
const netlifyToml = `[build]\n command = "npm run build"\n publish = "dist"\n\n[[redirects]]\n from = "/*"\n to = "/index.html"\n status = 200\n`
|
|
480
|
+
await fs.writeFile(path.join(targetDir, 'netlify.toml'), netlifyToml, 'utf8')
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (target === 'vercel') {
|
|
484
|
+
const vercelJson = `{"rewrites":[{"source":"/(.*)","destination":"/index.html"}]}`
|
|
485
|
+
await fs.writeFile(path.join(targetDir, 'vercel.json'), vercelJson, 'utf8')
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
175
489
|
function toKebabCaseName(input) {
|
|
176
490
|
return input
|
|
177
491
|
.trim()
|
|
@@ -334,6 +648,66 @@ async function main() {
|
|
|
334
648
|
process.exit(0)
|
|
335
649
|
}
|
|
336
650
|
|
|
651
|
+
const integrations = []
|
|
652
|
+
for (const key of Object.keys(INTEGRATIONS)) {
|
|
653
|
+
const enabled = await select({
|
|
654
|
+
message: `Enable ${INTEGRATIONS[key].label}?`,
|
|
655
|
+
options: [
|
|
656
|
+
{ value: 'yes', label: 'Yes' },
|
|
657
|
+
{ value: 'no', label: 'No' },
|
|
658
|
+
],
|
|
659
|
+
initialValue: 'no',
|
|
660
|
+
})
|
|
661
|
+
if (isCancel(enabled)) {
|
|
662
|
+
cancel('Cancelled')
|
|
663
|
+
process.exit(0)
|
|
664
|
+
}
|
|
665
|
+
if (enabled === 'yes') integrations.push(key)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const layout = await select({
|
|
669
|
+
message: 'Layout',
|
|
670
|
+
options: [
|
|
671
|
+
{ value: 'topNav', label: LAYOUTS.topNav.label },
|
|
672
|
+
{ value: 'sidebar', label: LAYOUTS.sidebar.label },
|
|
673
|
+
{ value: 'blank', label: LAYOUTS.blank.label },
|
|
674
|
+
],
|
|
675
|
+
initialValue: 'topNav',
|
|
676
|
+
})
|
|
677
|
+
if (isCancel(layout)) {
|
|
678
|
+
cancel('Cancelled')
|
|
679
|
+
process.exit(0)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const containerPreset = await select({
|
|
683
|
+
message: 'Content width',
|
|
684
|
+
options: [
|
|
685
|
+
{ value: 'narrow', label: CONTAINER_PRESETS.narrow.label },
|
|
686
|
+
{ value: 'default', label: CONTAINER_PRESETS.default.label },
|
|
687
|
+
{ value: 'wide', label: CONTAINER_PRESETS.wide.label },
|
|
688
|
+
],
|
|
689
|
+
initialValue: 'default',
|
|
690
|
+
})
|
|
691
|
+
if (isCancel(containerPreset)) {
|
|
692
|
+
cancel('Cancelled')
|
|
693
|
+
process.exit(0)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const deployTarget = await select({
|
|
697
|
+
message: 'Deploy to',
|
|
698
|
+
options: [
|
|
699
|
+
{ value: 'static', label: DEPLOY_TARGETS.static.label },
|
|
700
|
+
{ value: 'netlify', label: DEPLOY_TARGETS.netlify.label },
|
|
701
|
+
{ value: 'vercel', label: DEPLOY_TARGETS.vercel.label },
|
|
702
|
+
{ value: 'cloudflare', label: DEPLOY_TARGETS.cloudflare.label },
|
|
703
|
+
],
|
|
704
|
+
initialValue: 'static',
|
|
705
|
+
})
|
|
706
|
+
if (isCancel(deployTarget)) {
|
|
707
|
+
cancel('Cancelled')
|
|
708
|
+
process.exit(0)
|
|
709
|
+
}
|
|
710
|
+
|
|
337
711
|
const targetDir = path.resolve(process.cwd(), projectName)
|
|
338
712
|
// NOTE: npm downloads the CLI package before prompts run.
|
|
339
713
|
// To avoid downloading both templates, we fetch only the chosen template from npm.
|
|
@@ -387,6 +761,11 @@ async function main() {
|
|
|
387
761
|
|
|
388
762
|
await applyPrimaryTheme(targetDir, THEME_PRESETS[primaryColor] ?? THEME_PRESETS.slate)
|
|
389
763
|
await applyIconSet(targetDir, language, iconSet)
|
|
764
|
+
await applyEnvAndConfig(targetDir, language, integrations)
|
|
765
|
+
await applyIntegrations(targetDir, language, integrations)
|
|
766
|
+
await applyLayout(targetDir, language, layout, containerPreset)
|
|
767
|
+
await applyContainerPreset(targetDir, language, containerPreset)
|
|
768
|
+
await applyDeployTarget(targetDir, deployTarget)
|
|
390
769
|
|
|
391
770
|
// Update package name
|
|
392
771
|
const pkgJsonPath = path.join(targetDir, 'package.json')
|