@valbuild/server 0.21.1 → 0.22.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.
@@ -23,4 +23,5 @@ export declare class LocalValServer implements ValServer {
23
23
  authorize(req: express.Request, res: express.Response): Promise<void>;
24
24
  callback(req: express.Request, res: express.Response): Promise<void>;
25
25
  logout(req: express.Request, res: express.Response): Promise<void>;
26
+ getFiles(req: express.Request, res: express.Response): Promise<void>;
26
27
  }
@@ -1,6 +1,7 @@
1
1
  import { type Source, type SerializedSchema, ValidationErrors } from "@valbuild/core";
2
2
  import { ModuleId, type SourcePath } from "@valbuild/core/src/val";
3
3
  export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module"];
4
+ export type FatalErrorType = (typeof FATAL_ERROR_TYPES)[number];
4
5
  export type SerializedModuleContent = {
5
6
  source: Source;
6
7
  schema: SerializedSchema;
@@ -9,14 +10,14 @@ export type SerializedModuleContent = {
9
10
  } | {
10
11
  source?: Source;
11
12
  schema?: SerializedSchema;
12
- path?: SourcePath;
13
+ path: SourcePath;
13
14
  errors: {
14
15
  invalidModuleId?: ModuleId;
15
16
  validation?: ValidationErrors;
16
17
  fatal?: {
17
18
  message: string;
18
- stack?: string[];
19
- type?: (typeof FATAL_ERROR_TYPES)[number];
19
+ stack?: string;
20
+ type?: FatalErrorType;
20
21
  }[];
21
22
  };
22
23
  };
@@ -9,9 +9,15 @@ export type ServiceOptions = {
9
9
  /**
10
10
  * Relative path to the val.config.js file from the root directory.
11
11
  *
12
- * @example src/val.config
12
+ * @example "./val.config"
13
13
  */
14
14
  valConfigPath: string;
15
+ /**
16
+ * Disable cache for transpilation
17
+ *
18
+ * @default false
19
+ * */
20
+ disableCache?: boolean;
15
21
  };
16
22
  export declare function createService(projectRoot: string, opts: ServiceOptions, host?: IValFSHost, loader?: ValModuleLoader): Promise<Service>;
17
23
  export declare class Service {
@@ -7,7 +7,10 @@ export declare class ValModuleLoader {
7
7
  private readonly compilerOptions;
8
8
  private readonly sourceFileHandler;
9
9
  private readonly host;
10
- constructor(projectRoot: string, compilerOptions: ts.CompilerOptions, sourceFileHandler: ValSourceFileHandler, host?: IValFSHost);
10
+ private readonly disableCache;
11
+ private cache;
12
+ private cacheSize;
13
+ constructor(projectRoot: string, compilerOptions: ts.CompilerOptions, sourceFileHandler: ValSourceFileHandler, host?: IValFSHost, disableCache?: boolean);
11
14
  getModule(modulePath: string): string;
12
15
  resolveModulePath(containingFilePath: string, requestedModuleName: string): string;
13
16
  private findMatchingJsFile;
@@ -11,4 +11,5 @@ export interface ValServer {
11
11
  enable(req: express.Request, res: express.Response): Promise<void>;
12
12
  disable(req: express.Request, res: express.Response): Promise<void>;
13
13
  getTree(req: express.Request, res: express.Response): Promise<void>;
14
+ getFiles(req: express.Request, res: express.Response): Promise<void>;
14
15
  }
@@ -9,10 +9,12 @@ var core = require('@valbuild/core');
9
9
  var patch = require('@valbuild/core/patch');
10
10
  var path = require('path');
11
11
  var fs = require('fs');
12
+ var sucrase = require('sucrase');
12
13
  var express = require('express');
13
14
  var server = require('@valbuild/ui/server');
14
15
  var z = require('zod');
15
16
  var crypto = require('crypto');
17
+ var stream = require('stream');
16
18
  var sizeOf = require('image-size');
17
19
 
18
20
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
@@ -640,6 +642,7 @@ globalThis.valModule = {
640
642
  const error = result.error.consume(context.dump);
641
643
  console.error(`Fatal error reading val file: ${error.message}\n`, error.stack);
642
644
  return {
645
+ path: id,
643
646
  errors: {
644
647
  invalidModuleId: id,
645
648
  fatal: [{
@@ -655,12 +658,12 @@ globalThis.valModule = {
655
658
  fatalErrors.push(`Could not find any modules at: ${id}`);
656
659
  } else {
657
660
  if (valModule.id !== id) {
658
- fatalErrors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
661
+ fatalErrors.push(`Wrong val.content id! In the file of with: '${id}', found: '${valModule.id}'`);
659
662
  }
660
663
  if (!(valModule !== null && valModule !== void 0 && valModule.schema)) {
661
664
  fatalErrors.push(`Expected val id: '${id}' to have a schema`);
662
665
  }
663
- if (!(valModule !== null && valModule !== void 0 && valModule.source)) {
666
+ if ((valModule === null || valModule === void 0 ? void 0 : valModule.source) === undefined) {
664
667
  fatalErrors.push(`Expected val id: '${id}' to have a source`);
665
668
  }
666
669
  }
@@ -680,7 +683,7 @@ globalThis.valModule = {
680
683
  };
681
684
  }
682
685
  return {
683
- path: valModule.id,
686
+ path: valModule.id || id,
684
687
  // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
685
688
  source: valModule.source,
686
689
  schema: valModule.schema,
@@ -734,9 +737,6 @@ const patchValFile = async (id, valConfigPath, patch$1, sourceFileHandler, runti
734
737
  sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
735
738
  }
736
739
  }
737
- for (const [ref, patch] of Object.entries(derefRes.value.remotePatches)) {
738
- throw Error(`Cannot update remote ${ref} with ${JSON.stringify(patch)}: not implemented`);
739
- }
740
740
  sourceFileHandler.writeSourceFile(newSourceFile.value);
741
741
  return readValFile(id, valConfigPath, runtime);
742
742
  };
@@ -815,15 +815,23 @@ class ValSourceFileHandler {
815
815
  const JsFileLookupMapping = [
816
816
  // NOTE: first one matching will be used
817
817
  [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
818
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
819
+ const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
820
+
818
821
  class ValModuleLoader {
819
- constructor(projectRoot, compilerOptions, sourceFileHandler, host = {
822
+ constructor(projectRoot,
823
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
824
+ compilerOptions, sourceFileHandler, host = {
820
825
  ...ts__default["default"].sys,
821
826
  writeFile: fs__default["default"].writeFileSync
822
- }) {
827
+ }, disableCache = false) {
823
828
  this.projectRoot = projectRoot;
824
829
  this.compilerOptions = compilerOptions;
825
830
  this.sourceFileHandler = sourceFileHandler;
826
831
  this.host = host;
832
+ this.disableCache = disableCache;
833
+ this.cache = {};
834
+ this.cacheSize = 0;
827
835
  }
828
836
  getModule(modulePath) {
829
837
  if (!modulePath) {
@@ -833,18 +841,31 @@ class ValModuleLoader {
833
841
  if (!code) {
834
842
  throw Error(`Could not read file "${modulePath}"`);
835
843
  }
836
- return ts__default["default"].transpile(code, {
837
- ...this.compilerOptions,
838
- jsx: ts__default["default"].JsxEmit.React,
839
- // allowJs: true,
840
- // rootDir: this.compilerOptions.rootDir,
841
- module: ts__default["default"].ModuleKind.ESNext,
842
- 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 {} = {})
843
- // moduleResolution: ts.ModuleResolutionKind.NodeNext,
844
- // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
845
- });
846
- }
844
+ let compiledCode;
845
+ if (this.cache[code] && !this.disableCache) {
846
+ // TODO: use hash instead of code as key
847
+ compiledCode = this.cache[code];
848
+ } else {
849
+ compiledCode = sucrase.transform(code, {
850
+ filePath: modulePath,
851
+ disableESTransforms: true,
852
+ transforms: ["typescript"]
853
+ }).code;
854
+ if (!this.disableCache) {
855
+ if (this.cacheSize > MAX_CACHE_SIZE) {
856
+ console.warn("Cache size exceeded, clearing cache");
857
+ this.cache = {};
858
+ this.cacheSize = 0;
859
+ }
860
+ if (code.length < MAX_OBJECT_KEY_SIZE) {
861
+ this.cache[code] = compiledCode;
862
+ this.cacheSize += code.length + compiledCode.length; // code is mostly ASCII so 1 byte per char
863
+ }
864
+ }
865
+ }
847
866
 
867
+ return compiledCode;
868
+ }
848
869
  resolveModulePath(containingFilePath, requestedModuleName) {
849
870
  var _this$host$realpath, _this$host;
850
871
  let sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
@@ -1004,11 +1025,9 @@ class Service {
1004
1025
  path: sourcePath,
1005
1026
  schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
1006
1027
  source: resolved.source,
1007
- errors: valModule.errors && valModule.errors.validation && valModule.errors.validation[sourcePath] ? {
1008
- validation: valModule.errors.validation[sourcePath] ? {
1009
- [sourcePath]: valModule.errors.validation[sourcePath]
1010
- } : undefined,
1011
- fatal: valModule.errors && valModule.errors.fatal ? valModule.errors.fatal : undefined
1028
+ errors: valModule.errors && valModule.errors.validation ? {
1029
+ validation: valModule.errors.validation || undefined,
1030
+ fatal: valModule.errors.fatal || undefined
1012
1031
  } : false
1013
1032
  };
1014
1033
  } else {
@@ -1038,13 +1057,10 @@ function createRequestHandler(valServer) {
1038
1057
  router.get("/enable", valServer.enable.bind(valServer));
1039
1058
  router.get("/disable", valServer.disable.bind(valServer));
1040
1059
  router.get("/tree/*", valServer.getTree.bind(valServer));
1060
+ router.get("/files/*", valServer.getFiles.bind(valServer));
1041
1061
  return router;
1042
1062
  }
1043
1063
 
1044
- function getPathFromParams(params) {
1045
- return `/${params[0]}`;
1046
- }
1047
-
1048
1064
  const JSONValueT = z__default["default"].lazy(() => z__default["default"].union([z__default["default"].string(), z__default["default"].number(), z__default["default"].boolean(), z__default["default"].null(), z__default["default"].array(JSONValueT), z__default["default"].record(JSONValueT)]));
1049
1065
 
1050
1066
  /**
@@ -1082,6 +1098,7 @@ const OperationJSONT = z__default["default"].discriminatedUnion("op", [z__defaul
1082
1098
  }).strict(), z__default["default"].object({
1083
1099
  op: z__default["default"].literal("file"),
1084
1100
  path: z__default["default"].string(),
1101
+ filePath: z__default["default"].string(),
1085
1102
  value: z__default["default"].string()
1086
1103
  }).strict()]);
1087
1104
  const PatchJSON = z__default["default"].array(OperationJSONT);
@@ -1148,10 +1165,61 @@ function encodeJwt(payload, sessionKey) {
1148
1165
  const VAL_SESSION_COOKIE = core.Internal.VAL_SESSION_COOKIE;
1149
1166
  const VAL_STATE_COOKIE = core.Internal.VAL_STATE_COOKIE;
1150
1167
  const VAL_ENABLED_COOKIE = core.Internal.VAL_ENABLE_COOKIE_NAME;
1168
+ class BrowserReadableStreamWrapper extends stream.Readable {
1169
+ constructor(readableStream) {
1170
+ super();
1171
+ this.reader = readableStream.getReader();
1172
+ }
1173
+ _read() {
1174
+ this.reader.read().then(({
1175
+ done,
1176
+ value
1177
+ }) => {
1178
+ if (done) {
1179
+ this.push(null); // No more data to read
1180
+ } else {
1181
+ this.push(Buffer.from(value));
1182
+ }
1183
+ }).catch(error => {
1184
+ this.emit("error", error);
1185
+ });
1186
+ }
1187
+ }
1151
1188
  class ProxyValServer {
1152
1189
  constructor(options) {
1153
1190
  this.options = options;
1154
1191
  }
1192
+ async getFiles(req, res) {
1193
+ return this.withAuth(req, res, async data => {
1194
+ const url = new URL(`/v1/files/${this.options.valName}/${req.params["0"]}`, this.options.valContentUrl);
1195
+ if (typeof req.query.sha256 === "string") {
1196
+ url.searchParams.append("sha256", req.query.sha256);
1197
+ } else {
1198
+ console.warn("Missing sha256 query param");
1199
+ }
1200
+ const fetchRes = await fetch(url, {
1201
+ headers: this.getAuthHeaders(data.token)
1202
+ });
1203
+ const contentType = fetchRes.headers.get("content-type");
1204
+ if (contentType !== null) {
1205
+ res.setHeader("Content-Type", contentType);
1206
+ }
1207
+ const contentLength = fetchRes.headers.get("content-length");
1208
+ if (contentLength !== null) {
1209
+ res.setHeader("Content-Length", contentLength);
1210
+ }
1211
+ if (fetchRes.ok) {
1212
+ if (fetchRes.body) {
1213
+ new BrowserReadableStreamWrapper(fetchRes.body).pipe(res);
1214
+ } else {
1215
+ console.warn("No body in response");
1216
+ res.sendStatus(500);
1217
+ }
1218
+ } else {
1219
+ res.sendStatus(fetchRes.status);
1220
+ }
1221
+ });
1222
+ }
1155
1223
  async authorize(req, res) {
1156
1224
  const {
1157
1225
  redirect_to
@@ -1245,10 +1313,18 @@ class ProxyValServer {
1245
1313
  schema,
1246
1314
  source
1247
1315
  } = req.query;
1316
+ const commit = this.options.gitCommit;
1317
+ if (!commit) {
1318
+ res.status(401).json({
1319
+ error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1320
+ });
1321
+ return;
1322
+ }
1248
1323
  const params = new URLSearchParams({
1249
1324
  patch: (patch === "true").toString(),
1250
1325
  schema: (schema === "true").toString(),
1251
- source: (source === "true").toString()
1326
+ source: (source === "true").toString(),
1327
+ commit
1252
1328
  });
1253
1329
  const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
1254
1330
  const json = await fetch(url, {
@@ -1258,12 +1334,10 @@ class ProxyValServer {
1258
1334
  });
1259
1335
  }
1260
1336
  async postPatches(req, res) {
1261
- const {
1262
- commit
1263
- } = req.query;
1264
- if (typeof commit !== "string" || typeof commit === "undefined") {
1337
+ const commit = this.options.gitCommit;
1338
+ if (!commit) {
1265
1339
  res.status(401).json({
1266
- error: "Missing or invalid commit query param"
1340
+ error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1267
1341
  });
1268
1342
  return;
1269
1343
  }
@@ -1274,23 +1348,25 @@ class ProxyValServer {
1274
1348
  token
1275
1349
  }) => {
1276
1350
  // First validate that the body has the right structure
1277
- const patchJSON = PatchJSON.safeParse(req.body);
1351
+ const patchJSON = z.z.record(PatchJSON).safeParse(req.body);
1278
1352
  if (!patchJSON.success) {
1279
1353
  res.status(401).json(patchJSON.error.issues);
1280
1354
  return;
1281
1355
  }
1282
1356
  // Then parse/validate
1283
- const patch$1 = patch.parsePatch(patchJSON.data);
1284
- if (fp.result.isErr(patch$1)) {
1285
- res.status(401).json(patch$1.error);
1286
- return;
1287
- }
1288
- const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
1357
+ // TODO:
1358
+ const patch = patchJSON.data;
1359
+ // const patch = parsePatch(patchJSON.data);
1360
+ // if (result.isErr(patch)) {
1361
+ // res.status(401).json(patch.error);
1362
+ // return;
1363
+ // }
1364
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
1289
1365
  // Proxy patch to val.build
1290
1366
  const fetchRes = await fetch(url, {
1291
1367
  method: "POST",
1292
1368
  headers: this.getAuthHeaders(token, "application/json"),
1293
- body: JSON.stringify(patch$1)
1369
+ body: JSON.stringify(patch)
1294
1370
  });
1295
1371
  if (fetchRes.ok) {
1296
1372
  res.status(fetchRes.status).json(await fetchRes.json());
@@ -1566,7 +1642,8 @@ class LocalValServer {
1566
1642
  const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
1567
1643
  const module = {
1568
1644
  schema: serializedModuleContent.schema,
1569
- source: serializedModuleContent.source
1645
+ source: serializedModuleContent.source,
1646
+ errors: serializedModuleContent.errors
1570
1647
  };
1571
1648
  return [serializedModuleContent.path, module];
1572
1649
  }));
@@ -1574,9 +1651,7 @@ class LocalValServer {
1574
1651
  modules,
1575
1652
  git: this.options.git
1576
1653
  };
1577
- return walk(rootDir).then(async () => {
1578
- res.send(JSON.stringify(apiTreeResponse));
1579
- });
1654
+ res.send(JSON.stringify(apiTreeResponse));
1580
1655
  } catch (err) {
1581
1656
  console.error(err);
1582
1657
  res.sendStatus(500);
@@ -1589,24 +1664,23 @@ class LocalValServer {
1589
1664
  return disable(req, res);
1590
1665
  }
1591
1666
  async postPatches(req, res) {
1592
- var _getPathFromParams;
1593
- const id = (_getPathFromParams = getPathFromParams(req.params)) === null || _getPathFromParams === void 0 ? void 0 : _getPathFromParams.replace("/~", "");
1594
-
1595
1667
  // First validate that the body has the right structure
1596
- const patchJSON = PatchJSON.safeParse(req.body);
1597
- console.log("patch id", id, patchJSON);
1668
+ const patchJSON = z.z.record(PatchJSON).safeParse(req.body);
1598
1669
  if (!patchJSON.success) {
1599
1670
  res.status(401).json(patchJSON.error.issues);
1600
1671
  return;
1601
1672
  }
1602
- // Then parse/validate
1603
- const patch$1 = patch.parsePatch(patchJSON.data);
1604
- if (fp.result.isErr(patch$1)) {
1605
- res.status(401).json(patch$1.error);
1606
- return;
1607
- }
1608
1673
  try {
1609
- await this.options.service.patch(id, patch$1.value);
1674
+ for (const moduleId in patchJSON.data) {
1675
+ // Then parse/validate
1676
+ // TODO: validate all and then fail instead:
1677
+ const patch$1 = patch.parsePatch(patchJSON.data[moduleId]);
1678
+ if (fp.result.isErr(patch$1)) {
1679
+ res.status(401).json(patch$1.error);
1680
+ return;
1681
+ }
1682
+ await this.options.service.patch(moduleId, patch$1.value);
1683
+ }
1610
1684
  res.json({});
1611
1685
  } catch (err) {
1612
1686
  if (err instanceof patch.PatchError) {
@@ -1637,6 +1711,9 @@ class LocalValServer {
1637
1711
  logout(req, res) {
1638
1712
  return this.badRequest(req, res);
1639
1713
  }
1714
+ getFiles(req, res) {
1715
+ return this.badRequest(req, res);
1716
+ }
1640
1717
  }
1641
1718
 
1642
1719
  async function _createRequestListener(route, opts) {