chocola 1.2.6 → 1.3.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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) SadGabi.
3
+ Copyright (c) 2026 Sad Gabi
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Chocola is a lightweight, reactive component-based web framework that brings sim
16
16
  - **🔌 Component Lifecycle API** - Public APIs for mounting, manipulating, and removing components
17
17
  - **📦 Built-in Bundler** - Automatic compilation and optimization
18
18
  - **🔥 Hot Reload Development** - See changes instantly with the dev server
19
- - **🎨 Template Syntax** - Clean HTML templates with `${}` and `&{}` interpolation
19
+ - **🎨 Template Syntax** - Clean HTML templates with `{}` and `&{}` interpolation
20
20
  - **⚙️ Zero Config** - Works out of the box with sensible defaults
21
21
 
22
22
  ## 🚀 Quick Start
@@ -70,14 +70,14 @@ Create a `chocola.config.json` file:
70
70
  ### 1. Define Your HTML Template
71
71
 
72
72
  > NOTE: Reactivity, component APIs and global variables are not implemented yet.
73
- > Any of this features displayed here are for future references and may be modified.
73
+ > Any of these features displayed here are for future references and may be modified.
74
74
 
75
- Create `src/lib/html/counter.body.html`:
75
+ Create `src/lib/html/counter.html`:
76
76
 
77
77
  ```html
78
78
  <div class="counter">
79
- <h2>${ctx.title}</h2>
80
- <!-- Use & instead of $ to set reactivity -->
79
+ <h2>{ctx.title}</h2>
80
+ <!-- Use & to set reactivity -->
81
81
  <p>Count: &{sfx.count}</p>
82
82
  <button class="increment">+</button>
83
83
  <button class="decrement">-</button>
@@ -90,7 +90,7 @@ Create `src/lib/Counter.js`:
90
90
 
91
91
  ```javascript
92
92
  import { lib } from "chocola";
93
- import HTML from "./html/counter.body.html";
93
+ import HTML from "./html/counter.html";
94
94
 
