chocola 2.0.0-next.8 → 2.0.0-next.9

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,4 +1,7 @@
1
- ![Chochola • The sweetest way to build the web](https://chocolaframework.web.app/static/img/banner.jpg)
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
6
  ## What is Chocola
4
7
 
@@ -1,12 +1,12 @@
1
1
  import path from "path";
2
2
  import { parseHTML } from "linkedom";
3
3
  import { protectCurlyBraces } from "../utils.js";
4
- import { genRandomId, incrementAlfabet, throwError, deterministicHash } from "./utils.js";
4
+ import { genRandomId, incrementAlfabet, throwError, deterministicHash, warnConstantCondition, warnUnusedDeclaration, findElementLine } from "./utils.js";
5
5
  import {
6
- extractPropsDefaults, extractRuntime, extractTopLevelFunctions,
7
- extractContextFromElement, hasDelIfAttr, getDelIfAttr, removeDelIfAttr,
6
+ extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables,
7
+ extractCtxFromEl, hasMountIf, getMountIf,
8
8
  reservedAttrs, validateChainStructure, applyConditionalToElement, interpolateNode,
9
- scopeCss, compileExpression,
9
+ scopeCss, compileExpr, evaluateConstant,
10
10
  } from "../parser/index.js";
11
11
  import chalk from "./chalk.js";
12
12
 
@@ -24,6 +24,7 @@ class ProcessContext {
24
24
  this.scopedStyles = scopedStyles;
25
25
  this.staticCtxRegistry = staticCtxRegistry;
26
26
  this.csrClasses = csrClasses;
27
+ this.unusedWarned = new Set();
27
28
  }
28
29
  }
29
30
 
