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 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/guide/environments.md
Original file line number Diff line number Diff line change
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: {exportEsm: 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
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const defaultOptions = {
code: {
// NEW
es5: false,
exportEsm: false,
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
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"
exportEsm?: boolean // how functions should be exported - by default CJS is used, so the validate function(s)
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
// 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
13 changes: 13 additions & 0 deletions lib/compile/codegen/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ 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}`)
}
const name = key.toString()
let newName = name.replace(/[^\w$_]|[\s]/gi, "_")
if (/^[0-9]/.test(name)) {
newName = "_" + newName
}
return new _Code(`${newName}`)
}

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

export interface CodeOptions {
es5?: boolean
exportEsm?: boolean
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
lines?: boolean
optimize?: boolean | number
formats?: Code // code to require (or construct) map of available formats - for standalone code
Expand Down
16 changes: 14 additions & 2 deletions lib/standalone/index.ts
Original file line number Diff line number Diff line change
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.exportEsm) {
// 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,15 @@ 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}`
if (ajv.opts.code.exportEsm) {
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
code = _`${code}${_n}export const ${getEsmExportName(name)} = ${
v.source?.validateName
};${_n}${vCode}`
} else {
code = _`${code}${_n}exports${getProperty(name)} = ${
v.source?.validateName
};${_n}${vCode}`
}
}
}
return `${code}`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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
82 changes: 77 additions & 5 deletions spec/standalone.spec.ts
Original file line number Diff line number Diff line change
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,61 @@ 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, exportEsm: true}})
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
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, exportEsm: true}})
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
ajv.addSchema(numSchema)
ajv.addSchema(strSchema)
const moduleCode = standaloneCode(ajv)
testExportTypeEsm(moduleCode, false)
const m = importFromStringSync(moduleCode)
assert.strictEqual(Object.keys(m).length, 2)
testExports({
validateNumber: m.https___example_com_number_json,
validateString: m.https___example_com_string_json,
})
})
})

describe("with schema keys", () => {
Expand Down Expand Up @@ -223,13 +274,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 +294,26 @@ describe("standalone code generation", () => {
}
})

it("should generate module code with a single export - ESM", () => {
const ajv = new _Ajv({code: {source: true, exportEsm: true}})
rehanvdm marked this conversation as resolved.
Show resolved Hide resolved
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