satto 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Satto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "satto",
3
+ "version": "1.0.0",
4
+ "description": "A minimal server-rendered web framework for Node.js",
5
+ "main": "src/lib/index.js",
6
+ "bin": {
7
+ "satto": "./src/bin/satto.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Satto-js/satto.git"
12
+ },
13
+ "keywords": [
14
+ "satto",
15
+ "web-framework",
16
+ "ssr",
17
+ "nodejs"
18
+ ],
19
+ "author": "joaopedroleonel",
20
+ "license": "MIT",
21
+ "type": "commonjs",
22
+ "bugs": {
23
+ "url": "https://github.com/Satto-js/satto/issues"
24
+ },
25
+ "homepage": "https://github.com/Satto-js/satto#readme",
26
+ "dependencies": {
27
+ "ejs": "^3.1.10",
28
+ "esbuild": "^0.27.1",
29
+ "express": "^5.2.1"
30
+ }
31
+ }
package/readme.md ADDED
@@ -0,0 +1,197 @@
1
+ # Satto
2
+
3
+ **Satto** is a lightweight web framework for building server-rendered applications with a **file-based structure**, **simple routing**, and **fast builds** powered by **Node.js**, **Express**, and **esbuild**.
4
+
5
+ It is designed to be **minimal**, **opinionated**, and **easy to reason about**, focusing on productivity without unnecessary abstraction.
6
+
7
+ ## Key Features
8
+
9
+ * File-based page structure
10
+ * Simple and explicit routing
11
+ * Server-Side Rendering (SSR)
12
+ * Page-scoped CSS and JavaScript
13
+ * Fast production builds with esbuild
14
+ * Development mode with file watching
15
+ * Minimal templating syntax for SSR
16
+ * Zero configuration by default
17
+
18
+ ## Installation
19
+
20
+ Install globally to use the CLI:
21
+
22
+ ```bash
23
+ npm install -g satto
24
+ ```
25
+
26
+ Or install locally in a project:
27
+
28
+ ```bash
29
+ npm install satto
30
+ ```
31
+
32
+ ## Creating a New Project
33
+
34
+ ```bash
35
+ satto init my-app
36
+ ```
37
+
38
+ This command creates a new project with the following structure:
39
+
40
+ ```txt
41
+ my-app/
42
+ ├── src/
43
+ │ ├── app/
44
+ │ │ └── home/
45
+ │ │ ├── home.html
46
+ │ │ ├── home.css
47
+ │ │ └── home.js
48
+ │ ├── static/
49
+ │ │ └── styles.css
50
+ │ ├── index.html
51
+ │ └── server.js
52
+ └── package.json
53
+ ```
54
+
55
+ ## Development Server
56
+
57
+ Start the development server with file watching enabled:
58
+
59
+ ```bash
60
+ npm run dev
61
+ ```
62
+
63
+ or
64
+
65
+ ```bash
66
+ satto run dev
67
+ ```
68
+
69
+ The application will be available at:
70
+
71
+ ```
72
+ http://localhost:3000
73
+ ```
74
+
75
+ ## Production Build
76
+
77
+ Create an optimized production build:
78
+
79
+ ```bash
80
+ npm run build
81
+ ```
82
+
83
+ or
84
+
85
+ ```bash
86
+ satto run build
87
+ ```
88
+
89
+ This generates the following output:
90
+
91
+ ```txt
92
+ dist/
93
+ ├── app/
94
+ ├── static/
95
+ ├── index.html
96
+ └── server.js
97
+ ```
98
+
99
+ All assets are minified and ready for deployment.
100
+
101
+ ## Creating Routes
102
+
103
+ Generate a new route using the CLI:
104
+
105
+ ```bash
106
+ satto route blog
107
+ ```
108
+
109
+ This creates:
110
+
111
+ ```txt
112
+ src/app/blog/
113
+ ├── blog.html
114
+ ├── blog.css
115
+ └── blog.js
116
+ ```
117
+
118
+ And automatically updates `src/server.js`:
119
+
120
+ ```js
121
+ const routes = [
122
+ { path: "/", page: "home" },
123
+ { path: "/blog", page: "blog" },
124
+ ];
125
+ ```
126
+
127
+ ## Page Structure
128
+
129
+ Each route corresponds to a folder inside `src/app/` and may contain:
130
+
131
+ * `page.html` – Page markup
132
+ * `page.css` – Page-specific styles
133
+ * `page.js` – Page-specific scripts
134
+
135
+ The page content is automatically injected into `<routes></routes>` inside `index.html`.
136
+
137
+ ## Server-Side Rendering (SSR)
138
+
139
+ Satto provides a minimal SSR syntax for rendering data on the server.
140
+
141
+ ### Example
142
+
143
+ ```html
144
+ <ssr url="https://jsonplaceholder.typicode.com/posts" response="posts">
145
+ <section>
146
+ <for condition="let post in posts">
147
+ <div>
148
+ <h1>{{ post.title }}</h1>
149
+ </div>
150
+ </for>
151
+ </section>
152
+ </ssr>
153
+ ```
154
+
155
+ ### Supported Directives
156
+
157
+ * `{{ variable }}`
158
+ * `<for condition="let item in array">`
159
+ * `<if condition="expression">`
160
+ * Attribute binding: `[src]="image.url"`
161
+
162
+ ## Cache Busting
163
+
164
+ Satto automatically appends a version query string to assets:
165
+
166
+ ```txt
167
+ styles.css?v=timestamp
168
+ page.js?v=timestamp
169
+ ```
170
+
171
+ This ensures browsers always load the latest version.
172
+
173
+ ## API Reference
174
+
175
+ ### `createServer`
176
+
177
+ ```js
178
+ const createServer = require("satto");
179
+
180
+ const routes = [
181
+ { path: "/", page: "home" },
182
+ { path: "/blog", page: "blog" },
183
+ ];
184
+
185
+ createServer(__dirname, routes, 3000);
186
+ ```
187
+
188
+ **Parameters:**
189
+
190
+ * `root` – Project root directory
191
+ * `routes` – Array of route definitions
192
+ * `port` – Server port (default: 3000)
193
+
194
+ ## Requirements
195
+
196
+ * Node.js 18 or later
197
+ * npm
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+ const esbuild = require("esbuild");
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ const banner = [
11
+ " ____ _ _ ",
12
+ " / ___| __ _| |_| |_ ___ ",
13
+ " \\___ \\ / _` | __| __/ _ \\ ",
14
+ " ___) | (_| | |_| || (_) |",
15
+ " |____/ \\__,_|\\__|\\__\\___/ ",
16
+ " "
17
+ ].join("\n");
18
+
19
+ console.log(banner + "\n");
20
+
21
+ function write(filePath, content) {
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, content);
24
+ }
25
+
26
+ if (args[0] === "init") {
27
+ const appName = args[1];
28
+
29
+ if (!appName) {
30
+ console.error("Error: You must provide an app name.");
31
+ process.exit(1);
32
+ }
33
+
34
+ console.log(`Creating project '${appName}'...`);
35
+
36
+ const root = path.join(process.cwd(), appName);
37
+
38
+ const template = {
39
+ [`${root}/src/app/home/home.html`]: `<h1>Home</h1>`,
40
+ [`${root}/src/app/home/home.css`]: `h1 {color: green}`,
41
+ [`${root}/src/app/home/home.js`]: `console.log("home loaded");`,
42
+ [`${root}/src/static/styles.css`]: ``,
43
+ [`${root}/src/index.html`]: `<!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
48
+ <link rel="stylesheet" href="/styles.css">
49
+ <title>${appName}</title>
50
+ </head>
51
+ <body>
52
+ <routes></routes>
53
+ </body>
54
+ </html>`,
55
+ [`${root}/src/server.js`]: `const createServer = require("satto");
56
+
57
+ const routes = [
58
+ { path: "/", page: "home" },
59
+ ];
60
+
61
+ createServer(__dirname, routes);
62
+ `,
63
+ [`${root}/package.json`]: `{
64
+ "name": "${appName}",
65
+ "version": "1.0.0",
66
+ "type": "commonjs",
67
+ "scripts": {
68
+ "dev": "satto run dev",
69
+ "build": "satto run build"
70
+ }
71
+ }`
72
+ };
73
+
74
+ for (const file in template) {
75
+ write(file, template[file]);
76
+ }
77
+
78
+ execSync(
79
+ `npm i satto`,
80
+ {
81
+ stdio: "inherit",
82
+ cwd: root
83
+ }
84
+ );
85
+
86
+ console.log("Project created successfully!");
87
+ process.exit(0);
88
+ }
89
+
90
+ if (args[0] === "route") {
91
+ const routeName = args[1];
92
+
93
+ if (!routeName) {
94
+ console.error("Error: You must provide a route name.");
95
+ process.exit(1);
96
+ }
97
+
98
+ const root = process.cwd();
99
+
100
+ console.log(`Creating route '${routeName}'...`);
101
+
102
+ write(`${root}/src/app/${routeName}/${routeName}.html`, `<h1>${routeName}</h1>`);
103
+ write(`${root}/src/app/${routeName}/${routeName}.css`, ``);
104
+ write(`${root}/src/app/${routeName}/${routeName}.js`, ``);
105
+
106
+ const serverPath = `${root}/src/server.js`;
107
+
108
+ if (!fs.existsSync(serverPath)) {
109
+ console.error("Error: server.js not found. Run this command inside a project created using 'satto init'.");
110
+ process.exit(1);
111
+ }
112
+
113
+ let serverContent = fs.readFileSync(serverPath, "utf8");
114
+
115
+ const routeEntry = ` { path: "/${routeName}", page: "${routeName}" },`;
116
+
117
+ serverContent = serverContent.replace(
118
+ /const routes\s*=\s*\[/,
119
+ `const routes = [\n${routeEntry}`
120
+ );
121
+
122
+ fs.writeFileSync(serverPath, serverContent);
123
+
124
+ console.log(`Route '${routeName}' created.`);
125
+ process.exit(0);
126
+ }
127
+
128
+ if (args[0] === "run" && args[1] === "dev") {
129
+ console.log("Development server is starting...");
130
+ console.clear();
131
+
132
+ execSync(
133
+ "node --watch --watch-path=./src ./src/server.js",
134
+ {
135
+ stdio: "inherit",
136
+ cwd: process.cwd()
137
+ }
138
+ );
139
+
140
+ process.exit(0);
141
+ }
142
+
143
+ function copyRecursive(src, dest) {
144
+ if (!fs.existsSync(src)) return;
145
+
146
+ const stats = fs.lstatSync(src);
147
+
148
+ if (stats.isFile()) {
149
+ if(src.includes("css") || src.includes("js")) {
150
+ esbuild.buildSync({
151
+ entryPoints: [src],
152
+ outfile: dest,
153
+ minify: true,
154
+ bundle: false
155
+ });
156
+ } else {
157
+ fs.copyFileSync(src, dest);
158
+ }
159
+ } else if (stats.isDirectory()) {
160
+ if (!fs.existsSync(dest)) {
161
+ fs.mkdirSync(dest);
162
+ }
163
+
164
+ const entries = fs.readdirSync(src);
165
+
166
+ for (const entry of entries) {
167
+ const srcPath = path.join(src, entry);
168
+ const destPath = path.join(dest, entry);
169
+ copyRecursive(srcPath, destPath);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (args[0] === "run" && args[1] === "build") {
175
+ console.log("Build is starting...");
176
+
177
+ const root = process.cwd();
178
+ if (fs.existsSync(`${root}/dist`)) fs.rmSync(`${root}/dist`, { recursive: true, force: true });
179
+
180
+ esbuild.buildSync({
181
+ entryPoints: ["src/server.js"],
182
+ bundle: true,
183
+ platform: "node",
184
+ outfile: "dist/server.js",
185
+ format: "cjs",
186
+ minify: true,
187
+ });
188
+
189
+ copyRecursive(`${root}/src/index.html`, `${root}/dist/index.html`);
190
+ copyRecursive(`${root}/src/app`, `${root}/dist/app`);
191
+ copyRecursive(`${root}/src/static`, `${root}/dist/static`);
192
+
193
+ console.log("Build completed successfully!");
194
+ process.exit(0);
195
+ }
196
+
197
+ console.log("Unknown command.");
198
+ console.log("Usage:");
199
+ console.log(" satto init <app_name> # Initialize a new Satto application");
200
+ console.log(" satto route <route_name> # Create a new route in the application");
201
+ console.log(" satto run dev # Run the application in development mode");
202
+ console.log(" satto run build # Build the application for production");
203
+ process.exit(1);
@@ -0,0 +1,119 @@
1
+ const express = require("express");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const ejs = require("ejs");
5
+
6
+ function hasContent(filePath) {
7
+ try {
8
+ return fs.statSync(filePath).size > 0;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function applyParams(template, params) {
15
+ return template.replace(/{{\s*params\.(\w+)\s*}}/g, (_, key) => {
16
+ return params[key] ?? "";
17
+ });
18
+ }
19
+
20
+ async function renderPage(html, params) {
21
+ const match = html.match(/<ssr\s+url="([^"]+)"\s+.*response="([^"]+)"[^>]*>/);
22
+
23
+ if(match) {
24
+ const url = applyParams(match?.[1], params);
25
+ const resVar = match?.[2];
26
+ let data = {};
27
+
28
+ if (url) {
29
+ const res = await fetch(url);
30
+ data = await res.json();
31
+ }
32
+
33
+ html = html.replace(/<ssr[^>]*>([\s\S]*?)<\/ssr>/g, (_, content) => {
34
+ return content.replace(
35
+ /<ssr[^>]*>|<\/ssr>|\{\{\s*([\w.]+)\s*\}\}|<for\s+condition="let\s+(\w+)\s+in\s+([\w.]+)"\s*>|<\/for>|<if\s+condition="(.+?)"\s*>|<\/if>|\[(\w+)\]="([^"]+)"/g,
36
+ (match, expr, item, arr, cond, attr, val) => {
37
+ if (match.startsWith("<ssr")) return "";
38
+ if (match === "</ssr>") return "";
39
+ if (expr) return `<%= ${expr} %>`;
40
+ if (item && arr) return `<% ${arr}.forEach(${item} => { %>`;
41
+ if (match === "</for>") return "<% }) %>";
42
+ if (cond) return `<% if (${cond}) { %>`;
43
+ if (match === "</if>") return "<% } %>";
44
+ if (attr && val) return `${attr}="<%= ${val} %>"`;
45
+ return match;
46
+ }
47
+ );
48
+ });
49
+
50
+ return ejs.render(html, {params, [resVar]: data});
51
+ } else {
52
+ return html;
53
+ }
54
+ }
55
+
56
+ function createServer(__dirname, routes = [], port = 3000) {
57
+ const versionApp = Date.now();
58
+ const app = express();
59
+
60
+ app.set("views", path.join(__dirname, "app"));
61
+ app.engine("html", ejs.renderFile);
62
+ app.set("view engine", "html");
63
+
64
+ app.use(express.static(path.join(__dirname, "app")));
65
+ app.use(express.static(path.join(__dirname, "static")));
66
+
67
+ routes.forEach((route) => {
68
+ app.get(route.path, (req, res) => {
69
+ const page = route.page;
70
+ const htmlPath = path.join(__dirname, "app", page, page + ".html");
71
+ app.use(express.static(path.join(__dirname, "app", page)));
72
+
73
+ fs.readFile(htmlPath, "utf8", async (err, html) => {
74
+ if (err) {
75
+ return res.status(404).send();
76
+ }
77
+
78
+ const params = { ...req.params };
79
+ const index = fs.readFileSync(__dirname + "/index.html", "utf8");
80
+
81
+ const CssPath = path.join(__dirname, "app", page, page + ".css");
82
+ const JsPath = path.join(__dirname, "app", page, page + ".js");
83
+
84
+ if(hasContent(JsPath)) {
85
+ html += `\n<script src="./${page}.js?v=${versionApp}"></script>`;
86
+ }
87
+
88
+ html = index.replace("<routes></routes>", html);
89
+ html = html.replace(`<link rel="stylesheet" href="/styles.css">`, `<link rel="stylesheet" href="/styles.css?v=${versionApp}">`);
90
+
91
+ if(hasContent(CssPath)) {
92
+ html = html.replace(
93
+ "</head>",
94
+ `<link rel="stylesheet" href="./${page}.css?v=${versionApp}">
95
+ </head>`
96
+ );
97
+ }
98
+
99
+ res.send(await renderPage(html, params));
100
+ });
101
+ });
102
+ });
103
+
104
+ app.listen(port, () => {
105
+ const banner = [
106
+ " ____ _ _ ",
107
+ " / ___| __ _| |_| |_ ___ ",
108
+ " \\___ \\ / _` | __| __/ _ \\ ",
109
+ " ___) | (_| | |_| || (_) |",
110
+ " |____/ \\__,_|\\__|\\__\\___/ ",
111
+ " "
112
+ ].join("\n");
113
+
114
+ console.log(banner + "\n");
115
+ console.log(`Server running on http://localhost:${port}`);
116
+ });
117
+ }
118
+
119
+ module.exports = createServer;