import { stripIndent, transformObjectToInnerString } from "@kottster/common"; type TemplateVars = { 'vite.config.js': undefined; 'tsconfig.json': undefined; 'Dockerfile': undefined; 'docker-compose.yml': undefined; 'app/_server/app.js': undefined; 'app/_server/server.js': undefined; 'app/_server/data-sources/postgres/index.js': { connectionDetails?: Record; }; 'app/_server/data-sources/mysql/index.js': { connectionDetails?: Record; }; 'app/_server/data-sources/mariadb/index.js': { connectionDetails?: Record; }; 'app/_server/data-sources/sqlite/index.js': { connectionDetails?: Record; }; 'app/_server/data-sources/mssql/index.js': { connectionDetails?: Record; }; 'app/index.html': undefined; 'app/main.jsx': undefined; 'app/schemas/sidebar.json': undefined; }; /** * Service for storing file templates */ export class FileTemplateManager { constructor( private readonly usingTsc: boolean ) {} static templates: { [K in keyof TemplateVars]: TemplateVars[K] extends undefined ? string | ((usingTsc: boolean) => string) : (usingTsc: boolean, vars: TemplateVars[K]) => string; } = { 'vite.config.js': () => stripIndent(` import { defineConfig } from 'vite'; import { vitePlugin as kottster } from '@kottster/react'; import react from '@vitejs/plugin-react'; import schema from './kottster-app.json'; export default defineConfig({ root: './app', server: { port: 5480, open: false, }, build: { outDir: '../dist/client', emptyOutDir: true, chunkSizeWarningLimit: 10000, }, plugins: [ kottster({ schema }), react(), ], resolve: { alias: { '@': '/app' } }, }); `), 'tsconfig.json': stripIndent(` { "include": [ "**/app/**/*.ts", "**/app/**/*.tsx", "**/_server/**/*.ts", "**/_server/**/*.tsx", "**/.client/**/*.ts", "**/.client/**/*.tsx" ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["@types/node", "vite/client"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "allowJs": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", "outDir": "./dist", "paths": { "@/*": ["./app/*"] }, "noEmit": true } } `), 'Dockerfile': stripIndent(` # For production deployment FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /app/dist ./dist COPY app.db ./app.db ENV PORT=3000 EXPOSE $PORT CMD ["node", "dist/server/server.cjs"] `), 'docker-compose.yml': stripIndent(` # For production deployment version: '3.8' services: app: build: context: . dockerfile: Dockerfile ports: - "3000:3000" environment: - PORT=3000 `), 'app/_server/app.js': stripIndent(` import { createApp, createIdentityProvider } from '@kottster/server'; import schema from '../../kottster-app.json'; /* * For security, consider moving the secret data to environment variables. * See https://kottster.app/docs/deploying#before-you-deploy */ export const app = createApp({ schema, secretKey: '', /* * The identity provider configuration. * See https://kottster.app/docs/app-configuration/identity-provider */ identityProvider: createIdentityProvider('sqlite', { fileName: 'app.db', passwordHashAlgorithm: 'bcrypt', jwtSecretSalt: '', /* The root admin user credentials */ rootUsername: 'admin', rootPassword: 'admin', }), }); `), 'app/_server/server.js': stripIndent(` import { app } from './app'; async function bootstrap() { // Use the PORT environment variable to set the port in production await app.listen(); } bootstrap().catch(err => { console.error(err); process.exit(1); }); `), 'app/_server/data-sources/postgres/index.js': (usingTsc, vars) => stripIndent(` import { KnexPgAdapter } from '@kottster/server'; import knex from 'knex'; /**${!vars.connectionDetails ? ` \n * Replace the following with your connection options. ` : ''} * Learn more at https://knexjs.org/guide/#configuration-options */ const client = knex({ client: 'pg', \n${vars.connectionDetails ? transformObjectToInnerString(vars.connectionDetails, ' ') : ` connection: { host: 'localhost', port: 5432, user: 'myuser', password: 'mypassword', database: 'mydatabase', }, searchPath: ['public'],`} }); export default new KnexPgAdapter(client); `), 'app/_server/data-sources/mysql/index.js': (usingTsc, vars) => stripIndent(` import { KnexMysql2Adapter } from '@kottster/server'; import knex from 'knex'; /**${!vars.connectionDetails ? ` \n * Replace the following with your connection options. ` : ''} * Learn more at https://knexjs.org/guide/#configuration-options */ const client = knex({ client: 'mysql2', \n${vars.connectionDetails ? transformObjectToInnerString(vars.connectionDetails, ' ') : ` connection: { host: 'localhost', port: 3306, user: 'myuser', password: 'mypassword', database: 'mydatabase', },`} }); export default new KnexMysql2Adapter(client); `), 'app/_server/data-sources/mariadb/index.js': (usingTsc, vars) => stripIndent(` import { KnexMysql2Adapter } from '@kottster/server'; import knex from 'knex'; /**${!vars.connectionDetails ? ` \n * Replace the following with your connection options. ` : ''} * Learn more at https://knexjs.org/guide/#configuration-options */ const client = knex({ client: 'mysql2', \n${vars.connectionDetails ? transformObjectToInnerString(vars.connectionDetails, ' ') : ` connection: { host: 'localhost', port: 3307, user: 'myuser', password: 'mypassword', database: 'mydatabase', },`} }); export default new KnexMysql2Adapter(client); `), 'app/_server/data-sources/mssql/index.js': (usingTsc, vars) => stripIndent(` import { KnexTediousAdapter } from '@kottster/server'; import knex from 'knex'; /**${!vars.connectionDetails ? ` \n * Replace the following with your connection options. ` : ''} * Learn more at https://knexjs.org/guide/#configuration-options */ const client = knex({ client: 'mssql', \n${vars.connectionDetails ? transformObjectToInnerString(vars.connectionDetails, ' ') : ` connection: { host: 'localhost', port: 5432, user: 'myuser', password: 'mypassword', database: 'mydatabase', }, searchPath: ['dbo'],`} }); export default new KnexTediousAdapter(client); `), 'app/_server/data-sources/sqlite/index.js': (usingTsc, vars) => stripIndent(` import { KnexBetterSqlite3Adapter } from '@kottster/server'; import knex from 'knex'; /**${!vars.connectionDetails ? ` \n * Replace the following with your connection options. ` : ''} * Learn more at https://knexjs.org/guide/#configuration-options */ const client = knex({ client: 'better-sqlite3', \n${vars.connectionDetails ? transformObjectToInnerString(vars.connectionDetails, ' ') : ` connection: { filename: '/path/to/database.sqlite', },`} }); export default new KnexBetterSqlite3Adapter(client); `), 'app/index.html': usingTsc => stripIndent(` Kottster App
`), 'app/main.jsx': usingTsc => stripIndent(` import React from 'react'; import ReactDOM from 'react-dom/client'; import { KottsterApp } from '@kottster/react'; import '@kottster/react/dist/style.css'; const pageEntries = import.meta.glob('./pages/**/index.${usingTsc ? `{jsx,tsx}` : 'jsx'}', { eager: true }); ReactDOM.createRoot(document.getElementById('root')${usingTsc ? '!' : ''}).render( ); `), 'app/schemas/sidebar.json': () => stripIndent(` { "menuPageOrder": [] } `), }; /** * Get a template * @param name Template name * @param vars Variables to replace in the template * @returns The file content */ public getTemplate( name: T, vars: TemplateVars[T] = {} as TemplateVars[T] ): string { const template = FileTemplateManager.templates[name]; if (!template) { throw new Error(`Template ${name} not found`); } if (typeof template === 'function') { return template(this.usingTsc, vars as TemplateVars[T]); } return template; } }