@valbuild/server 0.15.0 → 0.16.1

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.
@@ -18,6 +18,7 @@ export type ProxyValServerOptions = {
18
18
  valBuildUrl: string;
19
19
  gitCommit: string;
20
20
  gitBranch: string;
21
+ valName: string;
21
22
  };
22
23
 
23
24
  export class ProxyValServer implements ValServer {
@@ -112,9 +113,10 @@ export class ProxyValServer implements ValServer {
112
113
  }
113
114
 
114
115
  async session(req: express.Request, res: express.Response): Promise<void> {
116
+ console.log("hit session");
115
117
  return this.withAuth(req, res, async (data) => {
116
118
  const url = new URL(
117
- "/api/val/auth/user/session",
119
+ `/api/val/${this.options.valName}/auth/session`,
118
120
  this.options.valBuildUrl
119
121
  );
120
122
  const fetchRes = await fetch(url, {
@@ -233,7 +235,10 @@ export class ProxyValServer implements ValServer {
233
235
  project: string;
234
236
  token: string;
235
237
  } | null> {
236
- const url = new URL("/api/val/auth/user/token", this.options.valBuildUrl);
238
+ const url = new URL(
239
+ `/api/val/${this.options.valName}/auth/token`,
240
+ this.options.valBuildUrl
241
+ );
237
242
  url.searchParams.set("code", encodeURIComponent(code));
238
243
  return fetch(url, {
239
244
  method: "POST",
@@ -262,7 +267,10 @@ export class ProxyValServer implements ValServer {
262
267
  }
263
268
 
264
269
  private getAuthorizeUrl(publicValApiRoute: string, token: string): string {
265
- const url = new URL("/authorize", this.options.valBuildUrl);
270
+ const url = new URL(
271
+ `/auth/${this.options.valName}/authorize`,
272
+ this.options.valBuildUrl
273
+ );
266
274
  url.searchParams.set(
267
275
  "redirect_uri",
268
276
  encodeURIComponent(`${publicValApiRoute}/callback`)
@@ -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,175 @@
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
+ // TODO: find a better name? transformFixesToPatch?
9
+ export async function createFixPatch(
10
+ config: { projectRoot: string },
11
+ apply: boolean,
12
+ sourcePath: SourcePath,
13
+ validationError: ValidationError
14
+ ): Promise<{ patch: Patch; remainingErrors: ValidationError[] } | undefined> {
15
+ async function getImageMetadata() {
16
+ const maybeRef =
17
+ validationError.value &&
18
+ typeof validationError.value === "object" &&
19
+ FILE_REF_PROP in validationError.value &&
20
+ typeof validationError.value[FILE_REF_PROP] === "string"
21
+ ? validationError.value[FILE_REF_PROP]
22
+ : undefined;
23
+
24
+ if (!maybeRef) {
25
+ // TODO:
26
+ throw Error("Cannot fix image without a file reference");
27
+ }
28
+ const localFile = path.join(config.projectRoot, maybeRef);
29
+ const buffer = fs.readFileSync(localFile);
30
+ const sha256 = await getSHA256Hash(buffer);
31
+ const imageSize = sizeOf(buffer);
32
+ return {
33
+ ...imageSize,
34
+ sha256,
35
+ };
36
+ }
37
+ const remainingErrors: ValidationError[] = [];
38
+ const patch: Patch = [];
39
+ for (const fix of validationError.fixes || []) {
40
+ if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
41
+ const imageMetadata = await getImageMetadata();
42
+ if (
43
+ imageMetadata.width === undefined ||
44
+ imageMetadata.height === undefined ||
45
+ imageMetadata.sha256 === undefined
46
+ ) {
47
+ remainingErrors.push({
48
+ ...validationError,
49
+ message: "Failed to get image metadata",
50
+ fixes: undefined,
51
+ });
52
+ } else if (fix === "image:replace-metadata") {
53
+ const currentValue = validationError.value;
54
+ const metadataIsCorrect =
55
+ // metadata is a prop that is an object
56
+ typeof currentValue === "object" &&
57
+ currentValue &&
58
+ "metadata" in currentValue &&
59
+ currentValue.metadata &&
60
+ typeof currentValue.metadata === "object" &&
61
+ // sha256 is correct
62
+ "sha256" in currentValue.metadata &&
63
+ currentValue.metadata.sha256 === imageMetadata.sha256 &&
64
+ // width is correct
65
+ "width" in currentValue.metadata &&
66
+ currentValue.metadata.width === imageMetadata.width &&
67
+ // height is correct
68
+ "height" in currentValue.metadata &&
69
+ currentValue.metadata.height === imageMetadata.height;
70
+
71
+ // skips if the metadata is already correct
72
+ if (!metadataIsCorrect) {
73
+ if (apply) {
74
+ patch.push({
75
+ op: "replace",
76
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
77
+ value: {
78
+ width: imageMetadata.width,
79
+ height: imageMetadata.height,
80
+ sha256: imageMetadata.sha256,
81
+ },
82
+ });
83
+ } else {
84
+ if (
85
+ typeof currentValue === "object" &&
86
+ currentValue &&
87
+ "metadata" in currentValue &&
88
+ currentValue.metadata &&
89
+ typeof currentValue.metadata === "object"
90
+ ) {
91
+ if (
92
+ !("sha256" in currentValue.metadata) ||
93
+ currentValue.metadata.sha256 !== imageMetadata.sha256
94
+ ) {
95
+ remainingErrors.push({
96
+ message:
97
+ "Image metadata sha256 is incorrect! Found: " +
98
+ ("sha256" in currentValue.metadata
99
+ ? currentValue.metadata.sha256
100
+ : "<empty>") +
101
+ ". Expected: " +
102
+ imageMetadata.sha256 +
103
+ ".",
104
+ fixes: undefined,
105
+ });
106
+ }
107
+ if (
108
+ !("width" in currentValue.metadata) ||
109
+ currentValue.metadata.width !== imageMetadata.width
110
+ ) {
111
+ remainingErrors.push({
112
+ message:
113
+ "Image metadata width is incorrect! Found: " +
114
+ ("width" in currentValue.metadata
115
+ ? currentValue.metadata.width
116
+ : "<empty>") +
117
+ ". Expected: " +
118
+ imageMetadata.width,
119
+ fixes: undefined,
120
+ });
121
+ }
122
+ if (
123
+ !("height" in currentValue.metadata) ||
124
+ currentValue.metadata.height !== imageMetadata.height
125
+ ) {
126
+ remainingErrors.push({
127
+ message:
128
+ "Image metadata height is incorrect! Found: " +
129
+ ("height" in currentValue.metadata
130
+ ? currentValue.metadata.height
131
+ : "<empty>") +
132
+ ". Expected: " +
133
+ imageMetadata.height,
134
+ fixes: undefined,
135
+ });
136
+ }
137
+ } else {
138
+ remainingErrors.push({
139
+ ...validationError,
140
+ message: "Image metadata is not an object!",
141
+ fixes: undefined,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ } else if (fix === "image:add-metadata") {
147
+ patch.push({
148
+ op: "add",
149
+ path: sourceToPatchPath(sourcePath).concat("metadata"),
150
+ value: {
151
+ width: imageMetadata.width,
152
+ height: imageMetadata.height,
153
+ sha256: imageMetadata.sha256,
154
+ },
155
+ });
156
+ }
157
+ }
158
+ }
159
+ if (!validationError.fixes || validationError.fixes.length === 0) {
160
+ remainingErrors.push(validationError);
161
+ }
162
+ return {
163
+ patch,
164
+ remainingErrors,
165
+ };
166
+ }
167
+
168
+ const getSHA256Hash = async (bits: Uint8Array) => {
169
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bits);
170
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
171
+ const hash = hashArray
172
+ .map((item) => item.toString(16).padStart(2, "0"))
173
+ .join("");
174
+ return hash;
175
+ };
package/src/hosting.ts CHANGED
@@ -73,6 +73,14 @@ type ValServerOverrides = Partial<{
73
73
  * @example "https://app.val.build"
74
74
  */
75
75
  valBuildUrl: string;
76
+ /**
77
+ * The full name of this Val project.
78
+ *
79
+ * Typically this is set using the VAL_NAME env var.
80
+ *
81
+ * @example "myorg/my-project"
82
+ */
83
+ valName: string;
76
84
  }>;
77
85
 
78
86
  async function _createRequestListener(
@@ -119,6 +127,10 @@ async function initHandlerOptions(
119
127
  if (!maybeGitBranch) {
120
128
  throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
121
129
  }
130
+ const maybeValName = opts.gitBranch || process.env.VAL_NAME;
131
+ if (!maybeValName) {
132
+ throw new Error("VAL_NAME env var must be set in proxy mode");
133
+ }
122
134
  return {
123
135
  mode: "proxy",
124
136
  route,
@@ -127,6 +139,7 @@ async function initHandlerOptions(
127
139
  valBuildUrl,
128
140
  gitCommit: maybeGitCommit,
129
141
  gitBranch: maybeGitBranch,
142
+ valName: maybeValName,
130
143
  };
131
144
  } else {
132
145
  const service = await createService(process.cwd(), opts);
package/src/index.ts CHANGED
@@ -10,3 +10,6 @@ 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";
15
+ export { PatchJSON } from "./patch/validation";
@@ -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 {