@valbuild/server 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import express from "express";
2
+ import { Service } from "./Service.js";
3
+ import { ValServer } from "./ValServer.js";
4
+ export type LocalValServerOptions = {
5
+ service: Service;
6
+ };
7
+ export declare class LocalValServer implements ValServer {
8
+ readonly options: LocalValServerOptions;
9
+ constructor(options: LocalValServerOptions);
10
+ session(_req: express.Request, res: express.Response): Promise<void>;
11
+ getIds(req: express.Request<{
12
+ 0: string;
13
+ }>, res: express.Response): Promise<void>;
14
+ patchIds(req: express.Request<{
15
+ 0: string;
16
+ }>, res: express.Response): Promise<void>;
17
+ private badRequest;
18
+ commit(req: express.Request, res: express.Response): Promise<void>;
19
+ authorize(req: express.Request, res: express.Response): Promise<void>;
20
+ callback(req: express.Request, res: express.Response): Promise<void>;
21
+ logout(req: express.Request, res: express.Response): Promise<void>;
22
+ }
@@ -1,7 +1,22 @@
1
- import { type Source, type SerializedSchema } from "@valbuild/core";
2
- import { type SourcePath } from "@valbuild/core/src/val";
1
+ import { type Source, type SerializedSchema, ValidationErrors } from "@valbuild/core";
2
+ import { ModuleId, type SourcePath } from "@valbuild/core/src/val";
3
+ export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module"];
3
4
  export type SerializedModuleContent = {
4
5
  source: Source;
5
6
  schema: SerializedSchema;
6
7
  path: SourcePath;
8
+ errors: false;
9
+ } | {
10
+ source?: Source;
11
+ schema?: SerializedSchema;
12
+ path?: SourcePath;
13
+ errors: {
14
+ invalidModuleId?: ModuleId;
15
+ validation?: ValidationErrors;
16
+ fatal?: {
17
+ message: string;
18
+ stack?: string[];
19
+ type?: (typeof FATAL_ERROR_TYPES)[number];
20
+ }[];
21
+ };
7
22
  };
@@ -1,5 +1,6 @@
1
1
  import { QuickJSRuntime } from "quickjs-emscripten";
2
2
  import { Patch } from "@valbuild/core/patch";
3
+ import { ValModuleLoader } from "./ValModuleLoader.js";
3
4
  import { ValSourceFileHandler } from "./ValSourceFileHandler.js";
4
5
  import { IValFSHost } from "./ValFSHost.js";
5
6
  import { SerializedModuleContent } from "./SerializedModuleContent.js";
@@ -12,7 +13,7 @@ export type ServiceOptions = {
12
13
  */
13
14
  valConfigPath: string;
14
15
  };
