@json-layout/core
Version:
Compilation and state management utilities for JSON Layout.
117 lines (102 loc) • 4.21 kB
JavaScript
// import Debug from 'debug'
import ajvModule from 'ajv/dist/2019.js'
import addFormats from 'ajv-formats'
import ajvErrors from 'ajv-errors'
import { ok } from 'assert/strict'
import standaloneCode from 'ajv/dist/standalone/index.js'
import { parseModule, generateCode, builders } from 'magicast'
import { clone } from '../utils/clone.js'
// @ts-ignore
const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
/**
* @param {import('./index.js').CompiledLayout} compiledLayout
* @returns {Promise<string>}
*/
export async function serialize (compiledLayout) {
ok(compiledLayout.schema)
ok(compiledLayout.options)
// we get the schemas that were extended by makeSleletonNode
/** @type {Record<string, any>} */
const schemas = {}
for (const [key, value] of Object.entries(compiledLayout.options.ajv.schemas)) {
if (key.startsWith('https://json-schema.org/')) continue
schemas[key] = value?.schema
}
/** @type {import('ajv').Options} */
const ajvOpts = {
allErrors: true,
strict: false,
verbose: true,
...compiledLayout.options.ajvOptions,
code: { source: true, esm: true, lines: true },
schemas
}
const ajv = new Ajv(ajvOpts)
addFormats.default(ajv)
ajvErrors.default(ajv)
/** @type {Record<string, string>} */
const validatesExports = {}
let i = 0
for (const pointer of Object.keys(compiledLayout.validates)) {
const fullPointer = ajv.opts.uriResolver.resolve(/** @type {string} */(compiledLayout.schema.$id), pointer)
const exportKey = `export${i++}`
ajv.addSchema({ $id: exportKey, $ref: fullPointer })
validatesExports[exportKey] = exportKey
}
let code = standaloneCode.default(ajv, validatesExports)
code = code.replace('"use strict";', '')
// some internal imports to ajv are not translated to esm, we do it here
// cf https://github.com/ajv-validator/ajv-formats/pull/73
if (code.includes('require("ajv-formats/dist/formats")')) {
code = 'import { fullFormats } from "ajv-formats/dist/formats.js";\n' + code
code = code.replace(/require\("ajv-formats\/dist\/formats"\)\.fullFormats/g, 'fullFormats')
}
if (code.includes('require("ajv/dist/runtime/ucs2length")')) {
code = 'import ucs2length from "ajv/dist/runtime/ucs2length.js";\n' + code
code = code.replace(/require\("ajv\/dist\/runtime\/ucs2length"\)/g, 'ucs2length')
}
// import only the current locale from ajv-i18n
let ajvI18nPath = `ajv-i18n/localize/${compiledLayout.locale}/index.js`
try {
await import(ajvI18nPath)
} catch (er) {
console.warn(`failed to load ${ajvI18nPath}, fallback to en`)
ajvI18nPath = 'ajv-i18n/localize/en/index.js'
}
code = `import localizeErrors from "${ajvI18nPath}";
export const exportLocalizeErrors = localizeErrors;\n` + code
i = 0
const expressionsNodes = []
for (const expression of compiledLayout.expressions) {
const id = `expression${i++}`
code += expression.toString().replace('function anonymous', `function ${id}`)
/* Previous code based on recast/esprima, suffered syntax limitations
const fn = parse(expression.toString()).program.body[0]
fn.id = id
code += `\n${print(fn)}\n` */
expressionsNodes.push(builders.raw(id))
}
const ast = parseModule(code)
ast.exports.compiledLayout = {
mainTree: compiledLayout.mainTree,
skeletonTrees: clone(compiledLayout.skeletonTrees),
skeletonNodes: clone(compiledLayout.skeletonNodes),
normalizedLayouts: clone(compiledLayout.normalizedLayouts),
validates: {},
validationErrors: compiledLayout.validationErrors,
expressions: expressionsNodes,
locale: compiledLayout.locale,
messages: compiledLayout.messages,
components: compiledLayout.options.components,
localizeErrors: ast.exports.exportLocalizeErrors
}
delete ast.exports.exportLocalizeErrors
i = 0
for (const pointer of Object.keys(compiledLayout.validates)) {
const exportKey = `export${i++}`
ast.exports.compiledLayout.validates[pointer] = ast.exports[exportKey]
delete ast.exports[exportKey]
}
const generatedCode = generateCode(ast).code.replace('export const compiledLayout = {', 'const compiledLayout = {')
return generatedCode
}