Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to generate ESM exports instead of CJS (#1523) #1861

Merged
merged 3 commits into from Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
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
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
? _`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