Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,26 @@ const stringify = require('stringify.js')
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```

By default the generated code uses CommonJS (`require`/`module.exports`). To emit ES modules
(`import`/`export default`) instead, enable the Ajv `code.esm` option, mirroring
[Ajv's standalone ESM output](https://ajv.js.org/standalone.html):

```js
const code = fastJson({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
}
}, { mode: 'standalone', ajv: { code: { esm: true } } })

fs.writeFileSync('stringify.mjs', code)
const { default: stringify } = await import('./stringify.mjs')
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```

<a name="acknowledgments"></a>
## Acknowledgments

Expand Down
21 changes: 18 additions & 3 deletions lib/standalone.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
'use strict'

function buildStandaloneCode (contextFunc, context, serializer, validator) {
// Mirror AJV's standalone `code.esm` option so ESM consumers can import the generated
// serializer directly instead of wrapping the CJS output in an interop layer.
const esm = context.options?.ajv?.code?.esm === true

// ESM resolution requires the explicit file extension, whereas CommonJS `require` does not.
const requireOrImport = (name, path) => esm
? `import ${name} from '${path}.js'\n`
: `const ${name} = require('${path}')\n`

let ajvDependencyCode = ''
if (context.validatorSchemasIds.size > 0) {
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
ajvDependencyCode += requireOrImport('Validator', 'fast-json-stringify/lib/validator')
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
} else {
Expand All @@ -14,16 +23,22 @@ function buildStandaloneCode (contextFunc, context, serializer, validator) {
// validatorState will hold external schemas if it needs them
const { schema, ...serializerState } = serializer.getState()

// `export default fn(...)` parses `fn` as a function declaration, so the immediate invocation
// is lost. Wrap it in parentheses to keep it an expression, unlike the `module.exports =` form.
const exportStatement = esm
? `export default (${contextFunc.toString()})(validator, serializer)`
: `module.exports = ${contextFunc.toString()}(validator, serializer)`

return `
'use strict'

const Serializer = require('fast-json-stringify/lib/serializer')
${requireOrImport('Serializer', 'fast-json-stringify/lib/serializer')}
const serializerState = ${JSON.stringify(serializerState)}
const serializer = Serializer.restoreFromState(serializerState)

${ajvDependencyCode}

module.exports = ${contextFunc.toString()}(validator, serializer)`
${exportStatement}`
}

module.exports = buildStandaloneCode
Expand Down
56 changes: 56 additions & 0 deletions test/standalone-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { test, after } = require('node:test')
const fjs = require('..')
const fs = require('fs')
const path = require('path')
const url = require('node:url')

function build (opts, schema) {
return fjs(schema || {
Expand Down Expand Up @@ -122,6 +123,61 @@ test('test ajv schema', async (t) => {
}))
})

test('standalone mode emits ESM syntax when ajv.code.esm is enabled', async (t) => {
t.plan(6)

after(async () => {
await fs.promises.rm(destination, { force: true })
})

const code = build({ mode: 'standalone', ajv: { code: { esm: true } } })
t.assert.ok(typeof code === 'string')
t.assert.ok(code.includes("import Serializer from 'fast-json-stringify/lib/serializer.js'"))
t.assert.ok(code.includes('export default'))
t.assert.equal(code.includes('require('), false, 'no CJS require')
t.assert.equal(code.includes('module.exports'), false, 'no CJS module.exports')

const destination = path.resolve(tmpDir, 'standalone-esm.mjs')
await fs.promises.writeFile(destination, code)
const { default: stringify } = await import(url.pathToFileURL(destination).href)
t.assert.equal(stringify({ firstName: 'Foo', surname: 'bar' }),
JSON.stringify({ firstName: 'Foo' }), 'surname evicted')
})

test('standalone ESM output imports the ajv validator', async (t) => {
t.plan(4)

after(async () => {
await fs.promises.rm(destination, { force: true })
})

const code = build({ mode: 'standalone', ajv: { code: { esm: true } } }, {
type: 'object',
if: {
type: 'object',
properties: { kind: { type: 'string', const: 'foo' } }
},
then: {
type: 'object',
properties: { kind: { type: 'string' }, foo: { type: 'string' } }
},
else: {
type: 'object',
properties: { kind: { type: 'string' }, bar: { type: 'string' } }
}
})
t.assert.ok(code.includes("import Validator from 'fast-json-stringify/lib/validator.js'"))
t.assert.equal(code.includes('require('), false, 'no CJS require even with a validator')

const destination = path.resolve(tmpDir, 'standalone-esm-ajv.mjs')
await fs.promises.writeFile(destination, code)
const { default: stringify } = await import(url.pathToFileURL(destination).href)
t.assert.equal(stringify({ kind: 'foo', foo: 'FOO', bar: 'BAR' }),
JSON.stringify({ kind: 'foo', foo: 'FOO' }), 'then branch serialized')
t.assert.equal(stringify({ kind: 'other', foo: 'FOO', bar: 'BAR' }),
JSON.stringify({ kind: 'other', bar: 'BAR' }), 'else branch serialized')
})

test('no need to keep external schemas once compiled', async (t) => {
t.plan(1)

Expand Down