15
- export declare function createService(projectRoot: string, opts: ServiceOptions, host?: IValFSHost): Promise<Service>;
16
+ export declare function createService(projectRoot: string, opts: ServiceOptions, host?: IValFSHost, loader?: ValModuleLoader): Promise<Service>;
16
17
  export declare class Service {
17
18
  private readonly sourceFileHandler;
18
19
  private readonly runtime;
@@ -0,0 +1,8 @@
1
+ import { SourcePath, ValidationError } from "@valbuild/core";
2
+ import { Patch } from "@valbuild/core/patch";
3
+ export declare function createFixPatch(config: {
4
+ projectRoot: string;
5
+ }, apply: boolean, sourcePath: SourcePath, validationError: ValidationError): Promise<{
6
+ patch: Patch;
7
+ remainingErrors: ValidationError[];
8
+ } | undefined>;
@@ -10,3 +10,5 @@ export type { IValFSHost } from "./ValFSHost.js";
10
10
  export type { ValFS } from "./ValFS.js";
11
11
  export { patchSourceFile } from "./patchValFile.js";
12
12
  export { formatSyntaxErrorTree } from "./patch/ts/syntax.js";
13
+ export { LocalValServer } from "./LocalValServer.js";
14
+ export { createFixPatch } from "./createFixPatch.js";
@@ -13,6 +13,7 @@ var express = require('express');
13
13
  var server = require('@valbuild/ui/server');
14
14
  var z = require('zod');
15
15
  var crypto = require('crypto');
16
+ var sizeOf = require('image-size');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
18
19
 
@@ -22,6 +23,7 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
22
23
  var express__default = /*#__PURE__*/_interopDefault(express);
23
24
  var z__default = /*#__PURE__*/_interopDefault(z);
24
25
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
26
+ var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
25
27
 
26
28
  class ValSyntaxError {
27
29
  constructor(message, node) {
@@ -571,41 +573,64 @@ globalThis.valModule = {
571
573
  id: valModule?.default && Internal.getValPath(valModule?.default),
572
574
  schema: valModule?.default && Internal.getSchema(valModule?.default)?.serialize(),
573
575
  source: valModule?.default && Internal.getRawSource(valModule?.default),
576
+ validation: valModule?.default && Internal.getSchema(valModule?.default)?.validate(
577
+ valModule?.default && Internal.getValPath(valModule?.default) || "/",
578
+ valModule?.default && Internal.getRawSource(valModule?.default)
579
+ )
574
580
  };
575
581
  `;
576
582
  const result = context.evalCode(code,
577
583
  // Synthetic module name
578
584
  path__default["default"].join(path__default["default"].dirname(valConfigPath), "<val>"));
585
+ const fatalErrors = [];
579
586
  if (result.error) {
580
587
  const error = result.error.consume(context.dump);
581
- console.error("Got error", error); // TODO: use this to figure out how to strip out QuickJS specific errors and get the actual stack
582
-
583
- throw new Error(`Could not read val id: ${id}. Cause:\n${error.name}: ${error.message}${error.stack ? error.stack : ""}`);
588
+ return {
589
+ errors: {
590
+ invalidModuleId: id,
591
+ fatal: [{
592
+ message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
593
+ stack: error.stack
594
+ }]
595
+ }
596
+ };
584
597
  } else {
585
598
  result.value.dispose();
586
599
  const valModule = context.getProp(context.global, "valModule").consume(context.dump);
587
- const errors = [];
588
600
  if (!valModule) {
589
- errors.push(`Could not find any modules at: ${id}`);
601
+ fatalErrors.push(`Could not find any modules at: ${id}`);
590
602
  } else {
591
603
  if (valModule.id !== id) {
592
- errors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
604
+ fatalErrors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
593
605
  }
594
606
  if (!(valModule !== null && valModule !== void 0 && valModule.schema)) {
595
- errors.push(`Expected val id: '${id}' to have a schema`);
607
+ fatalErrors.push(`Expected val id: '${id}' to have a schema`);
596
608
  }
597
609
  if (!(valModule !== null && valModule !== void 0 && valModule.source)) {
598
- errors.push(`Expected val id: '${id}' to have a source`);
610
+ fatalErrors.push(`Expected val id: '${id}' to have a source`);
599
611
  }
600
612
  }
601
- if (errors.length > 0) {
602
- throw Error(`While processing module of id: ${id}, we got the following errors:\n${errors.join("\n")}`);
613
+ let errors = false;
614
+ if (fatalErrors.length > 0) {
615
+ errors = {
616
+ invalidModuleId: valModule.id !== id ? id : undefined,
617
+ fatal: fatalErrors.map(message => ({
618
+ message
619
+ }))
620
+ };
621
+ }
622
+ if (valModule !== null && valModule !== void 0 && valModule.validation) {
623
+ errors = {
624
+ ...(errors ? errors : {}),
625
+ validation: valModule.validation
626
+ };
603
627
  }
604
628
  return {
605
629
  path: valModule.id,
606
- // This might not be the asked id/path, however, that should be handled further up in the call chain
630
+ // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
607
631
  source: valModule.source,
608
- schema: valModule.schema
632
+ schema: valModule.schema,
633
+ errors
609
634
  };
610
635
  }
611
636
  } finally {
@@ -752,7 +777,7 @@ class ValModuleLoader {
752
777
  // allowJs: true,
753
778
  // rootDir: this.compilerOptions.rootDir,
754
779
  module: ts__default["default"].ModuleKind.ESNext,
755
- target: ts__default["default"].ScriptTarget.ES2020 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
780
+ target: ts__default["default"].ScriptTarget.ES2015 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
756
781
  // moduleResolution: ts.ModuleResolutionKind.NodeNext,
757
782
  // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
758
783
  });
@@ -873,12 +898,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
873
898
  async function createService(projectRoot, opts, host = {
874
899
  ...ts__default["default"].sys,
875
900
  writeFile: fs__default["default"].writeFileSync
876
- }) {
901
+ }, loader) {
877
902
  const compilerOptions = getCompilerOptions(projectRoot, host);
878
903
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
879
- const loader = new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host);
880
904
  const module = await quickjsEmscripten.newQuickJSWASMModule();
881
- const runtime = await newValQuickJSRuntime(module, loader);
905
+ const runtime = await newValQuickJSRuntime(module, loader || new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host));
882
906
  return new Service(opts, sourceFileHandler, runtime);
883
907
  }
884
908
  class Service {
@@ -891,12 +915,23 @@ class Service {
891
915
  }
892
916
  async get(moduleId, modulePath) {
893
917
  const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
894
- const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
895
- return {
896
- path: [moduleId, resolved.path].join("."),
897
- schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
898
- source: resolved.source
899
- };
918
+ if (valModule.source && valModule.schema) {
919
+ const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
920
+ const sourcePath = [moduleId, resolved.path].join(".");
921
+ return {
922
+ path: sourcePath,
923
+ schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
924
+ source: resolved.source,
925
+ errors: valModule.errors && valModule.errors.validation && valModule.errors.validation[sourcePath] ? {
926
+ validation: valModule.errors.validation[sourcePath] ? {
927
+ [sourcePath]: valModule.errors.validation[sourcePath]
928
+ } : undefined,
929
+ fatal: valModule.errors && valModule.errors.fatal ? valModule.errors.fatal : undefined
930
+ } : false
931
+ };
932
+ } else {
933
+ return valModule;
934
+ }
900
935
  }
901
936
  async patch(moduleId, patch) {
902
937
  return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
@@ -1514,10 +1549,120 @@ class ValFSHost {
1514
1549
  }
1515
1550
  }
1516
1551
 
1552
+ async function createFixPatch(config, apply, sourcePath, validationError) {
1553
+ async function getImageMetadata() {
1554
+ const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
1555
+ if (!maybeRef) {
1556
+ // TODO:
1557
+ throw Error("Cannot fix image without a file reference");
1558
+ }
1559
+ const localFile = path__default["default"].join(config.projectRoot, maybeRef);
1560
+ const buffer = fs__default["default"].readFileSync(localFile);
1561
+ const sha256 = await getSHA256Hash(buffer);
1562
+ const imageSize = sizeOf__default["default"](buffer);
1563
+ return {
1564
+ ...imageSize,
1565
+ sha256
1566
+ };
1567
+ }
1568
+ const remainingErrors = [];
1569
+ const patch$1 = [];
1570
+ for (const fix of validationError.fixes || []) {
1571
+ if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
1572
+ const imageMetadata = await getImageMetadata();
1573
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
1574
+ remainingErrors.push({
1575
+ ...validationError,
1576
+ message: "Failed to get image metadata",
1577
+ fixes: undefined
1578
+ });
1579
+ } else if (fix === "image:replace-metadata") {
1580
+ const currentValue = validationError.value;
1581
+ const metadataIsCorrect =
1582
+ // metadata is a prop that is an object
1583
+ typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
1584
+ // sha256 is correct
1585
+ "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
1586
+ // width is correct
1587
+ "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
1588
+ // height is correct
1589
+ "height" in currentValue.metadata && currentValue.metadata.height === imageMetadata.height;
1590
+
1591
+ // skips if the metadata is already correct
1592
+ if (!metadataIsCorrect) {
1593
+ if (apply) {
1594
+ patch$1.push({
1595
+ op: "replace",
1596
+ path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
1597
+ value: {
1598
+ width: imageMetadata.width,
1599
+ height: imageMetadata.height,
1600
+ sha256: imageMetadata.sha256
1601
+ }
1602
+ });
1603
+ } else {
1604
+ if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
1605
+ if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
1606
+ remainingErrors.push({
1607
+ message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
1608
+ fixes: undefined
1609
+ });
1610
+ }
1611
+ if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
1612
+ remainingErrors.push({
1613
+ message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
1614
+ fixes: undefined
1615
+ });
1616
+ }
1617
+ if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
1618
+ remainingErrors.push({
1619
+ message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
1620
+ fixes: undefined
1621
+ });
1622
+ }
1623
+ } else {
1624
+ remainingErrors.push({
1625
+ ...validationError,
1626
+ message: "Image metadata is not an object!",
1627
+ fixes: undefined
1628
+ });
1629
+ }
1630
+ }
1631
+ }
1632
+ } else if (fix === "image:add-metadata") {
1633
+ patch$1.push({
1634
+ op: "add",
1635
+ path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
1636
+ value: {
1637
+ width: imageMetadata.width,
1638
+ height: imageMetadata.height,
1639
+ sha256: imageMetadata.sha256
1640
+ }
1641
+ });
1642
+ }
1643
+ }
1644
+ }
1645
+ if (!validationError.fixes || validationError.fixes.length === 0) {
1646
+ remainingErrors.push(validationError);
1647
+ }
1648
+ return {
1649
+ patch: patch$1,
1650
+ remainingErrors
1651
+ };
1652
+ }
1653
+ const getSHA256Hash = async bits => {
1654
+ const hashBuffer = await crypto__default["default"].subtle.digest("SHA-256", bits);
1655
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1656
+ const hash = hashArray.map(item => item.toString(16).padStart(2, "0")).join("");
1657
+ return hash;
1658
+ };
1659
+
1660
+ exports.LocalValServer = LocalValServer;
1517
1661
  exports.Service = Service;
1518
1662
  exports.ValFSHost = ValFSHost;
1519
1663
  exports.ValModuleLoader = ValModuleLoader;
1520
1664
  exports.ValSourceFileHandler = ValSourceFileHandler;
1665
+ exports.createFixPatch = createFixPatch;
1521
1666
  exports.createRequestHandler = createRequestHandler;
1522
1667
  exports.createRequestListener = createRequestListener;
1523
1668
  exports.createService = createService;
@@ -13,6 +13,7 @@ var express = require('express');
13
13
  var server = require('@valbuild/ui/server');
14
14
  var z = require('zod');
15
15
  var crypto = require('crypto');
16
+ var sizeOf = require('image-size');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
18
19
 
@@ -22,6 +23,7 @@ var fs__default = /*#__PURE__*/_interopDefault(fs);
22
23
  var express__default = /*#__PURE__*/_interopDefault(express);
23
24
  var z__default = /*#__PURE__*/_interopDefault(z);
24
25
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
26
+ var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
25
27
 
26
28
  class ValSyntaxError {
27
29
  constructor(message, node) {
@@ -571,41 +573,64 @@ globalThis.valModule = {
571
573
  id: valModule?.default && Internal.getValPath(valModule?.default),
572
574
  schema: valModule?.default && Internal.getSchema(valModule?.default)?.serialize(),
573
575
  source: valModule?.default && Internal.getRawSource(valModule?.default),
576
+ validation: valModule?.default && Internal.getSchema(valModule?.default)?.validate(
577
+ valModule?.default && Internal.getValPath(valModule?.default) || "/",
578
+ valModule?.default && Internal.getRawSource(valModule?.default)
579
+ )
574
580
  };
575
581
  `;
576
582
  const result = context.evalCode(code,
577
583
  // Synthetic module name
578
584
  path__default["default"].join(path__default["default"].dirname(valConfigPath), "<val>"));
585
+ const fatalErrors = [];
579
586
  if (result.error) {
580
587
  const error = result.error.consume(context.dump);
581
- console.error("Got error", error); // TODO: use this to figure out how to strip out QuickJS specific errors and get the actual stack
582
-
583
- throw new Error(`Could not read val id: ${id}. Cause:\n${error.name}: ${error.message}${error.stack ? error.stack : ""}`);
588
+ return {
589
+ errors: {
590
+ invalidModuleId: id,
591
+ fatal: [{
592
+ message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
593
+ stack: error.stack
594
+ }]
595
+ }
596
+ };
584
597
  } else {
585
598
  result.value.dispose();
586
599
  const valModule = context.getProp(context.global, "valModule").consume(context.dump);
587
- const errors = [];
588
600
  if (!valModule) {
589
- errors.push(`Could not find any modules at: ${id}`);
601
+ fatalErrors.push(`Could not find any modules at: ${id}`);
590
602
  } else {
591
603
  if (valModule.id !== id) {
592
- errors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
604
+ fatalErrors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
593
605
  }
594
606
  if (!(valModule !== null && valModule !== void 0 && valModule.schema)) {
595
- errors.push(`Expected val id: '${id}' to have a schema`);
607
+ fatalErrors.push(`Expected val id: '${id}' to have a schema`);
596
608
  }
597
609
  if (!(valModule !== null && valModule !== void 0 && valModule.source)) {
598
- errors.push(`Expected val id: '${id}' to have a source`);
610
+ fatalErrors.push(`Expected val id: '${id}' to have a source`);
599
611
  }
600
612
  }
601
- if (errors.length > 0) {
602
- throw Error(`While processing module of id: ${id}, we got the following errors:\n${errors.join("\n")}`);
613
+ let errors = false;
614
+ if (fatalErrors.length > 0) {
615
+ errors = {
616
+ invalidModuleId: valModule.id !== id ? id : undefined,
617
+ fatal: fatalErrors.map(message => ({
618
+ message
619
+ }))
620
+ };
621
+ }
622
+ if (valModule !== null && valModule !== void 0 && valModule.validation) {
623
+ errors = {
624
+ ...(errors ? errors : {}),
625
+ validation: valModule.validation
626
+ };
603
627
  }
604
628
  return {
605
629
  path: valModule.id,
606
- // This might not be the asked id/path, however, that should be handled further up in the call chain
630
+ // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
607
631
  source: valModule.source,
608
- schema: valModule.schema
632
+ schema: valModule.schema,
633
+ errors
609
634
  };
610
635
  }
611
636
  } finally {
@@ -752,7 +777,7 @@ class ValModuleLoader {
752
777
  // allowJs: true,
753
778
  // rootDir: this.compilerOptions.rootDir,
754
779
  module: ts__default["default"].ModuleKind.ESNext,
755
- target: ts__default["default"].ScriptTarget.ES2020 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
780
+ target: ts__default["default"].ScriptTarget.ES2015 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
756
781
  // moduleResolution: ts.ModuleResolutionKind.NodeNext,
757
782
  // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
758
783
  });
@@ -873,12 +898,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
873
898
  async function createService(projectRoot, opts, host = {
874
899
  ...ts__default["default"].sys,
875
900
  writeFile: fs__default["default"].writeFileSync
876
- }) {
901
+ }, loader) {
877
902
  const compilerOptions = getCompilerOptions(projectRoot, host);
878
903
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
879
- const loader = new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host);
880
904
  const module = await quickjsEmscripten.newQuickJSWASMModule();
881
- const runtime = await newValQuickJSRuntime(module, loader);
905
+ const runtime = await newValQuickJSRuntime(module, loader || new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host));
882
906
  return new Service(opts, sourceFileHandler, runtime);
883
907
  }
