Skip to content

Commit

Permalink
Add option to generate ESM exports instead of CJS (#1523) (#1861)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rehanvdm committed Dec 22, 2021
1 parent d21fa70 commit 418cd0f
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 7 deletions.
9 changes: 9 additions & 0 deletions docs/guide/environments.md
Expand Up @@ -94,6 +94,15 @@ const ajv = new Ajv({code: {es5: true}})

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

## CJS vs ESM exports

The default configuration of AJV is to generate code in ES6 with Common JS (CJS) exports. This can be changed by setting
the ES Modules(ESM) flag.

```javascript
const ajv = new Ajv({code: {esm: true}})
```

## Other JavaScript environments

Ajv is used in other JavaScript environments, including Electron apps, WeChat mini-apps and many others, where the same considerations apply as above:
Expand Down
4 changes: 4 additions & 0 deletions docs/options.md
Expand Up @@ -65,6 +65,7 @@ const defaultOptions = {
code: {
// NEW
es5: false,
esm: false,
lines: false,
source: false,
process: undefined, // (code: string) => string
Expand Down Expand Up @@ -347,6 +348,9 @@ Code generation options:
```typescript
type CodeOptions = {
es5?: boolean // to generate es5 code - by default code is es6, with "for-of" loops, "let" and "const"
esm?: boolean // how functions should be exported - by default CJS is used, so the validate function(s)
// file can be `required`. Set this value to true to export the validate function(s) as ES Modules, enabling
// bunlers to do their job.
lines?: boolean // add line-breaks to code - to simplify debugging of generated functions
source?: boolean // add `source` property (see Source below) to validating function.
process?: (code: string, schema?: SchemaEnv) => string // an optional function to process generated code
Expand Down
8 changes: 8 additions & 0 deletions lib/compile/codegen/code.ts
Expand Up @@ -155,6 +155,14 @@ export function getProperty(key: Code | string | number): Code {
return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`
}

//Does best effort to format the name properly
export function getEsmExportName(key: Code | string | number): Code {
if (typeof key == "string" && IDENTIFIER.test(key)) {
return new _Code(`${key}`)
}
throw new Error(`CodeGen: invalid export name: ${key}, use explicit $id name mapping`)
}

export function regexpCode(rx: RegExp): Code {
return new _Code(rx.toString())
}
1 change: 1 addition & 0 deletions lib/core.ts
Expand Up @@ -140,6 +140,7 @@ export interface CurrentOptions {

export interface CodeOptions {
es5?: boolean
esm?: boolean
lines?: boolean
optimize?: boolean | number
formats?: Code // code to require (or construct) map of available formats - for standalone code
Expand Down
11 changes: 9 additions & 2 deletions lib/standalone/index.ts
Expand Up @@ -2,7 +2,7 @@ import type AjvCore from "../core"
import type {AnyValidateFunction, SourceCode} from "../types"
import type {SchemaEnv} from "../compile"
import {UsedScopeValues, UsedValueState, ValueScopeName, varKinds} from "../compile/codegen/scope"
import {_, nil, _Code, Code, getProperty} from "../compile/codegen/code"
import {_, nil, _Code, Code, getProperty, getEsmExportName} from "../compile/codegen/code"

function standaloneCode(
ajv: AjvCore,
Expand Down Expand Up @@ -30,6 +30,10 @@ function standaloneCode(
const usedValues: UsedScopeValues = {}
const n = source?.validateName
const vCode = validateCode(usedValues, source)
if (ajv.opts.code.esm) {
// Always do named export as `validate` rather than the variable `n` which is `validateXX` for known export value
return `"use strict";${_n}export const validate = ${n};${_n}export default ${n};${_n}${vCode}`
}
return `"use strict";${_n}module.exports = ${n};${_n}module.exports.default = ${n};${_n}${vCode}`
}

Expand All @@ -43,7 +47,10 @@ function standaloneCode(
const v = getValidateFunc(schemas[name] as T)
if (v) {
const vCode = validateCode(usedValues, v.source)
code = _`${code}${_n}exports${getProperty(name)} = ${v.source?.validateName};${_n}${vCode}`
const exportSyntax = ajv.opts.code.esm
? _`export const ${getEsmExportName(name)}`
: _`exports${getProperty(name)}`
code = _`${code}${_n}${exportSyntax} = ${v.source?.validateName};${_n}${vCode}`
}
}
return `${code}`
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -94,6 +94,7 @@
"karma-mocha": "^2.0.0",
"lint-staged": "^12.1.1",
"mocha": "^9.0.2",
"module-from-string": "^3.1.3",
"node-fetch": "^3.0.0",
"nyc": "^15.0.0",
"prettier": "^2.3.1",
Expand Down
89 changes: 84 additions & 5 deletions spec/standalone.spec.ts
Expand Up @@ -4,8 +4,29 @@ import _Ajv from "./ajv"
import standaloneCode from "../dist/standalone"
import ajvFormats from "ajv-formats"
import requireFromString = require("require-from-string")
import {importFromStringSync} from "module-from-string"
import assert = require("assert")

function testExportTypeEsm(moduleCode: string, singleExport: boolean) {
//Must have
assert.strictEqual(moduleCode.includes("export const"), true)
if (singleExport) {
assert.strictEqual(moduleCode.includes("export default"), true)
}
//Must not have
assert.strictEqual(moduleCode.includes("module.exports"), false)
}
function testExportTypeCjs(moduleCode: string, singleExport: boolean) {
//Must have
if (singleExport) {
assert.strictEqual(moduleCode.includes("module.exports"), true)
} else {
assert.strictEqual(moduleCode.includes("exports.") || moduleCode.includes("exports["), true)
}
//Must not have
assert.strictEqual(moduleCode.includes("export const"), false)
}

describe("standalone code generation", () => {
describe("multiple exports", () => {
let ajv: Ajv
Expand All @@ -21,31 +42,68 @@ describe("standalone code generation", () => {
}

describe("without schema keys", () => {
beforeEach(() => {
it("should generate module code with named export - CJS", () => {
ajv = new _Ajv({code: {source: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv, {
validateNumber: "https://example.com/number.json",
validateString: "https://example.com/string.json",
})
testExportTypeCjs(moduleCode, false)
const m = requireFromString(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports(m)
})

it("should generate module code with named exports", () => {
it("should generate module code with named export - ESM", () => {
ajv = new _Ajv({code: {source: true, esm: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv, {
validateNumber: "https://example.com/number.json",
validateString: "https://example.com/string.json",
})
const m = requireFromString(moduleCode)
testExportTypeEsm(moduleCode, false)
const m = importFromStringSync(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports(m)
})

it("should generate module code with all exports", () => {
it("should generate module code with all exports - CJS", () => {
ajv = new _Ajv({code: {source: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv)
testExportTypeCjs(moduleCode, false)
const m = requireFromString(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports({
validateNumber: m["https://example.com/number.json"],
validateString: m["https://example.com/string.json"],
})
})

it("should generate module code with all exports - ESM", () => {
ajv = new _Ajv({code: {source: true, esm: true}})
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)

try {
standaloneCode(ajv)
} catch (err) {
if (err instanceof Error) {
const isMappingErr =
`CodeGen: invalid export name: ${numSchema.$id}, use explicit $id name mapping` ===
err.message ||
`CodeGen: invalid export name: ${strSchema.$id}, use explicit $id name mapping` ===
err.message
assert.strictEqual(isMappingErr, true)
} else {
throw err
}
}
})
})

describe("with schema keys", () => {
Expand Down Expand Up @@ -223,13 +281,14 @@ describe("standalone code generation", () => {
}
})

it("should generate module code with a single export (ESM compatible)", () => {
it("should generate module code with a single export - CJS", () => {
const ajv = new _Ajv({code: {source: true}})
const v = ajv.compile({
type: "number",
minimum: 0,
})
const moduleCode = standaloneCode(ajv, v)
testExportTypeCjs(moduleCode, true)
const m = requireFromString(moduleCode)
testExport(m)
testExport(m.default)
Expand All @@ -242,6 +301,26 @@ describe("standalone code generation", () => {
}
})

it("should generate module code with a single export - ESM", () => {
const ajv = new _Ajv({code: {source: true, esm: true}})
const v = ajv.compile({
type: "number",
minimum: 0,
})
const moduleCode = standaloneCode(ajv, v)
testExportTypeEsm(moduleCode, true)
const m = importFromStringSync(moduleCode)
testExport(m.validate)
testExport(m.default)

function testExport(validate: AnyValidateFunction<unknown>) {
assert.strictEqual(validate(1), true)
assert.strictEqual(validate(0), true)
assert.strictEqual(validate(-1), false)
assert.strictEqual(validate("1"), false)
}
})

describe("standalone code with ajv-formats", () => {
const schema = {
$schema: "http://json-schema.org/draft-07/schema#",
Expand Down

0 comments on commit 418cd0f

Please sign in to comment.