chocola 2.0.0-next.1 → 2.0.0-next.11

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 CHANGED
@@ -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
@@ -1,45 +1,55 @@
1
- # chocola
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="assets/banner_dark.jpg">
3
+ <img src="assets/banner.jpg" alt="Chocola • The sweetest way to build the web" />
4
+ </picture>
2
5
 
3
- Chocola is a JavaScript library for creating web user interfaces.
6
+ ## What is Chocola
4
7
 
5
- `chocola` only contains the functionality to build and serve web static builds. For production you may want to implement `chocola` to your workflow with other frameworks as Vite until official workflows are provided.
8
+ Chocola is a new and sweeter way to build your web apps.
6
9
 
7
- ## Usage
10
+ No bundler config. No virtual DOM. No hydration ceremony. Just `.html` files with `<template>`, `<script>`, and `<style>` compiled to HTML with scoped CSS the browser already understands, and optional runtime when you need it.
11
+
12
+ Import components. Instantiate them. Mount, update, remove. Client-side or server-side. Same file, minimal overhead.
8
13
 
9
14
  ```html
10
- <!-- counter.html -->
11
- <div>
12
- <button title={title}>{text}</button>
13
-
14
- <div class="main">
15
- <div class="number">{count}</div>
16
- </div>
17
- </div>
18
- ```
15
+ <script>
16
+ import CoolButton from './CoolButton.html';
17
+
18
+ export let title = "Hello";
19
+
20
+ let input;
21
+
22
+ function $runtime() {
23
+ input.focus();
24
+ }
25
+ </script>
19
26
 
20
- ```js
21
- import body from "./html/counter.html";
22
- import styles from "./css/counter.css";
23
-
24
- function RUNTIME(self, ctx) {
25
- const btn = self.querySelector("button");
26
- const number = self.querySelector(".number");
27
-
28
- btn.addEventListener("click", () => {
29
- ctx.count++;
30
- number.textContent = ctx.count;
31
- })
32
- }
33
-
34
- export default function Counter() {
35
- return {
36
- body,
37
- styles,
38
- script: RUNTIME
39
- }
40
- }
27
+ <template>
28
+ <div>
29
+ <h1>{title}</h1>
30
+ <input bind:self="input" type="text" placeholder="Your name">
31
+ <CoolButton label="Not lame button"></CoolButton>
32
+ </div>
33
+ </template>
34
+
35
+ <style>
36
+ h1 { color: chocolate; }
37
+ </style>
41
38
  ```
42
39
 
43
40
  ## Documentation
44
41
 
