odac 1.4.13 → 1.4.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ### 🛠️ Fixes & Improvements
2
2
 
3
+ - **docs:** update frontend scripts documentation to include code obfuscation details and enhance clarity
4
+ - **form:** add escapeHtmlPreservingTemplates method to handle template tokens
5
+ - **route:** enhance error handling to support object responses in error method
6
+ - **view:** use escapeHtmlPreservingTemplates consistently for form attributes
7
+
8
+
9
+
10
+ ---
11
+
12
+ Powered by [⚡ ODAC](https://odac.run)
13
+
14
+ ### 🛠️ Fixes & Improvements
15
+
3
16
  - **form:** enhance form handling with metadata and improved parsing logic
4
17
  - **form:** implement token rotation on successful form submissions
5
18
  - **view:** prevent script injection and handle escaped quotes in form configs
@@ -1,36 +1,92 @@
1
1
  ---
2
2
  name: frontend-scripts-typescript-skill
3
- description: ODAC frontend JS/TS pipeline guidelines for writing, bundling, and optimizing client-side scripts using esbuild.
3
+ description: ODAC frontend JS/TS pipeline guidelines for writing, bundling, and optimizing client-side scripts using esbuild and code obfuscation.
4
4
  metadata:
5
- tags: frontend, javascript, typescript, esbuild, bundling, minification, tree-shaking, scripts, assets
5
+ tags: frontend, javascript, typescript, esbuild, bundling, minification, tree-shaking, scripts, assets, obfuscation
6
6
  ---
7
7
 
8
8
  # Frontend Scripts & TypeScript Skill
9
9
 
10
- Zero-config frontend asset pipeline powered by esbuild for TypeScript transpilation, bundling, minification, and tree-shaking.
10
+ ODAC provides a built-in, Zero-Config frontend asset pipeline powered by **esbuild** for TypeScript transpilation, bundling, minification, tree-shaking, and multi-level code obfuscation.
11
11
 
12
- ## Core Rules
13
- 1. **Entry Points**: Place `.ts`, `.js`, `.mts`, or `.mjs` files in `view/js/`. Each becomes a separate bundle.
14
- 2. **Partials Convention**: Files starting with `_` (e.g., `_utils.ts`) are ignored as entry points — use them as shared imports only.
15
- 3. **Output Path**: Compiled files go to `public/assets/js/{name}.js`.
16
- 4. **No TypeScript Enforcement**: Both TypeScript and plain JavaScript are supported equally.
17
- 5. **Import Resolution**: Use standard ES module `import`/`export` between files. esbuild bundles everything into a single output per entry point.
18
- 6. **Configuration**: Optional `js` key in `odac.json` for `target`, `minify`, `sourcemap`, and `bundle` settings.
12
+ ## Core Rules & Conventions
19
13
 
20
- ## Development vs Production
21
- - **`odac dev`**: Watch mode with source maps, no minification, instant rebuilds.
22
- - **`odac build`**: Full minification, tree-shaking, and dead code elimination.
14
+ 1. **Entry Points**: Every `.ts`, `.js`, `.mts`, or `.mjs` file placed directly in `view/js/` represents a unique entry point and will compile into a separate bundle in `public/assets/js/{name}.js`.
15
+ 2. **Partials Convention**: Files starting with an underscore (e.g., `_utils.ts`, `_api.ts`) are treated as private modules/partials. They are **ignored** as entry points and should only be used as shared imports.
16
+ 3. **No TypeScript Enforcement**: TypeScript and plain JavaScript are supported equally out of the box.
17
+ 4. **Import Resolution**: Use standard ES module `import`/`export` syntax. esbuild bundles all imported modules into a single optimized bundle, eliminating extra runtime network requests.
18
+ 5. **Output Path**: Compiled output is saved statically under `public/assets/js/`.
19
+
20
+ ## Pipeline Modes
21
+
22
+ * **Development (`npm run dev` / `odac dev`)**:
23
+ * Watches all scripts in `view/js/` for instant sub-millisecond rebuilds.
24
+ * Source maps are always enabled to facilitate easy debugging.
25
+ * No minification or obfuscation is applied.
26
+ * **Production (`npm run build` / `odac build`)**:
27
+ * Enables full bundling, minification, and tree-shaking.
28
+ * Applies configured obfuscation levels.
29
+ * Exports clean production-ready assets to `public/assets/js/`.
30
+
31
+ ## Configuration (`odac.json`)
32
+
33
+ You can customize the pipeline behavior via the optional `js` key in the `odac.json` configuration file:
34
+
35
+ ```json
36
+ {
37
+ "js": {
38
+ "target": "es2020",
39
+ "minify": true,
40
+ "sourcemap": false,
41
+ "bundle": true,
42
+ "obfuscate": false
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Configuration Options
48
+
49
+ | Option | Default | Description |
50
+ |-------------|------------|-------------|
51
+ | `target` | `"es2020"` | JavaScript target version (`es2015`, `es2020`, `esnext`, etc.). |
52
+ | `minify` | `true` | Enables minification (whitespace removal, variable shortening, dead code elimination) in production. |
53
+ | `sourcemap` | `false` | Generates source maps in production builds (always enabled in dev mode). |
54
+ | `bundle` | `true` | Bundles all imported dependency modules into the output entry-point file. |
55
+ | `obfuscate` | `false` | Configures the level of production code obfuscation (`false`, `true`/`"low"`, `"medium"`, `"high"`). |
56
+
57
+ ---
58
+
59
+ ## Obfuscation Levels
60
+
61
+ ODAC supports three distinct levels of code obfuscation in production mode (`odac build`). Obfuscation is disabled by default and is never applied during development.
62
+
63
+ | Level | Behavior |
64
+ |-------|----------|
65
+ | `false` | **No Obfuscation**: Standard minification and tree-shaking only. |
66
+ | `true` / `"low"` | **Low Mangling**: Mangles properties starting with `_` (private-by-convention). |
67
+ | `"medium"` | **Medium Security**: Low level mangling + drops `debugger` statements + removes `console.debug` and `console.trace` calls. |
68
+ | `"high"` | **Maximum Hardening**: Mangles all `_` and `$` prefixed properties + drops all `console.*` calls and `debugger` statements. |
69
+
70
+ > [!WARNING]
71
+ > **High Obfuscation Compatibility Warning:**
72
+ > The `"high"` obfuscation level mangles `$`-prefixed properties. This can break frontend code interacting with external libraries or frameworks that rely heavily on the `$` naming convention (e.g., jQuery). Start with `"low"` or `"medium"` and verify compatibility thoroughly before deploying with `"high"`.
73
+
74
+ ---
75
+
76
+ ## Example Directory Structure
23
77
 
24
- ## Example Structure
25
78
  ```
26
79
  view/js/
27
- ├── app.ts → public/assets/js/app.js
28
- ├── admin.ts → public/assets/js/admin.js
29
- ├── _api.ts (shared module, not compiled)
30
- └── _utils.ts (shared module, not compiled)
80
+ ├── app.ts → compiled to public/assets/js/app.js (Entry Point)
81
+ ├── admin.ts → compiled to public/assets/js/admin.js (Entry Point)
82
+ ├── _api.ts (Shared API Module — Import only, not compiled on its own)
83
+ └── _utils.ts (Shared Utility Module — Import only, not compiled on its own)
31
84
  ```
32
85
 
33
86
  ## HTML Integration
87
+
88
+ Inject compiled scripts into your skeleton or layout templates using regular script tags:
89
+
34
90
  ```html
35
91
  <script src="/assets/js/app.js"></script>
36
92
  ```
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "mail@emre.red",
8
8
  "url": "https://emre.red"
9
9
  },
10
- "version": "1.4.13",
10
+ "version": "1.4.14",
11
11
  "license": "MIT",
12
12
  "engines": {
13
13
  "node": ">=18.0.0"
package/src/Request.js CHANGED
@@ -50,12 +50,18 @@ class OdacRequest {
50
50
  async abort(code) {
51
51
  this.status(code)
52
52
  let result = {401: 'Unauthorized', 404: 'Not Found', 408: 'Request Timeout'}[code] ?? null
53
- if (
54
- this.#odac.Route?.routes?.[this.route]?.error &&
55
- this.#odac.Route.routes[this.route].error[code] &&
56
- typeof this.#odac.Route.routes[this.route].error[code].cache === 'function'
57
- )
58
- result = await this.#odac.Route.routes[this.route].error[code].cache(this.#odac)
53
+ const errorRoute = this.#odac.Route?.routes?.[this.route]?.error?.[code]
54
+ if (errorRoute && typeof errorRoute.cache === 'function') {
55
+ try {
56
+ const handlerResult = await errorRoute.cache(this.#odac)
57
+ // If the handler returned nothing, assume it configured the view via Odac.View.set()
58
+ // and let Route.request() continue to View.print() like normal pages.
59
+ if (handlerResult === undefined) return
60
+ result = handlerResult
61
+ } catch (e) {
62
+ console.error(JSON.stringify({level: 'ERROR', message: `Error in custom error handler for ${code}`, error: e.message}))
63
+ }
64
+ }
59
65
  this.end(result)
60
66
  }
61
67
 
package/src/Route.js CHANGED
@@ -858,6 +858,13 @@ class Route {
858
858
  }
859
859
 
860
860
  error(code, file) {
861
+ if (typeof file === 'object' && file !== null && !Array.isArray(file)) {
862
+ this.set('error', code, _odac => {
863
+ _odac.set(file)
864
+ _odac.View.set(file)
865
+ })
866
+ return
867
+ }
861
868
  this.set('error', code, file)
862
869
  }
863
870
 
package/src/View/Form.js CHANGED
@@ -58,6 +58,30 @@ class Form {
58
58
  return String(value).replace(/[&<>"']/g, ch => map[ch])
59
59
  }
60
60
 
61
+ // Like escapeHtml but leaves {{ ... }} / {!! ... !!} template tokens intact
62
+ // so they survive into the view engine's {{ }} pass. Escaping inside the
63
+ // tokens would corrupt the JS expression (e.g. ' -> &#39;) and produce
64
+ // "Unexpected token '&'" at render time.
65
+ static escapeHtmlPreservingTemplates(value) {
66
+ if (value === null || value === undefined) return ''
67
+ const str = String(value)
68
+ const regex = /\{\{[\s\S]*?\}\}|\{!![\s\S]*?!!\}/g
69
+ let result = ''
70
+ let lastIndex = 0
71
+ let match
72
+ while ((match = regex.exec(str)) !== null) {
73
+ if (match.index > lastIndex) {
74
+ result += this.escapeHtml(str.substring(lastIndex, match.index))
75
+ }
76
+ result += match[0]
77
+ lastIndex = regex.lastIndex
78
+ }
79
+ if (lastIndex < str.length) {
80
+ result += this.escapeHtml(str.substring(lastIndex))
81
+ }
82
+ return result
83
+ }
84
+
61
85
  static parse(content, Odac) {
62
86
  for (const type of this.FORM_TYPES) {
63
87
  content = this.parseFormType(content, Odac, type)
@@ -484,16 +508,18 @@ class Form {
484
508
  let html = ''
485
509
  const escapedName = this.escapeHtml(field.name)
486
510
  const escapedType = this.escapeHtml(field.type)
487
- const escapedPlaceholder = this.escapeHtml(field.placeholder)
511
+ const escapedPlaceholder = this.escapeHtmlPreservingTemplates(field.placeholder)
488
512
 
489
513
  if (field.label && field.type !== 'checkbox') {
490
- const fieldId = this.escapeHtml(field.id || `odac-${field.name}`)
491
- html += `<label for="${fieldId}">${this.escapeHtml(field.label)}</label>\n`
514
+ const fieldId = this.escapeHtmlPreservingTemplates(field.id || `odac-${field.name}`)
515
+ html += `<label for="${fieldId}">${this.escapeHtmlPreservingTemplates(field.label)}</label>\n`
492
516
  }
493
517
 
494
- const classAttr = field.class ? ` class="${this.escapeHtml(field.class)}"` : ''
495
- const idAttr = field.id ? ` id="${this.escapeHtml(field.id)}"` : ` id="${this.escapeHtml(`odac-${field.name}`)}"`
496
- const valueAttr = field.value !== null ? ` value="${this.escapeHtml(field.value)}"` : ''
518
+ const classAttr = field.class ? ` class="${this.escapeHtmlPreservingTemplates(field.class)}"` : ''
519
+ const idAttr = field.id
520
+ ? ` id="${this.escapeHtmlPreservingTemplates(field.id)}"`
521
+ : ` id="${this.escapeHtmlPreservingTemplates(`odac-${field.name}`)}"`
522
+ const valueAttr = field.value !== null ? ` value="${this.escapeHtmlPreservingTemplates(field.value)}"` : ''
497
523
 
498
524
  if (field.type === 'checkbox') {
499
525
  const attrs = this.buildHtml5Attributes(field)
@@ -501,14 +527,14 @@ class Form {
501
527
  if (field.label) {
502
528
  html += `<label>\n`
503
529
  html += ` <input type="checkbox"${idAttr} name="${escapedName}" value="1"${classAttr}${checkedAttr}${attrs}>\n`
504
- html += ` ${this.escapeHtml(field.label)}\n`
530
+ html += ` ${this.escapeHtmlPreservingTemplates(field.label)}\n`
505
531
  html += `</label>\n`
506
532
  } else {
507
533
  html += `<input type="checkbox"${idAttr} name="${escapedName}" value="1"${classAttr}${checkedAttr}${attrs}>\n`
508
534
  }
509
535
  } else if (field.type === 'textarea') {
510
536
  const attrs = this.buildHtml5Attributes(field)
511
- html += `<textarea${idAttr} name="${escapedName}" placeholder="${escapedPlaceholder}"${classAttr}${attrs}>${this.escapeHtml(
537
+ html += `<textarea${idAttr} name="${escapedName}" placeholder="${escapedPlaceholder}"${classAttr}${attrs}>${this.escapeHtmlPreservingTemplates(
512
538
  field.value || ''
513
539
  )}</textarea>\n`
514
540
  } else {
@@ -524,7 +550,7 @@ class Form {
524
550
  for (const key in field.extraAttributes) {
525
551
  const val = field.extraAttributes[key]
526
552
  if (val === '') attrs += ` ${key}`
527
- else attrs += ` ${key}="${this.escapeHtml(val)}"`
553
+ else attrs += ` ${key}="${this.escapeHtmlPreservingTemplates(val)}"`
528
554
  }
529
555
  return attrs
530
556
  }