884
908
  class Service {
@@ -891,12 +915,23 @@ class Service {
891
915
  }
892
916
  async get(moduleId, modulePath) {
893
917
  const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
894
- const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
895
- return {
896
- path: [moduleId, resolved.path].join("."),
897
- schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
898
- source: resolved.source
899
- };
918
+ if (valModule.source && valModule.schema) {
919
+ const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
920
+ const sourcePath = [moduleId, resolved.path].join(".");
921
+ return {
922
+ path: sourcePath,
923
+ schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
924
+ source: resolved.source,
925
+ errors: valModule.errors && valModule.errors.validation && valModule.errors.validation[sourcePath] ? {
926
+ validation: valModule.errors.validation[sourcePath] ? {
927
+ [sourcePath]: valModule.errors.validation[sourcePath]
928
+ } : undefined,
929
+ fatal: valModule.errors && valModule.errors.fatal ? valModule.errors.fatal : undefined
930
+ } : false
931
+ };
932
+ } else {
933
+ return valModule;
934
+ }
900
935
  }
901
936
  async patch(moduleId, patch) {
902
937
  return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
@@ -1514,10 +1549,120 @@ class ValFSHost {
1514
1549
  }
1515
1550
  }
1516
1551
 
1552
+ async function createFixPatch(config, apply, sourcePath, validationError) {
1553
+ async function getImageMetadata() {
1554
+ const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
1555
+ if (!maybeRef) {
1556
+ // TODO:
1557
+ throw Error("Cannot fix image without a file reference");
1558
+ }
1559
+ const localFile = path__default["default"].join(config.projectRoot, maybeRef);
1560
+ const buffer = fs__default["default"].readFileSync(localFile);
1561
+ const sha256 = await getSHA256Hash(buffer);
1562
+ const imageSize = sizeOf__default["default"](buffer);
1563
+ return {
1564
+ ...imageSize,
1565
+ sha256
1566
+ };
1567
+ }
1568
+ const remainingErrors = [];
1569
+ const patch$1 = [];
1570
+ for (const fix of validationError.fixes || []) {
1571
+ if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
1572
+ const imageMetadata = await getImageMetadata();
1573
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
1574
+ remainingErrors.push({
1575
+ ...validationError,
1576
+ message: "Failed to get image metadata",
1577
+ fixes: undefined
1578
+ });
1579
+ } else if (fix === "image:replace-metadata") {
1580
+ const currentValue = validationError.value;
1581
+ const metadataIsCorrect =
1582
+ // metadata is a prop that is an object
1583
+ typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
1584
+ // sha256 is correct
1585
+ "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
1586
+ // width is correct
1587
+ "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
1588
+ // height is correct
1589
+ "height" in currentValue.metadata && currentValue.metadata.height === imageMetadata.height;
1590
+
1591
+ // skips if the metadata is already correct
1592
+ if (!metadataIsCorrect) {
1593
+ if (apply) {
1594
+ patch$1.push({
1595
+ op: "replace",
1596
+ path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
1597
+ value: {
1598
+ width: imageMetadata.width,
1599
+ height: imageMetadata.height,
1600
+ sha256: imageMetadata.sha256
1601
+ }
1602
+ });
1603
+ } else {
1604
+ if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
1605
+ if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
1606
+ remainingErrors.push({
1607
+ message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
1608
+ fixes: undefined
1609
+ });
1610
+ }
1611
+ if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
1612
+ remainingErrors.push({
1613
+ message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
1614
+ fixes: undefined
1615
+ });
1616
+ }
1617
+ if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
1618
+ remainingErrors.push({
1619
+ message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
1620
+ fixes: undefined
1621
+ });
1622
+ }
1623
+ } else {
1624
+ remainingErrors.push({
1625
+ ...validationError,
1626
+ message: "Image metadata is not an object!",
1627
+ fixes: undefined
1628
+ });
1629
+ }
1630
+ }
1631
+ }
1632
+ } else if (fix === "image:add-metadata") {
1633
+ patch$1.push({
1634
+ op: "add",
1635
+ path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
1636
+ value: {
1637
+ width: imageMetadata.width,
1638
+ height: imageMetadata.height,
1639
+ sha256: imageMetadata.sha256
1640
+ }
1641
+ });
1642
+ }
1643
+ }
1644
+ }
1645
+ if (!validationError.fixes || validationError.fixes.length === 0) {
1646
+ remainingErrors.push(validationError);
1647
+ }
1648
+ return {
1649
+ patch: patch$1,
1650
+ remainingErrors
1651
+ };
1652
+ }
1653
+ const getSHA256Hash = async bits => {
1654
+ const hashBuffer = await crypto__default["default"].subtle.digest("SHA-256", bits);
1655
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1656
+ const hash = hashArray.map(item => item.toString(16).padStart(2, "0")).join("");
1657
+ return hash;
1658
+ };
1659
+
1660
+ exports.LocalValServer = LocalValServer;
1517
1661
  exports.Service = Service;
