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