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 +13 -0
- package/docs/ai/skills/frontend/scripts.md +74 -18
- package/package.json +1 -1
- package/src/Request.js +12 -6
- package/src/Route.js +7 -0
- package/src/View/Form.js +35 -9
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-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 (
|
|
30
|
-
└── _utils.ts (
|
|
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
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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. ' -> ') 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.
|
|
511
|
+
const escapedPlaceholder = this.escapeHtmlPreservingTemplates(field.placeholder)
|
|
488
512
|
|
|
489
513
|
if (field.label && field.type !== 'checkbox') {
|
|
490
|
-
const fieldId = this.
|
|
491
|
-
html += `<label for="${fieldId}">${this.
|
|
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.
|
|
495
|
-
const idAttr = field.id
|
|
496
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
553
|
+
else attrs += ` ${key}="${this.escapeHtmlPreservingTemplates(val)}"`
|
|
528
554
|
}
|
|
529
555
|
return attrs
|
|
530
556
|
}
|