1518
1662
  exports.ValFSHost = ValFSHost;
1519
1663
  exports.ValModuleLoader = ValModuleLoader;
1520
1664
  exports.ValSourceFileHandler = ValSourceFileHandler;
1665
+ exports.createFixPatch = createFixPatch;
1521
1666
  exports.createRequestHandler = createRequestHandler;
1522
1667
  exports.createRequestListener = createRequestListener;
1523
1668
  exports.createService = createService;
@@ -2,13 +2,14 @@ import { newQuickJSWASMModule } from 'quickjs-emscripten';
2
2
  import ts from 'typescript';
3
3
  import { result, pipe } from '@valbuild/core/fp';
4
4
  import { FILE_REF_PROP, derefPatch, Internal, Schema } from '@valbuild/core';
5
- import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, parsePatch } from '@valbuild/core/patch';
5
+ import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, parsePatch, sourceToPatchPath } from '@valbuild/core/patch';
6
6
  import path from 'path';
7
7
  import fs from 'fs';
8
8
  import express, { Router } from 'express';
9
9
  import { createRequestHandler as createRequestHandler$1 } from '@valbuild/ui/server';
10
10
  import z, { z as z$1 } from 'zod';
11
11
  import crypto from 'crypto';
12
+ import sizeOf from 'image-size';
12
13
 