@@ -59,6 +60,7 @@ function generateCSRClass(compName, cx, explicitClassName) {
59
60
 
60
61
  const compProps = extractPropsDefaults(script);
61
62
  const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
63
+ const topVars = extractTopLevelVariables(script || "");
62
64
  let runtime = extractRuntime(script || "", compName);
63
65
 
64
66
  if (!cx.cssScopesMap.has(compName)) {
@@ -93,15 +95,25 @@ function generateCSRClass(compName, cx, explicitClassName) {
93
95
  }
94
96
 
95
97
  let csrRuntimeSource = null;
98
+ const injectedNames = new Set(compProps.map(p => p.name));
99
+ for (const varName of bindVarNames) injectedNames.add(varName);
100
+ const topVarsToInject = topVars.filter(v => !injectedNames.has(v.name));
96
101
  if (runtime) {
97
102
  let injectCode = "";
98
103
  for (const { name, defaultValue } of compProps) {
99
104
  if (defaultValue !== undefined) {
100
- injectCode += `let ${name} = ctx.${name}??${defaultValue};\n`;
105
+ injectCode += `let ${name} = ctx.${name}??(${defaultValue});\n`;
101
106
  } else {
102
107
  injectCode += `let ${name} = ctx.${name};\n`;
103
108
  }
104
109
  }
110
+ for (const { keyword, name, value } of topVarsToInject) {
111
+ if (value !== undefined) {
112
+ injectCode += `${keyword} ${name} = ctx.${name}??(${value});\n`;
113
+ } else {
114
+ injectCode += `${keyword} ${name};\n`;
115
+ }
116
+ }
105
117
  for (const varName of bindVarNames) {
106
118
  if (varName !== "self") {
107
119
  injectCode += `let ${varName} = ctx.${varName};\n`;
@@ -116,20 +128,110 @@ function generateCSRClass(compName, cx, explicitClassName) {
116
128
  }
117
129
 
118
130
  const className = explicitClassName || compName.replace(".html", "").replace(/^\w/, c => c.toUpperCase());
119
- const propsObj = {};
131
+ const propsParts = [];
120
132
  for (const { name, defaultValue } of compProps) {
121
- propsObj[name] = defaultValue !== undefined ? defaultValue : null;
133
+ propsParts.push(`${JSON.stringify(name)}: ${defaultValue !== undefined ? defaultValue : "null"}`);
134
+ }
135
+ for (const { name, value } of topVarsToInject) {
136
+ if (value !== undefined) {
137
+ propsParts.push(`${JSON.stringify(name)}: ${value}`);
138
+ }
122
139
  }
123
140
 
124
141
  const childrenPart = childMappings.length > 0
125
142
  ? ",\n children: [" + childMappings.map(m => `{tag:"${m.tag}",compClass:${m.compClass}}`).join(",") + "]"
126
143
  : "";
127
144
  const runtimePart = csrRuntimeSource ? `,\n runtime: ${csrRuntimeSource}` : "";
128
- 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}`;
145
+ const propsPart = propsParts.length > 0 ? `{ ${propsParts.join(", ")} }` : "{}";
146
+ const classDef = `class ${className} extends ChocolaComponent {\n constructor() {\n super({\n template: \`${escapeForTemplateLiteral(template)}\`,\n hash: "${cssId}",\n props: ${propsPart}${runtimePart}${childrenPart}\n });\n }\n}`;
129
147
  cx.csrClasses.set(compName, classDef);
130
148
  }
131
149
 
150
+ const BARE_IDENTIFIER_RE = /(?<![.\w$])[a-zA-Z_$][0-9a-zA-Z_$]*/g;
151
+ const PROPERTY_IDENTIFIER_RE = /\.([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
132
152
 
153
+ function countIdentifiers(text, counts) {
154
+ for (const re of [BARE_IDENTIFIER_RE, PROPERTY_IDENTIFIER_RE]) {
155
+ re.lastIndex = 0;
156
+ for (const m of text.matchAll(re)) counts.set(m[1] ?? m[0], (counts.get(m[1] ?? m[0]) || 0) + 1);
157
+ }
158
+ return counts;
159
+ }
160
+
161
+ function escapeNameForRegex(name) {
162
+ return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
163
+ }
164
+
165
+ function findLineInSource(source, regex) {
166
+ const match = regex.exec(source);
167
+ if (!match) return null;
168
+ return source.substring(0, match.index).split("\n").length;
169
+ }
170
+
171
+ function warnUnusedDeclarations(cx, compName, instance, script, fragment) {
172
+ if (!script && !fragment.querySelector("[bind\\:]")) return;
173
+ if (cx.unusedWarned.has(compName)) return;
174
+ cx.unusedWarned.add(compName);
175
+
176
+ const props = extractPropsDefaults(script).map(p => p.name);
177
+ const topVars = extractTopLevelVariables(script).map(v => v.name);
178
+ const topFuncs = extractTopLevelFunctions(script, RUNTIME_KW)
179
+ .map(src => src.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/)?.[1])
180
+ .filter(Boolean);
181
+
182
+ const bindings = [];
183
+ for (const el of fragment.querySelectorAll("*")) {
184
+ for (const attr of el.attributes) {
185
+ if (attr.name.startsWith("bind:")) bindings.push({ name: attr.value, element: el });
186
+ }
187
+ }
188
+
189
+ const counts = new Map();
190
+ if (script) countIdentifiers(script, counts);
191
+
192
+ const walk = (node) => {
193
+ if (node.nodeType === 3) {
194
+ for (const m of node.textContent.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
195
+ return;
196
+ }
197
+ if (node.attributes) {
198
+ for (const attr of node.attributes) {
199
+ if (attr.name.startsWith("bind:")) {
200
+ countIdentifiers(attr.value, counts);
201
+ } else {
202
+ for (const m of attr.value.matchAll(/\{([^}]+)\}/g)) countIdentifiers(m[1], counts);
203
+ }
204
+ }
205
+ }
206
+ for (const child of node.childNodes) walk(child);
207
+ };
208
+ walk(fragment);
209
+
210
+ const isUnused = (name) => (counts.get(name) || 0) <= 1;
211
+ const warn = (kind, name, declRegex) => {
212
+ const lineNum = findLineInSource(instance, declRegex);
213
+ warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, kind, name);
214
+ };
215
+
216
+ for (const name of props) {
217
+ if (isUnused(name)) warn("prop", name, new RegExp("export\\s+let\\s+" + escapeNameForRegex(name) + "\\b"));
218
+ }
219
+ for (const name of topVars) {
220
+ if (isUnused(name)) warn("variable", name, new RegExp("(?:^|[^\\w])(?:let|const)\\s+" + escapeNameForRegex(name) + "\\b"));
221
+ }
222
+ for (const name of topFuncs) {
223
+ if (isUnused(name)) warn("function", name, new RegExp("(?:async\\s+)?function\\s+" + escapeNameForRegex(name) + "\\b"));
224
+ }
225
+ const seenBindings = new Set();
226
+ for (const { name, element } of bindings) {
227
+ if (seenBindings.has(name)) continue;
228
+ seenBindings.add(name);
229
+ if (isUnused(name)) {
230
+ const lineNum = findElementLine(instance, element.outerHTML);
231
+ warnUnusedDeclaration(lineNum !== null ? `${compName}:${lineNum}` : compName, "binding", name);
232
+ }
233
+ }
234
+ }
133
235
 
134
236
  export function processComponentElement(
135
237
  element,
@@ -174,7 +276,7 @@ export function processComponentElement(
174
276
  if (cx.staticCtxRegistry && cx.staticCtxRegistry.has(element)) {
175
277
  ctx = cx.staticCtxRegistry.get(element);
176
278
  } else {
177
- ctx = extractContextFromElement(element);
279
+ ctx = extractCtxFromEl(element);
178
280
  cx.staticCtxRegistry && cx.staticCtxRegistry.set(element, ctx);
179
281
  }
180
282
 
@@ -182,7 +284,7 @@ export function processComponentElement(
182
284
  compProps.forEach(({ name, defaultValue }) => {
183
285
  if (defaultValue !== undefined && !(name in ctx)) {
184
286
  try {
185
- ctx[name] = compileExpression(defaultValue, false)();
287
+ ctx[name] = compileExpr(defaultValue, false)();
186
288
  } catch {
187
289
  ctx[name] = defaultValue;
188
290
  }
@@ -191,6 +293,15 @@ export function processComponentElement(
191
293
  }
192
294
 
193
295
  const topFuncSrc = extractTopLevelFunctions(script || "", RUNTIME_KW);
296
+ const topVars = extractTopLevelVariables(script || "");
297
+ for (const { name, value } of topVars) {
298
+ if (name in ctx) continue;
299
+ if (value !== undefined) {
300
+ try {
301
+ ctx[name] = compileExpr(value, false)();
302
+ } catch {}
303
+ }
304
+ }
194
305
  for (const src of topFuncSrc) {
195
306
  try {
196
307
  const fn = (0, eval)("(" + src + ")");
@@ -208,6 +319,8 @@ export function processComponentElement(
208
319
 
209
320
  const fragment = parseFragment(template, doc);
210
321
 
322
+ warnUnusedDeclarations(cx, compName, instance, script, fragment);
323
+
211
324
  const slotFragment = parseFragment(elInnerHtml, doc);
212
325
  if (sourceFile) {
213
326
  validateChainStructure(slotFragment, sourceFile, sourceContent, elInnerHtml);
@@ -226,6 +339,21 @@ export function processComponentElement(
226
339
  const elBindIds = new Map();
227
340
  let bindCounter = 0;
228
341
 
342
+ const conditionalLocations = new Map();
343
+ for (const { el } of childEntries) {
344
+ if (!el.hasAttribute("if") && !hasMountIf(el) && !el.hasAttribute("elif")) continue;
345
+ let location = sourceFile;
346
+ if (instance) {
347
+ const lineNum = findElementLine(instance, el.outerHTML);
348
+ if (lineNum !== null) location = `${compName}:${lineNum}`;
349
+ }
350
+ if (location === sourceFile && sourceContent) {
351
+ const lineNum = findElementLine(sourceContent, el.outerHTML);
352
+ if (lineNum !== null) location = `${sourceFile}:${lineNum}`;
353
+ }
354
+ conditionalLocations.set(el, location);
355
+ }
356
+
229
357
  childEntries.forEach(({ el: child, parent }) => {
230
358
  if (!condChains.has(parent)) {
231
359
  condChains.set(parent, { active: false, rendered: false });
@@ -233,13 +361,26 @@ export function processComponentElement(
233
361
  const condChain = condChains.get(parent);
234
362
 
235
363
  const hasIf = child.hasAttribute("if");
236
- const hasDelIf = hasDelIfAttr(child);
364
+ const hasDelIf = hasMountIf(child);
237
365
  const hasElif = child.hasAttribute("elif");
238
366
  const hasElse = child.hasAttribute("else");
239
367
 
368
+ if (hasIf || hasDelIf || hasElif) {
369
+ const location = conditionalLocations.get(child) || sourceFile;
370
+ const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
371
+ const tag = child.tagName.toLowerCase();
372
+ const warnIfConstant = (expr, attr) => {
373
+ const { constant, value } = evaluateConstant(expr);
374
+ if (constant) warnConstantCondition(location, tag, attr, Boolean(value));
375
+ };
376
+ if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), "if");
377
+ if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), "mount:if");
378
+ if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), "elif");
379
+ }
380
+
240
381
  if (hasElif || hasElse) {
241
382
  if (!condChain.active) {
242
- throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/del-if sibling`);
383
+ throwError(`${instance.__sourceFile || compName}: <${child.tagName.toLowerCase()}> has ${hasElif ? "elif" : "else"} without a preceding if/mount:if sibling`);
243
384
  }
244
385
  if (condChain.rendered) {
245
386
  child.remove();
@@ -254,7 +395,7 @@ export function processComponentElement(
254
395
  if (hasElif || hasElse) {
255
396
  if (hasElif) {
256
397
  const expr = child.getAttribute("elif").slice(1, -1);
257
- const fn = compileExpression(expr, true);
398
+ const fn = compileExpr(expr, true);
258
399
  if (!fn(ctxProxy)) {
259
400
  child.remove();
260
401
  return;
@@ -266,9 +407,9 @@ export function processComponentElement(
266
407
  condChain.active = false;
267
408
  }
268
409
  } else if (hasIf || hasDelIf) {
269
- const raw = hasIf ? child.getAttribute("if") : getDelIfAttr(child);
410
+ const raw = hasIf ? child.getAttribute("if") : getMountIf(child);
270
411
  const expr = raw.slice(1, -1);
271
- const fn = compileExpression(expr, true);
412
+ const fn = compileExpr(expr, true);
272
413
  condChain.active = true;
273
414
  if (fn(ctxProxy)) {
274
415
  child.replaceWith(...child.children);
@@ -309,7 +450,7 @@ export function processComponentElement(
309
450
  /\{([^}]+)\}/g,
310
451
  (_, expr) => {
311
452
  try {
312
- return compileExpression(expr, true)(ctxProxy);
453
+ return compileExpr(expr, true)(ctxProxy);
313
454
  } catch {
314
455
  return "";
315
456
  }
@@ -320,7 +461,7 @@ export function processComponentElement(
320
461
 
321
462
  const condAttrs = {};
322
463
  if (hasIf) condAttrs["if"] = child.getAttribute("if");
323
- if (hasDelIf) condAttrs["mount:if"] = getDelIfAttr(child);
464
+ if (hasDelIf) condAttrs["mount:if"] = getMountIf(child);
324
465
  if (hasElif) condAttrs["elif"] = child.getAttribute("elif");
325
466
  if (hasElse) condAttrs["else"] = "";
326
467
 
@@ -373,7 +514,7 @@ export function processComponentElement(
373
514
  for (const { name, defaultValue } of compProps) {
374
515
  if (declared.has(name)) continue;
375
516
  if (defaultValue !== undefined) {
376
- ctxDefParts.push(`let ${name} = ctx.${name}??${defaultValue};\n`);
517
+ ctxDefParts.push(`let ${name} = ctx.${name}??(${defaultValue});\n`);
377
518
  } else {
378
519
  ctxDefParts.push(`let ${name} = ctx.${name};\n`);
379
520
  }
@@ -393,6 +534,14 @@ export function processComponentElement(
393
534
  const topFuncs = topFuncSrc;
394
535
 
395
536
  let injectCode = ctxDef;
537
+ for (const b of bindings) declared.add(b.varName);
538
+ const undeclaredTopVars = topVars.filter(v => !declared.has(v.name));
539
+ if (undeclaredTopVars.length > 0) {
540
+ injectCode += "\n" + undeclaredTopVars.map(v => v.value !== undefined
541
+ ? `${v.keyword} ${v.name} = ctx.${v.name}??(${v.value});`
542
+ : `${v.keyword} ${v.name};`
543
+ ).join("\n") + "\n";
544
+ }
396
545
  if (bindings.length > 0) {
397
546
  injectCode += "\n" + bindings.map(b => {
398
547
  const accessor = b.prop === "self" ? "" : "." + b.prop;
@@ -1,8 +1,8 @@
1
1
  import path from "path";
2
- import { getChocolaConfig } from "../utils.js";
2
+ import { getConfig } from "../utils.js";
3
3
 
4
4
  export async function loadConfig(rootDir) {
5
- const config = await getChocolaConfig(rootDir);
5
+ const config = await getConfig(rootDir);
6
6
  const bundleConfig = config.bundle || {};
7
7
 
8
8
  const srcDir = bundleConfig.srcDir || "src";
@@ -2,12 +2,14 @@ import { parseHTML } from "linkedom";
2
2
  import { promises as fs } from "fs";
3
3
  import path from "path";
4
4
  import { protectCurlyBraces, restoreCurlyBraces } from "../utils.js";
5
- import { extractContextFromElement } from "../parser/context.js";
6
5
  import { throwError } from "./utils.js";
7
6
  import { readMyFile } from "./fs.js";
8
7
 
9
8
  export function createDOM(srcIndexContent) {
10
- return parseHTML(protectCurlyBraces(srcIndexContent));
9
+ const protectedContent = protectCurlyBraces(srcIndexContent);
10
+ const dom = parseHTML(protectedContent);
11
+ dom.protectedContent = protectedContent;
12
+ return dom;
11
13
  }
12
14
 
13
15
  export function validateAppContainer(doc) {
package/compiler/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
+ import { performance } from "perf_hooks";
3
4
  import chalk from "./chalk.js";
4
5
  import { loadConfig, resolvePaths } from "./config.js";
5
6
  import {
@@ -16,8 +17,8 @@ import {
16
17
  } from "./dom-processor.js";
17
18
  import { processAllComponents } from "./component-processor.js";
18
19
  import { generateRuntimeScript } from "./runtime-generator.js";
19
- import { genRandomId, throwError } from "./utils.js";
20
- import { compileExpression, hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "../parser/index.js";
20
+ import { genRandomId, throwError, warnConstantCondition, findElementLine } from "./utils.js";
21
+ import { compileExpr, evaluateConstant, hasMountIf, getMountIf, removeMountIf } from "../parser/index.js";
21
22
  import {
22
23
  copyStaticDir,
23
24
  getComponents,
@@ -56,12 +57,17 @@ function logBanner() {
56
57
  );
57
58
  }
58
59
 
59
- function logSuccess(outDirPath) {
60
+ function logSuccess(outDirPath, durationMs) {
60
61
  console.log(
61
62
  chalk.bold.green(">"),
62
63
  "Project bundled succesfully at",
63
64
  chalk.green.underline(outDirPath));
64
- console.log(chalk.bold.green(`\nJOB DONE!\n`));
65
+ console.log(chalk.bold.green(`\nJOB DONE!`) + chalk.hex(TEXT_FAINT)(` (${formatDuration(durationMs)})\n`));
66
+ }
67
+
68
+ function formatDuration(ms) {
69
+ if (ms >= 1000) return (ms / 1000).toFixed(2) + "s";
70
+ return Math.round(ms) + "ms";
65
71
  }
66
72
 
67
73
  async function setupOutputDirectory(outDirPath, emptyOutDir) {
@@ -107,26 +113,49 @@ async function processAssets(doc, rootDir, srcDir, outDirPath) {
107
113
  return cssContents
108
114
  }
109
115
 
110
- function processPageConditionals(parent) {
116
+ function processPageConditionals(parent, sourceFile, sourceContent) {
111
117
  const children = [...parent.children];
112
118
  let chainActive = false;
113
119
  let chainRendered = false;
114
120
 
121
+ const getLocation = (child) => {
122
+ if (!sourceContent) return sourceFile;
123
+ const lineNum = findElementLine(sourceContent, child.outerHTML);
124
+ return lineNum !== null ? `${sourceFile}:${lineNum}` : sourceFile;
125
+ };
126
+
127
+ const warnIfConstant = (expr, child, attr) => {
128
+ const { constant, value } = evaluateConstant(expr);
129
+ if (!constant) return;
130
+ warnConstantCondition(getLocation(child), child.tagName.toLowerCase(), attr, Boolean(value));
131
+ };
132
+
115
133
  for (const child of children) {
116
134
  const hasIf = child.hasAttribute("if");
117
- const hasDelIf = hasDelIfAttr(child);
135
+ const hasDelIf = hasMountIf(child);
118
136
  const hasElif = child.hasAttribute("elif");
119
137
  const hasElse = child.hasAttribute("else");
120
138
 
139
+ if (hasIf || hasDelIf || hasElif) {
140
+ const stripBraces = (raw) => raw.startsWith("{") ? raw.slice(1, -1) : raw;
141
+ if (hasIf) warnIfConstant(stripBraces(child.getAttribute("if")), child, "if");
142
+ if (hasDelIf) warnIfConstant(stripBraces(getMountIf(child)), child, "mount:if");
143
+ if (hasElif) warnIfConstant(stripBraces(child.getAttribute("elif")), child, "elif");
144
+ }
145
+
121
146
  if (hasElif || hasElse) {
122
- if (!chainActive) continue;
147
+ if (!chainActive) {
148
+ const tag = child.tagName.toLowerCase();
149
+ const attr = hasElif ? "elif" : "else";
150
+ throwError(`${getLocation(child)}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
151
+ }
123
152
  if (chainRendered) { child.remove(); continue; }
124
153
  }
125
154
 
126
155
  if (hasIf) {
127
156
  const raw = child.getAttribute("if");
128
157
  const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
129
- const fn = compileExpression(expr, false);
158
+ const fn = compileExpr(expr, false);
130
159
  const result = fn();
131
160
  chainActive = true;
132
161
  if (result) {
@@ -137,9 +166,9 @@ function processPageConditionals(parent) {
137
166
  }
138
167
  child.removeAttribute("if");
139
168
  } else if (hasDelIf) {
140
- const raw = getDelIfAttr(child);
169
+ const raw = getMountIf(child);
141
170
  const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
142
- const fn = compileExpression(expr, false);
171
+ const fn = compileExpr(expr, false);
143
172
  const result = fn();
144
173
  chainActive = true;
145
174
  if (result) {
@@ -148,11 +177,11 @@ function processPageConditionals(parent) {
148
177
  child.remove();
149
178
  chainRendered = false;
150
179
  }
151
- removeDelIfAttr(child);
180
+ removeMountIf(child);
152
181
  } else if (hasElif) {
153
182
  const raw = child.getAttribute("elif");
154
183
  const expr = raw.startsWith("{") ? raw.slice(1, -1) : raw;
155
- const fn = compileExpression(expr, false);
184
+ const fn = compileExpr(expr, false);
156
185
  const result = fn();
157
186
  if (result) {
158
187
  chainRendered = true;
@@ -170,13 +199,14 @@ function processPageConditionals(parent) {
170
199
  }
171
200
 
172
201
  if (child.parentNode) {
173
- processPageConditionals(child);
202
+ processPageConditionals(child, sourceFile, sourceContent);
174
203
  }
175
204
  }
176
205
  }
177
206
 
178
207
  export default async function compile(rootDir, buildConfig) {
179
208
  const isHotReload = buildConfig?.isHotReload || null;
209
+ const startTime = performance.now();
180
210
  !isHotReload && logBanner();
181
211
 
182
212
  const config = await loadConfig(rootDir);
@@ -195,7 +225,7 @@ export default async function compile(rootDir, buildConfig) {
195
225
  const doc = dom.document;
196
226
  const appContainer = validateAppContainer(doc);
197
227
 
198
- processPageConditionals(appContainer);
228
+ processPageConditionals(appContainer, pageSourcePath, dom.protectedContent);
199
229
 
200
230
  const appElements = getAppElements(appContainer);
201
231
  const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
@@ -225,8 +255,10 @@ export default async function compile(rootDir, buildConfig) {
225
255
  await fs.mkdir(chocolaDir, { recursive: true });
226
256
  await fs.writeFile(path.join(chocolaDir, "hashes.json"), JSON.stringify(hashMap, null, 2) + "\n");
227
257
 
228
- !isHotReload && logSuccess(paths.outDir);
229
- isHotReload && console.log("Dev server updated");
258
+ const durationMs = performance.now() - startTime;
259
+
260
+ !isHotReload && logSuccess(paths.outDir, durationMs);
261
+ isHotReload && console.log("Dev server updated " + chalk.hex(TEXT_FAINT)(`(${formatDuration(durationMs)})`));
230
262
  }
231
263
 
232
264
  /**
package/compiler/utils.js CHANGED
@@ -6,6 +6,84 @@ export function throwError(err) {
6
6
  throw new Error(err);
7
7
  }
8
8
 
9
+ export function warnConstantCondition(location, tag, attr, truthy) {
10
+ const article = /^[aeiou]/.test(attr) ? "an" : "a";
11
+ console.warn(
12
+ chalk.bold.yellow("WARNING!"),
13
+ `${location}: <${tag}> has ${article} \`${attr}\` condition that is always ${truthy ? "truthy" : "falsy"}`
14
+ );
15
+ }
16
+
17
+ export function warnUnusedDeclaration(location, kind, name) {
18
+ console.warn(
19
+ chalk.bold.yellow("WARNING!"),
20
+ `${location}: ${kind} \`${name}\` is never used`
21
+ );
22
+ }
23
+
24
+ function normalizeAttributeQuotes(html) {
25
+ return html
26
+ .replace(/(\s[\w:.-]+)=(['"])([\s\S]*?)\2/g, '$1="$3"')
27
+ .replace(/(\s[\w:.-]+)=([^\s"'<>=]+)/g, '$1="$2"');
28
+ }
29
+
30
+ function getStartTag(outerHTML) {
31
+ let inQuote = null;
32
+ for (let i = 0; i < outerHTML.length; i++) {
33
+ const c = outerHTML[i];
34
+ if (inQuote) {
35
+ if (c === inQuote) inQuote = null;
36
+ } else if (c === '"' || c === "'") {
37
+ inQuote = c;
38
+ } else if (c === ">") {
39
+ return outerHTML.slice(0, i + 1);
40
+ }
41
+ }
42
+ return outerHTML;
43
+ }
44
+
45
+ function escapeRegex(str) {
46
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ }
48
+
49
+ function flexibleStartTagRegex(startTag) {
50
+ return new RegExp(
51
+ startTag
52
+ .split(/\s+/)
53
+ .filter(Boolean)
54
+ .map(escapeRegex)
55
+ .join("\\s+")
56
+ );
57
+ }
58
+
59
+ export function findElementLine(sourceContent, outerHTML) {
60
+ const source = normalizeAttributeQuotes(sourceContent).toLowerCase().replace(/\/\s*>/g, ">");
61
+ const startTags = [
62
+ normalizeAttributeQuotes(getStartTag(outerHTML)),
63
+ normalizeAttributeQuotes(
64
+ getStartTag(
65
+ outerHTML
66
+ .replace(/&quot;/g, '"')
67
+ .replace(/&amp;/g, "&")
68
+ .replace(/&lt;/g, "<")
69
+ .replace(/&gt;/g, ">")
70
+ )
71
+ ),
72
+ ].map(tag => tag.toLowerCase());
73
+ const candidates = [
74
+ flexibleStartTagRegex(startTags[0]),
75
+ flexibleStartTagRegex(startTags[0].replace(/=""/g, "")),
76
+ flexibleStartTagRegex(startTags[1]),
77
+ ];
78
+ for (const re of candidates) {
79
+ const match = re.exec(source);
80
+ if (match) {
81
+ return source.substring(0, match.index).split("\n").length;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
9
87
  export function genRandomId(collection = null, length = 10, lettersOnly = false) {
10
88
  let id;
11
89
  if (lettersOnly) {
package/dev/index.js CHANGED
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import chalk from "../compiler/chalk.js";
5
5
  import compile from "../compiler/index.js";
6
6
  import { loadConfig, resolvePaths } from "../compiler/config.js";
7
- import { getChocolaConfig } from "../utils.js";
7
+ import { getConfig } from "../utils.js";
8
8
 
9
9
  export async function serve(__rootdir) {
10
10
  await compile(__rootdir);
@@ -17,7 +17,7 @@ export async function serve(__rootdir) {
17
17
 
18
18
  let lastBuildTime = Date.now();
19
19
 
20
- const fullConfig = await getChocolaConfig(__rootdir);
20
+ const fullConfig = await getConfig(__rootdir);
21
21
  const config = await loadConfig(__rootdir);
22
22
  const paths = resolvePaths(__rootdir, config);
23
23
  const devConfig = fullConfig.dev;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocola",
3
- "version": "2.0.0-next.8",
3
+ "version": "2.0.0-next.9",
4
4
  "description": "The sweetest way to build the web",
5
5
  "keywords": [
6
6
  "web",
@@ -44,7 +44,146 @@ export function extractRuntime(script, compName) {
44
44
  }
45
45
  }
46
46
 
47
- throw new Error(`${compName} $runtime function has unclosed curly braces.`);
47
+ throw new Error(`${compName} $runtime function has unclosed curly braces`);
48
+ }
49
+
50
+ export function extractTopLevelVariables(script) {
51
+ if (!script) return [];
52
+ const vars = [];
53
+ let i = 0;
54
+ let depth = 0;
55
+ let parenDepth = 0;
56
+ let bracketDepth = 0;
57
+ let inStr = false;
58
+ let strChar = null;
59
+
60
+ while (i < script.length) {
61
+ const ch = script[i];
62
+
63
+ if (inStr) {
64
+ if (ch === "\\") i++;
65
+ else if (ch === strChar) inStr = false;
66
+ i++;
67
+ continue;
68
+ }
69
+
70
+ if (ch === '"' || ch === "'" || ch === "`") {
71
+ inStr = true;
72
+ strChar = ch;
73
+ i++;
74
+ continue;
75
+ }
76
+
77
+ if (ch === "/" && script[i + 1] === "/") {
78
+ while (i < script.length && script[i] !== "\n") i++;
79
+ continue;
80
+ }
81
+ if (ch === "/" && script[i + 1] === "*") {
82
+ i += 2;
83
+ while (i < script.length && !(script[i] === "*" && script[i + 1] === "/")) i++;
84
+ i += 2;
85
+ continue;
86
+ }
87
+
88
+ if (ch === "{") { depth++; i++; continue; }
89
+ if (ch === "}") { depth--; i++; continue; }
90
+ if (ch === "(") { parenDepth++; i++; continue; }
91
+ if (ch === ")") { parenDepth--; i++; continue; }
92
+ if (ch === "[") { bracketDepth++; i++; continue; }
93
+ if (ch === "]") { bracketDepth--; i++; continue; }
94
+
95
+ if (depth === 0 && parenDepth === 0 && bracketDepth === 0) {
96
+ const rest = script.slice(i);
97
+ const m = rest.match(/^(let|const)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*/);
98
+ if (m) {
99
+ const keyword = m[1];
100
+ const before = script.slice(0, i).trimEnd();
101
+ if (!before.endsWith("export")) {
102
+ let name = m[2];
103
+ let p = i + m[0].length;
104
+ while (p < script.length && /\s/.test(script[p])) p++;
105
+ i = p;
106
+
107
+ while (p < script.length) {
108
+ i = p;
109
+ let q = p;
110
+
111
+ if (script[q] === "=") {
112
+ q++;
113
+ while (q < script.length && /\s/.test(script[q])) q++;
114
+ const delim = findDeclaratorEnd(script, q);
115
+ if (name !== "self" && name !== "ctx") {
116
+ vars.push({ keyword, name, value: script.slice(q, delim).trim() });
117
+ }
118
+ if (script[delim] === ";") { i = delim + 1; break; }
119
+ p = delim + 1;
120
+ while (p < script.length && /\s/.test(script[p])) p++;
121
+ } else {
122
+ if (name !== "self" && name !== "ctx") {
123
+ vars.push({ keyword, name, value: undefined });
124
+ }
125
+ if (script[q] === ";") { i = q + 1; break; }
126
+ if (script[q] !== ",") { i = q; break; }
127
+ p = q + 1;
128
+ while (p < script.length && /\s/.test(script[p])) p++;
129
+ }
130
+
131
+ const nameMatch = script.slice(p).match(/^([a-zA-Z_$][0-9a-zA-Z_$]*)\s*/);
132
+ if (!nameMatch) { i = p; break; }
133
+ name = nameMatch[1];
134
+ p += nameMatch[0].length;
135
+ }
136
+ continue;
137
+ }
138
+ }
139
+ }
140
+ i++;
141
+ }
142
+
143
+ return vars;
144
+ }
145
+
146
+ function findDeclaratorEnd(script, start) {
147
+ let i = start;
148
+ let parens = 0;
149
+ let brackets = 0;
150
+ let braces = 0;
151
+ let inStr = false;
152
+ let strChar = null;
153
+
154
+ while (i < script.length) {
155
+ const c = script[i];
156
+
157
+ if (inStr) {
158
+ if (c === "\\") i++;
159
+ else if (c === strChar) inStr = false;
160
+ i++;
161
+ continue;
162
+ }
163
+
164
+ if (c === "/" && script[i + 1] === "/") {
165
+ while (i < script.length && script[i] !== "\n") i++;
166
+ continue;
167
+ }
168
+ if (c === "/" && script[i + 1] === "*") {
169
+ i += 2;
170
+ while (i < script.length && !(script[i] === "*" && script[i + 1] === "/")) i++;
171
+ i += 2;
172
+ continue;
173
+ }
174
+
175
+ if (c === '"' || c === "'" || c === "`") { inStr = true; strChar = c; i++; continue; }
176
+ if (c === "(") { parens++; i++; continue; }
177
+ if (c === ")") { parens--; i++; continue; }
178
+ if (c === "[") { brackets++; i++; continue; }
179
+ if (c === "]") { brackets--; i++; continue; }
180
+ if (c === "{") { braces++; i++; continue; }
181
+ if (c === "}") { braces--; i++; continue; }
182
+ if ((c === ";" || c === ",") && parens === 0 && brackets === 0 && braces === 0) return i;
183
+ i++;
184
+ }
185
+
186
+ return i;
48
187
  }
49
188
 
50
189
  export function extractTopLevelFunctions(script, excludeName) {
package/parser/context.js CHANGED
@@ -1,7 +1,7 @@
1
- import { compileExpression } from "./utils.js";
1
+ import { compileExpr } from "./utils.js";
2
2
  import { restoreTemplateChars } from "../utils.js";
3
3
 
4
- export function extractContextFromElement(element) {
4
+ export function extractCtxFromEl(element) {
5
5
  const ctx = {};
6
6
  for (const attr of element.attributes) {
7
7
  const key = attr.name;
@@ -10,7 +10,7 @@ export function extractContextFromElement(element) {
10
10
  const matches = [...val.matchAll(/\{([^}]+)\}/g)];
11
11
  if (matches.length === 1 && matches[0][0] === val) {
12
12
  try {
13
- ctx[key] = compileExpression(matches[0][1], false)();
13
+ ctx[key] = compileExpr(matches[0][1], false)();
14
14
  continue;
15
15
  } catch {}
16
16
  }
@@ -19,12 +19,12 @@ export function extractContextFromElement(element) {
19
19
  return ctx;
20
20
  }
21
21
 
22
- export function hasDelIfAttr(el) {
22
+ export function hasMountIf(el) {
23
23
  return el.hasAttribute("mount:if");
24
24
  }
25
- export function getDelIfAttr(el) {
25
+ export function getMountIf(el) {
26
26
  return el.getAttribute("mount:if");
27
27
  }
28
- export function removeDelIfAttr(el) {
28
+ export function removeMountIf(el) {
29
29
  el.removeAttribute("mount:if");
30
30
  }
package/parser/css.js CHANGED
@@ -38,7 +38,7 @@ export function scopeCss(cssString, cssId) {
38
38
  }
39
39
  }
40
40
 
41
- function findMatchingBrace(s, openIndex) {
41
+ function findClosure(s, openIndex) {
42
42
  let depth = 0;
43
43
  for (let i = openIndex; i < s.length; i++) {
44
44
  const ch = s[i];
@@ -65,7 +65,7 @@ export function scopeCss(cssString, cssId) {
65
65
  break;
66
66
  }
67
67
  const header = str.substring(i, braceIndex);
68
- const endBrace = findMatchingBrace(str, braceIndex);
68
+ const endBrace = findClosure(str, braceIndex);
69
69
  const inner = str.substring(braceIndex + 1, endBrace);
70
70
 
71
71
  if (header.trim().startsWith("@")) {
package/parser/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { extractPropsDefaults, extractRuntime, extractTopLevelFunctions } from "./component.js";
2
- export { extractContextFromElement, hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
1
+ export { extractPropsDefaults, extractRuntime, extractTopLevelFunctions, extractTopLevelVariables } from "./component.js";
2
+ export { extractCtxFromEl, hasMountIf, getMountIf, removeMountIf } from "./context.js";
3
3
  export { reservedAttrs, getLineNumber, validateChainStructure, applyConditionalToElement, interpolateNode } from "./template.js";
4
4
  export { scopeCss } from "./css.js";
5
- export { compileExpression } from "./utils.js";
5
+ export { compileExpr, evaluateConstant } from "./utils.js";
@@ -1,16 +1,16 @@
1
- import { compileExpression } from "./utils.js";
2
- import { hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
1
+ import { compileExpr } from "./utils.js";
2
+ import { hasMountIf, getMountIf, removeMountIf } from "./context.js";
3
3
 
4
4
  export const reservedAttrs = ["if", "mount:if", "elif", "else"];
5
5
 
6
- export function getLineNumber(sourceContent, parentContent, idxInParent) {
7
- if (sourceContent === parentContent) {
6
+ export function getLineNumber(srcContent, parentContent, idxInParent) {
7
+ if (srcContent === parentContent) {
8
8
  return parentContent.substring(0, idxInParent).split("\n").length;
9
9
  }
10
- const templateStartMatch = sourceContent.match(/<template[^>]*>/);
10
+ const templateStartMatch = srcContent.match(/<template[^>]*>/);
11
11
  if (templateStartMatch) {
12
12
  const contentStart = templateStartMatch.index + templateStartMatch[0].length;
13
- const beforeContent = sourceContent.substring(0, contentStart);
13
+ const beforeContent = srcContent.substring(0, contentStart);
14
14
  const linesInBefore = beforeContent.split("\n").length;
15
15
  const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
16
16
  return linesInBefore + linesInTemplate - 1;
@@ -24,7 +24,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
24
24
 
25
25
  for (const child of children) {
26
26
  const hasIf = child.hasAttribute("if");
27
- const hasDelIf = hasDelIfAttr(child);
27
+ const hasDelIf = hasMountIf(child);
28
28
  const hasElif = child.hasAttribute("elif");
29
29
  const hasElse = child.hasAttribute("else");
30
30
 
@@ -40,7 +40,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
40
40
  if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
41
41
  }
42
42
  }
43
- throw new Error(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
43
+ throw new Error(`${loc}\n <${tag}> has ${attr} without a preceding if/mount:if sibling`);
44
44
  }
45
45
  if (hasElse) {
46
46
  chainActive = false;
@@ -60,7 +60,7 @@ export function validateChainStructure(parent, sourceFile, sourceContent, parent
60
60
  export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
61
61
  if (hasIf) {
62
62
  const expr = child.getAttribute("if").slice(1, -1);
63
- const fn = compileExpression(expr, true);
63
+ const fn = compileExpr(expr, true);
64
64
  condChain.active = true;
65
65
  if (fn(ctxProxy)) {
66
66
  condChain.rendered = true;
@@ -70,8 +70,8 @@ export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, has
70
70
  }
71
71
  child.removeAttribute("if");
72
72
  } else if (hasDelIf) {
73
- const expr = getDelIfAttr(child).slice(1, -1);
74
- const fn = compileExpression(expr, true);
73
+ const expr = getMountIf(child).slice(1, -1);
74
+ const fn = compileExpr(expr, true);
75
75
  condChain.active = true;
76
76
  if (fn(ctxProxy)) {
77
77
  condChain.rendered = true;
@@ -79,10 +79,10 @@ export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, has
79
79
  child.remove();
80
80
  condChain.rendered = false;
81
81
  }
82
- removeDelIfAttr(child);
82
+ removeMountIf(child);
83
83
  } else if (hasElif) {
84
84
  const expr = child.getAttribute("elif").slice(1, -1);
85
- const fn = compileExpression(expr, true);
85
+ const fn = compileExpr(expr, true);
86
86
  if (fn(ctxProxy)) {
87
87
  condChain.rendered = true;
88
88
  } else {
@@ -106,7 +106,7 @@ export function interpolateNode(root, ctxProxy) {
106
106
  if (node.nodeType === 3) {
107
107
  node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
108
108
  try {
109
- return compileExpression(expr, true)(ctxProxy);
109
+ return compileExpr(expr, true)(ctxProxy);
110
110
  } catch {
111
111
  return "";
112
112
  }
package/parser/utils.js CHANGED
@@ -2,7 +2,7 @@ import { restoreTemplateChars } from "../utils.js";
2
2
 
3
3
  const compiledCache = new Map();
4
4
 
5
- export function compileExpression(expr, useCtx) {
5
+ export function compileExpr(expr, useCtx) {
6
6
  expr = restoreTemplateChars(expr);
7
7
  const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
8
8
  let fn = compiledCache.get(key);
@@ -14,3 +14,19 @@ export function compileExpression(expr, useCtx) {
14
14
  }
15
15
  return fn;
16
16
  }
17
+
18
+ const constantEvalError = Symbol("constant-eval-error");
19
+
20
+ export function evaluateConstant(expr) {
21
+ const trap = new Proxy({}, {
22
+ has() { return true; },
23
+ get() { throw constantEvalError; },
24
+ });
25
+ try {
26
+ const fn = compileExpr(expr, true);
27
+ const value = fn(trap);
28
+ return { constant: true, value };
29
+ } catch {
30
+ return { constant: false };
31
+ }
32
+ }
package/runtime/index.js CHANGED
@@ -3,7 +3,7 @@ const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
3
3
 
4
4
  const exprCache = new Map();
5
5
 
6
- function compileExpression(expr, useCtx) {
6
+ function compileExp(expr, useCtx) {
7
7
  expr = expr.replace(/_%%CHOCOLA-(?:AMP|LT|GT)\d+%%_/g, char =>
8
8
  char.includes("AMP") ? "&" : char.includes("LT") ? "<" : ">"
9
9
  );
@@ -28,7 +28,7 @@ function interpolateNode(root, ctx) {
28
28
  .replace(new RegExp(RBRACE_PH, "g"), "}");
29
29
  node.textContent = text.replace(/\{([^}]+)\}/g, (_, expr) => {
30
30
  try {
31
- return String(compileExpression(expr, true)(ctx));
31
+ return String(compileExp(expr, true)(ctx));
32
32
  } catch {
33
33
  return "";
34
34
  }
@@ -41,21 +41,21 @@ function interpolateNode(root, ctx) {
41
41
  }
42
42
  }
43
43
 
44
- function applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse) {
44
+ function applyDirectiveToEl(child, ctx, chain, hasIf, hasMountIf, hasElif, hasElse) {
45
45
  if (hasIf) {
46
46
  const expr = child.getAttribute("if").slice(1, -1);
47
47
  chain.active = true;
48
- if (compileExpression(expr, true)(ctx)) {
48
+ if (compileExp(expr, true)(ctx)) {
49
49
  chain.rendered = true;
50
50
  } else {
51
51
  child.style.display = "none";
52
52
  chain.rendered = false;
53
53
  }
54
54
  child.removeAttribute("if");
55
- } else if (hasDelIf) {
55
+ } else if (hasMountIf) {
56
56
  const expr = child.getAttribute("mount:if").slice(1, -1);
57
57
  chain.active = true;
58
- if (compileExpression(expr, true)(ctx)) {
58
+ if (compileExp(expr, true)(ctx)) {
59
59
  chain.rendered = true;
60
60
  } else {
61
61
  child.remove();
@@ -64,7 +64,7 @@ function applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif,
64
64
  child.removeAttribute("mount:if");
65
65
  } else if (hasElif) {
66
66
  const expr = child.getAttribute("elif").slice(1, -1);
67
- if (compileExpression(expr, true)(ctx)) {
67
+ if (compileExp(expr, true)(ctx)) {
68
68
  chain.rendered = true;
69
69
  } else {
70
70
  child.remove();
@@ -90,7 +90,7 @@ function interpolateAttributes(element, ctx) {
90
90
  .replace(new RegExp(RBRACE_PH, "g"), "}");
91
91
  attr.value = raw.replace(/\{([^}]+)\}/g, (_, expr) => {
92
92
  try {
93
- return String(compileExpression(expr, true)(ctx));
93
+ return String(compileExp(expr, true)(ctx));
94
94
  } catch {
95
95
  return "";
96
96
  }
@@ -99,10 +99,10 @@ function interpolateAttributes(element, ctx) {
99
99
  }
100
100
  }
101
101
 
102
- function processConditionals(element, ctx, chain) {
102
+ function processDirectives(element, ctx, chain) {
103
103
  for (const child of [...element.children]) {
104
104
  const hasIf = child.hasAttribute("if");
105
- const hasDelIf = child.hasAttribute("mount:if");
105
+ const hasMountIf = child.hasAttribute("mount:if");
106
106
  const hasElif = child.hasAttribute("elif");
107
107
  const hasElse = child.hasAttribute("else");
108
108
 
@@ -112,46 +112,46 @@ function processConditionals(element, ctx, chain) {
112
112
  continue;
113
113
  }
114
114
 
115
- applyConditionalToElement(child, ctx, chain, hasIf, hasDelIf, hasElif, hasElse);
115
+ applyDirectiveToEl(child, ctx, chain, hasIf, hasMountIf, hasElif, hasElse);
116
116
 
117
117
  if (child.parentNode) {
118
- processConditionals(child, ctx, chain);
118
+ processDirectives(child, ctx, chain);
119
119
  }
120
120
  }
121
121
  }
122
122
 
123
- function processSlots(element, projectedChildren) {
124
- const slots = element.querySelectorAll("slot");
123
+ function processSlots(el, projected) {
124
+ const slots = el.querySelectorAll("slot");
125
125
  for (const slot of slots) {
126
- const clone = projectedChildren.map(c => c.cloneNode(true));
126
+ const clone = projected.map(c => c.cloneNode(true));
127
127
  slot.replaceWith(...clone);
128
128
  }
129
129
  }
130
130
 
131
131
  let bindCounter = 0;
132
132
 
133
- function processBindAttributes(element) {
133
+ function processBindings(el) {
134
134
  const bindings = [];
135
- for (const attr of [...element.attributes]) {
135
+ for (const attr of [...el.attributes]) {
136
136
  if (attr.name.startsWith("bind:")) {
137
137
  const prop = attr.name.slice(5);
138
138
  const varName = attr.value;
139
139
  const bindId = "b" + (bindCounter++);
140
- element.setAttribute("data-chbind-" + bindId, "");
140
+ el.setAttribute("data-chbind-" + bindId, "");
141
141
  bindings.push({ prop, varName, bindId });
142
- element.removeAttribute(attr.name);
142
+ el.removeAttribute(attr.name);
143
143
  }
144
144
  }
145
145
  return bindings;
146
146
  }
147
147
 
148
- function resolveDataBindings(element) {
148
+ function resolveBindings(el) {
149
149
  const ids = [];
150
- for (const attr of [...element.attributes]) {
150
+ for (const attr of [...el.attributes]) {
151
151
  const match = attr.name.match(/^data-chbind-(b\d+)$/);
152
152
  if (match) {
153
153
  ids.push(match[1]);
154
- element.removeAttribute(attr.name);
154
+ el.removeAttribute(attr.name);
155
155
  }
156
156
  }
157
157
  return ids;
@@ -162,8 +162,8 @@ function collectBindings(root) {
162
162
  const idToEl = {};
163
163
 
164
164
  for (const el of [root, ...root.querySelectorAll("*")]) {
165
- bindingDefs.push(...processBindAttributes(el));
166
- for (const id of resolveDataBindings(el)) {
165
+ bindingDefs.push(...processBindings(el));
166
+ for (const id of resolveBindings(el)) {
167
167
  idToEl[id] = el;
168
168
  }
169
169
  }
@@ -225,7 +225,7 @@ class ChocolaComponent {
225
225
  }
226
226
 
227
227
  processSlots(root, this.#children || []);
228
- processConditionals(root, mergedCtx, { active: false, rendered: false });
228
+ processDirectives(root, mergedCtx, { active: false, rendered: false });
229
229
  interpolateAttributes(root, mergedCtx);
230
230
  interpolateNode(root, mergedCtx);
231
231
  root.classList.add(this.#hash);
package/utils.js CHANGED
@@ -2,7 +2,7 @@ import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { throwError } from "./compiler/utils.js";
4
4
 
5
- export async function getChocolaConfig(__rootdir) {
5
+ export async function getConfig(__rootdir) {
6
6
  try {
7
7
  const config = await fs.readFile(path.join(__rootdir, "chocola.config.json"), "utf-8");
8
8
  return JSON.parse(config);