@tobi2409/fullsty 0.5.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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # fullsty
2
+
3
+ Fullsty is a modular fullstack framework starter built from three core parts:
4
+
5
+ - `template-engine` for reactive UI rendering
6
+ - `layout` for lightweight CSS layout composition
7
+ - `proc2rest` for generating REST server/client code from TypeScript server functions
8
+
9
+ This repository provides a project template and scaffolding script so you can create a new project, generate API/client artifacts, and run the generated server quickly.
10
+
11
+ Minimal Fullsty starter workflow.
12
+
13
+ **It is recommended to install fullsty globally.**
14
+
15
+ A sample application can be found at https://github.com/tobi2409/fullsty/tree/main/projects/db-test
16
+
17
+ ## 1) Create a project
18
+
19
+ From the repository root:
20
+
21
+ ```bash
22
+ npx create-fullsty-project projects/demo1
23
+ ```
24
+
25
+ This creates a new folder (for example [projects/demo1](projects/demo1)).
26
+
27
+ ## 2) Install project dependencies
28
+
29
+ Go to the created project and install dependencies:
30
+
31
+ ```bash
32
+ cd projects/demo1
33
+ npm install
34
+ ```
35
+
36
+ ## 3) Generate server/client output
37
+
38
+ Still inside the project folder:
39
+
40
+ ```bash
41
+ npm run generate
42
+ ```
43
+
44
+ This runs:
45
+
46
+ - `generate-server` (creates server output in [projects/demo1/generated/server](projects/demo1/generated/server))
47
+ - `generate-client` (creates client output in [projects/demo1/generated/client](projects/demo1/generated/client))
48
+
49
+ It also copies [project-frame/server-package.json](project-frame/server-package.json) to `generated/server/package.json` and [project-frame/client-package.json](project-frame/client-package.json) to `generated/client/package.json`.
50
+
51
+ ## 4) Use `fullsty-pkg` for wrapper packages
52
+
53
+ `fullsty-pkg.js` is a small project helper for wrapper-aware package changes.
54
+
55
+ Run it from the project root, for example:
56
+
57
+ ```bash
58
+ ../../scripts/fullsty-server-pkg.js add pg
59
+ ../../scripts/fullsty-server-pkg.js add sql-template-tag
60
+ ../../scripts/fullsty-server-pkg.js remove pg
61
+ ```
62
+
63
+ Alternatively, you can also use npx fullsty-server-pkg.
64
+
65
+ What it does:
66
+
67
+ - `add <package>`
68
+ - adds the package to the project's server package file, which is initially created from [project-frame/server-package.json](project-frame/server-package.json)
69
+ - copies a matching wrapper from [extension-wrappers](extension-wrappers) into [projects/demo1/src/server](projects/demo1/src/server)
70
+ - `remove <package>`
71
+ - removes the package from the project's server package file
72
+ - removes the matching wrapper from [projects/demo1/src/server](projects/demo1/src/server)
73
+
74
+ The script does not manage the project-level [package.json](project-frame/package.json).
75
+
76
+ After `fullsty-server-pkg.js add <package>` you should run `npm run generate` again so the updated project server package file is copied into `generated/server/package.json`. After that, run `npm install` again inside [projects/demo1/generated/server](projects/demo1/generated/server) so the generated server gets the updated dependencies.
77
+
78
+ `fullsty-server-pkg.js add sql-template-tag` copies [extension-wrappers/sql-template-tag/sql-template-tag-wrapper.ts](extension-wrappers/sql-template-tag/sql-template-tag-wrapper.ts) into [projects/demo1/src/server](projects/demo1/src/server). This keeps `sql-template-tag` visible as the recommended SQL style without coupling it to a specific DBMS wrapper. For PostgreSQL pooling and env handling you can add `pg` separately.
79
+
80
+ ## 5) Start the generated server
81
+
82
+ Move to the generated server folder and install/start it:
83
+
84
+ ```bash
85
+ cd generated/server
86
+ npm install
87
+ npm run start
88
+ ```
89
+
90
+ ## package.json responsibilities
91
+
92
+ - Project-level package file: [project-frame/package.json](project-frame/package.json)
93
+ - Used for generation tasks (`generate-server`, `generate-client`, `generate`).
94
+ - Generated server package file: [project-frame/server-package.json](project-frame/server-package.json)
95
+ - Becomes `generated/server/package.json`.
96
+ - Used to run the generated server (`npm run start`).
97
+
98
+ ## 6) Install client dependencies
99
+
100
+ Move to the generated client folder and install the dependencies:
101
+
102
+ ```bash
103
+ cd generated/client
104
+ npm install
105
+ ```
106
+
107
+ ## VS Code Setup
108
+
109
+ To prevent Live Server from reloading when server logs are written, add this to `.vscode/settings.json`:
110
+
111
+ ```json
112
+ {
113
+ "liveServer.settings.ignoreFiles": [
114
+ "**/generated/server/logs/**",
115
+ "**/*.log"
116
+ ]
117
+ }
118
+ ```
119
+
120
+ ## Quick summary
121
+
122
+ 1. `node create-project.js projects/<project-name>`
123
+ 2. `cd projects/<project-name>`
124
+ 3. `npm install`
125
+ 4. `npm run generate`
126
+ 5. `cd generated/server && npm install && npm run start`
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@tobi2409/fullsty",
3
+ "version": "0.5.0",
4
+ "description": "Fullsty is a modular fullstack framework starter built from three core parts: template-engine, layout and proc2rest. It provides a project frame for building fullstack applications with a clear separation of client and server code.",
5
+ "license": "ISC",
6
+ "author": "Tobias Hollstein",
7
+ "type": "module",
8
+ "main": "./scripts/create-project.js",
9
+ "bin": {
10
+ "create-fullsty-project": "./scripts/create-project.js",
11
+ "fullsty-server-pkg": "./scripts/fullsty-server-pkg.js"
12
+ },
13
+ "files": [
14
+ "scripts",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "codegen",
19
+ "typescript",
20
+ "express",
21
+ "expressjs",
22
+ "rest",
23
+ "rest-api",
24
+ "api",
25
+ "api-generator",
26
+ "client-generator",
27
+ "javascript-client",
28
+ "fetch",
29
+ "msgpack",
30
+ "messagepack",
31
+ "ts-morph",
32
+ "node",
33
+ "nodejs",
34
+ "npm-cli",
35
+ "automation",
36
+ "scaffolding",
37
+ "rpc",
38
+ "backend",
39
+ "template-engine",
40
+ "reactive",
41
+ "mvvm",
42
+ "frontend",
43
+ "declarative",
44
+ "data-binding",
45
+ "dom",
46
+ "html",
47
+ "vanilla-js",
48
+ "rendering",
49
+ "javascript",
50
+ "template"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "homepage": "https://github.com/tobi2409/fullsty#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/tobi2409/fullsty/issues"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/tobi2409/fullsty.git"
62
+ }
63
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { copyDirectory } from "./shared/file-copy.js";
6
+ import { readJson, writeJson } from "./shared/project-json-utils.js";
7
+
8
+ // Update package.json name in the copied project frame.
9
+ function updatePackageName(targetProjectDir, projectName) {
10
+ const packageJson = readJson(targetProjectDir, "package.json");
11
+ packageJson.name = projectName;
12
+ writeJson(targetProjectDir, "package.json", packageJson);
13
+ }
14
+
15
+ function main() {
16
+ const projectName = process.argv[2];
17
+
18
+ if (!projectName) {
19
+ console.error("Usage: node create-project.js <project-name>");
20
+ process.exit(1);
21
+ }
22
+
23
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
24
+ const sourceFrameDir = path.join(scriptDir, "project-frame");
25
+ const targetProjectDir = path.resolve(process.cwd(), projectName);
26
+
27
+ if (!fs.existsSync(sourceFrameDir)) {
28
+ throw new Error(`project-frame not found: ${sourceFrameDir}`);
29
+ }
30
+
31
+ if (fs.existsSync(targetProjectDir)) {
32
+ throw new Error(`Target directory already exists: ${targetProjectDir}`);
33
+ }
34
+
35
+ copyDirectory(sourceFrameDir, targetProjectDir);
36
+ updatePackageName(targetProjectDir, projectName);
37
+
38
+ console.log(`\nāœ… Project created: ${targetProjectDir}`);
39
+ }
40
+
41
+ try {
42
+ main();
43
+ } catch (error) {
44
+ console.error(`\nāŒ ${error.message}`);
45
+ process.exit(1);
46
+ }
@@ -0,0 +1,15 @@
1
+ # Connection name: default
2
+ DEFAULT_PGHOST=127.0.0.1
3
+ DEFAULT_PGPORT=5432
4
+ DEFAULT_PGUSER=postgres
5
+ DEFAULT_PGPASSWORD=postgres
6
+ DEFAULT_PGDATABASE=fullsty_demo
7
+ DEFAULT_POOL_MAX=10
8
+
9
+ # Optional second connection example: analytics
10
+ # ANALYTICS_PGHOST=127.0.0.1
11
+ # ANALYTICS_PGPORT=5432
12
+ # ANALYTICS_PGUSER=postgres
13
+ # ANALYTICS_PGPASSWORD=postgres
14
+ # ANALYTICS_PGDATABASE=fullsty_analytics
15
+ # ANALYTICS_POOL_MAX=10
@@ -0,0 +1,78 @@
1
+ import dotenv from "dotenv";
2
+ import fs from "fs";
3
+ import { Pool } from "pg";
4
+
5
+ const pools = new Map<string, Pool>();
6
+ let pgEnv: Record<string, string> = {};
7
+ let pgEnvLoaded = false;
8
+
9
+ function envKey(connectionName: string, key: string): string {
10
+ const prefix = connectionName.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
11
+ return `${prefix}_${key}`;
12
+ }
13
+
14
+ function loadPgEnv(): void {
15
+ if (pgEnvLoaded) {
16
+ return;
17
+ }
18
+
19
+ // Load .env.pg into a local cache instead of writing into process.env.
20
+ // This keeps the pg wrapper isolated from other wrappers such as auth,
21
+ // which may load their own .env files later.
22
+ const envFileContent = fs.existsSync(".env.pg")
23
+ ? fs.readFileSync(".env.pg", "utf8")
24
+ : "";
25
+
26
+ pgEnv = dotenv.parse(envFileContent);
27
+ pgEnvLoaded = true;
28
+ }
29
+
30
+ function getEnv(
31
+ connectionName: string,
32
+ key: string,
33
+ fallback?: string,
34
+ ): string {
35
+ loadPgEnv();
36
+ return pgEnv[envKey(connectionName, key)] ?? fallback ?? "";
37
+ }
38
+
39
+ export function getPgPool(connectionName: string = "default"): Pool {
40
+ const existing = pools.get(connectionName);
41
+ if (existing) {
42
+ return existing;
43
+ }
44
+
45
+ const pool = new Pool({
46
+ host: getEnv(connectionName, "PGHOST", "127.0.0.1"),
47
+ port: Number(getEnv(connectionName, "PGPORT", "5432")),
48
+ user: getEnv(connectionName, "PGUSER", "postgres"),
49
+ password: getEnv(connectionName, "PGPASSWORD", "postgres"),
50
+ database: getEnv(connectionName, "PGDATABASE", "fullsty_demo"),
51
+ max: Number(getEnv(connectionName, "POOL_MAX", "10")),
52
+ });
53
+
54
+ pools.set(connectionName, pool);
55
+ return pool;
56
+ }
57
+
58
+ export async function closePgPool(
59
+ connectionName: string = "default",
60
+ ): Promise<void> {
61
+ const pool = pools.get(connectionName);
62
+
63
+ if (!pool) {
64
+ return;
65
+ }
66
+
67
+ await pool.end();
68
+ pools.delete(connectionName);
69
+ }
70
+
71
+ export async function closeAllPgPools(): Promise<void> {
72
+ const all = [...pools.entries()];
73
+
74
+ for (const [name, pool] of all) {
75
+ await pool.end();
76
+ pools.delete(name);
77
+ }
78
+ }
@@ -0,0 +1 @@
1
+ export { default as sql } from "sql-template-tag";
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "path";
4
+ import { handleAdd, handleRemove } from "./shared/server-package-utils.js";
5
+
6
+ function main() {
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+
10
+ if (!command) {
11
+ console.log(
12
+ "Usage: node fullsty-server-pkg.js <add|remove> <package-name> [more-package-names...]",
13
+ );
14
+
15
+ process.exit(1);
16
+ }
17
+
18
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
19
+ const projectDir = process.cwd();
20
+ const serverDir = path.join(projectDir, "src", "server");
21
+ const packageNames = args.slice(1).filter((arg) => !arg.startsWith("-"));
22
+
23
+ if (packageNames.length === 0 || !["add", "remove"].includes(command)) {
24
+ console.log(
25
+ "Usage: node fullsty-server-pkg.js <add|remove> <package-name> [more-package-names...]",
26
+ );
27
+
28
+ process.exit(1);
29
+ }
30
+
31
+ if (command === "add") {
32
+ handleAdd(projectDir, scriptDir, serverDir, packageNames);
33
+ return;
34
+ }
35
+
36
+ handleRemove(projectDir, scriptDir, serverDir, packageNames);
37
+ }
38
+
39
+ try {
40
+ main();
41
+ } catch (error) {
42
+ console.error(
43
+ `\nāŒ ${error instanceof Error ? error.message : String(error)}`,
44
+ );
45
+ process.exit(1);
46
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "generated-client",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "@tobi2409/template-engine": "^0.9.1",
7
+ "@tobi2409/layout": "^0.5.0"
8
+ }
9
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "project-frame",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "scripts": {
9
+ "generate-server": "npx proc2rest-server && node -e \"require('fs').copyFileSync('server-package.json','generated/server/package.json'); require('fs').copyFileSync('server-tsconfig.json','generated/server/tsconfig.json')\"",
10
+ "generate-client": "npx proc2rest-client && node -e \"require('fs').copyFileSync('client-package.json','generated/client/package.json');\"",
11
+ "generate": "npm run generate-server && npm run generate-client",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "devDependencies": {
15
+ "@tobi2409/proc2rest": "^0.4.1"
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "jsClients": ["src/client/index.js"],
3
+ "servers": [
4
+ {
5
+ "src": "src/server/server.ts",
6
+ "namespace": "Server"
7
+ }
8
+ ],
9
+ "apiUrl": "http://localhost:3000",
10
+ "cors": {
11
+ "origin": "*"
12
+ },
13
+ "method-rules": {
14
+ "GET": "^(get|list|find)",
15
+ "POST": "^(create|add|insert)",
16
+ "PATCH": "^(update|set|patch)",
17
+ "DELETE": "^(delete|remove)"
18
+ },
19
+ "binaryTypes": [
20
+ "ArrayBuffer",
21
+ "Uint8Array",
22
+ "Uint8Array<ArrayBufferLike>",
23
+ "Buffer",
24
+ "Blob",
25
+ "ImageData",
26
+ "File"
27
+ ]
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "generated-server",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "tsx routes.generated.ts"
7
+ },
8
+ "dependencies": {
9
+ "@msgpack/msgpack": "^3.0.0",
10
+ "cors": "^2.8.5",
11
+ "dotenv": "^16.4.7",
12
+ "express": "^5.2.1",
13
+ "express-rate-limit": "^7.5.1"
14
+ },
15
+ "devDependencies": {
16
+ "@types/cors": "^2.8.17",
17
+ "@types/express": "^5.0.5",
18
+ "@types/node": "^24.10.1",
19
+ "tsx": "^4.20.6",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true
9
+ },
10
+ "include": ["*.ts"]
11
+ }
File without changes
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Document</title>
7
+
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "template-engine/": "./node_modules/@tobi2409/template-engine/src/"
12
+ }
13
+ }
14
+ </script>
15
+
16
+ <link rel="stylesheet" href="node_modules/@tobi2409/layout/src/layout.css">
17
+ <link rel="stylesheet" href="index.css">
18
+ </head>
19
+ <body>
20
+
21
+ <div id="app">
22
+ </div>
23
+
24
+ <template id="app-template">
25
+ <h1>šŸ‘‹ Hello, <get>name</get></h1>
26
+
27
+ <p><get>serverMessage</get></p>
28
+ </template>
29
+
30
+ <template-use id="app-template-use" template-id="app-template" mount-id="app">
31
+ </template-use>
32
+
33
+ <script type="module" src="index.js"></script>
34
+
35
+ </body>
36
+ </html>
@@ -0,0 +1,10 @@
1
+ // @server-import server.ts
2
+
3
+ import TemplateEngine from 'template-engine/template-engine.js'
4
+
5
+ const data = TemplateEngine.reactive({
6
+ name: 'my friend',
7
+ serverMessage: 'Server not responded yet'
8
+ }, document.getElementById('app-template-use'))
9
+
10
+ data.serverMessage = await Server.helloFromServer('my friend')
@@ -0,0 +1,4 @@
1
+ // @rest
2
+ export function helloFromServer(name: string): string {
3
+ return `Hello, ${name}! This is the server speaking.`
4
+ }
@@ -0,0 +1,19 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ // Recursively copy a directory and all nested files/folders.
5
+ export function copyDirectory(sourceDir, targetDir) {
6
+ fs.mkdirSync(targetDir, { recursive: true });
7
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
8
+
9
+ for (const entry of entries) {
10
+ const sourcePath = path.join(sourceDir, entry.name);
11
+ const targetPath = path.join(targetDir, entry.name);
12
+
13
+ if (entry.isDirectory()) {
14
+ copyDirectory(sourcePath, targetPath);
15
+ } else {
16
+ fs.copyFileSync(sourcePath, targetPath);
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function readJson(projectDir, fileName) {
5
+ const filePath = path.join(projectDir, fileName);
6
+
7
+ if (!fs.existsSync(filePath)) {
8
+ throw new Error(`${fileName} not found: ${filePath}`);
9
+ }
10
+
11
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
12
+ }
13
+
14
+ export function writeJson(projectDir, fileName, json) {
15
+ const filePath = path.join(projectDir, fileName);
16
+ fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`, "utf8");
17
+ }
@@ -0,0 +1,79 @@
1
+ import fs, { write } from "fs";
2
+ import path from "path";
3
+ import { copyDirectory } from "./file-copy.js";
4
+ import { readJson, writeJson } from "./project-json-utils.js";
5
+
6
+ function wrapperDirFromPackage(scriptDir, packageName) {
7
+ return path.join(scriptDir, "extension-wrappers", packageName);
8
+ }
9
+
10
+ function removeWrapperFiles(sourceWrapperDir, targetServerDir) {
11
+ const entries = fs.readdirSync(sourceWrapperDir, { withFileTypes: true });
12
+
13
+ for (const entry of entries) {
14
+ const sourcePath = path.join(sourceWrapperDir, entry.name);
15
+ const targetPath = path.join(targetServerDir, entry.name);
16
+
17
+ if (entry.isDirectory()) {
18
+ if (fs.existsSync(targetPath)) {
19
+ removeWrapperFiles(sourcePath, targetPath);
20
+ }
21
+
22
+ continue;
23
+ }
24
+
25
+ if (fs.existsSync(targetPath)) {
26
+ fs.rmSync(targetPath, { force: true });
27
+ }
28
+ }
29
+ }
30
+
31
+ export function handleAdd(projectDir, scriptDir, serverDir, packageNames) {
32
+ const json = readJson(projectDir, "server-package.json");
33
+
34
+ if (!json.dependencies || typeof json.dependencies !== "object") {
35
+ json.dependencies = {};
36
+ }
37
+
38
+ for (const packageName of packageNames) {
39
+ const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
40
+
41
+ json.dependencies[packageName] = "latest";
42
+
43
+ if (fs.existsSync(wrapperDir)) {
44
+ copyDirectory(wrapperDir, serverDir);
45
+ console.log(`\nāœ… Copied wrapper for ${packageName} to src/server`);
46
+ }
47
+ }
48
+
49
+ writeJson(projectDir, "server-package.json", json);
50
+ }
51
+
52
+ export function handleRemove(projectDir, scriptDir, serverDir, packageNames) {
53
+ const json = readJson(projectDir, "server-package.json");
54
+
55
+ if (!json.dependencies || typeof json.dependencies !== "object") {
56
+ json.dependencies = {};
57
+ }
58
+
59
+ if (!json.devDependencies || typeof json.devDependencies !== "object") {
60
+ json.devDependencies = {};
61
+ }
62
+
63
+ for (const packageName of packageNames) {
64
+ const wrapperDir = wrapperDirFromPackage(scriptDir, packageName);
65
+
66
+ delete json.dependencies[packageName];
67
+ delete json.devDependencies[packageName];
68
+
69
+ if (fs.existsSync(wrapperDir)) {
70
+ removeWrapperFiles(wrapperDir, serverDir);
71
+
72
+ console.log(
73
+ `\nāœ… Removed wrapper for ${packageName} from src/server`,
74
+ );
75
+ }
76
+ }
77
+
78
+ writeJson(projectDir, "server-package.json", json);
79
+ }