13
14
  class ValSyntaxError {
14
15
  constructor(message, node) {
@@ -558,41 +559,64 @@ globalThis.valModule = {
558
559
  id: valModule?.default && Internal.getValPath(valModule?.default),
559
560
  schema: valModule?.default && Internal.getSchema(valModule?.default)?.serialize(),
560
561
  source: valModule?.default && Internal.getRawSource(valModule?.default),
562
+ validation: valModule?.default && Internal.getSchema(valModule?.default)?.validate(
563
+ valModule?.default && Internal.getValPath(valModule?.default) || "/",
564
+ valModule?.default && Internal.getRawSource(valModule?.default)
565
+ )
561
566
  };
562
567
  `;
563
568
  const result = context.evalCode(code,
564
569
  // Synthetic module name
565
570
  path.join(path.dirname(valConfigPath), "<val>"));
571
+ const fatalErrors = [];
566
572
  if (result.error) {
567
573
  const error = result.error.consume(context.dump);
568
- console.error("Got error", error); // TODO: use this to figure out how to strip out QuickJS specific errors and get the actual stack
569
-
570
- throw new Error(`Could not read val id: ${id}. Cause:\n${error.name}: ${error.message}${error.stack ? error.stack : ""}`);
574
+ return {
575
+ errors: {
576
+ invalidModuleId: id,
577
+ fatal: [{
578
+ message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
579
+ stack: error.stack
580
+ }]
581
+ }
582
+ };
571
583
  } else {
572
584
  result.value.dispose();
573
585
  const valModule = context.getProp(context.global, "valModule").consume(context.dump);
574
- const errors = [];
575
586
  if (!valModule) {
576
- errors.push(`Could not find any modules at: ${id}`);
587
+ fatalErrors.push(`Could not find any modules at: ${id}`);
577
588
  } else {
578
589
  if (valModule.id !== id) {
579
- errors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
590
+ fatalErrors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
580
591
  }
581
592
  if (!(valModule !== null && valModule !== void 0 && valModule.schema)) {
582
- errors.push(`Expected val id: '${id}' to have a schema`);
593
+ fatalErrors.push(`Expected val id: '${id}' to have a schema`);
583
594
  }
584
595
  if (!(valModule !== null && valModule !== void 0 && valModule.source)) {
585
- errors.push(`Expected val id: '${id}' to have a source`);
596
+ fatalErrors.push(`Expected val id: '${id}' to have a source`);
586
597
  }
587
598
  }
588
- if (errors.length > 0) {
589
- throw Error(`While processing module of id: ${id}, we got the following errors:\n${errors.join("\n")}`);
599
+ let errors = false;
600
+ if (fatalErrors.length > 0) {
601
+ errors = {
602
+ invalidModuleId: valModule.id !== id ? id : undefined,
603
+ fatal: fatalErrors.map(message => ({
604
+ message
605
+ }))
606
+ };
607
+ }
608
+ if (valModule !== null && valModule !== void 0 && valModule.validation) {
609
+ errors = {
610
+ ...(errors ? errors : {}),
611
+ validation: valModule.validation
612
+ };
590
613
  }
591
614
  return {
592
615
  path: valModule.id,
593
- // This might not be the asked id/path, however, that should be handled further up in the call chain
616
+ // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
594
617
  source: valModule.source,
595
- schema: valModule.schema
618
+ schema: valModule.schema,
619
+ errors
596
620
  };
597
621
  }
598
622
  } finally {
@@ -739,7 +763,7 @@ class ValModuleLoader {
739
763
  // allowJs: true,
740
764
  // rootDir: this.compilerOptions.rootDir,
741
765
  module: ts.ModuleKind.ESNext,
742
- target: ts.ScriptTarget.ES2020 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
766
+ target: ts.ScriptTarget.ES2015 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
743
767
  // moduleResolution: ts.ModuleResolutionKind.NodeNext,
744
768
  // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
745
769
  });
@@ -860,12 +884,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
860
884
  async function createService(projectRoot, opts, host = {
861
885
  ...ts.sys,
862
886
  writeFile: fs.writeFileSync
863
- }) {
887
+ }, loader) {
864
888
  const compilerOptions = getCompilerOptions(projectRoot, host);
865
889
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
866
- const loader = new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host);
867
890
  const module = await newQuickJSWASMModule();
868
- const runtime = await newValQuickJSRuntime(module, loader);
891
+ const runtime = await newValQuickJSRuntime(module, loader || new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host));
869
892
  return new Service(opts, sourceFileHandler, runtime);
870
893
  }
871
894
  class Service {
@@ -878,12 +901,23 @@ class Service {
878
901
  }
879
902
  async get(moduleId, modulePath) {
880
903
  const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
881
- const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
882
- return {
883
- path: [moduleId, resolved.path].join("."),
884
- schema: resolved.schema instanceof Schema ? resolved.schema.serialize() : resolved.schema,
885
- source: resolved.source
886
- };
904
+ if (valModule.source && valModule.schema) {
905
+ const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
906
+ const sourcePath = [moduleId, resolved.path].join(".");
907
+ return {
908
+ path: sourcePath,
909
+ schema: resolved.schema instanceof Schema ? resolved.schema.serialize() : resolved.schema,
910
+ source: resolved.source,
911
+ errors: valModule.errors && valModule.errors.validation && valModule.errors.validation[sourcePath] ? {
912
+ validation: valModule.errors.validation[sourcePath] ? {
913
+ [sourcePath]: valModule.errors.validation[sourcePath]
914
+ } : undefined,
915
+ fatal: valModule.errors && valModule.errors.fatal ? valModule.errors.fatal : undefined
916
+ } : false
917
+ };
918
+ } else {
919
+ return valModule;
920
+ }
887
921
  }
888
922
  async patch(moduleId, patch) {
889
923
  return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
@@ -1501,4 +1535,112 @@ class ValFSHost {
1501
1535
  }
1502
1536
  }
1503
1537
 
1504
- export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createRequestHandler, createRequestListener, createService, formatSyntaxErrorTree, getCompilerOptions, patchSourceFile };
1538
+ async function createFixPatch(config, apply, sourcePath, validationError) {
1539
+ async function getImageMetadata() {
1540
+ const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
1541
+ if (!maybeRef) {
1542
+ // TODO:
1543
+ throw Error("Cannot fix image without a file reference");
1544
+ }
1545
+ const localFile = path.join(config.projectRoot, maybeRef);
1546
+ const buffer = fs.readFileSync(localFile);
1547
+ const sha256 = await getSHA256Hash(buffer);
1548
+ const imageSize = sizeOf(buffer);
1549
+ return {
1550
+ ...imageSize,
1551
+ sha256
1552
+ };
1553
+ }
1554
+ const remainingErrors = [];
1555
+ const patch = [];
1556
+ for (const fix of validationError.fixes || []) {
1557
+ if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
1558
+ const imageMetadata = await getImageMetadata();
1559
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
1560
+ remainingErrors.push({
1561
+ ...validationError,
1562
+ message: "Failed to get image metadata",
1563
+ fixes: undefined
1564
+ });
1565
+ } else if (fix === "image:replace-metadata") {
1566
+ const currentValue = validationError.value;
1567
+ const metadataIsCorrect =
1568
+ // metadata is a prop that is an object
1569
+ typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
1570
+ // sha256 is correct
1571
+ "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
1572
+ // width is correct
1573
+ "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
1574
+ // height is correct
1575
+ "height" in currentValue.metadata && currentValue.metadata.height === imageMetadata.height;
1576
+
1577
+ // skips if the metadata is already correct
1578
+ if (!metadataIsCorrect) {
1579
+ if (apply) {
1580
+ patch.push({
1581
+ op: "replace",
1582
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
1583
+ value: {
1584
+ width: imageMetadata.width,
1585
+ height: imageMetadata.height,
1586
+ sha256: imageMetadata.sha256
1587
+ }
1588
+ });
1589
+ } else {
1590
+ if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
1591
+ if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
1592
+ remainingErrors.push({
1593
+ message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
1594
+ fixes: undefined
1595
+ });
1596
+ }
1597
+ if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
1598
+ remainingErrors.push({
1599
+ message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
1600
+ fixes: undefined
1601
+ });
1602
+ }
1603
+ if (!("height" in currentValue.metadata) || currentValue.metadata.height !== imageMetadata.height) {
1604
+ remainingErrors.push({
1605
+ message: "Image metadata height is incorrect! Found: " + ("height" in currentValue.metadata ? currentValue.metadata.height : "<empty>") + ". Expected: " + imageMetadata.height,
1606
+ fixes: undefined
1607
+ });
1608
+ }
1609
+ } else {
1610
+ remainingErrors.push({
1611
+ ...validationError,
1612
+ message: "Image metadata is not an object!",
1613
+ fixes: undefined
1614
+ });
1615
+ }
1616
+ }
1617
+ }
1618
+ } else if (fix === "image:add-metadata") {
1619
+ patch.push({
1620
+ op: "add",
1621
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
1622
+ value: {
1623
+ width: imageMetadata.width,
1624
+ height: imageMetadata.height,
1625
+ sha256: imageMetadata.sha256
1626
+ }
1627
+ });
1628
+ }
1629
+ }
1630
+ }
1631
+ if (!validationError.fixes || validationError.fixes.length === 0) {
1632
+ remainingErrors.push(validationError);
1633
+ }
1634
+ return {
1635
+ patch,
1636
+ remainingErrors
1637
+ };
1638
+ }
1639
+ const getSHA256Hash = async bits => {
1640
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bits);
1641
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1642
+ const hash = hashArray.map(item => item.toString(16).padStart(2, "0")).join("");
1643
+ return hash;
1644
+ };
1645
+
1646
+ export { LocalValServer, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createRequestHandler, createRequestListener, createService, formatSyntaxErrorTree, getCompilerOptions, patchSourceFile };
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "./package.json": "./package.json"
13
13
  },
14
14
  "types": "dist/valbuild-server.cjs.d.ts",
15
- "version": "0.15.0",
15
+ "version": "0.16.0",
16
16
  "scripts": {
17
17
  "typecheck": "tsc --noEmit",
18
18
  "test": "jest",
@@ -25,9 +25,10 @@
25
25
  "concurrently": "^7.6.0"
26
26
  },
27
27
  "dependencies": {
28
- "@valbuild/core": "~0.15.0",
28
+ "@valbuild/core": "~0.16.0",
29
29
  "@valbuild/ui": "~0.15.0",
30
30
  "express": "^4.18.2",
31
+ "image-size": "^1.0.2",
31
32
  "quickjs-emscripten": "^0.21.1",
32
33
  "ts-morph": "^17.0.1",
33
34
  "typescript": "^4.9.4",
@@ -1,8 +1,35 @@
1
- import { type Source, type SerializedSchema } from "@valbuild/core";
2
- import { type SourcePath } from "@valbuild/core/src/val";
1
+ import {
2
+ type Source,
3
+ type SerializedSchema,
4
+ ValidationErrors,
5
+ } from "@valbuild/core";
6
+ import { ModuleId, type SourcePath } from "@valbuild/core/src/val";
3
7
 
4
- export type SerializedModuleContent = {
5
- source: Source;
6
- schema: SerializedSchema;
7
- path: SourcePath;
8
- };
8
+ export const FATAL_ERROR_TYPES = [
9
+ "no-schema",
10
+ "no-source",
11
+ "invalid-id",
12
+ "no-module",
13
+ ] as const;
14
+
15
+ export type SerializedModuleContent =
16
+ | {
17
+ source: Source;
18
+ schema: SerializedSchema;
19
+ path: SourcePath;
20
+ errors: false;
21
+ }
22
+ | {
23
+ source?: Source;
24
+ schema?: SerializedSchema;
25
+ path?: SourcePath;
26
+ errors: {
27
+ invalidModuleId?: ModuleId;
28
+ validation?: ValidationErrors;
29
+ fatal?: {
30
+ message: string;
31
+ stack?: string[];
32
+ type?: (typeof FATAL_ERROR_TYPES)[number];
33
+ }[];
34
+ };
35
+ };
package/src/Service.ts CHANGED
@@ -34,7 +34,8 @@ export async function createService(
34
34
  host: IValFSHost = {
35
35
  ...ts.sys,
36
36
  writeFile: fs.writeFileSync,
37
- }
37
+ },
38
+ loader?: ValModuleLoader
38
39
  ): Promise<Service> {
39
40
  const compilerOptions = getCompilerOptions(projectRoot, host);
40
41
  const sourceFileHandler = new ValSourceFileHandler(
@@ -42,14 +43,12 @@ export async function createService(
42
43
  compilerOptions,
43
44
  host
44
45
  );
45
- const loader = new ValModuleLoader(
46
- projectRoot,
47
- compilerOptions,
48
- sourceFileHandler,
49
- host
50
- );
51
46
  const module = await newQuickJSWASMModule();
52
- const runtime = await newValQuickJSRuntime(module, loader);
47
+ const runtime = await newValQuickJSRuntime(
48
+ module,
49
+ loader ||
50
+ new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host)
51
+ );
53
52
  return new Service(opts, sourceFileHandler, runtime);
54
53
  }
55
54
 
@@ -74,19 +73,40 @@ export class Service {
74
73
  this.runtime
75
74
  );
76
75
 
77
- const resolved = Internal.resolvePath(
78
- modulePath,
79
- valModule.source,
80
- valModule.schema
81
- );
82
- return {
83
- path: [moduleId, resolved.path].join(".") as SourcePath,
84
- schema:
85
- resolved.schema instanceof Schema<SelectorSource>
86
- ? resolved.schema.serialize()
87
- : resolved.schema,
88
- source: resolved.source,
89
- };
76
+ if (valModule.source && valModule.schema) {
77
+ const resolved = Internal.resolvePath(
78
+ modulePath,
79
+ valModule.source,
80
+ valModule.schema
81
+ );
82
+ const sourcePath = [moduleId, resolved.path].join(".") as SourcePath;
83
+ return {
84
+ path: sourcePath,
85
+ schema:
86
+ resolved.schema instanceof Schema<SelectorSource>
87
+ ? resolved.schema.serialize()
88
+ : resolved.schema,
89
+ source: resolved.source,
90
+ errors:
91
+ valModule.errors &&
92
+ valModule.errors.validation &&
93
+ valModule.errors.validation[sourcePath]
94
+ ? {
95
+ validation: valModule.errors.validation[sourcePath]
96
+ ? {
97
+ [sourcePath]: valModule.errors.validation[sourcePath],
98
+ }
99
+ : undefined,
100
+ fatal:
101
+ valModule.errors && valModule.errors.fatal
102
+ ? valModule.errors.fatal
103
+ : undefined,
104
+ }
105
+ : false,
106
+ };
107
+ } else {
108
+ return valModule;
109
+ }
90
110
  }
91
111
 
92
112
  async patch(
@@ -60,7 +60,7 @@ export class ValModuleLoader {
60
60
  // allowJs: true,
61
61
  // rootDir: this.compilerOptions.rootDir,
62
62
  module: ts.ModuleKind.ESNext,
63
- target: ts.ScriptTarget.ES2020, // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
63
+ target: ts.ScriptTarget.ES2015, // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
64
64
  // moduleResolution: ts.ModuleResolutionKind.NodeNext,
65
65
  // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
66
66
  });
@@ -0,0 +1,174 @@
1
+ import { FILE_REF_PROP, SourcePath, ValidationError } from "@valbuild/core";
2
+ import { Patch, sourceToPatchPath } from "@valbuild/core/patch";
3
+ import sizeOf from "image-size";
4
+ import path from "path";
5
+ import fs from "fs";
6
+ import crypto from "crypto";
7
+
8
+ export async function createFixPatch(
9
+ config: { projectRoot: string },
10
+ apply: boolean,
11
+ sourcePath: SourcePath,
12
+ validationError: ValidationError
13
+ ): Promise<{ patch: Patch; remainingErrors: ValidationError[] } | undefined> {
14
+ async function getImageMetadata() {
15
+ const maybeRef =
16
+ validationError.value &&
17
+ typeof validationError.value === "object" &&
18
+ FILE_REF_PROP in validationError.value &&
19
+ typeof validationError.value[FILE_REF_PROP] === "string"
20
+ ? validationError.value[FILE_REF_PROP]
21
+ : undefined;
22
+
23
+ if (!maybeRef) {
24
+ // TODO:
25
+ throw Error("Cannot fix image without a file reference");
26
+ }
27
+ const localFile = path.join(config.projectRoot, maybeRef);
28
+ const buffer = fs.readFileSync(localFile);
29
+ const sha256 = await getSHA256Hash(buffer);
30
+ const imageSize = sizeOf(buffer);
31
+ return {
32
+ ...imageSize,
33
+ sha256,
34
+ };
35
+ }
36
+ const remainingErrors: ValidationError[] = [];
37
+ const patch: Patch = [];
38
+ for (const fix of validationError.fixes || []) {
39
+ if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
40
+ const imageMetadata = await getImageMetadata();
41
+ if (
42
+ imageMetadata.width === undefined ||
43
+ imageMetadata.height === undefined ||
44
+ imageMetadata.sha256 === undefined
45
+ ) {
46
+ remainingErrors.push({
47
+ ...validationError,
48
+ message: "Failed to get image metadata",
49
+ fixes: undefined,
50
+ });
51
+ } else if (fix === "image:replace-metadata") {
52
+ const currentValue = validationError.value;
53
+ const metadataIsCorrect =
54
+ // metadata is a prop that is an object
55
+ typeof currentValue === "object" &&
56
+ currentValue &&
57
+ "metadata" in currentValue &&
58
+ currentValue.metadata &&
59
+ typeof currentValue.metadata === "object" &&
60
+ // sha256 is correct
61
+ "sha256" in currentValue.metadata &&
62
+ currentValue.metadata.sha256 === imageMetadata.sha256 &&
63
+ // width is correct
64
+ "width" in currentValue.metadata &&
65
+ currentValue.metadata.width === imageMetadata.width &&
66
+ // height is correct
67
+ "height" in currentValue.metadata &&
68
+ currentValue.metadata.height === imageMetadata.height;
69
+
70
+ // skips if the metadata is already correct
71
+ if (!metadataIsCorrect) {
72
+ if (apply) {
73
+ patch.push({
74
+ op: "replace",
75
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
76
+ value: {
77
+ width: imageMetadata.width,
78
+ height: imageMetadata.height,
79
+ sha256: imageMetadata.sha256,
80
+ },
81
+ });
82
+ } else {
83
+ if (
84
+ typeof currentValue === "object" &&
85
+ currentValue &&
86
+ "metadata" in currentValue &&
87
+ currentValue.metadata &&
88
+ typeof currentValue.metadata === "object"
89
+ ) {
90
+ if (
91
+ !("sha256" in currentValue.metadata) ||
92
+ currentValue.metadata.sha256 !== imageMetadata.sha256
93
+ ) {
94
+ remainingErrors.push({
95
+ message:
96
+ "Image metadata sha256 is incorrect! Found: " +
97
+ ("sha256" in currentValue.metadata
98
+ ? currentValue.metadata.sha256
99
+ : "<empty>") +
100
+ ". Expected: " +
101
+ imageMetadata.sha256 +
102
+ ".",
103
+ fixes: undefined,
104
+ });
105
+ }
106
+ if (
107
+ !("width" in currentValue.metadata) ||
108
+ currentValue.metadata.width !== imageMetadata.width
109
+ ) {
110
+ remainingErrors.push({
111
+ message:
112
+ "Image metadata width is incorrect! Found: " +
113
+ ("width" in currentValue.metadata
114
+ ? currentValue.metadata.width
115
+ : "<empty>") +
116
+ ". Expected: " +
117
+ imageMetadata.width,
118
+ fixes: undefined,
119
+ });
120
+ }
121
+ if (
122
+ !("height" in currentValue.metadata) ||
123
+ currentValue.metadata.height !== imageMetadata.height
124
+ ) {
125
+ remainingErrors.push({
126
+ message:
127
+ "Image metadata height is incorrect! Found: " +
128
+ ("height" in currentValue.metadata
129
+ ? currentValue.metadata.height
130
+ : "<empty>") +
131
+ ". Expected: " +
132
+ imageMetadata.height,
133
+ fixes: undefined,
134
+ });
135
+ }
136
+ } else {
137
+ remainingErrors.push({
138
+ ...validationError,
139
+ message: "Image metadata is not an object!",
140
+ fixes: undefined,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ } else if (fix === "image:add-metadata") {
146
+ patch.push({
147
+ op: "add",
148
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
149
+ value: {
150
+ width: imageMetadata.width,
151
+ height: imageMetadata.height,
152
+ sha256: imageMetadata.sha256,
153
+ },
154
+ });
155
+ }
156
+ }
157
+ }
158
+ if (!validationError.fixes || validationError.fixes.length === 0) {
159
+ remainingErrors.push(validationError);
160
+ }
161
+ return {
162
+ patch,
163
+ remainingErrors,
164
+ };
165
+ }
166
+
167
+ const getSHA256Hash = async (bits: Uint8Array) => {
168
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bits);
169
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
170
+ const hash = hashArray
171
+ .map((item) => item.toString(16).padStart(2, "0"))
172
+ .join("");
173
+ return hash;
174
+ };
package/src/index.ts CHANGED
@@ -10,3 +10,5 @@ export type { IValFSHost } from "./ValFSHost";
10
10
  export type { ValFS } from "./ValFS";
11
11
  export { patchSourceFile } from "./patchValFile";
12
12
  export { formatSyntaxErrorTree } from "./patch/ts/syntax";
13
+ export { LocalValServer } from "./LocalValServer";
14
+ export { createFixPatch } from "./createFixPatch";
@@ -1,3 +1,4 @@
1
+ import { ModuleId } from "@valbuild/core";
1
2
  import path from "path";
2
3
  import { QuickJSRuntime } from "quickjs-emscripten";
3
4
  import { SerializedModuleContent } from "./SerializedModuleContent";
@@ -16,6 +17,10 @@ globalThis.valModule = {
16
17
  id: valModule?.default && Internal.getValPath(valModule?.default),
17
18
  schema: valModule?.default && Internal.getSchema(valModule?.default)?.serialize(),
18
19
  source: valModule?.default && Internal.getRawSource(valModule?.default),
20
+ validation: valModule?.default && Internal.getSchema(valModule?.default)?.validate(
21
+ valModule?.default && Internal.getValPath(valModule?.default) || "/",
22
+ valModule?.default && Internal.getRawSource(valModule?.default)
23
+ )
19
24
  };
20
25
  `;
21
26
  const result = context.evalCode(
@@ -23,48 +28,61 @@ globalThis.valModule = {
23
28
  // Synthetic module name
24
29
  path.join(path.dirname(valConfigPath), "<val>")
25
30
  );
31
+ const fatalErrors: string[] = [];
26
32
  if (result.error) {
27
33
  const error = result.error.consume(context.dump);
28
- console.error("Got error", error); // TODO: use this to figure out how to strip out QuickJS specific errors and get the actual stack
29
-
30
- throw new Error(
31
- `Could not read val id: ${id}. Cause:\n${error.name}: ${error.message}${
32
- error.stack ? error.stack : ""
33
- }`
34
- );
34
+ return {
35
+ errors: {
36
+ invalidModuleId: id as ModuleId,
37
+ fatal: [
38
+ {
39
+ message: `${error.name || "Unknown error"}: ${
40
+ error.message || "<no message>"
41
+ }`,
42
+ stack: error.stack,
43
+ },
44
+ ],
45
+ },
46
+ };
35
47
  } else {
36
48
  result.value.dispose();
37
49
  const valModule = context
38
50
  .getProp(context.global, "valModule")
39
51
  .consume(context.dump);
40
52
 
41
- const errors: string[] = [];
42
-
43
53
  if (!valModule) {
44
- errors.push(`Could not find any modules at: ${id}`);
54
+ fatalErrors.push(`Could not find any modules at: ${id}`);
45
55
  } else {
46
56
  if (valModule.id !== id) {
47
- errors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
57
+ fatalErrors.push(
58
+ `Expected val id: '${id}' but got: '${valModule.id}'`
59
+ );
48
60
  }
49
61
  if (!valModule?.schema) {
50
- errors.push(`Expected val id: '${id}' to have a schema`);
62
+ fatalErrors.push(`Expected val id: '${id}' to have a schema`);
51
63
  }
52
64
  if (!valModule?.source) {
53
- errors.push(`Expected val id: '${id}' to have a source`);
65
+ fatalErrors.push(`Expected val id: '${id}' to have a source`);
54
66
  }
55
67
  }
56
-
57
- if (errors.length > 0) {
58
- throw Error(
59
- `While processing module of id: ${id}, we got the following errors:\n${errors.join(
60
- "\n"
61
- )}`
62
- );
68
+ let errors: SerializedModuleContent["errors"] = false;
69
+ if (fatalErrors.length > 0) {
70
+ errors = {
71
+ invalidModuleId: valModule.id !== id ? (id as ModuleId) : undefined,
72
+ fatal: fatalErrors.map((message) => ({ message })),
73
+ };
74
+ }
75
+ if (valModule?.validation) {
76
+ errors = {
77
+ ...(errors ? errors : {}),
78
+ validation: valModule.validation,
79
+ };
63
80
  }
64
81
  return {
65
- path: valModule.id, // This might not be the asked id/path, however, that should be handled further up in the call chain
82
+ path: valModule.id, // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
66
83
  source: valModule.source,
67
84
  schema: valModule.schema,
85
+ errors,
68
86
  };
69
87
  }
70
88
  } finally {