@ata-project/unplugin 0.3.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
@@ -6,6 +6,11 @@ The generated module imports nothing. A small schema compiles to about 1.3 KB gz
6
6
 
7
7
  Schemas can be authored as `.json`, `.js` or `.ts`.
8
8
 
9
+ It also compiles away `new Validator(schema)` in your own code wherever the schema
10
+ is known at build time and the result is the same, so the runtime compiler stays
11
+ out of the bundle: for the test entry, 122.5 KB gzipped becomes 16.7 KB. See
12
+ [compileAway](#compileaway-keep-new-validator-drop-the-compiler).
13
+
9
14
  ## Install
10
15
 
11
16
  ```bash
@@ -145,12 +150,12 @@ Other sources (`.json` without the `.schema` suffix, `.js`, `.ts`) produce
145
150
  | `nameFromFile` | file name in PascalCase | Type name for schemas without `title` or `$id`. |
146
151
  | `root` | from the bundler | Where the globs resolve from. Vite's root, Webpack's and Rspack's `context` and esbuild's `absWorkingDir` are read; Rollup and Rolldown need it passed. |
147
152
  | `alias` | Vite's `resolve.alias` | Import aliases inside `.ts` schema files. tsconfig `paths` work without configuration. |
148
- | `compileAway` | `false` | Replace `new Validator(schema)` in your code with a validator compiled at build time, where that gives the same results. See below. Needs ata-validator 1.36.0. |
153
+ | `compileAway` | `true` | Replace `new Validator(schema)` in your code with a validator compiled at build time, where that gives the same results. See below. Needs ata-validator 1.36.0; with an older version the runtime stays, and a warning is printed only if you set the option yourself. |
149
154
 
150
155
  ## compileAway: keep `new Validator`, drop the compiler
151
156
 
152
- With `compileAway: true`, code written against the runtime API is compiled at
153
- build time without being changed:
157
+ Code written against the runtime API is compiled at build time without being
158
+ changed. This is on by default; `compileAway: false` turns it off.
154
159
 
155
160
  ```js
156
161
  import { Validator } from 'ata-validator'
@@ -168,10 +173,14 @@ The plugin puts a compiled validator in place of the `new Validator(...)` call,
168
173
  and once nothing else in the file uses the `ata-validator` import, the import
169
174
  goes with it, so the runtime compiler is not in the bundle. For the three-schema
170
175
  entry in `test/fixtures/compile-away`, one of them with defaults, a minified Vite
171
- library build is 115.7 KB gzipped without it and 15.5 KB with it, on
172
- ata-validator 1.36.0. Across the 977 schemas of SchemaStore, 725 can be compiled
176
+ library build is 122.5 KB gzipped without it and 16.7 KB with it (gzip level 9), on
177
+ ata-validator 1.39.2. Across the 977 schemas of SchemaStore, 725 can be compiled
173
178
  away this way.
174
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
+
175
184
  The replacement answers `validate()`, `isValidObject()`, `validateJSON()` and
176
185
  `isValidJSON()` as a `Validator` with default options does: the same verdicts,
177
186
  defaults filled in, `data` on success, and the same errors, enriched the same
@@ -192,7 +201,9 @@ otherwise:
192
201
  the runtime;
193
202
  - the result goes into a `const` that is not exported and is only used as
194
203
  `name.validate(...)`, `name.isValidObject(...)`, `name.validateJSON(...)` or
195
- `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);
196
207
  - ata-validator can compile the schema to the same results. It declines custom
197
208
  `errorMessage`s and shapes its code generator cannot express, which the
198
209
  runtime answers with its interpreted engine.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ata-project/unplugin",
3
- "version": "0.3.0",
4
- "description": "Compile JSON, JS and TS schema files to ata-validator standalone modules at build time, in Vite, Webpack, Rollup, Rolldown, esbuild and Rspack.",
3
+ "version": "0.5.0",
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",
7
7
  "exports": {
@@ -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.37.0",
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
@@ -41,7 +41,7 @@ const DEFAULT_OPTIONS = {
41
41
  format: 'esm',
42
42
  abortEarly: false,
43
43
  types: true,
44
- compileAway: false,
44
+ compileAway: true,
45
45
  nameFromFile: (file) => {
46
46
  const base = path.basename(file, path.extname(file)).replace(/\.schema$/i, '')
47
47
  return pascal(base)
@@ -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
 
package/src/index.d.ts CHANGED
@@ -27,7 +27,7 @@ export interface Options {
27
27
  * only used through validate(), isValidObject(), validateJSON() and
28
28
  * isValidJSON(), so the runtime compiler stays out of the bundle. Results are
29
29
  * the ones the runtime gives. Needs ata-validator 1.35.0 or later.
30
- * Default: `false`.
30
+ * Default: `true`; `false` turns it off.
31
31
  */
32
32
  compileAway?: boolean
33
33
  }
package/src/index.js CHANGED
@@ -34,8 +34,11 @@ export const unpluginFactory = (userOptions = {}) => {
34
34
  // compileAway: `new Validator(schema)` with a schema the build can read
35
35
  // becomes a validator compiled here, and the runtime compiler leaves the
36
36
  // bundle. See src/compile-away.js for what is replaced and what is not.
37
+ // On unless the caller turned it off. It replaces a call only where the
38
+ // compiled validator answers exactly as the runtime would, so being on by
39
+ // default can cost a saving but not change a result.
37
40
  transformInclude(id) {
38
- if (!userOptions.compileAway) return false
41
+ if (userOptions.compileAway === false) return false
39
42
  const file = id.split('?')[0]
40
43
  return SOURCE.test(file) && !file.split(/[\\/]/).includes('node_modules')
41
44
  },
@@ -44,7 +47,9 @@ export const unpluginFactory = (userOptions = {}) => {
44
47
  if (!code.includes('ata-validator')) return null
45
48
  const api = await session.api()
46
49
  if (!api.compiledModuleFor) {
47
- if (!session.warnedCompileAway) {
50
+ // Only a caller who asked for it hears that it could not happen; on
51
+ // by default, an older ata-validator just keeps the runtime.
52
+ if (userOptions.compileAway === true && !session.warnedCompileAway) {
48
53
  session.warnedCompileAway = true
49
54
  session.logger?.warn?.('[unplugin-ata] compileAway needs ata-validator 1.36.0 or later; nothing was replaced')
50
55
  }