ajv-formats icon indicating copy to clipboard operation
ajv-formats copied to clipboard

account for ajv.opts.code.esm option in addFormats

Open sethvincent opened this issue 2 years ago • 6 comments

Closes #68

This checks ajv.opts.code.esm and uses a dynamic import if true, otherwise it uses previous behavior.

I'm not sure how best to add a test for this so would be happy to add that based on any suggestions you have.

sethvincent avatar Sep 16 '22 22:09 sethvincent

One drawback now is that this would output top-level awaits instead of "proper" es6 imports.

Additionally, this also happens with non-ajv-format imports if code has been generated with standaloneCode.

So, in short, all code-generating code would need to handle ES6 imports properly instead of defaulting to commonjs-requires.

For anyone needing a workaround to post-process the output of standaloneCode:

// modify to fit your output of standaloneCode
let moduleCode = standaloneCode(/*...*/);

// regexp to capture the "old" commonjs code
const requireRewriteRegex: RegExp = /const (func\d*) = require\("(.*?)"\)\.default;/m;

// used to find the first possible start position in which to insert the re-written imports
const USE_STRICT_TEXT = "\"use strict\";";

let match: RegExpMatchArray | null = null;
while (match = moduleCode.match(requireRewriteRegex)) {
  moduleCode = moduleCode.substring(0, match.index) + moduleCode.substring(match.index + match[0].length);
  const useStrictStart = moduleCode.indexOf(USE_STRICT_TEXT);
  let insertionIndex = 0;
  if (useStrictStart !== -1) {
    insertionIndex = useStrictStart + USE_STRICT_TEXT.length;
  }
  moduleCode = moduleCode.substring(0, insertionIndex) + `import ${match[1]} from "${match[2]}";` + moduleCode.substring(insertionIndex);
}

// write moduleCode to a file or whatever.

revidee avatar Sep 28 '22 14:09 revidee

This change seems to make code that uses ajv-format asynchronous, which is a major breaking change to validators. I think converting to import {} from "" form would be better.

jfrconley avatar Mar 24 '23 19:03 jfrconley

I hope this code helps someone.

const ajv = new Ajv({
  formats: { 'iso-date-time': true },
  schemas: [schema],
  code: {
    source: true,
    esm: true,
  },
});
addFormats(ajv, ['iso-date-time']);

const moduleCode = standaloneCode(ajv, {
  validate: schema.$id,
});

const importAjvFormatsDict = new Map();
const splitBySemiColon = moduleCode.split(';');

splitBySemiColon.forEach((item, idx) => {
  if (item.includes('require("ajv-formats/dist/formats")')) {
    importAjvFormatsDict.set(idx, item);
  }
});
importAjvFormatsDict.forEach((value, key) => {
  const formatVariable = (value.match(/const formats\d+/) ?? [])[0];
  const formatName = value.match(/fullFormats\["(.+?)"\]/)[1];
  splitBySemiColon[
    key
  ] = `import { fullFormats } from "ajv-formats/dist/formats";${formatVariable} = fullFormats["${formatName}"]`;
});

fs.writeFileSync(path.join(__dirname, '../src/services/ajv/index.ts'), `// @ts-nocheck\n${splitBySemiColon.join(';')}`);

kooku0 avatar May 18 '23 14:05 kooku0

@kooku0 thank you, I slightly improved your version to:

  • get rid of duplicate imports
  • add support for ajv-keywords' transform
  • remove unused nullish coalescing, because we should want it to fail, if it doesn't match, as that's how we'd know we need to modify the script in the future
let codeRaw = standaloneCode(ajv, validate)
const codeArr = codeRaw.split(`;`)
const mapAjvFormats = new Map<number, string>()
const mapAjvTransform = new Map<number, string>()

codeArr.forEach((item, idx) => {
  if (item.includes(`require("ajv-formats/dist/formats")`)) {
    return mapAjvFormats.set(idx, item)
  }
  if (item.includes(`require("ajv-keywords/dist/definitions/transform")`)) {
    return mapAjvTransform.set(idx, item)
  }
})

mapAjvFormats.forEach((value, key) => {
  const varDeclaration = /const formats\d+/.exec(value)![0]
  const name = /fullFormats\.(.*)/.exec(value)![1]
  codeArr[key] = `${varDeclaration} = fullFormats["${name}"]`
})
mapAjvTransform.forEach((value, key) => {
  const varDeclaration = /const func\d+/.exec(value)![0]
  const name = /transform\.(.*)/.exec(value)![1]
  codeArr[key] = `${varDeclaration} = transformDef.transform["${name}"]`
})
if (mapAjvFormats.size) {
  const idx = mapAjvFormats.keys().next().value
  codeArr[
    idx
  ] = `import { fullFormats } from "ajv-formats/dist/formats";${codeArr[idx]}`
}
if (mapAjvTransform.size) {
  const idx = mapAjvTransform.keys().next().value
  codeArr[
    idx
  ] = `import transformDef from "ajv-keywords/dist/definitions/transform";${codeArr[idx]}`
}
codeRaw = codeArr.join(`;`)

This is the result in the validator file:

import transformDef from "ajv-keywords/dist/definitions/transform"
const func2 = transformDef.transform[`trim`]
const func3 = transformDef.transform[`toLowerCase`]
import { fullFormats } from "ajv-formats/dist/formats"
const formats0 = fullFormats[`int32`]

o-alexandrov avatar May 18 '23 14:05 o-alexandrov

Hey @epoberezkin, @ChALkeR, any timeline on when this PR and some of the other open ones can be merged? The module ajv-formats is blowing up in some of my native esm projects currently, because it still has some strange imports like the ones fixed with this PR. It would be great to have a current, completely esm compliant version of ajv-formats available for use in modern projects.

itpropro avatar Aug 29 '23 22:08 itpropro

I arrived here from the error from my use of ajv standalone when updating a project to ES modules. Still very much learning to adapt to ES modules but I did find that appending the following to the top of the standalone output seemed to be a very simple solution:

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

brainsiq avatar Dec 04 '23 17:12 brainsiq