json-schema-to-typescript icon indicating copy to clipboard operation
json-schema-to-typescript copied to clipboard

Add option to cache generated interfaces

Open vsund opened this issue 7 years ago • 4 comments

Hey,

I made this part of my build process to make sure that the interfaces are in sync with the schema (which by the way works great). Unfortunately this takes some additional seconds every build. Since these schemas don't really change often it would be cool to have an option to cache generated interfaces (e.g. only generate interfaces when the timestamp on the schema file changed (similar to make)).

Or is there another way that I missed? Either way, happy for suggestions 👍

vsund avatar Mar 29 '18 21:03 vsund

This is a really cool idea! I wonder if we could use a more general utility for caching. Something like:

cat schema.json | fromCache | json2ts | toCache > schema.d.ts

One implementation of something like this: https://bitbucket.org/sivann/runcached/src

If you can make a standalone package for this, I'd love to add it to the docs as the recommended way of using the CLI! What do you think?

bcherny avatar Apr 04 '18 18:04 bcherny

I'm pretty sure that I could solve this with a few lines of Bash (I currently don't have the capacity for bringing this up as a standalone side project). But though I think I'd like this more to be a part of json2ts itself. It's not much logic and could be as simple as activating/deactivating it with --[no]cache.

Due to https://github.com/bcherny/json-schema-to-typescript/issues/16 my call to json2ts is already very very complex :D (If I can simplify this, please let me know.)

I can understand that you like to keep such state out of json2ts but in this case I'd say that I'm more for making this a feature of json2ts itself. Not sure though how much you'd like to keep this external, I can surely work on a small script that solves this externally.

vsund avatar Apr 05 '18 12:04 vsund

@vsund Is there any requirement that you use the cli binary? I am currently using something like this:

import * as Ajv from 'ajv'
import * as pack from 'ajv-pack'
import { join, basename, relative, dirname } from 'path'
import { outputFile, outputJson } from 'fs-extra'
import * as walk from 'klaw'
import {
  compile,
  Options as TypeCompilerOptions
} from 'json-schema-to-typescript'

import { JSONSchema4 } from 'json-schema'

const ajv = new Ajv({ sourceCode: true })

const SRC_ROOT = join(__dirname, 'src/')
const JSON_ROOT = join(__dirname, 'json/')
const DIST_ROOT = join(__dirname, 'dist/')

const SUFFIX = '.schema.ts'

const typerOptions: Partial<TypeCompilerOptions> = { bannerComment: '' }

const results: Promise<void | { error: any }>[] = []
walk(SRC_ROOT)
  .on('data', item => {
    if (!item.stats.isFile()) return
    const filename = basename(item.path)
    console.log(filename)
    if (!filename.endsWith(SUFFIX)) return
    const name = filename.slice(0, -SUFFIX.length)
    const schemaObject = import(item.path).then(
      module => module.default as JSONSchema4
    )

    const jsonDir = join(JSON_ROOT, relative(SRC_ROOT, dirname(item.path)))

    results.push(
      schemaObject.then(schemaObject =>
        outputJson(join(jsonDir, name + '.schema.json'), schemaObject).catch(
          error => {
            console.error("Couldn't make schema json", name, error)
            return { error }
          }
        )
      )
    )

    const outDir = join(DIST_ROOT, relative(SRC_ROOT, dirname(item.path)))

    results.push(
      schemaObject
        .then(schemaObject => compile(schemaObject, name, typerOptions))
        .then(typings => outputFile(join(outDir, name + '.d.ts'), typings))
        .catch(error => {
          console.error("Couldn't make schema typings", name, error)
          return { error }
        })
    )

    results.push(
      schemaObject
        .then(schemaObject => {
          const validator = ajv.compile(schemaObject)
          const validatorCode: string = pack(ajv, validator)

          return outputFile(join(outDir, 'is' + name + '.js'), validatorCode)
        })
        .catch(error => {
          console.error("Couldn't make validator code", name, error)
          return { error }
        })
    )

    const validatorTypings = `import {Validate} from '${relative(
      outDir,
      join(__dirname, './types/Validate')
    )}'
import {${name}} from './${name}'
declare const isFile: Validate<${name}>
export = isFile
`
    results.push(
      outputFile(join(outDir, 'is' + name + '.d.ts'), validatorTypings).catch(
        error => {
          console.error("Couldn't make validator typings", name, error)
          return { error }
        }
      )
    )
  })
  .on('end', async () => {
    console.log('Finishing...')
    const result = await Promise.all(results)
    console.log('Done!')
  })

The reason it uses import is because my schemas actually look like this, not JSON

import { JSONSchema4 } from 'json-schema'
import { freeze } from '../../utils/schema'
export default freeze({
  $schema: 'http://json-schema.org/draft-07/schema#',
  $id: 'http://bepis.com/Event.schema.json',
  title: 'Event',
  description: 'A full event',
  properties: {
    type: { description: 'The event type name', type: 'string' },
    ts: {
      description: 'A Unix timestamp, currently valid between 2010 and 2110',
      type: 'number',
      minimum: 1262304000000,
      maximum: 4417977600000
    },
    data: {}
  }
}) as JSONSchema4

I haven't solved external $refs yet.

qm3ster avatar Aug 23 '18 21:08 qm3ster

Here's a slightly trimmed down portion of a checksum script I threw together to wrap our usage of json2ts, in case it's useful.

I'm computing a checksum of all json input files and all d.ts output files, and only regenerating the d.ts files when anything changes.

snippet
#!/usr/bin/env bash

set -eo pipefail

cd "$(dirname -- "$(readlink -f -- "$0" || echo "$0")")"

CHECKSUM_CACHE_FILE="CHANGE ME"
DST_DIR="CHANGE ME"
SRC_DIR="CHANGE ME"

main() {
  preconditions
  initialize
  codegen
  cache-checksums
}

preconditions() {
  # exit early if checksums are up-to-date
  if validate-checksums; then
    exit 0
  fi
}

initialize() {
  rm -rf "$DST_DIR" || true
}

codegen() {
  echo "generating type definitions..." >&2
  mkdir -p "$DST_DIR"

  # generate .d.ts files
  npx json2ts -i "$SRC_DIR" -o "$DST_DIR" \
    [YOUR_OPTS_HERE]
}

validate-checksums() {
  if [[ ! -d "$DST_DIR" ]]; then
    echo "no generated files found" >&2
    return 1
  elif [[ ! -f "$CHECKSUM_CACHE_FILE" ]]; then
    echo "cached checksum not found" >&2
    return 1
  fi

  local current cached
  current=$(checksum-all)
  cached=$(<"$CHECKSUM_CACHE_FILE")

  if [[ $current != "$cached" ]]; then
    echo "changes detected" >&2
    return 1
  fi
}

cache-checksums() {
  checksum-all > "$CHECKSUM_CACHE_FILE"
}

checksum-all() {
  {
    # digest < "<ANY_OTHER_FILE>";
    checksum-dir "$SRC_DIR" "*.json";
    checksum-dir "$DST_DIR" "*.d.ts";
  } | digest
}

checksum-dir() {
  local dir="$1"
  local pattern="$2"
  find "$dir" -type f -iname "$pattern" -print0 | while IFS= read -r -d '' file; do
    digest < "$file"
  done |
    sort |
    digest
}

digest() {
  openssl dgst -sha1
}

main "$@"

dhvector avatar Aug 02 '22 23:08 dhvector