45
- See https://github.com/sad-gabi/chocola/wiki
42
+ - [Getting started](documentation/01-introduction/02-getting-started.md)
43
+ - [Project structure](documentation/01-introduction/03-project-structure.md)
44
+ - [CLI reference](documentation/07-reference/04-cli.md)
45
+
46
+ ## Quick start
47
+
48
+ ```sh
49
+ npm install chocola
50
+ npx chocola dev # dev server
51
+ npx chocola build # production build
52
+ npx chocola serve # SSR production server
53
+ ```
54
+
55
+ No init scripts required: `chocola.config.json` is optional. See [Getting started](documentation/01-introduction/02-getting-started.md) for more.
package/bin/chocola.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import path from "path";
3
+ import { fileURLToPath, pathToFileURL } from "url";
4
+ import { createRequire } from "module";
5
+ import { readFile } from "fs/promises";
6
+ import { getConfig, isMissingConfigFile } from "../utils.js";
7
+ import { loadConfig } from "../compiler/config.js";
8
+
9
+ import chalk from "../compiler/chalk.js";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const isPlain = process.env.NO_COLOR === "1" || process.argv.includes("--plain");
14
+ const c = isPlain ? {
15
+ hex: () => (s) => s, bold: (s) => s, green: (s) => s, yellow: (s) => s, red: (s) => s, underline: (s) => s,
16
+ } : chalk;
17
+
18
+ export default __dirname;
19
+
20
+ export async function resolveConfig(rootDir, cliOverrides, configPathOpt) {
21
+ const customPath = configPathOpt ? path.resolve(rootDir, configPathOpt) : null;
22
+ const fullConfig = customPath
23
+ ? await readFile(customPath, "utf-8").then(r => JSON.parse(r))
24
+ : await getConfig(rootDir, { silent: true });
25
+ const isMissing = customPath ? false : isMissingConfigFile(fullConfig);
26
+ const base = await loadConfig(rootDir, { silent: true, customPath });
27
+ if (cliOverrides.outDir) base.outDir = path.resolve(rootDir, cliOverrides.outDir);
28
+ if (cliOverrides.srcDir) base.srcDir = path.resolve(rootDir, cliOverrides.srcDir);
29
+ if (cliOverrides.libDir) base.libDir = path.resolve(rootDir, cliOverrides.libDir);
30
+ if (cliOverrides.emptyOutDir != null) base.emptyOutDir = cliOverrides.emptyOutDir;
31
+ const effectiveDev = { hostname: "localhost", port: 3000, ...(!isMissing && fullConfig.dev || {}), ...cliOverrides.dev };
32
+ const effectiveServer = { hostname: "localhost", port: 8080, middleware: null, ...(!isMissing && fullConfig.server || {}), ...cliOverrides.server };
33
+ if (!cliOverrides.server?.port && process.env.PORT) {
34
+ const envPort = parseInt(process.env.PORT, 10);
35
+ if (Number.isFinite(envPort)) effectiveServer.port = envPort;
36
+ }
37
+ if (!cliOverrides.server?.hostname && !fullConfig.server?.hostname && !fullConfig.server?.host && process.env.PORT) {
38
+ effectiveServer.hostname = "0.0.0.0";
39
+ }
40
+ return { fullConfig, base, effectiveDev, effectiveServer, isMissing };
41
+ }
42
+
43
+ function parsePort(val) {
44
+ const n = parseInt(val, 10);
45
+ if (Number.isNaN(n) || n < 1 || n > 65535) {
46
+ throw new Error(`Invalid port: ${val}. Must be between 1 and 65535.`);
47
+ }
48
+ return n;
49
+ }
50
+
51
+ function kebabToCamel(str) {
52
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
53
+ }
54
+
55
+ export function parseArgs(argv) {
56
+ const args = argv.slice(2).filter(a => a !== "--plain");
57
+ const cmd = args[0];
58
+ if (!cmd || cmd === "--help" || cmd === "-h") return { cmd: "help", rest: [] };
59
+ if (cmd === "--version" || cmd === "-v") return { cmd: "version", rest: [] };
60
+ const positional = [];
61
+ const flags = {};
62
+ let configPath = null;
63
+ let i = 1;
64
+ while (i < args.length) {
65
+ const arg = args[i];
66
+ if (arg === "--config" || arg === "-c") {
67
+ configPath = args[++i];
68
+ } else if (arg.startsWith("--")) {
69
+ const key = kebabToCamel(arg.slice(2));
70
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
71
+ flags[key] = args[++i];
72
+ } else {
73
+ flags[key] = true;
74
+ }
75
+ } else {
76
+ positional.push(arg);
77
+ }
78
+ i++;
79
+ }
80
+ return { cmd, root: positional[0] || undefined, flags, configPath, rest: args };
81
+ }
82
+
83
+ async function getLocalModules(rootDir) {
84
+ try {
85
+ const req = createRequire(pathToFileURL(path.join(rootDir, "package.json")).href);
86
+ const localPkgPath = req.resolve("chocola/package.json");
87
+ const localRoot = path.dirname(localPkgPath);
88
+ const cliDir = path.dirname(path.dirname(__filename));
89
+ if (localRoot === cliDir) return null;
90
+ const mod = await import(pathToFileURL(path.join(localRoot, "compiler/index.js")).href);
91
+ return { rootDir: localRoot, mod };
92
+ } catch { return null; }
93
+ }
94
+
95
+ async function banner() {
96
+ if (!isPlain) {
97
+ const { logBanner } = await import("../compiler/index.js");
98
+ logBanner();
99
+ }
100
+ }
101
+
102
+ export async function main() {
103
+ const { cmd, root, flags, configPath } = parseArgs(process.argv);
104
+
105
+ if (cmd === "help") {
106
+ await banner();
107
+ console.log(c.bold("Usage: ") + "chocola <command> [root] [options]");
108
+ console.log("");
109
+ console.log("Commands:");
110
+ console.log(" build Build for production");
111
+ console.log(" dev Start dev server with hot-reload");
112
+ console.log(" serve Start SSR production server");
113
+ console.log("");
114
+ console.log("Options:");
115
+ console.log(" -c, --config <path> Path to config file");
116
+ console.log(" --help, -h Show help");
117
+ console.log(" --version, -v Show version");
118
+ console.log(" --plain, NO_COLOR=1 Disable color output");
119
+ console.log("");
120
+ console.log("build options:");
121
+ console.log(" --srcDir <dir> Source directory (default: src)");
122
+ console.log(" --outDir <dir> Output directory (default: dist)");
123
+ console.log(" --libDir <dir> Components subdir (default: lib)");
124
+ console.log(" --no-emptyOutDir Do not clean outDir before build");
125
+ console.log("");
126
+ console.log("dev options:");
127
+ console.log(" --host <hostname> Hostname (default: localhost)");
128
+ console.log(" --port <number> Port (default: 3000)");
129
+ console.log(" --open Open browser after start");
130
+ console.log("");
131
+ console.log("serve options:");
132
+ console.log(" --host <hostname> Hostname (default: localhost)");
133
+ console.log(" --port <number> Port (default: 8080)");
134
+ console.log(" --middleware <path> Path to middleware file");
135
+ return;
136
+ }
137
+ if (cmd === "version") {
138
+ const pkg = await readFile(path.join(__dirname, "../package.json"), "utf-8");
139
+ console.log(JSON.parse(pkg).version);
140
+ return;
141
+ }
142
+
143
+ if (!["build", "dev", "serve"].includes(cmd)) {
144
+ console.error(c.red(`Unknown command: ${cmd}. Use "chocola --help" for usage.`));
145
+ process.exit(2);
146
+ }
147
+
148
+ const rootDir = path.resolve(root || process.cwd());
149
+
150
+ try {
151
+ if (configPath) {
152
+ await readFile(path.resolve(rootDir, configPath), "utf-8");
153
+ }
154
+ const rawPort = flags.port ? parsePort(flags.port) : undefined;
155
+ const effectiveHost = flags.host || flags.hostname;
156
+
157
+ const { base, effectiveDev, effectiveServer } = await resolveConfig(rootDir, {
158
+ outDir: flags.outDir,
159
+ srcDir: flags.srcDir,
160
+ libDir: flags.libDir,
161
+ emptyOutDir: flags.emptyOutDir === false ? false : (flags.noEmptyOutDir === true ? false : undefined),
162
+ dev: { port: rawPort, hostname: effectiveHost },
163
+ server: { port: rawPort, hostname: effectiveHost },
164
+ }, configPath);
165
+
166
+ let serverInstance = null;
167
+ let devServer = null;
168
+ const cleanup = () => {
169
+ if (devServer) { try { devServer.close(); } catch {} }
170
+ if (serverInstance) { try { serverInstance.close(); } catch {} }
171
+ process.exit(0);
172
+ };
173
+ process.on("SIGINT", cleanup);
174
+ process.on("SIGTERM", cleanup);
175
+
176
+ switch (cmd) {
177
+ case "build": {
178
+ const local = await getLocalModules(rootDir);
179
+ const compile = local ? local.mod.default : (await import("../compiler/index.js")).default;
180
+ await compile(rootDir, { overrides: base });
181
+ process.exit(0);
182
+ }
183
+ case "dev": {
184
+ const local = await getLocalModules(rootDir);
185
+ const devMod = local
186
+ ? await import(pathToFileURL(path.join(local.rootDir, "dev/index.js")).href)
187
+ : await import("../dev/index.js");
188
+ const serve = devMod.serve || devMod.default;
189
+ serverInstance = await serve(rootDir, {
190
+ port: rawPort || effectiveDev.port,
191
+ hostname: effectiveHost || effectiveDev.hostname,
192
+ open: flags.open || false,
193
+ silent: true,
194
+ });
195
+ devServer = serverInstance;
196
+ break;
197
+ }
198
+ case "serve": {
199
+ const local = await getLocalModules(rootDir);
200
+ const serverMod = local
201
+ ? await import(pathToFileURL(path.join(local.rootDir, "server/index.js")).href)
202
+ : await import("../server/index.js");
203
+ const serve = serverMod.serve || serverMod.default?.serve;
204
+ serverInstance = await serve(rootDir, {
205
+ port: rawPort || effectiveServer.port,
206
+ hostname: effectiveHost || effectiveServer.hostname,
207
+ middleware: flags.middleware || null,
208
+ silent: true,
209
+ });
210
+ }
211
+ }
212
+ } catch (err) {
213
+ if (err.code === "ENOENT" && configPath) {
214
+ console.error(c.red(`Config file not found: ${path.resolve(rootDir, configPath)}`));
215
+ process.exit(1);
216
+ }
217
+ console.error(c.red(err.message || err));
218
+ process.exit(1);
219
+ }
220
+ }
221
+
222
+ if (process.argv[1] === __filename) {
223
+ main().catch(err => {
224
+ console.error(err.message || err);
225
+ process.exit(1);
226
+ });
227
+ }
@@ -0,0 +1,34 @@
1
+ const ansi = {
2
+ bold: "\x1b[1m",
3
+ underline: "\x1b[4m",
4
+ red: "\x1b[31m",
5
+ green: "\x1b[32m",
6
+ yellow: "\x1b[33m",
7
+ blue: "\x1b[36m",
8
+ dim: "\x1b[90m"
9
+ };
10
+
11
+ const reset = "\x1b[0m";
12
+
13
+ function hexToAnsi(h) {
14
+ const r = parseInt(h.slice(1, 3), 16);
15
+ const g = parseInt(h.slice(3, 5), 16);
16
+ const b = parseInt(h.slice(5, 7), 16);
17
+ return `\x1b[38;2;${r};${g};${b}m`;
18
+ }
19
+
20
+ function makeChalk(stack) {
21
+ const fn = (s) => {
22
+ if (stack.length === 0) return s;
23
+ return stack.map(n => ansi[n] || hexToAnsi(n)).join("") + s + reset;
24
+ };
25
+ return new Proxy(fn, {
26
+ get(_, prop) {
27
+ if (prop === "hex") return (h) => makeChalk([...stack, h]);
28
+ return makeChalk([...stack, prop]);
29
+ },
30
+ });
31
+ }
32
+
33
+ const chalk = makeChalk([]);
34
+ export default chalk;