Skip to content

Commit 418cd0f

Browse files
authoredDec 22, 2021
Add option to generate ESM exports instead of CJS (#1523) (#1861)
* feat: add option to generate ESM exports instead of CJS (#1523) * feat: add option to generate ESM exports instead of CJS (#1523) - Renamed exportEsm to esm - Extracted common code - Changed invalid export names to rather throw an error
1 parent d21fa70 commit 418cd0f

File tree

7 files changed

+116
-7
lines changed

7 files changed

+116
-7
lines changed
 

‎docs/guide/environments.md

+9
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ const ajv = new Ajv({code: {es5: true}})
9494

9595
See [Advanced options](https://github.com/ajv-validator/ajv/blob/master/docs/api.md#advanced-options).
9696

97+
## CJS vs ESM exports
98+
99+
The default configuration of AJV is to generate code in ES6 with Common JS (CJS) exports. This can be changed by setting
100+
the ES Modules(ESM) flag.
101+
102+
```javascript
103+
const ajv = new Ajv({code: {esm: true}})
104+
```
105+
97106
## Other JavaScript environments
98107

99108
Ajv is used in other JavaScript environments, including Electron apps, WeChat mini-apps and many others, where the same considerations apply as above:

‎docs/options.md

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const defaultOptions = {
6565
code: {
6666
// NEW
6767
es5: false,
68+
esm: false,
6869
lines: false,
6970
source: false,
7071
process: undefined, // (code: string) => string
@@ -347,6 +348,9 @@ Code generation options:
347348
```typescript
348349
type CodeOptions = {
349350
es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const"
351+
esm?: boolean // how functions should be exported - by default CJS is used, so the validate function(s)
352+
// file can be `required`. Set this value to true to export the validate function(s) as ES Modules, enabling
353+
// bunlers to do their job.
350354
lines?: boolean // add line-breaks to code - to simplify debugging of generated functions
351355
source?: boolean // add `source` property (see Source below) to validating function.
352356
process?: (code: string, schema?: SchemaEnv) => string // an optional function to process generated code

‎lib/compile/codegen/code.ts

+8
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ export function getProperty(key: Code | string | number): Code {
155155
return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`
156156
}
157157

158+
//Does best effort to format the name properly
159+
export function getEsmExportName(key: Code | string | number): Code {
160+
if (typeof key == "string" && IDENTIFIER.test(key)) {
161+
return new _Code(`${key}`)
162+
}
163+
throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`)
164+
}
165+
158166
export function regexpCode(rx: RegExp): Code {
159167
return new _Code(rx.toString())
160168
}

‎lib/core.ts

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export interface CurrentOptions {
140140

141141
export interface CodeOptions {
142142
es5?: boolean
143+
esm?: boolean
143144
lines?: boolean
144145
optimize?: boolean | number
145146
formats?: Code // code to require (or construct) map of available formats - for standalone code

‎lib/standalone/index.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type AjvCore from "../core"
22
import type {AnyValidateFunction, SourceCode} from "../types"
33
import type {SchemaEnv} from "../compile"
44
import {UsedScopeValues, UsedValueState, ValueScopeName, varKinds} from "../compile/codegen/scope"
5-
import {_, nil, _Code, Code, getProperty} from "../compile/codegen/code"
5+
import {_, nil, _Code, Code, getProperty, getEsmExportName} from "../compile/codegen/code"
66

77
function standaloneCode(
88
ajv: AjvCore,
@@ -30,6 +30,10 @@ function standaloneCode(
3030
const usedValues: UsedScopeValues = {}
3131
const n = source?.validateName
3232
const vCode = validateCode(usedValues, source)
33+
if (ajv.opts.code.esm) {
34+
// Always do named export as `validate` rather than the variable `n` which is `validateXX` for known export value
35+
return `"use strict";${_n}export const validate = ${n};${_n}export default ${n};${_n}${vCode}`
36+
}
3337
return `"use strict";${_n}module.exports = ${n};${_n}module.exports.default = ${n};${_n}${vCode}`
3438
}
3539

@@ -43,7 +47,10 @@ function standaloneCode(
4347
const v = getValidateFunc(schemas[name] as T)
4448
if (v) {
4549
const vCode = validateCode(usedValues, v.source)
46-
code = _`${code}${_n}exports${getProperty(name)} = ${v.source?.validateName};${_n}${vCode}`
50+
const exportSyntax = ajv.opts.code.esm
51+
? _`export const ${getEsmExportName(name)}`
52+
: _`exports${getProperty(name)}`
53+
code = _`${code}${_n}${exportSyntax} = ${v.source?.validateName};${_n}${vCode}`
4754
}
4855
}
4956
return `${code}`

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"karma-mocha": "^2.0.0",
9595
"lint-staged": "^12.1.1",
9696
"mocha": "^9.0.2",
97+
"module-from-string": "^3.1.3",
9798
"node-fetch": "^3.0.0",
9899
"nyc": "^15.0.0",
99100
"prettier": "^2.3.1",

‎spec/standalone.spec.ts

+84-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@ import _Ajv from "./ajv"
44
import standaloneCode from "../dist/standalone"
55
import ajvFormats from "ajv-formats"
66
import requireFromString = require("require-from-string")
7+
import {importFromStringSync} from "module-from-string"
78
import assert = require("assert")
89

10+
function testExportTypeEsm(moduleCode: string, singleExport: boolean) {
11+
//Must have
12+
assert.strictEqual(moduleCode.includes("export const"), true)
13+
if (singleExport) {
14+
assert.strictEqual(moduleCode.includes("export default"), true)
15+
}
16+
//Must not have
17+
assert.strictEqual(moduleCode.includes("module.exports"), false)
18+
}
19+
function testExportTypeCjs(moduleCode: string, singleExport: boolean) {
20+
//Must have
21+
if (singleExport) {
22+
assert.strictEqual(moduleCode.includes("module.exports"), true)
23+
} else {
24+
assert.strictEqual(moduleCode.includes("exports.") || moduleCode.includes("exports["), true)
25+
}
26+
//Must not have
27+
assert.strictEqual(moduleCode.includes("export const"), false)
28+
}
29+
930
describe("standalone code generation", () => {
1031
describe("multiple exports", () => {
1132
let ajv: Ajv
@@ -21,31 +42,68 @@ describe("standalone code generation", () => {
2142
}
2243

2344
describe("without schema keys", () => {
24-
beforeEach(() => {
45+
it("should generate module code with named export - CJS", () => {
2546
ajv = new _Ajv({code: {source: true}})
2647
ajv.addSchema(numSchema)
2748
ajv.addSchema(strSchema)
49+
const moduleCode = standaloneCode(ajv, {
50+
validateNumber: "https://example.com/number.json",
51+
validateString: "https://example.com/string.json",
52+
})
53+
testExportTypeCjs(moduleCode, false)
54+
const m = requireFromString(moduleCode)
55+
assert.strictEqual(Object.keys(m).length, 2)
56+
testExports(m)
2857
})
2958

30-
it("should generate module code with named exports", () => {
59+
it("should generate module code with named export - ESM", () => {
60+
ajv = new _Ajv({code: {source: true, esm: true}})
61+
ajv.addSchema(numSchema)
62+
ajv.addSchema(strSchema)
3163
const moduleCode = standaloneCode(ajv, {
3264
validateNumber: "https://example.com/number.json",
3365
validateString: "https://example.com/string.json",
3466
})
35-
const m = requireFromString(moduleCode)
67+
testExportTypeEsm(moduleCode, false)
68+
const m = importFromStringSync(moduleCode)
3669
assert.strictEqual(Object.keys(m).length, 2)
3770
testExports(m)
3871
})
3972

40-
it("should generate module code with all exports", () => {
73+
it("should generate module code with all exports - CJS", () => {
74+
ajv = new _Ajv({code: {source: true}})
75+
ajv.addSchema(numSchema)
76+
ajv.addSchema(strSchema)
4177
const moduleCode = standaloneCode(ajv)
78+
testExportTypeCjs(moduleCode, false)
4279
const m = requireFromString(moduleCode)
4380
assert.strictEqual(Object.keys(m).length, 2)
4481
testExports({
4582
validateNumber: m["https://example.com/number.json"],
4683
validateString: m["https://example.com/string.json"],
4784
})
4885
})
86+
87+
it("should generate module code with all exports - ESM", () => {
88+
ajv = new _Ajv({code: {source: true, esm: true}})
89+
ajv.addSchema(numSchema)
90+
ajv.addSchema(strSchema)
91+
92+
try {
93+
standaloneCode(ajv)
94+
} catch (err) {
95+
if (err instanceof Error) {
96+
const isMappingErr =
97+
`CodeGen: invalid export name: ${numSchema.$id}, use explicit $id name mapping` ===
98+
err.message ||
99+
`CodeGen: invalid export name: ${strSchema.$id}, use explicit $id name mapping` ===
100+
err.message
101+
assert.strictEqual(isMappingErr, true)
102+
} else {
103+
throw err
104+
}
105+
}
106+
})
49107
})
50108

51109
describe("with schema keys", () => {
@@ -223,13 +281,14 @@ describe("standalone code generation", () => {
223281
}
224282
})
225283

226-
it("should generate module code with a single export (ESM compatible)", () => {
284+
it("should generate module code with a single export - CJS", () => {
227285
const ajv = new _Ajv({code: {source: true}})
228286
const v = ajv.compile({
229287
type: "number",
230288
minimum: 0,
231289
})
232290
const moduleCode = standaloneCode(ajv, v)
291+
testExportTypeCjs(moduleCode, true)
233292
const m = requireFromString(moduleCode)
234293
testExport(m)
235294
testExport(m.default)
@@ -242,6 +301,26 @@ describe("standalone code generation", () => {
242301
}
243302
})
244303

304+
it("should generate module code with a single export - ESM", () => {
305+
const ajv = new _Ajv({code: {source: true, esm: true}})
306+
const v = ajv.compile({
307+
type: "number",
308+
minimum: 0,
309+
})
310+
const moduleCode = standaloneCode(ajv, v)
311+
testExportTypeEsm(moduleCode, true)
312+
const m = importFromStringSync(moduleCode)
313+
testExport(m.validate)
314+
testExport(m.default)
315+
316+
function testExport(validate: AnyValidateFunction<unknown>) {
317+
assert.strictEqual(validate(1), true)
318+
assert.strictEqual(validate(0), true)
319+
assert.strictEqual(validate(-1), false)
320+
assert.strictEqual(validate("1"), false)
321+
}
322+
})
323+
245324
describe("standalone code with ajv-formats", () => {
246325
const schema = {
247326
$schema: "http://json-schema.org/draft-07/schema#",

0 commit comments

Comments
 (0)
Please sign in to comment.