chocola 2.0.0-next.6 → 2.0.0-next.7

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 CHANGED
@@ -1,13 +1,12 @@
1
1
  # chocola
2
2
 
3
- Chocola is a JavaScript library for creating web user interfaces.
3
+ **The sweetest way to build the web.**
4
4
 
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 until official workflows are provided.
5
+ No bundler config. No virtual DOM. No hydration ceremony. Just `.html` files with `<template>`, `<script>`, and `<style>` — compiled to static HTML with scoped CSS, and optional runtime when you need it.
6
6
 
7
- ## Usage
7
+ Import components. Instantiate them. Mount, update, remove. All client-side. Same file, zero overhead.
8
8
 
9
9
  ```html
10
- <!-- Greeting.html -->
11
10
  <script>
12
11
  let self = new HTMLDivElement;
13
12
  let input = new HTMLInputElement;
@@ -33,4 +32,4 @@ Chocola is a JavaScript library for creating web user interfaces.
33
32
 
34
33
  ## Documentation
35
34
 
36
- See https://github.com/chocolajs/chocola/wiki/Overview
35
+ https://github.com/chocolajs/chocola/wiki/Overview
package/compiler/chalk.js CHANGED
@@ -1,32 +1,32 @@
1
- const ansi = {
2
- bold: "\x1b[1m",
3
- underline: "\x1b[4m",
4
- red: "\x1b[31m",
5
- green: "\x1b[32m",
6
- yellow: "\x1b[33m",
7
- };
8
-
9
- const reset = "\x1b[0m";
10
-
11
- function hexToAnsi(h) {
12
- const r = parseInt(h.slice(1, 3), 16);
13
- const g = parseInt(h.slice(3, 5), 16);
14
- const b = parseInt(h.slice(5, 7), 16);
15
- return `\x1b[38;2;${r};${g};${b}m`;
16
- }
17
-
18
- function makeChalk(stack) {
19
- const fn = (s) => {
20
- if (stack.length === 0) return s;
21
- return stack.map(n => ansi[n] || hexToAnsi(n)).join("") + s + reset;
22
- };
23
- return new Proxy(fn, {
24
- get(_, prop) {
25
- if (prop === "hex") return (h) => makeChalk([...stack, h]);
26
- return makeChalk([...stack, prop]);
27
- },
28
- });
29
- }
30
-
31
- const chalk = makeChalk([]);
32
- export default chalk;
1
+ const ansi = {
2
+ bold: "\x1b[1m",
3
+ underline: "\x1b[4m",
4
+ red: "\x1b[31m",
5
+ green: "\x1b[32m",
6
+ yellow: "\x1b[33m",
7
+ };
8
+
9
+ const reset = "\x1b[0m";
10
+
11
+ function hexToAnsi(h) {
12
+ const r = parseInt(h.slice(1, 3), 16);
13
+ const g = parseInt(h.slice(3, 5), 16);
14
+ const b = parseInt(h.slice(5, 7), 16);
15
+ return `\x1b[38;2;${r};${g};${b}m`;
16
+ }
17
+
18
+ function makeChalk(stack) {
19
+ const fn = (s) => {
20
+ if (stack.length === 0) return s;
21
+ return stack.map(n => ansi[n] || hexToAnsi(n)).join("") + s + reset;
22
+ };
23
+ return new Proxy(fn, {
24
+ get(_, prop) {
25
+ if (prop === "hex") return (h) => makeChalk([...stack, h]);
26
+ return makeChalk([...stack, prop]);
27
+ },
28
+ });
29
+ }
30
+
31
+ const chalk = makeChalk([]);
32
+ export default chalk;
@@ -1,3 +1,4 @@
1
+ import path from "path";
1
2
  import { JSDOM } from "jsdom";
2
3
  import { protectCurlyBraces } from "../utils.js";
3
4
  import { genRandomId, incrementAlfabet, throwError, deterministicHash } from "./utils.js";
@@ -12,7 +13,7 @@ import chalk from "./chalk.js";
12
13
 
13
14
 
14
15
  class ProcessContext {
15
- constructor(loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry) {
16
+ constructor(loadedComponents, runtimeChunks, compIdColl, letterState, runtimeMap, cssScopes, cssScopesMap, scopedStyles, staticCtxRegistry, csrClasses) {
16
17
  this.loadedComponents = loadedComponents;
17
18
  this.runtimeChunks = runtimeChunks;
18
19
  this.compIdColl = compIdColl;
@@ -22,9 +23,101 @@ class ProcessContext {
22
23
  this.cssScopesMap = cssScopesMap;
23
24
  this.scopedStyles = scopedStyles;
24
25
  this.staticCtxRegistry = staticCtxRegistry;
26
+ this.csrClasses = csrClasses;
25
27
  }
26
28
  }
27
29
 
30
+ function escapeForTemplateLiteral(str) {
31
+ return str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
32
+ }
33
+
34
+ function generateCSRClass(compName, cx, explicitClassName) {
35
+ if (cx.csrClasses.has(compName)) return;
36
+
37
+ let instance = cx.loadedComponents.get(compName);
38
+ if (!instance) return;
39
+
40
+ instance = protectCurlyBraces(instance);
41
+ const dom = new JSDOM(instance);
42
+ const doc = dom.window.document;
43
+ const script = doc.querySelector("script")?.innerHTML;
44
+ const template = doc.querySelector("template")?.innerHTML;
45
+ const styles = doc.querySelector("style")?.innerHTML;
46
+
47
+ if (!template) return;
48
+
49
+ const compProps = extractPropsDefaults(script);
50
+ const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
51
+ let runtime = extractRuntime(script || "", compName);
52
+
53
+ if (!cx.cssScopesMap.has(compName)) {
54
+ cx.cssScopesMap.set(compName, deterministicHash(compName, 8));
55
+ }
56
+ const cssId = cx.cssScopesMap.get(compName);
57
+
58
+ if (styles) {
59
+ cx.scopedStyles.push(scopeCss(styles, cssId));
60
+ }
61
+
62
+ const childMappings = [];
63
+ const bindVarNames = new Set();
64
+ const templateDoc = JSDOM.fragment(template);
65
+ const seenTags = new Set();
66
+ for (const el of templateDoc.querySelectorAll("*")) {
67
+ const tag = el.tagName.toLowerCase();
68
+ if (!seenTags.has(tag)) {
69
+ seenTags.add(tag);
70
+ const childCompName = tag + ".html";
71
+ if (cx.loadedComponents.has(childCompName)) {
72
+ generateCSRClass(childCompName, cx);
73
+ const childClassName = childCompName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
74
+ childMappings.push({ tag, compClass: childClassName });
75
+ }
76
+ }
77
+ for (const attr of el.attributes) {
78
+ if (attr.name.startsWith("bind:")) {
79
+ bindVarNames.add(attr.value);
80
+ }
81
+ }
82
+ }
83
+
84
+ let csrRuntimeSource = null;
85
+ if (runtime) {
86
+ let injectCode = "";
87
+ for (const { name, defaultValue } of compProps) {
88
+ if (defaultValue !== undefined) {
89
+ injectCode += `let ${name} = ctx.${name}||${defaultValue};\n`;
90
+ } else {
91
+ injectCode += `let ${name} = ctx.${name};\n`;
92
+ }
93
+ }
94
+ for (const varName of bindVarNames) {
95
+ if (varName !== "self") {
96
+ injectCode += `let ${varName} = ctx.${varName};\n`;
97
+ }
98
+ }
99
+ if (topFuncSrc.length > 0) {
100
+ injectCode += "\n" + topFuncSrc.join("\n\n") + "\n";
101
+ }
102
+ runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
103
+ runtime = runtime.replace(`${RUNTIME_KW}()`, `function(self, ctx)`);
104
+ csrRuntimeSource = runtime.replace(/^(async\s+)?function\s+\w+/, "$1function");
105
+ }
106
+
107
+ const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
108
+ const propsObj = {};
109
+ for (const { name, defaultValue } of compProps) {
110
+ propsObj[name] = defaultValue !== undefined ? defaultValue : null;
111
+ }
112
+
113
+ const childrenPart = childMappings.length > 0
114
+ ? ",\n children: [" + childMappings.map(m => `{tag:"${m.tag}",compClass:${m.compClass}}`).join(",") + "]"
115
+ : "";
116
+ const runtimePart = csrRuntimeSource ? `,\n runtime: ${csrRuntimeSource}` : "";
117
+ const classDef = `class ${className} extends ChocolaComponent {\n constructor() {\n super({\n template: \`${escapeForTemplateLiteral(template)}\`,\n hash: "${cssId}",\n props: ${JSON.stringify(propsObj)}${runtimePart}${childrenPart}\n });\n }\n}`;
118
+ cx.csrClasses.set(compName, classDef);
119
+ }
120
+
28
121
 
29
122
 
30
123
  export function processComponentElement(
@@ -53,6 +146,17 @@ export function processComponentElement(
53
146
  return false;
54
147
  }
55
148
 
149
+ if (script) {
150
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
151
+ script = script.replace(importRegex, (_, importedName, importPath) => {
152
+ const importedCompName = path.basename(importPath).toLowerCase();
153
+ if (cx.loadedComponents.has(importedCompName)) {
154
+ generateCSRClass(importedCompName, cx, importedName);
155
+ }
156
+ return "";
157
+ });
158
+ }
159
+
56
160
  const compProps = extractPropsDefaults(script);
57
161
 
58
162
  let ctx;
@@ -213,6 +317,7 @@ export function processComponentElement(
213
317
  interpolateNode(child, ctxProxy)
214
318
  });
215
319
 
320
+ let csrRuntimeSource = null;
216
321
  const firstChild = fragment.children[0];
217
322
 
218
323
  if (firstChild && firstChild.nodeType === 1) {
@@ -272,6 +377,7 @@ export function processComponentElement(
272
377
  runtime = runtime.replace(/\$runtime\([^)]*\)\s*\{/, match => match + "\n" + injectCode);
273
378
 
274
379
  runtime = runtime.replace(`${RUNTIME_KW}()`, `${letter}r(self, ctx)`);
380
+ csrRuntimeSource = runtime.replace(/^function\s+\w+/, "function");
275
381
  cx.runtimeChunks.push(runtime);
276
382
  cx.runtimeMap && cx.runtimeMap.set(compName, { letter });
277
383
  } else {
@@ -296,15 +402,34 @@ export function processComponentElement(
296
402
  cx.scopedStyles.push(styles);
297
403
  }
298
404
 
405
+ if (csrRuntimeSource && !cx.csrClasses.has(compName)) {
406
+ generateCSRClass(compName, cx);
407
+ }
408
+
299
409
  element.replaceWith(fragment);
300
410
  return true;
301
411
  }
302
412
 
303
413
  export function processAllComponents(appElements, loadedComponents, pageSourceFile, pageSourceContent) {
304
414
  const cx = new ProcessContext(
305
- loadedComponents, [], [], { value: null }, new Map(), [], new Map(), [], new Map()
415
+ loadedComponents, [], [], { value: null }, new Map(), [], new Map(), [], new Map(), new Map()
306
416
  );
307
417
 
418
+ for (const [compName, instance] of cx.loadedComponents) {
419
+ const script = instance.match(/<script>([\s\S]*?)<\/script>/i);
420
+ if (script) {
421
+ const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]\s*;?\s*/g;
422
+ let match;
423
+ while ((match = importRegex.exec(script[1])) !== null) {
424
+ const importedName = match[1];
425
+ const importedCompName = path.basename(match[2]).toLowerCase();
426
+ if (cx.loadedComponents.has(importedCompName)) {
427
+ generateCSRClass(importedCompName, cx, importedName);
428
+ }
429
+ }
430
+ }
431
+ }
432
+
308
433
  appElements.forEach(el => {
309
434
  if (!el.isConnected) return;
310
435
  processComponentElement(el, cx, [], pageSourceFile, pageSourceContent);
@@ -312,13 +437,14 @@ export function processAllComponents(appElements, loadedComponents, pageSourceFi
312
437
  const runtimeScript = cx.runtimeChunks.join("\n");
313
438
  const hasComponents = cx.runtimeChunks.length > 0;
314
439
  const scopesCss = cx.scopedStyles.join("\n");
440
+ const csrClasses = [...cx.csrClasses.values()].join("\n\n");
315
441
 
316
442
  const hashMap = {};
317
443
  for (const [compName, hash] of cx.cssScopesMap) {
318
444
  hashMap[compName] = hash;
319
445
  }
320
446
 
321
- return { runtimeScript, hasComponents, scopesCss, hashMap };
447
+ return { runtimeScript, hasComponents, scopesCss, hashMap, csrClasses };
322
448
  }
323
449
 
324
450
  function getNextLetter(letterState) {
@@ -54,7 +54,6 @@ export function getScriptElements(doc) {
54
54
 
55
55
  export function appendRuntimeScript(doc, filename) {
56
56
  const runtimeScriptEl = doc.createElement("script");
57
- runtimeScriptEl.type = "module";
58
57
  runtimeScriptEl.src = "./" + filename;
59
58
  doc.body.appendChild(runtimeScriptEl);
60
59
  }
package/compiler/index.js CHANGED
@@ -209,8 +209,9 @@ export default async function compile(rootDir, buildConfig) {
209
209
  processPageConditionals(appContainer);
210
210
 
211
211
  const appElements = getAppElements(appContainer);
212
- const { runtimeScript, scopesCss, hashMap } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
213
- const runtimeFilename = await generateRuntimeScript(runtimeScript, paths.outDir);
212
+ const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
213
+ const csrSource = await fs.readFile(new URL("../runtime/index.js", import.meta.url), "utf-8");
214
+ const runtimeFilenames = await generateRuntimeScript(runtimeScript, paths.outDir, csrSource, csrClasses);
214
215
  await processAssets(doc, rootDir, config.srcDir, paths.outDir);
215
216
 
216
217
  if (scopesCss) {
@@ -219,7 +220,9 @@ export default async function compile(rootDir, buildConfig) {
219
220
  appendStylesheetLink(doc, fileName);
220
221
  };
221
222
 
222
- appendRuntimeScript(doc, runtimeFilename);
223
+ for (const name of runtimeFilenames) {
224
+ appendRuntimeScript(doc, name);
225
+ }
223
226
  const html = await serializeDOM(dom);
224
227
  await writeHTMLOutput(html, paths.outDir);
225
228
 
@@ -2,12 +2,29 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { genRandomId } from "./utils.js";
4
4
 
5
- export async function generateRuntimeScript(runtimeScript, outDirPath) {
5
+ export async function generateRuntimeScript(runtimeScript, outDirPath, csrSource, csrClasses) {
6
6
  const fileIds = [];
7
- const runtimeFilename = "run-" + genRandomId(fileIds, 6) + ".js";
8
- const runtimeFileContents = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
7
+ const filenames = [];
9
8
 
10
- await fs.writeFile(path.join(outDirPath, runtimeFilename), runtimeFileContents);
9
+ if (csrSource) {
10
+ const name = "run-" + genRandomId(fileIds, 6) + ".js";
11
+ const source = csrSource.replace(/^export\s+\{?\s*[^}]+\s*}?\s*;?\s*$/m, "");
12
+ await fs.writeFile(path.join(outDirPath, name), source);
13
+ filenames.push(name);
14
+ }
11
15
 
12
- return runtimeFilename;
16
+ if (csrClasses) {
17
+ const name = "run-" + genRandomId(fileIds, 6) + ".js";
18
+ await fs.writeFile(path.join(outDirPath, name), csrClasses);
19
+ filenames.push(name);
20
+ }
21
+
22
+ if (runtimeScript) {
23
+ const name = "run-" + genRandomId(fileIds, 6) + ".js";
24
+ const content = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
25
+ await fs.writeFile(path.join(outDirPath, name), content);
26
+ filenames.push(name);
27
+ }
28
+
29
+ return filenames;
13
30
  }
package/compiler/utils.js CHANGED
@@ -1,52 +1,52 @@
1
- import { createHash } from "crypto";
2
- import chalk from "./chalk.js";
3
-
4
- export function throwError(err) {
5
- console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
6
- throw new Error(err);
7
- }
8
-
9
- export function genRandomId(collection = null, length = 10, lettersOnly = false) {
10
- let id;
11
- if (lettersOnly) {
12
- id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
13
- } else {
14
- id = Math.random().toString(36).substring(2, length + 2);
15
- }
16
- if (!collection) return id;
17
- if (collection.includes(id)) {
18
- return genRandomId(collection, length, lettersOnly);
19
- } else {
20
- collection.push(id);
21
- return id;
22
- }
23
- }
24
-
25
- export function incrementAlfabet(letters) {
26
- let arr = letters.split("");
27
- let i = arr.length - 1;
28
-
29
- while (i >= 0) {
30
- let pos = arr[i].charCodeAt(0) - 97;
31
- if (pos < 25) {
32
- arr[i] = String.fromCharCode(pos + 97 + 1);
33
- return arr.join("");
34
- } else {
35
- arr[i] = "a";
36
- i--;
37
- }
38
- }
39
-
40
- return "a" + arr.join("");
41
- }
42
-
43
- export function deterministicHash(content, length = 8) {
44
- const hash = createHash("sha256").update(content).digest();
45
- let result = "";
46
- for (let i = 0; i < length; i++) {
47
- result += String.fromCharCode(97 + (hash[i % hash.length] % 26));
48
- }
49
- return result;
50
- }
51
-
52
- const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
1
+ import { createHash } from "crypto";
2
+ import chalk from "./chalk.js";
3
+
4
+ export function throwError(err) {
5
+ console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
6
+ throw new Error(err);
7
+ }
8
+
9
+ export function genRandomId(collection = null, length = 10, lettersOnly = false) {
10
+ let id;
11
+ if (lettersOnly) {
12
+ id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
13
+ } else {
14
+ id = Math.random().toString(36).substring(2, length + 2);
15
+ }
16
+ if (!collection) return id;
17
+ if (collection.includes(id)) {
18
+ return genRandomId(collection, length, lettersOnly);
19
+ } else {
20
+ collection.push(id);
21
+ return id;
22
+ }
23
+ }
24
+
25
+ export function incrementAlfabet(letters) {
26
+ let arr = letters.split("");
27
+ let i = arr.length - 1;
28
+
29
+ while (i >= 0) {
30
+ let pos = arr[i].charCodeAt(0) - 97;
31
+ if (pos < 25) {
32
+ arr[i] = String.fromCharCode(pos + 97 + 1);
33
+ return arr.join("");
34
+ } else {
35
+ arr[i] = "a";
36
+ i--;
37
+ }
38
+ }
39
+
40
+ return "a" + arr.join("");
41
+ }
42
+
43
+ export function deterministicHash(content, length = 8) {
44
+ const hash = createHash("sha256").update(content).digest();
45
+ let result = "";
46
+ for (let i = 0; i < length; i++) {
47
+ result += String.fromCharCode(97 + (hash[i % hash.length] % 26));
48
+ }
49
+ return result;
50
+ }
51
+
52
+ const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocola",
3
- "version": "2.0.0-next.6",
3
+ "version": "2.0.0-next.7",
4
4
  "description": "The sweetest way to build the web",
5
5
  "keywords": [
6
6
  "web",