@ata-project/unplugin 0.4.0 → 0.5.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 +7 -1
- package/package.json +3 -2
- package/src/compile-away.js +43 -10
- package/src/core.js +20 -0
package/README.md
CHANGED
|
@@ -177,6 +177,10 @@ 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.1 KB gzipped.
|
|
183
|
+
|
|
180
184
|
The replacement answers `validate()`, `isValidObject()`, `validateJSON()` and
|
|
181
185
|
`isValidJSON()` as a `Validator` with default options does: the same verdicts,
|
|
182
186
|
defaults filled in, `data` on success, and the same errors, enriched the same
|
|
@@ -197,7 +201,9 @@ otherwise:
|
|
|
197
201
|
the runtime;
|
|
198
202
|
- the result goes into a `const` that is not exported and is only used as
|
|
199
203
|
`name.validate(...)`, `name.isValidObject(...)`, `name.validateJSON(...)` or
|
|
200
|
-
`name.isValidJSON(...)`;
|
|
204
|
+
`name.isValidJSON(...)`; the call may be wrapped in `withKeywords(...)` from
|
|
205
|
+
`@ata-project/keywords`, which then registers its keywords on the compiled
|
|
206
|
+
validator as it does on a `Validator` (needs ata-validator 1.40.0);
|
|
201
207
|
- ata-validator can compile the schema to the same results. It declines custom
|
|
202
208
|
`errorMessage`s and shapes its code generator cannot express, which the
|
|
203
209
|
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.5.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
|
@@ -163,26 +163,44 @@ function constantsOf(ast, id) {
|
|
|
163
163
|
// `const name = new Validator(...)`, declared once, not exported, and used only
|
|
164
164
|
// as `name.<method>(...)` with a supported method.
|
|
165
165
|
function replaceableBinding(newExpr, ctx) {
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
let decl = newExpr.__parent
|
|
167
|
+
// `withKeywords(new Validator(schema))` from @ata-project/keywords: the
|
|
168
|
+
// wrapper takes the keywords' check the way a Validator does, so the inner
|
|
169
|
+
// call is replaced and withKeywords stays to register the check on it.
|
|
170
|
+
if (decl && decl.type === 'CallExpression' && ctx.withKeywords && ctx.extendChecks &&
|
|
171
|
+
decl.callee.type === 'Identifier' && decl.callee.name === ctx.withKeywords &&
|
|
172
|
+
ctx.declared.get(ctx.withKeywords) === 1 && decl.arguments.length === 1 && decl.arguments[0] === newExpr) {
|
|
173
|
+
decl = decl.__parent
|
|
174
|
+
if (!decl || decl.type !== 'VariableDeclarator' || decl.init !== newExpr.__parent) return null
|
|
175
|
+
}
|
|
176
|
+
if (!decl || decl.type !== 'VariableDeclarator' || decl.id.type !== 'Identifier') return null
|
|
177
|
+
if (decl.init !== newExpr && decl.init !== newExpr.__parent) return null
|
|
168
178
|
const list = decl.__parent
|
|
169
179
|
if (!list || list.type !== 'VariableDeclaration' || list.kind !== 'const') return null
|
|
170
180
|
if (list.__parent && list.__parent.type === 'ExportNamedDeclaration') return null
|
|
171
181
|
const name = decl.id.name
|
|
172
182
|
if (ctx.declared.get(name) !== 1) return null
|
|
183
|
+
const methods = new Set()
|
|
173
184
|
for (const r of ctx.refs) {
|
|
174
185
|
if (r.node.name !== name) continue
|
|
175
186
|
const p = r.parent
|
|
176
187
|
const member = p && (p.type === 'MemberExpression' || p.type === 'OptionalMemberExpression') && r.key === 'object' && !p.computed && p.property.type === 'Identifier' && METHODS.has(p.property.name)
|
|
177
188
|
const called = member && p.__parent && (p.__parent.type === 'CallExpression' || p.__parent.type === 'OptionalCallExpression') && p.__parent.callee === p
|
|
178
189
|
if (!called) return null
|
|
190
|
+
methods.add(p.property.name)
|
|
179
191
|
}
|
|
180
|
-
return name
|
|
192
|
+
return { name, methods }
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
|
|
195
|
+
// Code that only asks for a boolean never reads an error.
|
|
196
|
+
const VERDICT_METHODS = new Set(['isValidObject', 'isValidJSON'])
|
|
197
|
+
const verdictOnly = (methods) => [...methods].every((m) => VERDICT_METHODS.has(m))
|
|
198
|
+
|
|
199
|
+
// A verdict-only module hands out isValid alone, so the error function inside
|
|
200
|
+
// it is unreferenced and the bundler drops it.
|
|
201
|
+
function inlineModule(src, index, verdict) {
|
|
184
202
|
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`
|
|
203
|
+
return `const __ataCompiled${index} = (() => {\n${body}\nreturn ${verdict ? '{ isValid }' : '{ validate, isValid }'};\n})();\n`
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
// The options of `new Validator(schema, options)` as a third argument for
|
|
@@ -226,14 +244,24 @@ export function compileAway(code, id, ata) {
|
|
|
226
244
|
}
|
|
227
245
|
}
|
|
228
246
|
if (!validatorName) return null
|
|
247
|
+
let withKeywords = null
|
|
248
|
+
for (const stmt of ast.program.body) {
|
|
249
|
+
if (stmt.type !== 'ImportDeclaration' || stmt.source.value !== '@ata-project/keywords' || stmt.importKind === 'type') continue
|
|
250
|
+
for (const sp of stmt.specifiers) {
|
|
251
|
+
if (sp.type !== 'ImportSpecifier' || sp.importKind === 'type') continue
|
|
252
|
+
const imported = sp.imported.type === 'Identifier' ? sp.imported.name : sp.imported.value
|
|
253
|
+
if (imported === 'withKeywords') withKeywords = sp.local.name
|
|
254
|
+
}
|
|
255
|
+
}
|
|
229
256
|
|
|
230
257
|
const { declared, refs } = scan(ast)
|
|
231
258
|
if (declared.get(validatorName) !== 1) return null
|
|
232
|
-
const ctx = { declared, refs, constants: constantsOf(ast, id), defineSchema }
|
|
259
|
+
const ctx = { declared, refs, constants: constantsOf(ast, id), defineSchema, withKeywords, extendChecks: ata.compiledExtendChecks === true }
|
|
233
260
|
|
|
234
261
|
const s = new MagicString(code)
|
|
235
262
|
const modules = []
|
|
236
263
|
const done = new Set()
|
|
264
|
+
const wrappers = { full: false, verdict: false }
|
|
237
265
|
for (const r of refs) {
|
|
238
266
|
if (r.node.name !== validatorName) continue
|
|
239
267
|
const expr = r.parent
|
|
@@ -244,15 +272,19 @@ export function compileAway(code, id, ata) {
|
|
|
244
272
|
optionsArg = compiledOptionsArg(expr.arguments[1], ctx, supportedOptions)
|
|
245
273
|
if (optionsArg === NOT_STATIC) continue
|
|
246
274
|
}
|
|
247
|
-
|
|
275
|
+
const binding = replaceableBinding(expr, ctx)
|
|
276
|
+
if (!binding) continue
|
|
248
277
|
const schema = staticValue(expr.arguments[0], ctx)
|
|
249
278
|
if (schema === NOT_STATIC || schema === null || typeof schema !== 'object' || Array.isArray(schema)) continue
|
|
250
279
|
let src = null
|
|
251
280
|
try { src = compiledModuleFor(schema, { format: 'esm' }) } catch { src = null }
|
|
252
281
|
if (!src) continue
|
|
253
282
|
const index = modules.length
|
|
254
|
-
|
|
255
|
-
|
|
283
|
+
const verdict = ata.compiledVerdict === true && verdictOnly(binding.methods)
|
|
284
|
+
modules.push(inlineModule(src, index, verdict))
|
|
285
|
+
const wrap = verdict ? '__ataFromCompiledVerdict' : '__ataFromCompiled'
|
|
286
|
+
if (verdict) wrappers.verdict = true; else wrappers.full = true
|
|
287
|
+
s.overwrite(expr.start, expr.end, `${wrap}(__ataCompiled${index}, ${JSON.stringify(compiledSchemaFor(schema))}${optionsArg ? ', ' + optionsArg : ''})`)
|
|
256
288
|
done.add(expr)
|
|
257
289
|
}
|
|
258
290
|
if (done.size === 0) return null
|
|
@@ -278,7 +310,8 @@ export function compileAway(code, id, ata) {
|
|
|
278
310
|
const clause = [def ? text(def) : null, named.length ? `{ ${named.map(text).join(', ')} }` : null].filter(Boolean).join(', ')
|
|
279
311
|
s.overwrite(importDecl.start, importDecl.end, `import ${clause} from ${code.slice(importDecl.source.start, importDecl.source.end)}`)
|
|
280
312
|
}
|
|
281
|
-
s.prepend(`import {
|
|
313
|
+
if (wrappers.verdict) s.prepend(`import { fromCompiledVerdict as __ataFromCompiledVerdict } from 'ata-validator/compiled-verdict';\n`)
|
|
314
|
+
if (wrappers.full) s.prepend(`import { fromCompiled as __ataFromCompiled } from 'ata-validator/compiled';\n`)
|
|
282
315
|
s.appendLeft(lastImportEnd, '\n' + modules.join(''))
|
|
283
316
|
return { code: s.toString(), map: s.generateMap({ hires: true, source: id, includeContent: true }), replaced: done.size }
|
|
284
317
|
}
|
package/src/core.js
CHANGED
|
@@ -64,6 +64,24 @@ 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
|
+
let compiledExtendChecks = false
|
|
78
|
+
if (canCompileAway) {
|
|
79
|
+
try {
|
|
80
|
+
const c = await import('ata-validator/compiled')
|
|
81
|
+
const fromCompiled = (c.default ?? c).fromCompiled ?? c.fromCompiled
|
|
82
|
+
compiledExtendChecks = typeof fromCompiled({ validate: () => ({ valid: true, errors: [] }), isValid: () => true }, {}, { useDefaults: false })._extendChecks === 'function'
|
|
83
|
+
} catch { compiledExtendChecks = false }
|
|
84
|
+
}
|
|
67
85
|
return {
|
|
68
86
|
...api,
|
|
69
87
|
toStandaloneModule: build.toStandaloneModule,
|
|
@@ -72,6 +90,8 @@ async function loadAta() {
|
|
|
72
90
|
// The Validator options fromCompiled() reproduces, from ata-validator
|
|
73
91
|
// 1.37.0; before it, only calls without options are replaced.
|
|
74
92
|
compiledOptions: canCompileAway && Array.isArray(build.compiledOptions) ? build.compiledOptions : [],
|
|
93
|
+
compiledVerdict,
|
|
94
|
+
compiledExtendChecks,
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
|