@podosoft/podokit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create.d.ts +26 -0
- package/dist/create.js +44 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +99 -0
- package/dist/prompt.d.ts +21 -0
- package/dist/prompt.js +38 -0
- package/dist/templates/base/README.md +19 -0
- package/dist/templates/base/apps/api/package.json +10 -0
- package/dist/templates/base/apps/api/src/main.ts +5 -0
- package/dist/templates/base/apps/web/package.json +10 -0
- package/dist/templates/base/apps/web/src/app.css +1 -0
- package/dist/templates/base/dot-env.example +15 -0
- package/dist/templates/base/dot-gitignore +9 -0
- package/dist/templates/base/package.json +17 -0
- package/dist/templates/fullstack-nest-svelte/README.md +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/nest-cli.json +5 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +32 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/common/all-exceptions.filter.ts +43 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +18 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +13 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.module.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/test/health.e2e-spec.ts +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/tsconfig.json +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/components.json +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +28 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.d.ts +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/README.md +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/en.ts +10 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +8 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +9 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +26 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/health/+server.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/static/.gitkeep +0 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/svelte.config.js +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/tsconfig.json +9 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +16 -0
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +9 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +29 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +24 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +10 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +18 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/namespace.yaml +4 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/secret.example.yaml +10 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/services.yaml +21 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/web-deployment.yaml +20 -0
- package/dist/templates/fullstack-nest-svelte/package.json +13 -0
- package/package.json +31 -0
package/dist/create.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PackageManager = "npm" | "pnpm" | "yarn";
|
|
2
|
+
export declare const DEFAULT_TEMPLATE = "fullstack-nest-svelte";
|
|
3
|
+
export interface CreateOptions {
|
|
4
|
+
/** Project name; also the default directory name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Directory that holds the template sets (each in its own subfolder). */
|
|
7
|
+
templatesDir: string;
|
|
8
|
+
/** Template subfolder to use. Defaults to `fullstack-nest-svelte`. */
|
|
9
|
+
template?: string;
|
|
10
|
+
/** Where to create the project. Defaults to `<cwd>/<name>`. */
|
|
11
|
+
targetDir?: string;
|
|
12
|
+
/** Package manager recorded in the generated project. Defaults to `npm`. */
|
|
13
|
+
packageManager?: PackageManager;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateResult {
|
|
16
|
+
projectDir: string;
|
|
17
|
+
packageManager: PackageManager;
|
|
18
|
+
template: string;
|
|
19
|
+
}
|
|
20
|
+
/** Validate a project name: no path separators, npm-friendly characters. */
|
|
21
|
+
export declare function assertValidName(name: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Scaffold a new project from the `base` template. Pure enough to test:
|
|
24
|
+
* given a name and a templates directory, it writes files and returns where.
|
|
25
|
+
*/
|
|
26
|
+
export declare function create(options: CreateOptions): CreateResult;
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TEMPLATE = void 0;
|
|
4
|
+
exports.assertValidName = assertValidName;
|
|
5
|
+
exports.create = create;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
9
|
+
exports.DEFAULT_TEMPLATE = "fullstack-nest-svelte";
|
|
10
|
+
const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-._]*[a-z0-9])?$/i;
|
|
11
|
+
/** Validate a project name: no path separators, npm-friendly characters. */
|
|
12
|
+
function assertValidName(name) {
|
|
13
|
+
if (!name || !NAME_PATTERN.test(name)) {
|
|
14
|
+
throw new Error(`Invalid project name "${name}". Use letters, digits, "-", "_", "." and no path separators.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isEmptyDir(dir) {
|
|
18
|
+
return !(0, node_fs_1.existsSync)(dir) || (0, node_fs_1.readdirSync)(dir).length === 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scaffold a new project from the `base` template. Pure enough to test:
|
|
22
|
+
* given a name and a templates directory, it writes files and returns where.
|
|
23
|
+
*/
|
|
24
|
+
function create(options) {
|
|
25
|
+
const { name, templatesDir } = options;
|
|
26
|
+
assertValidName(name);
|
|
27
|
+
const template = options.template ?? exports.DEFAULT_TEMPLATE;
|
|
28
|
+
const packageManager = options.packageManager ?? "npm";
|
|
29
|
+
const projectDir = options.targetDir
|
|
30
|
+
? (0, node_path_1.isAbsolute)(options.targetDir)
|
|
31
|
+
? options.targetDir
|
|
32
|
+
: (0, node_path_1.resolve)(process.cwd(), options.targetDir)
|
|
33
|
+
: (0, node_path_1.resolve)(process.cwd(), name);
|
|
34
|
+
if (!isEmptyDir(projectDir)) {
|
|
35
|
+
throw new Error(`Target directory is not empty: ${projectDir}`);
|
|
36
|
+
}
|
|
37
|
+
const templateDir = (0, node_path_1.join)(templatesDir, template);
|
|
38
|
+
if (!(0, node_fs_1.existsSync)(templateDir)) {
|
|
39
|
+
throw new Error(`Template "${template}" not found at ${templateDir}`);
|
|
40
|
+
}
|
|
41
|
+
const vars = { projectName: name, packageManager };
|
|
42
|
+
(0, podokit_template_engine_1.copyTemplate)(templateDir, projectDir, vars);
|
|
43
|
+
return { projectDir, packageManager, template };
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type PackageManager } from "./create";
|
|
3
|
+
interface ParsedArgs {
|
|
4
|
+
command?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
template?: string;
|
|
7
|
+
dir?: string;
|
|
8
|
+
pm?: PackageManager;
|
|
9
|
+
yes: boolean;
|
|
10
|
+
help: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.parseArgs = parseArgs;
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const promises_1 = require("node:readline/promises");
|
|
7
|
+
const create_1 = require("./create");
|
|
8
|
+
const prompt_1 = require("./prompt");
|
|
9
|
+
const HELP = `podo — PodoKit project generator
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
podo create <name> [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--template <t> Template: fullstack-nest-svelte | base (default: fullstack-nest-svelte)
|
|
16
|
+
--dir <path> Target directory (default: ./<name>)
|
|
17
|
+
--pm <name> Package manager: npm | pnpm | yarn (default: npm)
|
|
18
|
+
-y, --yes Skip prompts and accept defaults
|
|
19
|
+
-h, --help Show this help
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
npx @podosoft/podokit create my-app
|
|
23
|
+
`;
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const parsed = { help: false, yes: false };
|
|
26
|
+
const positionals = [];
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg === "-h" || arg === "--help") {
|
|
30
|
+
parsed.help = true;
|
|
31
|
+
}
|
|
32
|
+
else if (arg === "-y" || arg === "--yes") {
|
|
33
|
+
parsed.yes = true;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--template") {
|
|
36
|
+
parsed.template = argv[++i];
|
|
37
|
+
}
|
|
38
|
+
else if (arg === "--dir") {
|
|
39
|
+
parsed.dir = argv[++i];
|
|
40
|
+
}
|
|
41
|
+
else if (arg === "--pm") {
|
|
42
|
+
parsed.pm = argv[++i];
|
|
43
|
+
}
|
|
44
|
+
else if (arg !== undefined && !arg.startsWith("-")) {
|
|
45
|
+
positionals.push(arg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
parsed.command = positionals[0];
|
|
49
|
+
parsed.name = positionals[1];
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function fail(message) {
|
|
53
|
+
process.stderr.write(`error: ${message}\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
async function main(argv) {
|
|
57
|
+
const args = parseArgs(argv);
|
|
58
|
+
if (args.help || !args.command) {
|
|
59
|
+
process.stdout.write(HELP);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (args.command !== "create") {
|
|
63
|
+
fail(`Unknown command "${args.command}". Run "podo --help".`);
|
|
64
|
+
}
|
|
65
|
+
if (!args.name) {
|
|
66
|
+
fail('Missing project name. Usage: podo create <name>');
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
(0, create_1.assertValidName)(args.name);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
fail(err.message);
|
|
73
|
+
}
|
|
74
|
+
const interactive = Boolean(process.stdin.isTTY) && !args.yes;
|
|
75
|
+
const rl = interactive ? (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout }) : undefined;
|
|
76
|
+
const ask = async (question) => (rl ? (await rl.question(question)).trim() : "");
|
|
77
|
+
const templatesDir = (0, node_path_1.join)(__dirname, "templates");
|
|
78
|
+
try {
|
|
79
|
+
const resolved = await (0, prompt_1.resolveCreateOptions)({ template: args.template, pm: args.pm }, ask, interactive);
|
|
80
|
+
const result = (0, create_1.create)({
|
|
81
|
+
name: args.name,
|
|
82
|
+
templatesDir,
|
|
83
|
+
template: resolved.template,
|
|
84
|
+
targetDir: args.dir,
|
|
85
|
+
packageManager: resolved.packageManager,
|
|
86
|
+
});
|
|
87
|
+
const relPath = (0, node_path_1.relative)(process.cwd(), result.projectDir) || ".";
|
|
88
|
+
const rel = relPath.startsWith("..") ? result.projectDir : relPath;
|
|
89
|
+
const pm = result.packageManager;
|
|
90
|
+
process.stdout.write(`\nCreated ${args.name} (${result.template}) in ${rel}\n\nNext steps:\n cd ${rel}\n ${pm} install\n ${pm} run dev\n`);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
fail(err.message);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
rl?.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
void main(process.argv.slice(2));
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type PackageManager } from "./create";
|
|
2
|
+
export declare const TEMPLATES: readonly ["fullstack-nest-svelte", "base"];
|
|
3
|
+
export declare const PACKAGE_MANAGERS: PackageManager[];
|
|
4
|
+
/** Asks a single question and resolves to the trimmed answer (empty if skipped). */
|
|
5
|
+
export type Ask = (question: string) => Promise<string>;
|
|
6
|
+
export interface RawCreateArgs {
|
|
7
|
+
template?: string;
|
|
8
|
+
pm?: PackageManager;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedCreateOptions {
|
|
11
|
+
template: string;
|
|
12
|
+
packageManager: PackageManager;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the template and package manager for `create`.
|
|
16
|
+
*
|
|
17
|
+
* Precedence: an explicit flag wins; otherwise, when `interactive` is true the
|
|
18
|
+
* user is prompted (blank answer = default); otherwise the default is used.
|
|
19
|
+
* Kept free of I/O — the caller injects `ask` — so it is unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveCreateOptions(args: RawCreateArgs, ask: Ask, interactive: boolean): Promise<ResolvedCreateOptions>;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PACKAGE_MANAGERS = exports.TEMPLATES = void 0;
|
|
4
|
+
exports.resolveCreateOptions = resolveCreateOptions;
|
|
5
|
+
const create_1 = require("./create");
|
|
6
|
+
exports.TEMPLATES = ["fullstack-nest-svelte", "base"];
|
|
7
|
+
exports.PACKAGE_MANAGERS = ["npm", "pnpm", "yarn"];
|
|
8
|
+
function isPackageManager(value) {
|
|
9
|
+
return exports.PACKAGE_MANAGERS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the template and package manager for `create`.
|
|
13
|
+
*
|
|
14
|
+
* Precedence: an explicit flag wins; otherwise, when `interactive` is true the
|
|
15
|
+
* user is prompted (blank answer = default); otherwise the default is used.
|
|
16
|
+
* Kept free of I/O — the caller injects `ask` — so it is unit-testable.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveCreateOptions(args, ask, interactive) {
|
|
19
|
+
let template = args.template;
|
|
20
|
+
if (!template && interactive) {
|
|
21
|
+
const answer = await ask(`Template (${exports.TEMPLATES.join(" / ")}) [${create_1.DEFAULT_TEMPLATE}]: `);
|
|
22
|
+
template = answer || undefined;
|
|
23
|
+
}
|
|
24
|
+
template = template ?? create_1.DEFAULT_TEMPLATE;
|
|
25
|
+
if (!exports.TEMPLATES.includes(template)) {
|
|
26
|
+
throw new Error(`Unknown template "${template}". Choose one of: ${exports.TEMPLATES.join(", ")}.`);
|
|
27
|
+
}
|
|
28
|
+
let pm = args.pm;
|
|
29
|
+
if (!pm && interactive) {
|
|
30
|
+
const answer = await ask(`Package manager (${exports.PACKAGE_MANAGERS.join(" / ")}) [npm]: `);
|
|
31
|
+
pm = answer || undefined;
|
|
32
|
+
}
|
|
33
|
+
pm = pm ?? "npm";
|
|
34
|
+
if (!isPackageManager(pm)) {
|
|
35
|
+
throw new Error(`Invalid package manager "${pm}". Choose one of: ${exports.PACKAGE_MANAGERS.join(", ")}.`);
|
|
36
|
+
}
|
|
37
|
+
return { template, packageManager: pm };
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Generated with [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
4
|
+
|
|
5
|
+
A full-stack TypeScript workspace with a NestJS API (`apps/api`) and a
|
|
6
|
+
SvelteKit web app (`apps/web`).
|
|
7
|
+
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
{{packageManager}} install
|
|
12
|
+
{{packageManager}} run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Copy the example environment file before running:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cp .env.example .env
|
|
19
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* {{projectName}} web styles. TailwindCSS is wired up by the full template. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copy to .env and adjust. Never commit real secrets.
|
|
2
|
+
POSTGRES_HOST=localhost
|
|
3
|
+
POSTGRES_PORT=5432
|
|
4
|
+
POSTGRES_USER=podokit
|
|
5
|
+
POSTGRES_PASSWORD=podokit
|
|
6
|
+
POSTGRES_DB=podokit
|
|
7
|
+
|
|
8
|
+
REDIS_HOST=localhost
|
|
9
|
+
REDIS_PORT=6379
|
|
10
|
+
|
|
11
|
+
APP_BASE_URL=http://localhost:5173
|
|
12
|
+
API_BASE_URL=http://localhost:3000
|
|
13
|
+
BACKEND_INTERNAL_URL=http://localhost:3000
|
|
14
|
+
CORS_ORIGIN=http://localhost:5173
|
|
15
|
+
PORT=3000
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20"
|
|
7
|
+
},
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"apps/*"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "echo \"Run 'dev' in apps/api and apps/web\" && exit 0",
|
|
13
|
+
"build": "npm run build --workspaces --if-present",
|
|
14
|
+
"lint": "npm run lint --workspaces --if-present",
|
|
15
|
+
"test": "npm run test --workspaces --if-present"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Full-stack TypeScript app generated with [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
4
|
+
|
|
5
|
+
- `apps/api` — NestJS API (config validation, health checks, standard error envelope)
|
|
6
|
+
- `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) that talks to the API through a server-side proxy
|
|
7
|
+
- `infra/` — Docker Compose and k3s manifests
|
|
8
|
+
|
|
9
|
+
## Getting started
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
{{packageManager}} install
|
|
13
|
+
cp .env.example .env
|
|
14
|
+
{{packageManager}} run dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- API: http://localhost:3000 (health at `/health`)
|
|
18
|
+
- Web: http://localhost:5173
|
|
19
|
+
|
|
20
|
+
## Local services
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
docker compose -f infra/docker/docker-compose.yml up -d
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Deploy
|
|
27
|
+
|
|
28
|
+
Docker Compose manifests live in `infra/docker`; example k3s manifests in `infra/k3s`
|
|
29
|
+
(use `secret.example.yaml` as a template — never commit real secrets).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:20-alpine AS deps
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY package.json ./
|
|
5
|
+
RUN npm install --omit=dev=false --no-audit --no-fund
|
|
6
|
+
|
|
7
|
+
FROM node:20-alpine AS build
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
FROM node:20-alpine AS runtime
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
17
|
+
COPY --from=build /app/dist ./dist
|
|
18
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
19
|
+
COPY package.json ./
|
|
20
|
+
USER app
|
|
21
|
+
EXPOSE 3000
|
|
22
|
+
CMD ["node", "dist/main"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-api",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nest start --watch",
|
|
7
|
+
"build": "nest build",
|
|
8
|
+
"start": "node dist/main",
|
|
9
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@nestjs/common": "^10.4.0",
|
|
14
|
+
"@nestjs/core": "^10.4.0",
|
|
15
|
+
"@nestjs/platform-express": "^10.4.0",
|
|
16
|
+
"class-transformer": "^0.5.1",
|
|
17
|
+
"class-validator": "^0.14.1",
|
|
18
|
+
"reflect-metadata": "^0.2.2",
|
|
19
|
+
"rxjs": "^7.8.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@nestjs/cli": "^10.4.0",
|
|
23
|
+
"@nestjs/testing": "^10.4.0",
|
|
24
|
+
"@types/express": "^4.17.21",
|
|
25
|
+
"@types/node": "^20.17.0",
|
|
26
|
+
"@types/supertest": "^6.0.2",
|
|
27
|
+
"jest": "^29.7.0",
|
|
28
|
+
"supertest": "^7.0.0",
|
|
29
|
+
"ts-jest": "^29.2.5",
|
|
30
|
+
"typescript": "^5.6.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
|
|
2
|
+
import type { Request, Response } from "express";
|
|
3
|
+
import { AppException } from "./app-exception";
|
|
4
|
+
|
|
5
|
+
interface ErrorBody {
|
|
6
|
+
success: false;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
path: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Catch()
|
|
17
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
18
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
19
|
+
const ctx = host.switchToHttp();
|
|
20
|
+
const response = ctx.getResponse<Response>();
|
|
21
|
+
const request = ctx.getRequest<Request>();
|
|
22
|
+
|
|
23
|
+
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
24
|
+
let code = "INTERNAL_ERROR";
|
|
25
|
+
let message = "Internal server error";
|
|
26
|
+
|
|
27
|
+
if (exception instanceof AppException) {
|
|
28
|
+
statusCode = exception.statusCode;
|
|
29
|
+
code = exception.code;
|
|
30
|
+
message = exception.message;
|
|
31
|
+
} else if (exception instanceof HttpException) {
|
|
32
|
+
statusCode = exception.getStatus();
|
|
33
|
+
code = "HTTP_ERROR";
|
|
34
|
+
message = exception.message;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body: ErrorBody = {
|
|
38
|
+
success: false,
|
|
39
|
+
error: { code, message, statusCode, path: request.url, timestamp: new Date().toISOString() },
|
|
40
|
+
};
|
|
41
|
+
response.status(statusCode).json(body);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Stable, language-independent error codes. The frontend branches on `code`,
|
|
2
|
+
// not on the human-readable message.
|
|
3
|
+
export class AppException extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly code: string,
|
|
6
|
+
message: string,
|
|
7
|
+
readonly statusCode = 400,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AppException";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Minimal typed environment validation. Extend as the app grows.
|
|
2
|
+
export interface AppEnv {
|
|
3
|
+
nodeEnv: string;
|
|
4
|
+
port: number;
|
|
5
|
+
corsOrigin: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function validateEnv(env: NodeJS.ProcessEnv = process.env): AppEnv {
|
|
9
|
+
const port = Number(env.PORT ?? 3000);
|
|
10
|
+
if (Number.isNaN(port)) {
|
|
11
|
+
throw new Error(`Invalid PORT: ${env.PORT}`);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
nodeEnv: env.NODE_ENV ?? "development",
|
|
15
|
+
port,
|
|
16
|
+
corsOrigin: env.CORS_ORIGIN,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
|
|
3
|
+
@Controller("health")
|
|
4
|
+
export class HealthController {
|
|
5
|
+
@Get()
|
|
6
|
+
liveness(): { status: string; uptime: number; timestamp: string } {
|
|
7
|
+
return {
|
|
8
|
+
status: "ok",
|
|
9
|
+
uptime: process.uptime(),
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NestFactory } from "@nestjs/core";
|
|
2
|
+
import { ValidationPipe } from "@nestjs/common";
|
|
3
|
+
import { AppModule } from "./app.module";
|
|
4
|
+
import { AllExceptionsFilter } from "./common/all-exceptions.filter";
|
|
5
|
+
|
|
6
|
+
async function bootstrap(): Promise<void> {
|
|
7
|
+
const app = await NestFactory.create(AppModule);
|
|
8
|
+
|
|
9
|
+
app.useGlobalPipes(
|
|
10
|
+
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
|
11
|
+
);
|
|
12
|
+
app.useGlobalFilters(new AllExceptionsFilter());
|
|
13
|
+
|
|
14
|
+
const corsOrigin = process.env.CORS_ORIGIN?.split(",").map((o) => o.trim());
|
|
15
|
+
app.enableCors({ origin: corsOrigin ?? true, credentials: true });
|
|
16
|
+
|
|
17
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
18
|
+
await app.listen(port);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
void bootstrap();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing";
|
|
2
|
+
import type { INestApplication } from "@nestjs/common";
|
|
3
|
+
import request from "supertest";
|
|
4
|
+
import { HealthModule } from "../src/health/health.module";
|
|
5
|
+
|
|
6
|
+
describe("Health (e2e)", () => {
|
|
7
|
+
let app: INestApplication;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
const moduleRef = await Test.createTestingModule({ imports: [HealthModule] }).compile();
|
|
11
|
+
app = moduleRef.createNestApplication();
|
|
12
|
+
await app.init();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await app.close();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("GET /health returns ok", async () => {
|
|
20
|
+
const res = await request(app.getHttpServer()).get("/health").expect(200);
|
|
21
|
+
expect(res.body.status).toBe("ok");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "CommonJS",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"declaration": false,
|
|
9
|
+
"emitDecoratorMetadata": true,
|
|
10
|
+
"experimentalDecorators": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noUnusedLocals": true,
|
|
13
|
+
"noUnusedParameters": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:20-alpine AS deps
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY package.json ./
|
|
5
|
+
RUN npm install --no-audit --no-fund
|
|
6
|
+
|
|
7
|
+
FROM node:20-alpine AS build
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
FROM node:20-alpine AS runtime
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
17
|
+
COPY --from=build /app/build ./build
|
|
18
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
19
|
+
COPY package.json ./
|
|
20
|
+
USER app
|
|
21
|
+
EXPOSE 3000
|
|
22
|
+
CMD ["node", "build"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-svelte.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"tailwind": {
|
|
5
|
+
"css": "src/app.css",
|
|
6
|
+
"baseColor": "zinc"
|
|
7
|
+
},
|
|
8
|
+
"aliases": {
|
|
9
|
+
"components": "$lib/components",
|
|
10
|
+
"ui": "$lib/components/ui",
|
|
11
|
+
"utils": "$lib/utils"
|
|
12
|
+
},
|
|
13
|
+
"typescript": true,
|
|
14
|
+
"registry": "https://shadcn-svelte.com/registry"
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-web",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev --port 5173",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"lint": "svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
+
"test": "echo \"add Playwright tests here\" && exit 0"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"mode-watcher": "^0.5.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@sveltejs/adapter-node": "^5.2.0",
|
|
18
|
+
"@sveltejs/kit": "^2.8.0",
|
|
19
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
20
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
21
|
+
"svelte": "^5.1.0",
|
|
22
|
+
"svelte-check": "^4.0.0",
|
|
23
|
+
"tailwindcss": "^4.0.0",
|
|
24
|
+
"typesafe-i18n": "^5.26.2",
|
|
25
|
+
"typescript": "^5.6.3",
|
|
26
|
+
"vite": "^5.4.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Semantic tokens. shadcn-svelte components read these CSS variables. */
|
|
4
|
+
:root {
|
|
5
|
+
--background: oklch(1 0 0);
|
|
6
|
+
--foreground: oklch(0.15 0 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root.dark {
|
|
10
|
+
--background: oklch(0.15 0 0);
|
|
11
|
+
--foreground: oklch(0.98 0 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
background-color: var(--background);
|
|
16
|
+
color: var(--foreground);
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
%sveltekit.head%
|
|
7
|
+
</head>
|
|
8
|
+
<body data-sveltekit-preload-data="hover" class="h-full">
|
|
9
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# i18n
|
|
2
|
+
|
|
3
|
+
Locales use [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n).
|
|
4
|
+
|
|
5
|
+
- `en.ts` is the base locale; `ko.ts` mirrors its shape.
|
|
6
|
+
- Run `npx typesafe-i18n` to generate the typed runtime and Svelte store,
|
|
7
|
+
then use `$LL.appTitle()` in components. Add keys to every locale.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BaseTranslation } from "typesafe-i18n";
|
|
2
|
+
|
|
3
|
+
// Base locale. Run `npx typesafe-i18n` to generate the typed runtime,
|
|
4
|
+
// then access strings as `$LL.appTitle()` in components.
|
|
5
|
+
const en = {
|
|
6
|
+
appTitle: "{{projectName}}",
|
|
7
|
+
checkHealth: "Check API health",
|
|
8
|
+
} satisfies BaseTranslation;
|
|
9
|
+
|
|
10
|
+
export default en;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Server-side proxy boundary. The browser never talks to the API directly:
|
|
2
|
+
// only these allowlisted headers are forwarded to BACKEND_INTERNAL_URL.
|
|
3
|
+
const FORWARDED_HEADERS = ["authorization", "cookie", "content-type"];
|
|
4
|
+
|
|
5
|
+
export function backendBaseUrl(): string {
|
|
6
|
+
return process.env.BACKEND_INTERNAL_URL ?? "http://localhost:3000";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function backendProxyHeaders(request: Request): Headers {
|
|
10
|
+
const headers = new Headers();
|
|
11
|
+
for (const name of FORWARDED_HEADERS) {
|
|
12
|
+
const value = request.headers.get(name);
|
|
13
|
+
if (value) headers.set(name, value);
|
|
14
|
+
}
|
|
15
|
+
return headers;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Health = { status: string } | { error: string };
|
|
3
|
+
|
|
4
|
+
let health = $state<Health | null>(null);
|
|
5
|
+
|
|
6
|
+
async function check(): Promise<void> {
|
|
7
|
+
const res = await fetch("/api/health");
|
|
8
|
+
health = res.ok ? await res.json() : { error: `HTTP ${res.status}` };
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<main class="mx-auto flex min-h-full max-w-2xl flex-col gap-6 p-8">
|
|
13
|
+
<h1 class="text-3xl font-bold">{{projectName}}</h1>
|
|
14
|
+
<p class="text-sm opacity-70">Full-stack starter generated with PodoKit.</p>
|
|
15
|
+
|
|
16
|
+
<button
|
|
17
|
+
class="w-fit rounded-md border border-current/20 px-4 py-2 text-sm font-medium hover:opacity-80"
|
|
18
|
+
onclick={check}
|
|
19
|
+
>
|
|
20
|
+
Check API health
|
|
21
|
+
</button>
|
|
22
|
+
|
|
23
|
+
{#if health}
|
|
24
|
+
<pre class="rounded-md bg-current/5 p-4 text-sm">{JSON.stringify(health, null, 2)}</pre>
|
|
25
|
+
{/if}
|
|
26
|
+
</main>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RequestHandler } from "@sveltejs/kit";
|
|
2
|
+
import { backendBaseUrl, backendProxyHeaders } from "$lib/server/backend-proxy";
|
|
3
|
+
|
|
4
|
+
export const GET: RequestHandler = async ({ request }) => {
|
|
5
|
+
const upstream = await fetch(`${backendBaseUrl()}/health`, {
|
|
6
|
+
headers: backendProxyHeaders(request),
|
|
7
|
+
});
|
|
8
|
+
return new Response(await upstream.text(), {
|
|
9
|
+
status: upstream.status,
|
|
10
|
+
headers: { "content-type": "application/json" },
|
|
11
|
+
});
|
|
12
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import adapter from "@sveltejs/adapter-node";
|
|
2
|
+
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
preprocess: vitePreprocess(),
|
|
7
|
+
kit: {
|
|
8
|
+
adapter: adapter(),
|
|
9
|
+
alias: {
|
|
10
|
+
$i18n: "src/lib/i18n",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default config;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copy to .env and adjust. Never commit real secrets.
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=3000
|
|
4
|
+
|
|
5
|
+
POSTGRES_HOST=localhost
|
|
6
|
+
POSTGRES_PORT=5432
|
|
7
|
+
POSTGRES_USER=podokit
|
|
8
|
+
POSTGRES_PASSWORD=podokit
|
|
9
|
+
POSTGRES_DB=podokit
|
|
10
|
+
|
|
11
|
+
REDIS_HOST=localhost
|
|
12
|
+
REDIS_PORT=6379
|
|
13
|
+
|
|
14
|
+
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
15
|
+
BACKEND_INTERNAL_URL=http://localhost:3000
|
|
16
|
+
CORS_ORIGIN=http://localhost:5173
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_USER: ${POSTGRES_USER:-podokit}
|
|
6
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-podokit}
|
|
7
|
+
POSTGRES_DB: ${POSTGRES_DB:-podokit}
|
|
8
|
+
ports:
|
|
9
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
10
|
+
volumes:
|
|
11
|
+
- pgdata:/var/lib/postgresql/data
|
|
12
|
+
healthcheck:
|
|
13
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-podokit}"]
|
|
14
|
+
interval: 5s
|
|
15
|
+
timeout: 5s
|
|
16
|
+
retries: 5
|
|
17
|
+
|
|
18
|
+
redis:
|
|
19
|
+
image: redis:7-alpine
|
|
20
|
+
ports:
|
|
21
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
22
|
+
healthcheck:
|
|
23
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
24
|
+
interval: 5s
|
|
25
|
+
timeout: 5s
|
|
26
|
+
retries: 5
|
|
27
|
+
|
|
28
|
+
volumes:
|
|
29
|
+
pgdata:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: api
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
replicas: 1
|
|
8
|
+
selector:
|
|
9
|
+
matchLabels: { app: api }
|
|
10
|
+
template:
|
|
11
|
+
metadata:
|
|
12
|
+
labels: { app: api }
|
|
13
|
+
spec:
|
|
14
|
+
containers:
|
|
15
|
+
- name: api
|
|
16
|
+
image: ghcr.io/example/podokit-api:latest
|
|
17
|
+
ports:
|
|
18
|
+
- containerPort: 3000
|
|
19
|
+
envFrom:
|
|
20
|
+
- configMapRef: { name: app-config }
|
|
21
|
+
- secretRef: { name: app-secrets }
|
|
22
|
+
readinessProbe:
|
|
23
|
+
httpGet: { path: /health, port: 3000 }
|
|
24
|
+
initialDelaySeconds: 5
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
apiVersion: networking.k8s.io/v1
|
|
2
|
+
kind: Ingress
|
|
3
|
+
metadata:
|
|
4
|
+
name: app
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
rules:
|
|
8
|
+
- host: example.com
|
|
9
|
+
http:
|
|
10
|
+
paths:
|
|
11
|
+
- path: /api
|
|
12
|
+
pathType: Prefix
|
|
13
|
+
backend:
|
|
14
|
+
service: { name: api, port: { number: 3000 } }
|
|
15
|
+
- path: /
|
|
16
|
+
pathType: Prefix
|
|
17
|
+
backend:
|
|
18
|
+
service: { name: web, port: { number: 3000 } }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Example only. Never commit real secrets. Create the real Secret out-of-band:
|
|
2
|
+
# kubectl -n podokit create secret generic app-secrets --from-literal=POSTGRES_PASSWORD=...
|
|
3
|
+
apiVersion: v1
|
|
4
|
+
kind: Secret
|
|
5
|
+
metadata:
|
|
6
|
+
name: app-secrets
|
|
7
|
+
namespace: podokit
|
|
8
|
+
type: Opaque
|
|
9
|
+
stringData:
|
|
10
|
+
POSTGRES_PASSWORD: "change-me"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Service
|
|
3
|
+
metadata:
|
|
4
|
+
name: api
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
selector: { app: api }
|
|
8
|
+
ports:
|
|
9
|
+
- port: 3000
|
|
10
|
+
targetPort: 3000
|
|
11
|
+
---
|
|
12
|
+
apiVersion: v1
|
|
13
|
+
kind: Service
|
|
14
|
+
metadata:
|
|
15
|
+
name: web
|
|
16
|
+
namespace: podokit
|
|
17
|
+
spec:
|
|
18
|
+
selector: { app: web }
|
|
19
|
+
ports:
|
|
20
|
+
- port: 3000
|
|
21
|
+
targetPort: 3000
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: web
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
replicas: 1
|
|
8
|
+
selector:
|
|
9
|
+
matchLabels: { app: web }
|
|
10
|
+
template:
|
|
11
|
+
metadata:
|
|
12
|
+
labels: { app: web }
|
|
13
|
+
spec:
|
|
14
|
+
containers:
|
|
15
|
+
- name: web
|
|
16
|
+
image: ghcr.io/example/podokit-web:latest
|
|
17
|
+
ports:
|
|
18
|
+
- containerPort: 3000
|
|
19
|
+
envFrom:
|
|
20
|
+
- configMapRef: { name: app-config }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"engines": { "node": ">=20" },
|
|
6
|
+
"workspaces": ["apps/*"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "npm run dev --workspaces --if-present",
|
|
9
|
+
"build": "npm run build --workspaces --if-present",
|
|
10
|
+
"lint": "npm run lint --workspaces --if-present",
|
|
11
|
+
"test": "npm run test --workspaces --if-present"
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@podosoft/podokit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/podosoft-dev/podokit.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["nestjs", "sveltekit", "starter", "cli", "scaffold", "tailwindcss", "shadcn"],
|
|
12
|
+
"bin": {
|
|
13
|
+
"podo": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"provenance": true
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-templates.mjs",
|
|
25
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@podosoft/podokit-template-engine": "^0.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|