95
95
  function RUNTIME(self, ctx) {
96
96
  // Acces to component effects
@@ -154,8 +154,7 @@ In your `src/index.html`:
154
154
  </head>
155
155
  <body>
156
156
  <app>
157
- <!-- Set mutable sfx variables with & -->
158
- <Counter ctx.title="My Counter" sfx.&initialCount="0"></Counter>
157
+ <Counter ctx.title="My Counter" sfx.initialCount="0"></Counter>
159
158
  </app>
160
159
  </body>
161
160
  </html>
@@ -165,7 +164,7 @@ In your `src/index.html`:
165
164
 
166
165
  ### Template (`body`)
167
166
  - Standard HTML with template variables
168
- - **Static context**: `${ctx.propertyName}`, `${sfx.propertyName}` - rendered once at initialization
167
+ - **Static context**: `{ctx.propertyName}`, `{sfx.propertyName}` - rendered once at initialization
169
168
  - **Reactive state**: `&{sfx.propertyName}` - automatically updates on change
170
169
  - Clean separation of markup and logic
171
170
 
@@ -260,7 +259,7 @@ Share state across your entire application:
260
259
  // In any component
261
260
  import * as globals from "path/to/globals.js";
262
261
 
263
- function RUNTIME(self, ctx, sfx) {
262
+ function RUNTIME(self, ctx) {
264
263
  // Set global variables
265
264
  globals.userTheme = "dark";
266
265
  globals.notifications = [];
@@ -280,7 +279,7 @@ import { lib } from "chocola";
280
279
  import Counter from "./lib/Counter.js";
281
280
 
282
281
  // Mount a component programmatically in RUNTIME and/or EFFECTS
283
- function RUNTIME(self. ctx) {
282
+ function RUNTIME(self, ctx) {
284
283
  const counterInstance = lib.mount(Counter)
285
284
  .defCtx({
286
285
  title: "Dynamic Counter",
@@ -325,7 +324,7 @@ import { fileURLToPath } from "url";
325
324
  const __filename = fileURLToPath(import.meta.url);
326
325
  const __dirname = path.dirname(__filename);
327
326
 
328
- dev.server(__dirname, "src", "dist");
327
+ dev.server(__dirname);
329
328
  ```
330
329
 
331
330
  Run:
@@ -17,7 +17,8 @@ export function processComponentElement(
17
17
  loadedComponents,
18
18
  runtimeChunks,
19
19
  compIdColl,
20
- letterState
20
+ letterState,
21
+ runtimeMap
21
22
  ) {
22
23
  const tagName = element.tagName.toLowerCase();
23
24
  const compName = tagName + ".js";
@@ -28,7 +29,7 @@ export function processComponentElement(
28
29
 
29
30
  if (instance && instance.body) {
30
31
  let body = instance.body;
31
- body = body.replace(/\$\{ctx\.(\w+)\}/g, (_, key) => ctx[key] || "");
32
+ body = body.replace(/\{ctx\.(\w+)\}/g, (_, key) => ctx[key] || "");
32
33
  const fragment = JSDOM.fragment(body);
33
34
  const firstChild = fragment.firstChild;
34
35
 
@@ -38,14 +39,37 @@ export function processComponentElement(
38
39
  firstChild.setAttribute("chid", compId);
39
40
 
40
41
  let script = instance.script && instance.script.toString();
41
- const letter = getNextLetter(letterState);
42
42
 
43
- script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
43
+ const ctxRegex = /ctx\s*=\s*({.*?})/;
44
+ const ctxMatch = script.match(ctxRegex);
45
+ let runtimeCtx = {};
46
+ if (ctxMatch) {
47
+ try {
48
+ runtimeCtx = JSON.parse(ctxMatch[1].replace(/(\w+):/g, '"$1":'));
49
+ } catch (e) {
50
+ runtimeCtx = {};
51
+ }
52
+ }
53
+ let ctxDef = "";
54
+ for (const [key, value] of Object.entries(runtimeCtx)) {
55
+ ctxDef += `ctx.${key} = ctx.${key}||${JSON.stringify(value)};\n`;
56
+ }
57
+ script = script.replace(ctxRegex, "ctx");
58
+ script = script.replace(/RUNTIME\([^)]*\)\s*{/, match => match + "\n" + ctxDef);
44
59
 
45
- runtimeChunks.push(`
46
- const ${letter} = document.querySelector('[chid="${compId}"]');
47
- ${script}
48
- ${letter}RUNTIME(${letter}, ${JSON.stringify(ctx)});`);
60
+ // Determine or create a single runtime function per component
61
+ let letterEntry = runtimeMap && runtimeMap.get(compName);
62
+ let letter;
63
+ if (!letterEntry) {
64
+ letter = getNextLetter(letterState);
65
+ script = script.replace(/RUNTIME/g, `${letter}RUNTIME`);
66
+ runtimeChunks.push(script);
67
+ runtimeMap && runtimeMap.set(compName, { letter });
68
+ } else {
69
+ letter = letterEntry.letter;
70
+ }
71
+
72
+ runtimeChunks.push(`${letter}RUNTIME(document.querySelector('[chid="${compId}"]'), ${JSON.stringify(ctx)});`);
49
73
  }
50
74
  }
51
75
  element.replaceWith(fragment);
@@ -69,9 +93,10 @@ export function processAllComponents(appElements, loadedComponents) {
69
93
  const runtimeChunks = [];
70
94
  const compIdColl = [];
71
95
  const letterState = { value: null };
96
+ const runtimeMap = new Map();
72
97
 
73
98
  appElements.forEach(el => {
74
- processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState);
99
+ processComponentElement(el, loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap);
75
100
  });
76
101
 
77
102
  const runtimeScript = runtimeChunks.join("\n");
@@ -7,8 +7,8 @@ import path from "path";
7
7
  // ===== Component Loading =====
8
8
 
9
9
  /**
10
- * Discovers and loads all components from a library directory
11
- * Components are JavaScript files that start with an uppercase letter
10
+ * Discovers and loads all components from a library directory.
11
+ * Components are JavaScript files that start with an uppercase letter.
12
12
  * They must have a default export that is a function
13
13
  * @param {import("node:fs").PathLike} libDir - Directory containing component files
14
14
  * @returns {Promise<{
package/dev/index.js CHANGED
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import chalk from "chalk";
5
5
  import compile from "../compiler/index.js";
6
+ import { loadConfig, resolvePaths } from "../compiler/config.js";
6
7
  import { getChocolaConfig } from "../utils.js";
7
8
 
8
9
  export async function serve(__rootdir) {
@@ -14,8 +15,12 @@ export async function serve(__rootdir) {
14
15
  port: 3000,
15
16
  }
16
17
 
17
- const config = await getChocolaConfig(__rootdir);
18
- const devConfig = config.dev;
18
+ let lastBuildTime = Date.now();
19
+
20
+ const fullConfig = await getChocolaConfig(__rootdir);
21
+ const config = await loadConfig(__rootdir);
22
+ const paths = resolvePaths(__rootdir, config);
23
+ const devConfig = fullConfig.dev;
19
24
 
20
25
  if (devConfig.hostname) { __config.hostname = devConfig.hostname }
21
26
  else { console.warn(chalk.bold.yellow("WARNING!"), `hostname not defined in chocola.config.json file: using default ${__config.hostname} hostname.`) }
@@ -23,7 +28,42 @@ export async function serve(__rootdir) {
23
28
  if (devConfig.port) { __config.port = devConfig.port }
24
29
  else { console.warn(chalk.bold.yellow("WARNING!"), `port not defined in chocola.config.json file: using default ${__config.port} port.`) }
25
30
 
31
+ const srcDir = paths.src;
32
+ const componentsDir = paths.components;
33
+
34
+ let compileTimeout;
35
+ const scheduleRecompile = () => {
36
+ clearTimeout(compileTimeout);
37
+ compileTimeout = setTimeout(async () => {
38
+ try {
39
+ await compile(__rootdir, { isHotReload: true });
40
+ lastBuildTime = Date.now();
41
+ console.log(chalk.green("✓"), "Hot reload: compiled successfully");
42
+ } catch (error) {
43
+ console.error(chalk.red("✗"), "Hot reload compilation failed:", error.message);
44
+ }
45
+ }, 300);
46
+ };
47
+
48
+
49
+ fs.watch(srcDir, { recursive: true }, (eventType, filename) => {
50
+ if (filename && !filename.includes("node_modules")) {
51
+ scheduleRecompile();
52
+ }
53
+ });
54
+ fs.watch(componentsDir, { recursive: true }, (eventType, filename) => {
55
+ if (filename && !filename.includes("node_modules")) {
56
+ scheduleRecompile();
57
+ }
58
+ });
59
+
26
60
  const server = http.createServer((req, res) => {
61
+ if (req.url === "/api/hot-reload") {
62
+ res.writeHead(200, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ buildTime: lastBuildTime }));
64
+ return;
65
+ }
66
+
27
67
  let filePath = path.join(
28
68
  __outdir,
29
69
  req.url === "/" ? "index.html" : req.url
@@ -78,8 +118,33 @@ export async function serve(__rootdir) {
78
118
  res.end("Error interno: " + error.code);
79
119
  }
80
120
  } else {
81
- res.writeHead(200, { "Content-Type": contentType });
82
- res.end(content, "utf-8");
121
+ if (extname === ".html" || extname === ".htm") {
122
+ const hotReloadScript = `
123
+ <script>
124
+ (function() {
125
+ let lastBuildTime = ${lastBuildTime};
126
+ setInterval(async () => {
127
+ try {
128
+ const res = await fetch('/api/hot-reload');
129
+ const data = await res.json();
130
+ if (data.buildTime > lastBuildTime) {
131
+ lastBuildTime = data.buildTime;
132
+ console.log('[Hot Reload] Changes detected, reloading...');
133
+ window.location.reload();
134
+ }
135
+ } catch (e) {
136
+ console.error('[Hot Reload] Check failed:', e);
137
+ }
138
+ }, 1000);
139
+ })();
140
+ </script>`;
141
+ const htmlWithReload = content.toString().replace('</body>', hotReloadScript + '</body>');
142
+ res.writeHead(200, { "Content-Type": contentType });
143
+ res.end(htmlWithReload, "utf-8");
144
+ } else {
145
+ res.writeHead(200, { "Content-Type": contentType });
146
+ res.end(content, "utf-8");
147
+ }
83
148
  }
84
149
  });
85
150
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocola",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "Chocola pipeline for web apps.",
5
5
  "keywords": [
6
6
  "web",
@@ -11,13 +11,25 @@
11
11
  "components",
12
12
  "ui"
13
13
  ],
14
- "license": "ISC",
14
+ "homepage": "https://github.com/sad-gabi/chocola#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/sad-gabi/chocola/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/sad-gabi/chocola.git"
21
+ },
22
+ "license": "MIT",
15
23
  "author": "SadGabi",
16
24
  "type": "module",
17
25
  "main": "index.js",
18
26
  "scripts": {
19
27
  "test": "echo \"Error: no test specified\" && exit 1"
20
28
  },
29
+ "exports": {
30
+ ".": "./index.js",
31
+ "./types": "./types/index.js"
32
+ },
21
33
  "dependencies": {
22
34
  "chalk": "^5.6.2",
23
35
  "js-beautify": "^1.15.4",
package/types/index.js ADDED
@@ -0,0 +1,135 @@
1
+ const iconsType = "home" | "search" | "menu" | "settings" | "favorite" | "account_circle" | "shopping_cart" | "notifications" | "info" | "help" | "exit_to_app" | "check_circle" | "close" | "edit" | "delete" | "add" | "arrow_back" | "arrow_forward" | "play_arrow" | "pause" | "stop" | "camera_alt" | "photo" | "alarm" | "event" | "attach_file" | "print" | "share" | "cloud" | "cloud_upload" | "cloud_download" | "lock" | "lock_open" | "visibility" | "visibility_off" | "phone" | "email" | "map" | "place" | "directions" | "train" | "directions_car" | "directions_bike" | "school" | "work" | "lightbulb" | "battery_full" | "battery_std" | "wifi" | "bluetooth";
2
+ const iconsArray = [
3
+ "home", "search", "menu", "settings", "favorite", "account_circle",
4
+ "shopping_cart", "notifications", "info", "help", "exit_to_app",
5
+ "check_circle", "close", "edit", "delete", "add", "arrow_back",
6
+ "arrow_forward", "play_arrow", "pause", "stop", "camera_alt",
7
+ "photo", "alarm", "event", "attach_file", "print", "share",
8
+ "cloud", "cloud_upload", "cloud_download", "lock", "lock_open",
9
+ "visibility", "visibility_off", "phone", "email", "map", "place",
10
+ "directions", "train", "directions_car", "directions_bike", "school",
11
+ "work", "lightbulb", "battery_full", "battery_std", "wifi", "bluetooth"
12
+ ];
13
+ /**
14
+ * An instrinsic object that contains chocola types
15
+ */
16
+ const ct = {};
17
+ /**
18
+ * Creates an interface for the component static context
19
+ * @param {object} ctxInterface
20
+ */
21
+ ct.defCtx = (ctxInterface) => {
22
+ if (!ctxInterface) return undefined;
23
+ if (typeof ctxInterface !== "object") return;
24
+ return ctxInterface;
25
+ }
26
+
27
+ ct.Number = class Number {
28
+ /**
29
+ * @param {number} value
30
+ * @returns
31
+ */
32
+ constructor(value) {
33
+ if (!value) return undefined;
34
+ if (typeof value !== "number") return;
35
+ return value;
36
+ }
37
+ /**
38
+ * @param {number} min
39
+ * @param {number} max
40
+ * @returns {number}
41
+ */
42
+ clamp(min, max) {
43
+ return Math.min(Math.max(this.value, min), max);
44
+ }
45
+ /**
46
+ * @param {number} min
47
+ * @returns {number}
48
+ */
49
+ min(min) {
50
+ return Math.min(this.value, min);
51
+ }
52
+ /**
53
+ * @param {number} max
54
+ * @returns {number}
55
+ */
56
+ max(max) {
57
+ return Math.max(this.value, max);
58
+ }
59
+ }
60
+ /**
61
+ * @param {number} value
62
+ */
63
+ ct.Float = (value) => {
64
+ if (!value) return undefined;
65
+ if (Number.isInteger(value)) return;
66
+ return value;
67
+ }
68
+ /**
69
+ * @param {number} value
70
+ */
71
+ ct.Int = (value) => {
72
+ if (!value) return undefined;
73
+ if (!Number.isInteger(value)) return;
74
+ return value;
75
+ }
76
+ /**
77
+ * @param {string} value
78
+ */
79
+ ct.String = (value) => {
80
+ if (!value) return undefined;
81
+ if (typeof value !== "string") return;
82
+ return value;
83
+ }
84
+ /**
85
+ * @param {boolean} value
86
+ */
87
+ ct.Boolean = (value) => {
88
+ if (value === null) return undefined;
89
+ if (typeof value !== "boolean") return;
90
+ return value;
91
+ }
92
+ /**
93
+ * @param {object} value
94
+ */
95
+ ct.Object = (value) => {
96
+ if (!value) return undefined;
97
+ if (typeof value !== "object") return;
98
+ return value;
99
+ }
100
+ /**
101
+ * @param {function} value
102
+ */
103
+ ct.Function = (value) => {
104
+ if (!value) return undefined;
105
+ if (typeof value !== "function") return;
106
+ return value;
107
+ }
108
+ /**
109
+ * @param {symbol} value
110
+ */
111
+ ct.Symbol = (value) => {
112
+ if (!value) return undefined;
113
+ if (typeof value !== "symbol") return;
114
+ return value;
115
+ }
116
+ /**
117
+ * @param {URLPattern | "none"} value
118
+ */
119
+ ct.Url = (value) => {
120
+ if (!value) return undefined;
121
+ if (!value.startsWith("http://") || !value.startsWith("https://")) return;
122
+ return value;
123
+ }
124
+ /**
125
+ *
126
+ * @param {"home" | "search" | "menu" | "settings" | "favorite" | "account_circle" | "shopping_cart" | "notifications" | "info" | "help" | "exit_to_app" | "check_circle" | "close" | "edit" | "delete" | "add" | "arrow_back" | "arrow_forward" | "play_arrow" | "pause" | "stop" | "camera_alt" | "photo" | "alarm" | "event" | "attach_file" | "print" | "share" | "cloud" | "cloud_upload" | "cloud_download" | "lock" | "lock_open" | "visibility" | "visibility_off" | "phone" | "email" | "map" | "place" | "directions" | "train" | "directions_car" | "directions_bike" | "school" | "work" | "lightbulb" | "battery_full" | "battery_std" | "wifi" | "bluetooth"} value
127
+ * @returns
128
+ */
129
+ ct.Icon = (value) => {
130
+ if (!value) return "help";
131
+ if (!iconsArray.includes(value)) return "help";
132
+ return value;
133
+ }
134
+
135
+ export default ct;