@atscript/ui-fns 0.1.58
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 +21 -0
- package/README.md +26 -0
- package/dist/index.cjs +169 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.mts +123 -0
- package/dist/index.mjs +163 -0
- package/dist/plugin.cjs +156 -0
- package/dist/plugin.d.cts +19 -0
- package/dist/plugin.d.mts +20 -0
- package/dist/plugin.mjs +156 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artem Maltsev <artem@maltsev.nl>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @atscript/ui-fns
|
|
2
|
+
|
|
3
|
+
Opt-in plugin for [`@atscript/ui`](../ui) that adds **dynamic** field properties driven by `@ui.fn.*` annotations.
|
|
4
|
+
|
|
5
|
+
Part of the [atscript-ui](https://github.com/moostjs/atscript-ui) monorepo.
|
|
6
|
+
|
|
7
|
+
## Why it's a separate package
|
|
8
|
+
|
|
9
|
+
`@ui.fn.*` annotations let `.as` schemas declare computed UI behaviour (e.g. `@ui.fn.disabled (form) => form.value.kind !== 'admin'`). Evaluating them requires `new Function` at runtime, which is incompatible with strict CSPs. Splitting the dynamic resolver into its own package keeps `@atscript/ui` CSP-safe by default — consumers opt in only when they need expressions.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add @atscript/ui-fns
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Entry points
|
|
18
|
+
|
|
19
|
+
| Subpath | What it exports |
|
|
20
|
+
| ------------------------- | ----------------------------------------------------------------------- |
|
|
21
|
+
| `@atscript/ui-fns` | `FnFieldResolver` — drop-in replacement for the default `FieldResolver` |
|
|
22
|
+
| `@atscript/ui-fns/plugin` | atscript build-time plugin that compiles `@ui.fn.*` bodies |
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT © Artem Maltsev
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _atscript_ui = require("@atscript/ui");
|
|
3
|
+
//#region src/runtime/fn-compiler.ts
|
|
4
|
+
const pool = new (require("@prostojs/deserialize-fn")).FNPool();
|
|
5
|
+
/**
|
|
6
|
+
* Compiles a field-level function string from a `@ui.form.fn.*` / `@ui.table.fn.*` annotation
|
|
7
|
+
* into a callable function. Uses FNPool for caching.
|
|
8
|
+
*
|
|
9
|
+
* The function string should be an arrow or regular function expression:
|
|
10
|
+
* `"(v, data, ctx, entry) => !data.firstName"`
|
|
11
|
+
*
|
|
12
|
+
* The compiled function receives a single TFnScope object:
|
|
13
|
+
* `{ v, data, context, entry }`
|
|
14
|
+
*/
|
|
15
|
+
function compileFieldFn(fnStr) {
|
|
16
|
+
const code = `return (${fnStr})(v, data, context, entry)`;
|
|
17
|
+
return pool.getFn(code);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compiles a form-level function string from a `@ui.form.fn.title` or similar annotation.
|
|
21
|
+
*
|
|
22
|
+
* The function string should be:
|
|
23
|
+
* `"(data, ctx) => someExpression"`
|
|
24
|
+
*
|
|
25
|
+
* The compiled function receives a single TFnScope object:
|
|
26
|
+
* `{ data, context }`
|
|
27
|
+
*/
|
|
28
|
+
function compileTopFn(fnStr) {
|
|
29
|
+
const code = `return (${fnStr})(data, context)`;
|
|
30
|
+
return pool.getFn(code);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compiles a validator function string.
|
|
34
|
+
* Delegates to `compileFieldFn` with a narrowed return type.
|
|
35
|
+
*
|
|
36
|
+
* Returns `true` for valid, or a string error message for invalid.
|
|
37
|
+
*/
|
|
38
|
+
function compileValidatorFn(fnStr) {
|
|
39
|
+
return compileFieldFn(fnStr);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/runtime/dynamic-resolver.ts
|
|
43
|
+
/** Fn keys that store `[{ name, fn }]` arrays rather than a single fn string. */
|
|
44
|
+
const ATTR_FN_KEYS = new Set([_atscript_ui.UI_FORM_FN_ATTR, _atscript_ui.UI_TABLE_FN_ATTR]);
|
|
45
|
+
/**
|
|
46
|
+
* Dynamic field resolver — extends static resolution with `new Function` compilation
|
|
47
|
+
* for `ui.fn.*` annotation keys.
|
|
48
|
+
*
|
|
49
|
+
* Install via `installDynamicResolver()` from the package index.
|
|
50
|
+
*/
|
|
51
|
+
var DynamicFieldResolver = class {
|
|
52
|
+
resolveFieldProp(prop, fnKey, staticKey, scope, opts) {
|
|
53
|
+
if (ATTR_FN_KEYS.has(fnKey)) return this.resolveAttrFns(prop, fnKey, scope);
|
|
54
|
+
return resolveAnnotatedProp(prop.metadata, fnKey, staticKey, scope, compileFieldFn, opts);
|
|
55
|
+
}
|
|
56
|
+
resolveFormProp(type, fnKey, staticKey, scope, opts) {
|
|
57
|
+
return resolveAnnotatedProp(type.metadata, fnKey, staticKey, scope, compileTopFn, opts);
|
|
58
|
+
}
|
|
59
|
+
hasComputedAnnotations(prop) {
|
|
60
|
+
for (const key of prop.metadata.keys()) {
|
|
61
|
+
const k = key;
|
|
62
|
+
if (k.startsWith(_atscript_ui.UI_FORM_FN_PREFIX) || k.startsWith(_atscript_ui.UI_TABLE_FN_PREFIX)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
resolveAttrFns(prop, fnKey, scope) {
|
|
67
|
+
const fnAttrs = prop.metadata.get(fnKey);
|
|
68
|
+
if (!fnAttrs) return void 0;
|
|
69
|
+
const result = {};
|
|
70
|
+
let hasAttrs = false;
|
|
71
|
+
for (const item of (0, _atscript_ui.asArray)(fnAttrs)) if (typeof item === "object" && item !== null && "name" in item && "fn" in item) {
|
|
72
|
+
const { name, fn } = item;
|
|
73
|
+
result[name] = compileFieldFn(fn)(scope);
|
|
74
|
+
hasAttrs = true;
|
|
75
|
+
}
|
|
76
|
+
return hasAttrs ? result : void 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function resolveAnnotatedProp(metadata, fnKey, staticKey, scope, compileFn, opts) {
|
|
80
|
+
const fnStr = metadata.get(fnKey);
|
|
81
|
+
if (typeof fnStr === "string") return compileFn(fnStr)(scope);
|
|
82
|
+
return (0, _atscript_ui.resolveStatic)(metadata, staticKey, opts);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds a `TFieldEvaluated` entry and returns a full scope containing it.
|
|
86
|
+
*
|
|
87
|
+
* Implements the dual-scope pattern:
|
|
88
|
+
* 1. Resolve constraints (disabled/hidden/readonly) from `baseScope`
|
|
89
|
+
* 2. Assemble the entry object
|
|
90
|
+
* 3. Build full scope (`{ ...baseScope, entry }`)
|
|
91
|
+
* 4. Resolve options into the entry using the full scope
|
|
92
|
+
*/
|
|
93
|
+
function buildFieldEntry(prop, baseScope, path, opts) {
|
|
94
|
+
const boolOpts = { staticAsBoolean: true };
|
|
95
|
+
const scopeAsRecord = baseScope;
|
|
96
|
+
const entry = {
|
|
97
|
+
field: path,
|
|
98
|
+
type: opts?.type ?? (0, _atscript_ui.getFieldMeta)(prop, _atscript_ui.UI_FORM_TYPE) ?? (0, _atscript_ui.getFieldMeta)(prop, _atscript_ui.UI_TYPE) ?? "text",
|
|
99
|
+
component: opts?.component ?? (0, _atscript_ui.getFieldMeta)(prop, _atscript_ui.UI_FORM_COMPONENT),
|
|
100
|
+
name: opts?.name ?? path.slice(path.lastIndexOf(".") + 1),
|
|
101
|
+
optional: opts?.optional ?? prop.optional,
|
|
102
|
+
disabled: opts?.disabled ?? (0, _atscript_ui.resolveFieldProp)(prop, _atscript_ui.UI_FORM_FN_DISABLED, _atscript_ui.UI_FORM_DISABLED, scopeAsRecord, boolOpts),
|
|
103
|
+
hidden: opts?.hidden ?? (0, _atscript_ui.resolveFieldProp)(prop, _atscript_ui.UI_FORM_FN_HIDDEN, _atscript_ui.UI_FORM_HIDDEN, scopeAsRecord, boolOpts),
|
|
104
|
+
readonly: opts?.readonly ?? (0, _atscript_ui.resolveFieldProp)(prop, _atscript_ui.UI_FORM_FN_READONLY, _atscript_ui.META_READONLY, scopeAsRecord, boolOpts)
|
|
105
|
+
};
|
|
106
|
+
const scope = {
|
|
107
|
+
...baseScope,
|
|
108
|
+
entry
|
|
109
|
+
};
|
|
110
|
+
entry.options = (0, _atscript_ui.resolveOptions)(prop, scope);
|
|
111
|
+
return scope;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/runtime/validator-plugin.ts
|
|
115
|
+
/**
|
|
116
|
+
* Creates an ATScript validator plugin that processes `@ui.form.validate` annotations
|
|
117
|
+
* using compiled function strings.
|
|
118
|
+
*
|
|
119
|
+
* Usage:
|
|
120
|
+
* const plugin = uiFnsValidatorPlugin()
|
|
121
|
+
* const validator = new Validator(field.prop, { plugins: [plugin] })
|
|
122
|
+
* validator.validate(value, true, { data: formData, context })
|
|
123
|
+
*/
|
|
124
|
+
function uiFnsValidatorPlugin() {
|
|
125
|
+
return (ctx, def, value) => {
|
|
126
|
+
const hasValidators = (0, _atscript_ui.getFieldMeta)(def, _atscript_ui.UI_FORM_VALIDATE);
|
|
127
|
+
if (!hasValidators) return void 0;
|
|
128
|
+
const fnsCtx = ctx.context;
|
|
129
|
+
const scope = buildFieldEntry(def, {
|
|
130
|
+
v: value,
|
|
131
|
+
data: fnsCtx?.data ?? {},
|
|
132
|
+
context: fnsCtx?.context ?? {},
|
|
133
|
+
entry: void 0
|
|
134
|
+
}, ctx.path);
|
|
135
|
+
const fns = (0, _atscript_ui.asArray)(hasValidators);
|
|
136
|
+
for (const fnStr of fns) {
|
|
137
|
+
if (typeof fnStr !== "string") continue;
|
|
138
|
+
const result = compileValidatorFn(fnStr)(scope);
|
|
139
|
+
if (result !== true) {
|
|
140
|
+
ctx.error(typeof result === "string" ? result : "Validation failed");
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/index.ts
|
|
148
|
+
/**
|
|
149
|
+
* Installs the dynamic field resolver into @atscript/ui.
|
|
150
|
+
* Call this once at app startup to enable `ui.fn.*` annotation resolution
|
|
151
|
+
* and `@ui.form.validate` custom validator strings.
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { installDynamicResolver } from '@atscript/ui-fns'
|
|
155
|
+
* installDynamicResolver()
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
function installDynamicResolver() {
|
|
159
|
+
(0, _atscript_ui.setResolver)(new DynamicFieldResolver());
|
|
160
|
+
(0, _atscript_ui.setDefaultValidatorPlugins)([uiFnsValidatorPlugin()]);
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
exports.DynamicFieldResolver = DynamicFieldResolver;
|
|
164
|
+
exports.buildFieldEntry = buildFieldEntry;
|
|
165
|
+
exports.compileFieldFn = compileFieldFn;
|
|
166
|
+
exports.compileTopFn = compileTopFn;
|
|
167
|
+
exports.compileValidatorFn = compileValidatorFn;
|
|
168
|
+
exports.installDynamicResolver = installDynamicResolver;
|
|
169
|
+
exports.uiFnsValidatorPlugin = uiFnsValidatorPlugin;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { FieldResolver, TFormEntryOptions, TResolveOptions } from "@atscript/ui";
|
|
2
|
+
import { TAtscriptAnnotatedType, TValidatorPlugin } from "@atscript/typescript/utils";
|
|
3
|
+
|
|
4
|
+
//#region src/runtime/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Scope object passed to compiled functions.
|
|
7
|
+
* Properties become variables inside compiled function strings:
|
|
8
|
+
* v, data, context, entry
|
|
9
|
+
*/
|
|
10
|
+
interface TFnScope<V = unknown, D = Record<string, unknown>, C = Record<string, unknown>> {
|
|
11
|
+
v?: V;
|
|
12
|
+
data: D;
|
|
13
|
+
context: C;
|
|
14
|
+
entry?: TFieldEvaluated;
|
|
15
|
+
action?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A value that is either static or a function of the fn scope.
|
|
19
|
+
*/
|
|
20
|
+
type TComputed<T> = T | ((scope: TFnScope) => T);
|
|
21
|
+
/**
|
|
22
|
+
* Minimal evaluated snapshot of a field — passed to validators and
|
|
23
|
+
* computed functions as `entry`.
|
|
24
|
+
*/
|
|
25
|
+
interface TFieldEvaluated {
|
|
26
|
+
field: string;
|
|
27
|
+
type: string;
|
|
28
|
+
component?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
hidden?: boolean;
|
|
33
|
+
readonly?: boolean;
|
|
34
|
+
options?: TFormEntryOptions[];
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/runtime/fn-compiler.d.ts
|
|
38
|
+
/**
|
|
39
|
+
* Compiles a field-level function string from a `@ui.form.fn.*` / `@ui.table.fn.*` annotation
|
|
40
|
+
* into a callable function. Uses FNPool for caching.
|
|
41
|
+
*
|
|
42
|
+
* The function string should be an arrow or regular function expression:
|
|
43
|
+
* `"(v, data, ctx, entry) => !data.firstName"`
|
|
44
|
+
*
|
|
45
|
+
* The compiled function receives a single TFnScope object:
|
|
46
|
+
* `{ v, data, context, entry }`
|
|
47
|
+
*/
|
|
48
|
+
declare function compileFieldFn<R = unknown>(fnStr: string): (scope: TFnScope) => R;
|
|
49
|
+
/**
|
|
50
|
+
* Compiles a form-level function string from a `@ui.form.fn.title` or similar annotation.
|
|
51
|
+
*
|
|
52
|
+
* The function string should be:
|
|
53
|
+
* `"(data, ctx) => someExpression"`
|
|
54
|
+
*
|
|
55
|
+
* The compiled function receives a single TFnScope object:
|
|
56
|
+
* `{ data, context }`
|
|
57
|
+
*/
|
|
58
|
+
declare function compileTopFn<R = unknown>(fnStr: string): (scope: TFnScope) => R;
|
|
59
|
+
/**
|
|
60
|
+
* Compiles a validator function string.
|
|
61
|
+
* Delegates to `compileFieldFn` with a narrowed return type.
|
|
62
|
+
*
|
|
63
|
+
* Returns `true` for valid, or a string error message for invalid.
|
|
64
|
+
*/
|
|
65
|
+
declare function compileValidatorFn(fnStr: string): (scope: TFnScope) => string | boolean;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/runtime/dynamic-resolver.d.ts
|
|
68
|
+
/** Options for buildFieldEntry — allows pre-resolved overrides. */
|
|
69
|
+
type TBuildFieldEntryOpts = Partial<Pick<TFieldEvaluated, "name" | "type" | "component" | "optional" | "disabled" | "hidden" | "readonly">>;
|
|
70
|
+
/**
|
|
71
|
+
* Dynamic field resolver — extends static resolution with `new Function` compilation
|
|
72
|
+
* for `ui.fn.*` annotation keys.
|
|
73
|
+
*
|
|
74
|
+
* Install via `installDynamicResolver()` from the package index.
|
|
75
|
+
*/
|
|
76
|
+
declare class DynamicFieldResolver implements FieldResolver {
|
|
77
|
+
resolveFieldProp<T>(prop: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record<string, unknown>, opts?: TResolveOptions<T>): T | undefined;
|
|
78
|
+
resolveFormProp<T>(type: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record<string, unknown>, opts?: TResolveOptions<T>): T | undefined;
|
|
79
|
+
hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
|
|
80
|
+
private resolveAttrFns;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds a `TFieldEvaluated` entry and returns a full scope containing it.
|
|
84
|
+
*
|
|
85
|
+
* Implements the dual-scope pattern:
|
|
86
|
+
* 1. Resolve constraints (disabled/hidden/readonly) from `baseScope`
|
|
87
|
+
* 2. Assemble the entry object
|
|
88
|
+
* 3. Build full scope (`{ ...baseScope, entry }`)
|
|
89
|
+
* 4. Resolve options into the entry using the full scope
|
|
90
|
+
*/
|
|
91
|
+
declare function buildFieldEntry(prop: TAtscriptAnnotatedType, baseScope: TFnScope, path: string, opts?: TBuildFieldEntryOpts): TFnScope;
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/runtime/validator-plugin.d.ts
|
|
94
|
+
/** Per-call context passed via `validator.validate(value, safe, context)`. */
|
|
95
|
+
interface TValidatorContext {
|
|
96
|
+
data: Record<string, unknown>;
|
|
97
|
+
context: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates an ATScript validator plugin that processes `@ui.form.validate` annotations
|
|
101
|
+
* using compiled function strings.
|
|
102
|
+
*
|
|
103
|
+
* Usage:
|
|
104
|
+
* const plugin = uiFnsValidatorPlugin()
|
|
105
|
+
* const validator = new Validator(field.prop, { plugins: [plugin] })
|
|
106
|
+
* validator.validate(value, true, { data: formData, context })
|
|
107
|
+
*/
|
|
108
|
+
declare function uiFnsValidatorPlugin(): TValidatorPlugin;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/index.d.ts
|
|
111
|
+
/**
|
|
112
|
+
* Installs the dynamic field resolver into @atscript/ui.
|
|
113
|
+
* Call this once at app startup to enable `ui.fn.*` annotation resolution
|
|
114
|
+
* and `@ui.form.validate` custom validator strings.
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { installDynamicResolver } from '@atscript/ui-fns'
|
|
118
|
+
* installDynamicResolver()
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function installDynamicResolver(): void;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { DynamicFieldResolver, type TBuildFieldEntryOpts, type TComputed, type TFieldEvaluated, type TFnScope, type TValidatorContext, buildFieldEntry, compileFieldFn, compileTopFn, compileValidatorFn, installDynamicResolver, uiFnsValidatorPlugin };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { FieldResolver, TFormEntryOptions, TResolveOptions } from "@atscript/ui";
|
|
2
|
+
import { TAtscriptAnnotatedType, TValidatorPlugin } from "@atscript/typescript/utils";
|
|
3
|
+
|
|
4
|
+
//#region src/runtime/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Scope object passed to compiled functions.
|
|
7
|
+
* Properties become variables inside compiled function strings:
|
|
8
|
+
* v, data, context, entry
|
|
9
|
+
*/
|
|
10
|
+
interface TFnScope<V = unknown, D = Record<string, unknown>, C = Record<string, unknown>> {
|
|
11
|
+
v?: V;
|
|
12
|
+
data: D;
|
|
13
|
+
context: C;
|
|
14
|
+
entry?: TFieldEvaluated;
|
|
15
|
+
action?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A value that is either static or a function of the fn scope.
|
|
19
|
+
*/
|
|
20
|
+
type TComputed<T> = T | ((scope: TFnScope) => T);
|
|
21
|
+
/**
|
|
22
|
+
* Minimal evaluated snapshot of a field — passed to validators and
|
|
23
|
+
* computed functions as `entry`.
|
|
24
|
+
*/
|
|
25
|
+
interface TFieldEvaluated {
|
|
26
|
+
field: string;
|
|
27
|
+
type: string;
|
|
28
|
+
component?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
optional?: boolean;
|
|
32
|
+
hidden?: boolean;
|
|
33
|
+
readonly?: boolean;
|
|
34
|
+
options?: TFormEntryOptions[];
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/runtime/fn-compiler.d.ts
|
|
38
|
+
/**
|
|
39
|
+
* Compiles a field-level function string from a `@ui.form.fn.*` / `@ui.table.fn.*` annotation
|
|
40
|
+
* into a callable function. Uses FNPool for caching.
|
|
41
|
+
*
|
|
42
|
+
* The function string should be an arrow or regular function expression:
|
|
43
|
+
* `"(v, data, ctx, entry) => !data.firstName"`
|
|
44
|
+
*
|
|
45
|
+
* The compiled function receives a single TFnScope object:
|
|
46
|
+
* `{ v, data, context, entry }`
|
|
47
|
+
*/
|
|
48
|
+
declare function compileFieldFn<R = unknown>(fnStr: string): (scope: TFnScope) => R;
|
|
49
|
+
/**
|
|
50
|
+
* Compiles a form-level function string from a `@ui.form.fn.title` or similar annotation.
|
|
51
|
+
*
|
|
52
|
+
* The function string should be:
|
|
53
|
+
* `"(data, ctx) => someExpression"`
|
|
54
|
+
*
|
|
55
|
+
* The compiled function receives a single TFnScope object:
|
|
56
|
+
* `{ data, context }`
|
|
57
|
+
*/
|
|
58
|
+
declare function compileTopFn<R = unknown>(fnStr: string): (scope: TFnScope) => R;
|
|
59
|
+
/**
|
|
60
|
+
* Compiles a validator function string.
|
|
61
|
+
* Delegates to `compileFieldFn` with a narrowed return type.
|
|
62
|
+
*
|
|
63
|
+
* Returns `true` for valid, or a string error message for invalid.
|
|
64
|
+
*/
|
|
65
|
+
declare function compileValidatorFn(fnStr: string): (scope: TFnScope) => string | boolean;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/runtime/dynamic-resolver.d.ts
|
|
68
|
+
/** Options for buildFieldEntry — allows pre-resolved overrides. */
|
|
69
|
+
type TBuildFieldEntryOpts = Partial<Pick<TFieldEvaluated, "name" | "type" | "component" | "optional" | "disabled" | "hidden" | "readonly">>;
|
|
70
|
+
/**
|
|
71
|
+
* Dynamic field resolver — extends static resolution with `new Function` compilation
|
|
72
|
+
* for `ui.fn.*` annotation keys.
|
|
73
|
+
*
|
|
74
|
+
* Install via `installDynamicResolver()` from the package index.
|
|
75
|
+
*/
|
|
76
|
+
declare class DynamicFieldResolver implements FieldResolver {
|
|
77
|
+
resolveFieldProp<T>(prop: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record<string, unknown>, opts?: TResolveOptions<T>): T | undefined;
|
|
78
|
+
resolveFormProp<T>(type: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record<string, unknown>, opts?: TResolveOptions<T>): T | undefined;
|
|
79
|
+
hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
|
|
80
|
+
private resolveAttrFns;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds a `TFieldEvaluated` entry and returns a full scope containing it.
|
|
84
|
+
*
|
|
85
|
+
* Implements the dual-scope pattern:
|
|
86
|
+
* 1. Resolve constraints (disabled/hidden/readonly) from `baseScope`
|
|
87
|
+
* 2. Assemble the entry object
|
|
88
|
+
* 3. Build full scope (`{ ...baseScope, entry }`)
|
|
89
|
+
* 4. Resolve options into the entry using the full scope
|
|
90
|
+
*/
|
|
91
|
+
declare function buildFieldEntry(prop: TAtscriptAnnotatedType, baseScope: TFnScope, path: string, opts?: TBuildFieldEntryOpts): TFnScope;
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/runtime/validator-plugin.d.ts
|
|
94
|
+
/** Per-call context passed via `validator.validate(value, safe, context)`. */
|
|
95
|
+
interface TValidatorContext {
|
|
96
|
+
data: Record<string, unknown>;
|
|
97
|
+
context: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Creates an ATScript validator plugin that processes `@ui.form.validate` annotations
|
|
101
|
+
* using compiled function strings.
|
|
102
|
+
*
|
|
103
|
+
* Usage:
|
|
104
|
+
* const plugin = uiFnsValidatorPlugin()
|
|
105
|
+
* const validator = new Validator(field.prop, { plugins: [plugin] })
|
|
106
|
+
* validator.validate(value, true, { data: formData, context })
|
|
107
|
+
*/
|
|
108
|
+
declare function uiFnsValidatorPlugin(): TValidatorPlugin;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/index.d.ts
|
|
111
|
+
/**
|
|
112
|
+
* Installs the dynamic field resolver into @atscript/ui.
|
|
113
|
+
* Call this once at app startup to enable `ui.fn.*` annotation resolution
|
|
114
|
+
* and `@ui.form.validate` custom validator strings.
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { installDynamicResolver } from '@atscript/ui-fns'
|
|
118
|
+
* installDynamicResolver()
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function installDynamicResolver(): void;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { DynamicFieldResolver, type TBuildFieldEntryOpts, type TComputed, type TFieldEvaluated, type TFnScope, type TValidatorContext, buildFieldEntry, compileFieldFn, compileTopFn, compileValidatorFn, installDynamicResolver, uiFnsValidatorPlugin };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { META_READONLY, UI_FORM_COMPONENT, UI_FORM_DISABLED, UI_FORM_FN_ATTR, UI_FORM_FN_DISABLED, UI_FORM_FN_HIDDEN, UI_FORM_FN_PREFIX, UI_FORM_FN_READONLY, UI_FORM_HIDDEN, UI_FORM_TYPE, UI_FORM_VALIDATE, UI_TABLE_FN_ATTR, UI_TABLE_FN_PREFIX, UI_TYPE, asArray, getFieldMeta, resolveFieldProp, resolveOptions, resolveStatic, setDefaultValidatorPlugins, setResolver } from "@atscript/ui";
|
|
2
|
+
import { FNPool } from "@prostojs/deserialize-fn";
|
|
3
|
+
//#region src/runtime/fn-compiler.ts
|
|
4
|
+
const pool = new FNPool();
|
|
5
|
+
/**
|
|
6
|
+
* Compiles a field-level function string from a `@ui.form.fn.*` / `@ui.table.fn.*` annotation
|
|
7
|
+
* into a callable function. Uses FNPool for caching.
|
|
8
|
+
*
|
|
9
|
+
* The function string should be an arrow or regular function expression:
|
|
10
|
+
* `"(v, data, ctx, entry) => !data.firstName"`
|
|
11
|
+
*
|
|
12
|
+
* The compiled function receives a single TFnScope object:
|
|
13
|
+
* `{ v, data, context, entry }`
|
|
14
|
+
*/
|
|
15
|
+
function compileFieldFn(fnStr) {
|
|
16
|
+
const code = `return (${fnStr})(v, data, context, entry)`;
|
|
17
|
+
return pool.getFn(code);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compiles a form-level function string from a `@ui.form.fn.title` or similar annotation.
|
|
21
|
+
*
|
|
22
|
+
* The function string should be:
|
|
23
|
+
* `"(data, ctx) => someExpression"`
|
|
24
|
+
*
|
|
25
|
+
* The compiled function receives a single TFnScope object:
|
|
26
|
+
* `{ data, context }`
|
|
27
|
+
*/
|
|
28
|
+
function compileTopFn(fnStr) {
|
|
29
|
+
const code = `return (${fnStr})(data, context)`;
|
|
30
|
+
return pool.getFn(code);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compiles a validator function string.
|
|
34
|
+
* Delegates to `compileFieldFn` with a narrowed return type.
|
|
35
|
+
*
|
|
36
|
+
* Returns `true` for valid, or a string error message for invalid.
|
|
37
|
+
*/
|
|
38
|
+
function compileValidatorFn(fnStr) {
|
|
39
|
+
return compileFieldFn(fnStr);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/runtime/dynamic-resolver.ts
|
|
43
|
+
/** Fn keys that store `[{ name, fn }]` arrays rather than a single fn string. */
|
|
44
|
+
const ATTR_FN_KEYS = new Set([UI_FORM_FN_ATTR, UI_TABLE_FN_ATTR]);
|
|
45
|
+
/**
|
|
46
|
+
* Dynamic field resolver — extends static resolution with `new Function` compilation
|
|
47
|
+
* for `ui.fn.*` annotation keys.
|
|
48
|
+
*
|
|
49
|
+
* Install via `installDynamicResolver()` from the package index.
|
|
50
|
+
*/
|
|
51
|
+
var DynamicFieldResolver = class {
|
|
52
|
+
resolveFieldProp(prop, fnKey, staticKey, scope, opts) {
|
|
53
|
+
if (ATTR_FN_KEYS.has(fnKey)) return this.resolveAttrFns(prop, fnKey, scope);
|
|
54
|
+
return resolveAnnotatedProp(prop.metadata, fnKey, staticKey, scope, compileFieldFn, opts);
|
|
55
|
+
}
|
|
56
|
+
resolveFormProp(type, fnKey, staticKey, scope, opts) {
|
|
57
|
+
return resolveAnnotatedProp(type.metadata, fnKey, staticKey, scope, compileTopFn, opts);
|
|
58
|
+
}
|
|
59
|
+
hasComputedAnnotations(prop) {
|
|
60
|
+
for (const key of prop.metadata.keys()) {
|
|
61
|
+
const k = key;
|
|
62
|
+
if (k.startsWith(UI_FORM_FN_PREFIX) || k.startsWith(UI_TABLE_FN_PREFIX)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
resolveAttrFns(prop, fnKey, scope) {
|
|
67
|
+
const fnAttrs = prop.metadata.get(fnKey);
|
|
68
|
+
if (!fnAttrs) return void 0;
|
|
69
|
+
const result = {};
|
|
70
|
+
let hasAttrs = false;
|
|
71
|
+
for (const item of asArray(fnAttrs)) if (typeof item === "object" && item !== null && "name" in item && "fn" in item) {
|
|
72
|
+
const { name, fn } = item;
|
|
73
|
+
result[name] = compileFieldFn(fn)(scope);
|
|
74
|
+
hasAttrs = true;
|
|
75
|
+
}
|
|
76
|
+
return hasAttrs ? result : void 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function resolveAnnotatedProp(metadata, fnKey, staticKey, scope, compileFn, opts) {
|
|
80
|
+
const fnStr = metadata.get(fnKey);
|
|
81
|
+
if (typeof fnStr === "string") return compileFn(fnStr)(scope);
|
|
82
|
+
return resolveStatic(metadata, staticKey, opts);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds a `TFieldEvaluated` entry and returns a full scope containing it.
|
|
86
|
+
*
|
|
87
|
+
* Implements the dual-scope pattern:
|
|
88
|
+
* 1. Resolve constraints (disabled/hidden/readonly) from `baseScope`
|
|
89
|
+
* 2. Assemble the entry object
|
|
90
|
+
* 3. Build full scope (`{ ...baseScope, entry }`)
|
|
91
|
+
* 4. Resolve options into the entry using the full scope
|
|
92
|
+
*/
|
|
93
|
+
function buildFieldEntry(prop, baseScope, path, opts) {
|
|
94
|
+
const boolOpts = { staticAsBoolean: true };
|
|
95
|
+
const scopeAsRecord = baseScope;
|
|
96
|
+
const entry = {
|
|
97
|
+
field: path,
|
|
98
|
+
type: opts?.type ?? getFieldMeta(prop, UI_FORM_TYPE) ?? getFieldMeta(prop, UI_TYPE) ?? "text",
|
|
99
|
+
component: opts?.component ?? getFieldMeta(prop, UI_FORM_COMPONENT),
|
|
100
|
+
name: opts?.name ?? path.slice(path.lastIndexOf(".") + 1),
|
|
101
|
+
optional: opts?.optional ?? prop.optional,
|
|
102
|
+
disabled: opts?.disabled ?? resolveFieldProp(prop, UI_FORM_FN_DISABLED, UI_FORM_DISABLED, scopeAsRecord, boolOpts),
|
|
103
|
+
hidden: opts?.hidden ?? resolveFieldProp(prop, UI_FORM_FN_HIDDEN, UI_FORM_HIDDEN, scopeAsRecord, boolOpts),
|
|
104
|
+
readonly: opts?.readonly ?? resolveFieldProp(prop, UI_FORM_FN_READONLY, META_READONLY, scopeAsRecord, boolOpts)
|
|
105
|
+
};
|
|
106
|
+
const scope = {
|
|
107
|
+
...baseScope,
|
|
108
|
+
entry
|
|
109
|
+
};
|
|
110
|
+
entry.options = resolveOptions(prop, scope);
|
|
111
|
+
return scope;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/runtime/validator-plugin.ts
|
|
115
|
+
/**
|
|
116
|
+
* Creates an ATScript validator plugin that processes `@ui.form.validate` annotations
|
|
117
|
+
* using compiled function strings.
|
|
118
|
+
*
|
|
119
|
+
* Usage:
|
|
120
|
+
* const plugin = uiFnsValidatorPlugin()
|
|
121
|
+
* const validator = new Validator(field.prop, { plugins: [plugin] })
|
|
122
|
+
* validator.validate(value, true, { data: formData, context })
|
|
123
|
+
*/
|
|
124
|
+
function uiFnsValidatorPlugin() {
|
|
125
|
+
return (ctx, def, value) => {
|
|
126
|
+
const hasValidators = getFieldMeta(def, UI_FORM_VALIDATE);
|
|
127
|
+
if (!hasValidators) return void 0;
|
|
128
|
+
const fnsCtx = ctx.context;
|
|
129
|
+
const scope = buildFieldEntry(def, {
|
|
130
|
+
v: value,
|
|
131
|
+
data: fnsCtx?.data ?? {},
|
|
132
|
+
context: fnsCtx?.context ?? {},
|
|
133
|
+
entry: void 0
|
|
134
|
+
}, ctx.path);
|
|
135
|
+
const fns = asArray(hasValidators);
|
|
136
|
+
for (const fnStr of fns) {
|
|
137
|
+
if (typeof fnStr !== "string") continue;
|
|
138
|
+
const result = compileValidatorFn(fnStr)(scope);
|
|
139
|
+
if (result !== true) {
|
|
140
|
+
ctx.error(typeof result === "string" ? result : "Validation failed");
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/index.ts
|
|
148
|
+
/**
|
|
149
|
+
* Installs the dynamic field resolver into @atscript/ui.
|
|
150
|
+
* Call this once at app startup to enable `ui.fn.*` annotation resolution
|
|
151
|
+
* and `@ui.form.validate` custom validator strings.
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { installDynamicResolver } from '@atscript/ui-fns'
|
|
155
|
+
* installDynamicResolver()
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
function installDynamicResolver() {
|
|
159
|
+
setResolver(new DynamicFieldResolver());
|
|
160
|
+
setDefaultValidatorPlugins([uiFnsValidatorPlugin()]);
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { DynamicFieldResolver, buildFieldEntry, compileFieldFn, compileTopFn, compileValidatorFn, installDynamicResolver, uiFnsValidatorPlugin };
|
package/dist/plugin.cjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
let _atscript_core = require("@atscript/core");
|
|
2
|
+
//#region src/plugin/annotations.ts
|
|
3
|
+
/**
|
|
4
|
+
* Validates a function string by attempting to compile it with `new Function`.
|
|
5
|
+
* Used by `ui.form.fn.*` / `ui.table.fn.*` and `ui.form.validate` annotation validate hooks.
|
|
6
|
+
*/
|
|
7
|
+
function validateFnString(fnStr, range) {
|
|
8
|
+
try {
|
|
9
|
+
new Function("v", "data", "context", "entry", `return (${fnStr})(v, data, context, entry)`);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return [{
|
|
12
|
+
severity: 1,
|
|
13
|
+
message: `Invalid function string: ${error.message}`,
|
|
14
|
+
range
|
|
15
|
+
}];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function makeFnAnnotation(description, mode) {
|
|
19
|
+
return new _atscript_core.AnnotationSpec({
|
|
20
|
+
description,
|
|
21
|
+
nodeType: mode === "field" ? ["prop", "type"] : ["interface", "type"],
|
|
22
|
+
argument: {
|
|
23
|
+
name: "fn",
|
|
24
|
+
type: "string",
|
|
25
|
+
description: mode === "field" ? "JS function string: (value, data, context, entry) => result" : "JS function string: (data, context) => result"
|
|
26
|
+
},
|
|
27
|
+
validate: validateFirstArg
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Shared validate hook: validates the fn string at args[0]. */
|
|
31
|
+
function validateFirstArg(_token, args) {
|
|
32
|
+
if (args[0]) return validateFnString(args[0].text, args[0].range);
|
|
33
|
+
}
|
|
34
|
+
const fnAnnotation = (description) => makeFnAnnotation(description, "field");
|
|
35
|
+
const fnTopAnnotation = (description) => makeFnAnnotation(description, "top");
|
|
36
|
+
const TABLE_ROW_SCOPE_DOC = "Receives `{ row, ctx }` where `row` is the current row's data object and `ctx` carries table-level context (minimum keys: `searchTerm`, `filters`, `sorters`, `rowIndex`). Per-row+cell scope only — every expression must be meaningful when applied to a single cell.";
|
|
37
|
+
function tableFnAnnotation(description) {
|
|
38
|
+
return new _atscript_core.AnnotationSpec({
|
|
39
|
+
description: `${description}\n\n${TABLE_ROW_SCOPE_DOC}`,
|
|
40
|
+
nodeType: ["prop", "type"],
|
|
41
|
+
argument: {
|
|
42
|
+
name: "fn",
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "JS function string evaluated against the per-row scope `{ row, ctx }`."
|
|
45
|
+
},
|
|
46
|
+
validate: validateFirstArg
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const fnAttrSpec = new _atscript_core.AnnotationSpec({
|
|
50
|
+
description: "Computed custom attribute/prop. Name is the attribute/prop name, fn returns the value.",
|
|
51
|
+
nodeType: ["prop", "type"],
|
|
52
|
+
multiple: true,
|
|
53
|
+
mergeStrategy: "replace",
|
|
54
|
+
argument: [{
|
|
55
|
+
name: "name",
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Attribute/prop name (e.g., \"data-testid\", \"variant\", \"size\")"
|
|
58
|
+
}, {
|
|
59
|
+
name: "fn",
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "JS function string: (value, data, context, entry) => any"
|
|
62
|
+
}],
|
|
63
|
+
validate(_token, args) {
|
|
64
|
+
if (args[1]) return validateFnString(args[1].text, args[1].range);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const tableFnAttrSpec = new _atscript_core.AnnotationSpec({
|
|
68
|
+
description: "Per-row computed attribute/prop applied to the rendered `<td>`. Name is the attribute/prop name, fn returns the value.\n\n" + TABLE_ROW_SCOPE_DOC,
|
|
69
|
+
nodeType: ["prop", "type"],
|
|
70
|
+
multiple: true,
|
|
71
|
+
mergeStrategy: "replace",
|
|
72
|
+
argument: [{
|
|
73
|
+
name: "name",
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Attribute/prop name (e.g., \"title\", \"data-row\", \"aria-label\")"
|
|
76
|
+
}, {
|
|
77
|
+
name: "fn",
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "JS function string evaluated against the per-row scope `{ row, ctx }`."
|
|
80
|
+
}],
|
|
81
|
+
validate(_token, args) {
|
|
82
|
+
if (args[1]) return validateFnString(args[1].text, args[1].range);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Annotation specs for dynamic computed annotations and `ui.form.validate`.
|
|
87
|
+
*
|
|
88
|
+
* Registered as atscript annotations via the `uiFnsPlugin()`.
|
|
89
|
+
* Static `@ui.*` annotations and primitives are provided by `@atscript/ui/plugin`.
|
|
90
|
+
*/
|
|
91
|
+
const uiFnsAnnotations = { ui: {
|
|
92
|
+
form: {
|
|
93
|
+
validate: new _atscript_core.AnnotationSpec({
|
|
94
|
+
description: "Custom JS validator function string. Returns true for pass, or an error message string.",
|
|
95
|
+
nodeType: ["prop", "type"],
|
|
96
|
+
multiple: true,
|
|
97
|
+
mergeStrategy: "append",
|
|
98
|
+
argument: {
|
|
99
|
+
name: "fn",
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "JS function string: (value, data, context, entry) => boolean | string"
|
|
102
|
+
},
|
|
103
|
+
validate: validateFirstArg
|
|
104
|
+
}),
|
|
105
|
+
fn: {
|
|
106
|
+
title: fnTopAnnotation("Computed form title: (data, context) => string"),
|
|
107
|
+
submit: {
|
|
108
|
+
text: fnTopAnnotation("Computed submit button text: (data, context) => string"),
|
|
109
|
+
disabled: fnTopAnnotation("Computed submit disabled state: (data, context) => boolean")
|
|
110
|
+
},
|
|
111
|
+
label: fnAnnotation("Computed label: (value, data, context, entry) => string"),
|
|
112
|
+
description: fnAnnotation("Computed description: (value, data, context, entry) => string"),
|
|
113
|
+
hint: fnAnnotation("Computed hint: (value, data, context, entry) => string"),
|
|
114
|
+
placeholder: fnAnnotation("Computed placeholder: (value, data, context, entry) => string"),
|
|
115
|
+
disabled: fnAnnotation("Computed disabled state: (value, data, context, entry) => boolean"),
|
|
116
|
+
hidden: fnAnnotation("Computed hidden state: (value, data, context, entry) => boolean"),
|
|
117
|
+
readonly: fnAnnotation("Computed readonly state: (value, data, context, entry) => boolean"),
|
|
118
|
+
value: fnAnnotation("Computed default value: (value, data, context, entry) => any"),
|
|
119
|
+
classes: fnAnnotation("Computed CSS classes: (value, data, context, entry) => string | Record<string, boolean>"),
|
|
120
|
+
styles: fnAnnotation("Computed inline styles: (value, data, context, entry) => string | Record<string, string>"),
|
|
121
|
+
options: fnAnnotation("Computed select/radio options: (value, data, context, entry) => Array"),
|
|
122
|
+
attr: fnAttrSpec
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
table: { fn: {
|
|
126
|
+
attr: tableFnAttrSpec,
|
|
127
|
+
classes: tableFnAnnotation("Per-row computed CSS classes for the cell `<td>`: `(row, ctx) => string | Record<string, boolean>`"),
|
|
128
|
+
styles: tableFnAnnotation("Per-row computed inline styles for the cell `<td>`: `(row, ctx) => string | Record<string, string>`")
|
|
129
|
+
} }
|
|
130
|
+
} };
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/plugin.ts
|
|
133
|
+
/**
|
|
134
|
+
* ATScript plugin that registers `ui.form.fn.*` / `ui.table.fn.*` computed annotations and `ui.form.validate`.
|
|
135
|
+
*
|
|
136
|
+
* Static `@ui.*` annotations and UI primitives are provided by `@atscript/ui/plugin`.
|
|
137
|
+
*
|
|
138
|
+
* Install in your `atscript.config.ts`:
|
|
139
|
+
* ```ts
|
|
140
|
+
* import uiFnsPlugin from '@atscript/ui-fns/plugin'
|
|
141
|
+
*
|
|
142
|
+
* export default {
|
|
143
|
+
* plugins: [uiFnsPlugin()],
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
function uiFnsPlugin() {
|
|
148
|
+
return {
|
|
149
|
+
name: "ui-fns",
|
|
150
|
+
config() {
|
|
151
|
+
return { annotations: uiFnsAnnotations };
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
module.exports = uiFnsPlugin;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TAtscriptPlugin } from "@atscript/core";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* ATScript plugin that registers `ui.form.fn.*` / `ui.table.fn.*` computed annotations and `ui.form.validate`.
|
|
6
|
+
*
|
|
7
|
+
* Static `@ui.*` annotations and UI primitives are provided by `@atscript/ui/plugin`.
|
|
8
|
+
*
|
|
9
|
+
* Install in your `atscript.config.ts`:
|
|
10
|
+
* ```ts
|
|
11
|
+
* import uiFnsPlugin from '@atscript/ui-fns/plugin'
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* plugins: [uiFnsPlugin()],
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare function uiFnsPlugin(): TAtscriptPlugin;
|
|
19
|
+
export = uiFnsPlugin;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TAtscriptPlugin } from "@atscript/core";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* ATScript plugin that registers `ui.form.fn.*` / `ui.table.fn.*` computed annotations and `ui.form.validate`.
|
|
6
|
+
*
|
|
7
|
+
* Static `@ui.*` annotations and UI primitives are provided by `@atscript/ui/plugin`.
|
|
8
|
+
*
|
|
9
|
+
* Install in your `atscript.config.ts`:
|
|
10
|
+
* ```ts
|
|
11
|
+
* import uiFnsPlugin from '@atscript/ui-fns/plugin'
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* plugins: [uiFnsPlugin()],
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare function uiFnsPlugin(): TAtscriptPlugin;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { uiFnsPlugin as default };
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { AnnotationSpec } from "@atscript/core";
|
|
2
|
+
//#region src/plugin/annotations.ts
|
|
3
|
+
/**
|
|
4
|
+
* Validates a function string by attempting to compile it with `new Function`.
|
|
5
|
+
* Used by `ui.form.fn.*` / `ui.table.fn.*` and `ui.form.validate` annotation validate hooks.
|
|
6
|
+
*/
|
|
7
|
+
function validateFnString(fnStr, range) {
|
|
8
|
+
try {
|
|
9
|
+
new Function("v", "data", "context", "entry", `return (${fnStr})(v, data, context, entry)`);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
return [{
|
|
12
|
+
severity: 1,
|
|
13
|
+
message: `Invalid function string: ${error.message}`,
|
|
14
|
+
range
|
|
15
|
+
}];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function makeFnAnnotation(description, mode) {
|
|
19
|
+
return new AnnotationSpec({
|
|
20
|
+
description,
|
|
21
|
+
nodeType: mode === "field" ? ["prop", "type"] : ["interface", "type"],
|
|
22
|
+
argument: {
|
|
23
|
+
name: "fn",
|
|
24
|
+
type: "string",
|
|
25
|
+
description: mode === "field" ? "JS function string: (value, data, context, entry) => result" : "JS function string: (data, context) => result"
|
|
26
|
+
},
|
|
27
|
+
validate: validateFirstArg
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Shared validate hook: validates the fn string at args[0]. */
|
|
31
|
+
function validateFirstArg(_token, args) {
|
|
32
|
+
if (args[0]) return validateFnString(args[0].text, args[0].range);
|
|
33
|
+
}
|
|
34
|
+
const fnAnnotation = (description) => makeFnAnnotation(description, "field");
|
|
35
|
+
const fnTopAnnotation = (description) => makeFnAnnotation(description, "top");
|
|
36
|
+
const TABLE_ROW_SCOPE_DOC = "Receives `{ row, ctx }` where `row` is the current row's data object and `ctx` carries table-level context (minimum keys: `searchTerm`, `filters`, `sorters`, `rowIndex`). Per-row+cell scope only — every expression must be meaningful when applied to a single cell.";
|
|
37
|
+
function tableFnAnnotation(description) {
|
|
38
|
+
return new AnnotationSpec({
|
|
39
|
+
description: `${description}\n\n${TABLE_ROW_SCOPE_DOC}`,
|
|
40
|
+
nodeType: ["prop", "type"],
|
|
41
|
+
argument: {
|
|
42
|
+
name: "fn",
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "JS function string evaluated against the per-row scope `{ row, ctx }`."
|
|
45
|
+
},
|
|
46
|
+
validate: validateFirstArg
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const fnAttrSpec = new AnnotationSpec({
|
|
50
|
+
description: "Computed custom attribute/prop. Name is the attribute/prop name, fn returns the value.",
|
|
51
|
+
nodeType: ["prop", "type"],
|
|
52
|
+
multiple: true,
|
|
53
|
+
mergeStrategy: "replace",
|
|
54
|
+
argument: [{
|
|
55
|
+
name: "name",
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Attribute/prop name (e.g., \"data-testid\", \"variant\", \"size\")"
|
|
58
|
+
}, {
|
|
59
|
+
name: "fn",
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "JS function string: (value, data, context, entry) => any"
|
|
62
|
+
}],
|
|
63
|
+
validate(_token, args) {
|
|
64
|
+
if (args[1]) return validateFnString(args[1].text, args[1].range);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const tableFnAttrSpec = new AnnotationSpec({
|
|
68
|
+
description: "Per-row computed attribute/prop applied to the rendered `<td>`. Name is the attribute/prop name, fn returns the value.\n\n" + TABLE_ROW_SCOPE_DOC,
|
|
69
|
+
nodeType: ["prop", "type"],
|
|
70
|
+
multiple: true,
|
|
71
|
+
mergeStrategy: "replace",
|
|
72
|
+
argument: [{
|
|
73
|
+
name: "name",
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Attribute/prop name (e.g., \"title\", \"data-row\", \"aria-label\")"
|
|
76
|
+
}, {
|
|
77
|
+
name: "fn",
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "JS function string evaluated against the per-row scope `{ row, ctx }`."
|
|
80
|
+
}],
|
|
81
|
+
validate(_token, args) {
|
|
82
|
+
if (args[1]) return validateFnString(args[1].text, args[1].range);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Annotation specs for dynamic computed annotations and `ui.form.validate`.
|
|
87
|
+
*
|
|
88
|
+
* Registered as atscript annotations via the `uiFnsPlugin()`.
|
|
89
|
+
* Static `@ui.*` annotations and primitives are provided by `@atscript/ui/plugin`.
|
|
90
|
+
*/
|
|
91
|
+
const uiFnsAnnotations = { ui: {
|
|
92
|
+
form: {
|
|
93
|
+
validate: new AnnotationSpec({
|
|
94
|
+
description: "Custom JS validator function string. Returns true for pass, or an error message string.",
|
|
95
|
+
nodeType: ["prop", "type"],
|
|
96
|
+
multiple: true,
|
|
97
|
+
mergeStrategy: "append",
|
|
98
|
+
argument: {
|
|
99
|
+
name: "fn",
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "JS function string: (value, data, context, entry) => boolean | string"
|
|
102
|
+
},
|
|
103
|
+
validate: validateFirstArg
|
|
104
|
+
}),
|
|
105
|
+
fn: {
|
|
106
|
+
title: fnTopAnnotation("Computed form title: (data, context) => string"),
|
|
107
|
+
submit: {
|
|
108
|
+
text: fnTopAnnotation("Computed submit button text: (data, context) => string"),
|
|
109
|
+
disabled: fnTopAnnotation("Computed submit disabled state: (data, context) => boolean")
|
|
110
|
+
},
|
|
111
|
+
label: fnAnnotation("Computed label: (value, data, context, entry) => string"),
|
|
112
|
+
description: fnAnnotation("Computed description: (value, data, context, entry) => string"),
|
|
113
|
+
hint: fnAnnotation("Computed hint: (value, data, context, entry) => string"),
|
|
114
|
+
placeholder: fnAnnotation("Computed placeholder: (value, data, context, entry) => string"),
|
|
115
|
+
disabled: fnAnnotation("Computed disabled state: (value, data, context, entry) => boolean"),
|
|
116
|
+
hidden: fnAnnotation("Computed hidden state: (value, data, context, entry) => boolean"),
|
|
117
|
+
readonly: fnAnnotation("Computed readonly state: (value, data, context, entry) => boolean"),
|
|
118
|
+
value: fnAnnotation("Computed default value: (value, data, context, entry) => any"),
|
|
119
|
+
classes: fnAnnotation("Computed CSS classes: (value, data, context, entry) => string | Record<string, boolean>"),
|
|
120
|
+
styles: fnAnnotation("Computed inline styles: (value, data, context, entry) => string | Record<string, string>"),
|
|
121
|
+
options: fnAnnotation("Computed select/radio options: (value, data, context, entry) => Array"),
|
|
122
|
+
attr: fnAttrSpec
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
table: { fn: {
|
|
126
|
+
attr: tableFnAttrSpec,
|
|
127
|
+
classes: tableFnAnnotation("Per-row computed CSS classes for the cell `<td>`: `(row, ctx) => string | Record<string, boolean>`"),
|
|
128
|
+
styles: tableFnAnnotation("Per-row computed inline styles for the cell `<td>`: `(row, ctx) => string | Record<string, string>`")
|
|
129
|
+
} }
|
|
130
|
+
} };
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/plugin.ts
|
|
133
|
+
/**
|
|
134
|
+
* ATScript plugin that registers `ui.form.fn.*` / `ui.table.fn.*` computed annotations and `ui.form.validate`.
|
|
135
|
+
*
|
|
136
|
+
* Static `@ui.*` annotations and UI primitives are provided by `@atscript/ui/plugin`.
|
|
137
|
+
*
|
|
138
|
+
* Install in your `atscript.config.ts`:
|
|
139
|
+
* ```ts
|
|
140
|
+
* import uiFnsPlugin from '@atscript/ui-fns/plugin'
|
|
141
|
+
*
|
|
142
|
+
* export default {
|
|
143
|
+
* plugins: [uiFnsPlugin()],
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
function uiFnsPlugin() {
|
|
148
|
+
return {
|
|
149
|
+
name: "ui-fns",
|
|
150
|
+
config() {
|
|
151
|
+
return { annotations: uiFnsAnnotations };
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { uiFnsPlugin as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atscript/ui-fns",
|
|
3
|
+
"version": "0.1.58",
|
|
4
|
+
"description": "Dynamic fn-compiled field properties for @atscript/ui (opt-in, uses new Function)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"annotations",
|
|
7
|
+
"atscript",
|
|
8
|
+
"dynamic",
|
|
9
|
+
"expressions",
|
|
10
|
+
"form",
|
|
11
|
+
"metadata",
|
|
12
|
+
"type-driven",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/moostjs/atscript-ui/tree/main/packages/ui-fns#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/moostjs/atscript-ui/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Artem Maltsev <artem@maltsev.nl>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/moostjs/atscript-ui.git",
|
|
24
|
+
"directory": "packages/ui-fns"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/index.mjs",
|
|
31
|
+
"types": "dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
},
|
|
38
|
+
"./plugin": {
|
|
39
|
+
"types": "./dist/plugin.d.mts",
|
|
40
|
+
"import": "./dist/plugin.mjs",
|
|
41
|
+
"require": "./dist/plugin.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@prostojs/deserialize-fn": "^0.0.5",
|
|
50
|
+
"@atscript/ui": "^0.1.58"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@atscript/core": "^0.1.54",
|
|
54
|
+
"@atscript/typescript": "^0.1.54",
|
|
55
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@atscript/core": "^0.1.54",
|
|
59
|
+
"@atscript/typescript": "^0.1.54"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "vp pack",
|
|
63
|
+
"dev": "vp pack --watch",
|
|
64
|
+
"test": "vp test",
|
|
65
|
+
"check": "vp check"
|
|
66
|
+
}
|
|
67
|
+
}
|