@ata-project/unplugin 0.4.0 → 0.6.0
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 +13 -2
- package/package.json +3 -2
- package/src/compile-away.js +81 -11
- package/src/core.js +27 -0
package/README.md
CHANGED
|
@@ -177,6 +177,11 @@ library build is 122.5 KB gzipped without it and 16.7 KB with it (gzip level 9),
|
|
|
177
177
|
ata-validator 1.39.2. Across the 977 schemas of SchemaStore, 725 can be compiled
|
|
178
178
|
away this way.
|
|
179
179
|
|
|
180
|
+
Where the code only ever calls `isValidObject()` or `isValidJSON()`, the plugin
|
|
181
|
+
uses a smaller wrapper without the error machinery (needs ata-validator 1.40.0):
|
|
182
|
+
a small app that only asks for a boolean bundles to 2.6 KB gzipped, against 12.1 KB with
|
|
183
|
+
the full wrapper and 102.0 KB for the runtime, on 0.5.0 with ata-validator 1.40.0.
|
|
184
|
+
|
|
180
185
|
The replacement answers `validate()`, `isValidObject()`, `validateJSON()` and
|
|
181
186
|
`isValidJSON()` as a `Validator` with default options does: the same verdicts,
|
|
182
187
|
defaults filled in, `data` on success, and the same errors, enriched the same
|
|
@@ -190,14 +195,20 @@ otherwise:
|
|
|
190
195
|
- `Validator` is a named import from `ata-validator`;
|
|
191
196
|
- the schema argument is an object literal, a top-level `const` bound to one,
|
|
192
197
|
the default import of a relative `.json` file, or `defineSchema(...)` around
|
|
193
|
-
one of those;
|
|
198
|
+
one of those; calls to the `t` builder from `ata-validator/t` whose arguments are
|
|
199
|
+
such values are run at build time, since the builder returns plain JSON Schema,
|
|
200
|
+
and are marked pure so a schema only the replaced call used leaves the bundle
|
|
201
|
+
with the builder (`t.refine` and `t.recursive`, which take a function, stay on
|
|
202
|
+
the runtime);
|
|
194
203
|
- there is no second argument, or it is `{ useDefaults: false }` (or
|
|
195
204
|
`{ useDefaults: true }`), written as a literal or a top-level `const`; that
|
|
196
205
|
one option needs ata-validator 1.37.0, and older versions keep such calls on
|
|
197
206
|
the runtime;
|
|
198
207
|
- the result goes into a `const` that is not exported and is only used as
|
|
199
208
|
`name.validate(...)`, `name.isValidObject(...)`, `name.validateJSON(...)` or
|
|
200
|
-
`name.isValidJSON(...)`;
|
|
209
|
+
`name.isValidJSON(...)`; the call may be wrapped in `withKeywords(...)` from
|
|
210
|
+
`@ata-project/keywords`, which then registers its keywords on the compiled
|
|
211
|
+
validator as it does on a `Validator` (needs ata-validator 1.40.0);
|
|
201
212
|
- ata-validator can compile the schema to the same results. It declines custom
|
|
202
213
|
`errorMessage`s and shapes its code generator cannot express, which the
|
|
203
214
|
runtime answers with its interpreted engine.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ata-project/unplugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Build-time JSON Schema for Vite, Webpack, Rollup, Rolldown, esbuild and Rspack. compileAway takes the runtime compiler out of the bundle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -77,8 +77,9 @@
|
|
|
77
77
|
"unplugin": "^3.3.0"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
+
"@ata-project/keywords": "^0.3.3",
|
|
80
81
|
"@rspack/core": "^1.7.0",
|
|
81
|
-
"ata-validator": "^1.
|
|
82
|
+
"ata-validator": "^1.40.0",
|
|
82
83
|
"esbuild": "^0.28.0",
|
|
83
84
|
"rolldown": "^1.2.8",
|
|
84
85
|
"rollup": "^4.60.0",
|
package/src/compile-away.js
CHANGED
|
@@ -29,6 +29,9 @@ import MagicString from 'magic-string'
|
|
|
29
29
|
|
|
30
30
|
const METHODS = new Set(['validate', 'isValidObject', 'validateJSON', 'isValidJSON'])
|
|
31
31
|
const NOT_STATIC = Symbol('not static')
|
|
32
|
+
// The builders of ata-validator/t that take only values. refine and
|
|
33
|
+
// recursive take a function and stay on the runtime.
|
|
34
|
+
const T_PURE = new Set(['string', 'number', 'integer', 'boolean', 'null', 'literal', 'const', 'enum', 'array', 'tuple', 'record', 'object', 'optional', 'union', 'intersect', 'ref', 'any', 'unknown', 'never', 'pick', 'omit', 'partial', 'required', 'composite'])
|
|
32
35
|
|
|
33
36
|
// Keys that hold child nodes. Types are left out on purpose: they are erased
|
|
34
37
|
// before anything runs, so a name used in a type annotation says nothing about
|
|
@@ -134,9 +137,30 @@ function staticValue(node, ctx, seen = new Set()) {
|
|
|
134
137
|
seen.delete(node.name)
|
|
135
138
|
return v
|
|
136
139
|
}
|
|
137
|
-
case 'CallExpression':
|
|
140
|
+
case 'CallExpression': {
|
|
138
141
|
if (node.callee.type === 'Identifier' && node.callee.name === ctx.defineSchema && node.arguments.length === 1) return staticValue(node.arguments[0], ctx, seen)
|
|
142
|
+
// `t.object(...)` and the rest of the builder from ata-validator/t. It
|
|
143
|
+
// returns plain JSON Schema and holds no state, so a call whose
|
|
144
|
+
// arguments the build can read gives here what it gives at run time.
|
|
145
|
+
// The builders that take a function are left out.
|
|
146
|
+
const c = node.callee
|
|
147
|
+
if (ctx.t && ctx.tName && c.type === 'MemberExpression' && !c.computed && c.object.type === 'Identifier' &&
|
|
148
|
+
c.object.name === ctx.tName && ctx.declared.get(ctx.tName) === 1 && c.property.type === 'Identifier' && T_PURE.has(c.property.name) &&
|
|
149
|
+
typeof ctx.t[c.property.name] === 'function') {
|
|
150
|
+
const args = []
|
|
151
|
+
for (const a of node.arguments) {
|
|
152
|
+
if (a.type === 'SpreadElement') return NOT_STATIC
|
|
153
|
+
const v = staticValue(a, ctx, seen)
|
|
154
|
+
if (v === NOT_STATIC) return NOT_STATIC
|
|
155
|
+
args.push(v)
|
|
156
|
+
}
|
|
157
|
+
let v
|
|
158
|
+
try { v = ctx.t[c.property.name](...args) } catch { return NOT_STATIC }
|
|
159
|
+
ctx.tCalls.add(node)
|
|
160
|
+
return v
|
|
161
|
+
}
|
|
139
162
|
return NOT_STATIC
|
|
163
|
+
}
|
|
140
164
|
}
|
|
141
165
|
return NOT_STATIC
|
|
142
166
|
}
|
|
@@ -163,26 +187,44 @@ function constantsOf(ast, id) {
|
|
|
163
187
|
// `const name = new Validator(...)`, declared once, not exported, and used only
|
|
164
188
|
// as `name.<method>(...)` with a supported method.
|
|
165
189
|
function replaceableBinding(newExpr, ctx) {
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
let decl = newExpr.__parent
|
|
191
|
+
// `withKeywords(new Validator(schema))` from @ata-project/keywords: the
|
|
192
|
+
// wrapper takes the keywords' check the way a Validator does, so the inner
|
|
193
|
+
// call is replaced and withKeywords stays to register the check on it.
|
|
194
|
+
if (decl && decl.type === 'CallExpression' && ctx.withKeywords && ctx.extendChecks &&
|
|
195
|
+
decl.callee.type === 'Identifier' && decl.callee.name === ctx.withKeywords &&
|
|
196
|
+
ctx.declared.get(ctx.withKeywords) === 1 && decl.arguments.length === 1 && decl.arguments[0] === newExpr) {
|
|
197
|
+
decl = decl.__parent
|
|
198
|
+
if (!decl || decl.type !== 'VariableDeclarator' || decl.init !== newExpr.__parent) return null
|
|
199
|
+
}
|
|
200
|
+
if (!decl || decl.type !== 'VariableDeclarator' || decl.id.type !== 'Identifier') return null
|
|
201
|
+
if (decl.init !== newExpr && decl.init !== newExpr.__parent) return null
|
|
168
202
|
const list = decl.__parent
|
|
169
203
|
if (!list || list.type !== 'VariableDeclaration' || list.kind !== 'const') return null
|
|
170
204
|
if (list.__parent && list.__parent.type === 'ExportNamedDeclaration') return null
|
|
171
205
|
const name = decl.id.name
|
|
172
206
|
if (ctx.declared.get(name) !== 1) return null
|
|
207
|
+
const methods = new Set()
|
|
173
208
|
for (const r of ctx.refs) {
|
|
174
209
|
if (r.node.name !== name) continue
|
|
175
210
|
const p = r.parent
|
|
176
211
|
const member = p && (p.type === 'MemberExpression' || p.type === 'OptionalMemberExpression') && r.key === 'object' && !p.computed && p.property.type === 'Identifier' && METHODS.has(p.property.name)
|
|
177
212
|
const called = member && p.__parent && (p.__parent.type === 'CallExpression' || p.__parent.type === 'OptionalCallExpression') && p.__parent.callee === p
|
|
178
213
|
if (!called) return null
|
|
214
|
+
methods.add(p.property.name)
|
|
179
215
|
}
|
|
180
|
-
return name
|
|
216
|
+
return { name, methods }
|
|
181
217
|
}
|
|
182
218
|
|
|
183
|
-
|
|
219
|
+
// Code that only asks for a boolean never reads an error.
|
|
220
|
+
const VERDICT_METHODS = new Set(['isValidObject', 'isValidJSON'])
|
|
221
|
+
const verdictOnly = (methods) => [...methods].every((m) => VERDICT_METHODS.has(m))
|
|
222
|
+
|
|
223
|
+
// A verdict-only module hands out isValid alone, so the error function inside
|
|
224
|
+
// it is unreferenced and the bundler drops it.
|
|
225
|
+
function inlineModule(src, index, verdict) {
|
|
184
226
|
const body = src.split('\n').filter((l) => !/^export\s/.test(l)).join('\n')
|
|
185
|
-
return `const __ataCompiled${index} = (() => {\n${body}\nreturn { validate, isValid };\n})();\n`
|
|
227
|
+
return `const __ataCompiled${index} = (() => {\n${body}\nreturn ${verdict ? '{ isValid }' : '{ validate, isValid }'};\n})();\n`
|
|
186
228
|
}
|
|
187
229
|
|
|
188
230
|
// The options of `new Validator(schema, options)` as a third argument for
|
|
@@ -226,14 +268,33 @@ export function compileAway(code, id, ata) {
|
|
|
226
268
|
}
|
|
227
269
|
}
|
|
228
270
|
if (!validatorName) return null
|
|
271
|
+
let tName = null
|
|
272
|
+
for (const stmt of ast.program.body) {
|
|
273
|
+
if (stmt.type !== 'ImportDeclaration' || stmt.source.value !== 'ata-validator/t' || stmt.importKind === 'type') continue
|
|
274
|
+
for (const sp of stmt.specifiers) {
|
|
275
|
+
if (sp.type !== 'ImportSpecifier' || sp.importKind === 'type') continue
|
|
276
|
+
const imported = sp.imported.type === 'Identifier' ? sp.imported.name : sp.imported.value
|
|
277
|
+
if (imported === 't') tName = sp.local.name
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
let withKeywords = null
|
|
281
|
+
for (const stmt of ast.program.body) {
|
|
282
|
+
if (stmt.type !== 'ImportDeclaration' || stmt.source.value !== '@ata-project/keywords' || stmt.importKind === 'type') continue
|
|
283
|
+
for (const sp of stmt.specifiers) {
|
|
284
|
+
if (sp.type !== 'ImportSpecifier' || sp.importKind === 'type') continue
|
|
285
|
+
const imported = sp.imported.type === 'Identifier' ? sp.imported.name : sp.imported.value
|
|
286
|
+
if (imported === 'withKeywords') withKeywords = sp.local.name
|
|
287
|
+
}
|
|
288
|
+
}
|
|
229
289
|
|
|
230
290
|
const { declared, refs } = scan(ast)
|
|
231
291
|
if (declared.get(validatorName) !== 1) return null
|
|
232
|
-
const ctx = { declared, refs, constants: constantsOf(ast, id), defineSchema }
|
|
292
|
+
const ctx = { declared, refs, constants: constantsOf(ast, id), defineSchema, withKeywords, extendChecks: ata.compiledExtendChecks === true, t: ata.t || null, tName, tCalls: new Set() }
|
|
233
293
|
|
|
234
294
|
const s = new MagicString(code)
|
|
235
295
|
const modules = []
|
|
236
296
|
const done = new Set()
|
|
297
|
+
const wrappers = { full: false, verdict: false }
|
|
237
298
|
for (const r of refs) {
|
|
238
299
|
if (r.node.name !== validatorName) continue
|
|
239
300
|
const expr = r.parent
|
|
@@ -244,15 +305,19 @@ export function compileAway(code, id, ata) {
|
|
|
244
305
|
optionsArg = compiledOptionsArg(expr.arguments[1], ctx, supportedOptions)
|
|
245
306
|
if (optionsArg === NOT_STATIC) continue
|
|
246
307
|
}
|
|
247
|
-
|
|
308
|
+
const binding = replaceableBinding(expr, ctx)
|
|
309
|
+
if (!binding) continue
|
|
248
310
|
const schema = staticValue(expr.arguments[0], ctx)
|
|
249
311
|
if (schema === NOT_STATIC || schema === null || typeof schema !== 'object' || Array.isArray(schema)) continue
|
|
250
312
|
let src = null
|
|
251
313
|
try { src = compiledModuleFor(schema, { format: 'esm' }) } catch { src = null }
|
|
252
314
|
if (!src) continue
|
|
253
315
|
const index = modules.length
|
|
254
|
-
|
|
255
|
-
|
|
316
|
+
const verdict = ata.compiledVerdict === true && verdictOnly(binding.methods)
|
|
317
|
+
modules.push(inlineModule(src, index, verdict))
|
|
318
|
+
const wrap = verdict ? '__ataFromCompiledVerdict' : '__ataFromCompiled'
|
|
319
|
+
if (verdict) wrappers.verdict = true; else wrappers.full = true
|
|
320
|
+
s.overwrite(expr.start, expr.end, `${wrap}(__ataCompiled${index}, ${JSON.stringify(compiledSchemaFor(schema))}${optionsArg ? ', ' + optionsArg : ''})`)
|
|
256
321
|
done.add(expr)
|
|
257
322
|
}
|
|
258
323
|
if (done.size === 0) return null
|
|
@@ -278,7 +343,12 @@ export function compileAway(code, id, ata) {
|
|
|
278
343
|
const clause = [def ? text(def) : null, named.length ? `{ ${named.map(text).join(', ')} }` : null].filter(Boolean).join(', ')
|
|
279
344
|
s.overwrite(importDecl.start, importDecl.end, `import ${clause} from ${code.slice(importDecl.source.start, importDecl.source.end)}`)
|
|
280
345
|
}
|
|
281
|
-
|
|
346
|
+
// The builder calls read here are pure, and marked so: a schema constant
|
|
347
|
+
// only the replaced call used, and the builder with it, can then leave the
|
|
348
|
+
// bundle.
|
|
349
|
+
for (const call of ctx.tCalls) s.appendLeft(call.start, '/*#__PURE__*/ ')
|
|
350
|
+
if (wrappers.verdict) s.prepend(`import { fromCompiledVerdict as __ataFromCompiledVerdict } from 'ata-validator/compiled-verdict';\n`)
|
|
351
|
+
if (wrappers.full) s.prepend(`import { fromCompiled as __ataFromCompiled } from 'ata-validator/compiled';\n`)
|
|
282
352
|
s.appendLeft(lastImportEnd, '\n' + modules.join(''))
|
|
283
353
|
return { code: s.toString(), map: s.generateMap({ hires: true, source: id, includeContent: true }), replaced: done.size }
|
|
284
354
|
}
|
package/src/core.js
CHANGED
|
@@ -64,6 +64,30 @@ async function loadAta() {
|
|
|
64
64
|
// compiledModuleFor exists from ata-validator 1.35.0; compile-away needs it
|
|
65
65
|
// and is skipped with a warning when it is missing.
|
|
66
66
|
const canCompileAway = typeof build.compiledModuleFor === 'function' && typeof build.compiledSchemaFor === 'function'
|
|
67
|
+
// The verdict-only wrapper, from ata-validator 1.40.0: code that never reads
|
|
68
|
+
// errors gets it and the error pipeline stays out of the bundle.
|
|
69
|
+
let compiledVerdict = false
|
|
70
|
+
if (canCompileAway) {
|
|
71
|
+
try { compiledVerdict = typeof (await import('ata-validator/compiled-verdict')).fromCompiledVerdict === 'function' } catch { compiledVerdict = false }
|
|
72
|
+
}
|
|
73
|
+
// Whether the wrappers take a check registered with _extendChecks, as
|
|
74
|
+
// withKeywords from @ata-project/keywords registers one; from 1.40.0. Asked
|
|
75
|
+
// of a wrapper itself, so an ata-validator without it keeps such calls on
|
|
76
|
+
// the runtime.
|
|
77
|
+
// The ata-validator/t builder, run at build time on calls whose arguments
|
|
78
|
+
// the build can read.
|
|
79
|
+
let t = null
|
|
80
|
+
if (canCompileAway) {
|
|
81
|
+
try { const m = await import('ata-validator/t'); t = (m.default ?? m).t ?? m.t ?? null } catch { t = null }
|
|
82
|
+
}
|
|
83
|
+
let compiledExtendChecks = false
|
|
84
|
+
if (canCompileAway) {
|
|
85
|
+
try {
|
|
86
|
+
const c = await import('ata-validator/compiled')
|
|
87
|
+
const fromCompiled = (c.default ?? c).fromCompiled ?? c.fromCompiled
|
|
88
|
+
compiledExtendChecks = typeof fromCompiled({ validate: () => ({ valid: true, errors: [] }), isValid: () => true }, {}, { useDefaults: false })._extendChecks === 'function'
|
|
89
|
+
} catch { compiledExtendChecks = false }
|
|
90
|
+
}
|
|
67
91
|
return {
|
|
68
92
|
...api,
|
|
69
93
|
toStandaloneModule: build.toStandaloneModule,
|
|
@@ -72,6 +96,9 @@ async function loadAta() {
|
|
|
72
96
|
// The Validator options fromCompiled() reproduces, from ata-validator
|
|
73
97
|
// 1.37.0; before it, only calls without options are replaced.
|
|
74
98
|
compiledOptions: canCompileAway && Array.isArray(build.compiledOptions) ? build.compiledOptions : [],
|
|
99
|
+
compiledVerdict,
|
|
100
|
+
compiledExtendChecks,
|
|
101
|
+
t,
|
|
75
102
|
}
|
|
76
103
|
}
|
|
77
104
|
|