sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.99k stars 158 forks source link

AOT TypeCompiler - Code does not return Errors function #1045

Closed peterlundberg closed 1 week ago

peterlundberg commented 1 week ago

In a recent project I could not use TypeBoxes validation in the front end web app due to CSP rules not allowing unsafe-eval'. To work around this I followed TypeCompile docs and added a build step in our workspace to generate the validator as code instead.

Note that even with language: 'typescript' I had to tweek the output to get something existing typescript code could import easily. That is fine but perhaps this issue and other docs could help others getting this to work.

const analyticSchemaValidatorCode =
  '/* eslint-disable @typescript-eslint/ban-ts-comment */\n' +
  '/* eslint-disable @typescript-eslint/no-explicit-any */\n' +
  '// @ts-nocheck\n\n' +
  '// This code is generated from generate-typecheck.ts\n\n' +
  TypeCompiler.Code(AnalyticalViewConfigSchema, {
    language: 'typescript',
  }).replace('return function check', 'export default function check');
fs.writeFileSync(checkTargetFile, analyticSchemaValidatorCode);

However, the biggest problem is that I did not get the diagnostic errors for when check fails. Seams that .Compile and .Code are not functionally equivalent? Is there some way to get the .Errors functionality from AOT code that I have missed?

sinclairzx81 commented 1 week ago

@peterlundberg Hi,

The AOT options for TypeBox are currently limited to Check functionality only. For errors, these are obtained via calls to Value.Errors() which uses dynamic property checking which are not compiled. The Errors() function is shared between TypeCompiler and Value.* operations.

Content-Security

The recommendation for Content Security restricted environments is to simply use Value.* functions instead of the compiler. These functions are simpler to use, and do not require eval to work. They do however come at a performance cost, but should be more than adequate for most browser level workloads.

Experimental

If you do need high performance checking browser side while operating in a eval restricted context, you can try the following. This approach uses the compiler to generate Check code, which is then loaded via dynamic ESM import to a generated ObjectURL (avoiding explicit calls to eval). This approach works in Deno, it may also work in Browser environments that are behind content security restriction (but it would need testing)

import { TypeCompiler, TypeCheck } from 'npm:@sinclair/typebox/compiler'
import { TSchema, Type } from 'npm:@sinclair/typebox'

/** 
 * Experimental workaround for content security restricted environments. Should work
 * in Deno and Browser environments. Other enviroments such as Node, Bun, Cloudflare
 * may have issues importing from instanced ObjectURL's.
 */
async function DynamicCompile<T extends TSchema>(schema: T, references: TSchema[] = []): Promise<TypeCheck<T>> {
  const code = `export default function() { ${TypeCompiler.Code(schema, references)} }`
  const blob = new Blob([code])
  const url = URL.createObjectURL(blob)
  const mod = await import(url)
  const check = mod.default()
  URL.revokeObjectURL(url)
  return new TypeCheck(schema, references, check, code)
}

// ------------------------------------------------------------------
// Usage
// ------------------------------------------------------------------

const T = Type.Object({
  x: Type.Number(),
  y: Type.Number(),
  z: Type.Number(),
})

const C = await DynamicCompile(T)       // Compile

const R = C.Check({ x: 1, y: 2, z: 3 }) // true

const E = [...C.Errors({ x: 1 })]       // [...errors]

You can try the above, but overall the recommendation is simply to use Value.* operations when working in content security restricted environments.

Hope this helps S

peterlundberg commented 1 week ago

It does, thank you.

Will go the Value route then. As you said in the browser it is rarely a performance issue, just fiddely to use the right constuct in the right environment.

The suggested workaround will probably get shut down by browsers sooner or later as is still in effect dynamic eval. And for true high perf situations the diagnostics errors are not that critical.

On Fri, 25 Oct 2024, 17:44 Haydn Paterson, @.***> wrote:

@peterlundberg https://github.com/peterlundberg Hi,

The AOT options for TypeBox are currently limited to Check functionality only. For errors, these are obtained via calls to Value.Errors() which uses dynamic property checking which are not compiled. The Errors() function is shared between TypeCompiler and Value.* operations. Content-Security

The recommendation for Content Security restricted environments is to simply use Value.* functions instead of the compiler. These functions are simpler to use, and do not require eval to work. They do however come at a performance cost, but are perfectly adequate for most browser level workloads. Experimental

If you do need high performance checking browser side while operating in a eval restricted context, you can try the following. This approach uses the compiler to generate Check code, which is then loaded via dynamic ESM import to a generated ObjectURL (avoiding explicit calls to eval). This approach works in Deno, it may also work in Browser environments that are behind content security restriction (but it would need testing)

import { TypeCompiler, TypeCheck } from @./typebox/compiler'import { TSchema, Type } from @./typebox' /* Experimental workaround for content security restricted environments. Should work in Deno and Browser environments. Other enviroments such as Node, Bun, Cloudflare may have issues imported from instance ObjectURL. */async function DynamicCompile(schema: T, references: TSchema[] = []): Promise<TypeCheck> { const code = export default function() { ${TypeCompiler.Code(T)} } const blob = new Blob([code]) const url = URL.createObjectURL(blob) const mod = await import(url) const check = mod.default() URL.revokeObjectURL(url) return new TypeCheck(schema, references, check, code)} // ------------------------------------------------------------------// Usage// ------------------------------------------------------------------ const T = Type.Object({ x: Type.Number(), y: Type.Number(), z: Type.Number(),}) const C = await DynamicCompile(T) // Compile const R = C.Check({ x: 1, y: 2, z: 3 }) // true const E = [...C.Errors({ x: 1 })] // [...errors]


You can try the above, but overall the recommendation is simply to use Value.* operations when working in content security restricted environments.

Hope this helps S

— Reply to this email directly, view it on GitHub https://github.com/sinclairzx81/typebox/issues/1045#issuecomment-2438164296, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACHTN7WISZNKIJRX5UM4MDZ5JRNTAVCNFSM6AAAAABQTI5AQKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIMZYGE3DIMRZGY . You are receiving this because you were mentioned.Message ID: @.***>

sinclairzx81 commented 1 week ago

@peterlundberg Alright cool. Will close off this issue for now.

Cheers! S