@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 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.4.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.39.2",
82
+ "ata-validator": "^1.40.0",
82
83
  "esbuild": "^0.28.0",
83
84
  "rolldown": "^1.2.8",
84
85
  "rollup": "^4.60.0",
@@ -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
- const decl = newExpr.__parent
167
- if (!decl || decl.type !== 'VariableDeclarator' || decl.init !== newExpr || decl.id.type !== 'Identifier') return null
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
- function inlineModule(src, index) {
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
- if (!replaceableBinding(expr, ctx)) continue
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
- modules.push(inlineModule(src, index))
255
- s.overwrite(expr.start, expr.end, `__ataFromCompiled(__ataCompiled${index}, ${JSON.stringify(compiledSchemaFor(schema))}${optionsArg ? ', ' + optionsArg : ''})`)
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
- s.prepend(`import { fromCompiled as __ataFromCompiled } from 'ata-validator/compiled';\n`)
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