@valbuild/server 0.12.0 → 0.13.3
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.
- package/jest.config.js +4 -0
- package/package.json +5 -3
- package/src/LocalValServer.ts +94 -0
- package/src/ProxyValServer.ts +403 -0
- package/src/SerializedModuleContent.ts +8 -0
- package/src/Service.ts +108 -0
- package/src/ValFS.ts +22 -0
- package/src/ValFSHost.ts +66 -0
- package/src/ValModuleLoader.test.ts +75 -0
- package/src/ValModuleLoader.ts +128 -0
- package/src/ValQuickJSRuntime.ts +47 -0
- package/src/ValServer.ts +23 -0
- package/src/ValSourceFileHandler.ts +57 -0
- package/src/createRequestHandler.ts +24 -0
- package/src/expressHelpers.ts +5 -0
- package/src/getCompilerOptions.ts +50 -0
- package/src/hosting.ts +156 -0
- package/src/index.ts +12 -0
- package/src/jwt.ts +83 -0
- package/src/patch/ts/ops.test.ts +820 -0
- package/src/patch/ts/ops.ts +803 -0
- package/src/patch/ts/syntax.ts +371 -0
- package/src/patch/ts/valModule.test.ts +26 -0
- package/src/patch/ts/valModule.ts +110 -0
- package/src/patch/validation.ts +73 -0
- package/src/patchValFile.ts +102 -0
- package/src/readValFile.test.ts +49 -0
- package/src/readValFile.ts +73 -0
package/src/ValFSHost.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { ValFS } from "./ValFS";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An implementation of methods in the various ts.*Host interfaces
|
|
7
|
+
* that uses ValFS to resolve modules and read/write files.
|
|
8
|
+
*/
|
|
9
|
+
export interface IValFSHost
|
|
10
|
+
extends ts.ParseConfigHost,
|
|
11
|
+
ts.ModuleResolutionHost {
|
|
12
|
+
useCaseSensitiveFileNames: boolean;
|
|
13
|
+
|
|
14
|
+
writeFile(fileName: string, text: string, encoding: "binary" | "utf8"): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ValFSHost implements IValFSHost {
|
|
18
|
+
constructor(
|
|
19
|
+
protected readonly valFS: ValFS,
|
|
20
|
+
protected readonly currentDirectory: string
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
useCaseSensitiveFileNames = true;
|
|
24
|
+
readDirectory(
|
|
25
|
+
rootDir: string,
|
|
26
|
+
extensions: readonly string[],
|
|
27
|
+
excludes: readonly string[] | undefined,
|
|
28
|
+
includes: readonly string[],
|
|
29
|
+
depth?: number | undefined
|
|
30
|
+
): readonly string[] {
|
|
31
|
+
return this.valFS.readDirectory(
|
|
32
|
+
rootDir,
|
|
33
|
+
extensions,
|
|
34
|
+
excludes,
|
|
35
|
+
includes,
|
|
36
|
+
depth
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writeFile(fileName: string, text: string, encoding: "binary" | "utf8"): void {
|
|
41
|
+
this.valFS.writeFile(fileName, text, encoding);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getCurrentDirectory(): string {
|
|
45
|
+
return this.currentDirectory;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getCanonicalFileName(fileName: string): string {
|
|
49
|
+
if (path.isAbsolute(fileName)) {
|
|
50
|
+
return path.normalize(fileName);
|
|
51
|
+
}
|
|
52
|
+
return path.resolve(this.getCurrentDirectory(), fileName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fileExists(fileName: string): boolean {
|
|
56
|
+
return this.valFS.fileExists(fileName);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
readFile(fileName: string): string | undefined {
|
|
60
|
+
return this.valFS.readFile(fileName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
realpath(path: string): string {
|
|
64
|
+
return this.valFS.realpath(path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createModuleLoader } from "./ValModuleLoader";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const TestCaseDir = "../test/example-projects";
|
|
5
|
+
const TestCases = [
|
|
6
|
+
{ name: "basic-next-typescript", valConfigDir: ".", ext: "ts" },
|
|
7
|
+
{
|
|
8
|
+
name: "basic-next-src-typescript",
|
|
9
|
+
valConfigDir: "./src",
|
|
10
|
+
ext: "ts",
|
|
11
|
+
},
|
|
12
|
+
{ name: "basic-next-javascript", valConfigDir: ".", ext: "js" },
|
|
13
|
+
{ name: "typescript-description-files", valConfigDir: ".", ext: "js" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("val module loader", () => {
|
|
17
|
+
test.each(TestCases)(
|
|
18
|
+
"resolution and smoke test transpilation for: $name",
|
|
19
|
+
async (testCase) => {
|
|
20
|
+
const rootDir = path.resolve(__dirname, TestCaseDir, testCase.name);
|
|
21
|
+
const loader = createModuleLoader(rootDir);
|
|
22
|
+
expect(
|
|
23
|
+
await loader.getModule(
|
|
24
|
+
loader.resolveModulePath(
|
|
25
|
+
`${testCase.valConfigDir}/val-system.${testCase.ext}`,
|
|
26
|
+
"./pages/blogs.val"
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
).toContain("/pages/blogs");
|
|
30
|
+
expect(
|
|
31
|
+
await loader.getModule(
|
|
32
|
+
loader.resolveModulePath(
|
|
33
|
+
`${testCase.valConfigDir}/pages/blogs.val.${testCase.ext}`,
|
|
34
|
+
"../val.config"
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
).toContain("@valbuild/core");
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
test("resolution based on baseDir / paths in tsconfig", () => {
|
|
42
|
+
const rootDir = path.resolve(
|
|
43
|
+
__dirname,
|
|
44
|
+
TestCaseDir,
|
|
45
|
+
"basic-next-src-typescript"
|
|
46
|
+
);
|
|
47
|
+
const moduleLoader = createModuleLoader(rootDir);
|
|
48
|
+
|
|
49
|
+
const containingFile = "./src/pages/blogs.val.ts";
|
|
50
|
+
const baseCase = moduleLoader.resolveModulePath(
|
|
51
|
+
containingFile,
|
|
52
|
+
"../val.config"
|
|
53
|
+
); // tsconfig maps @ to src
|
|
54
|
+
const pathsMapping = moduleLoader.resolveModulePath(
|
|
55
|
+
containingFile,
|
|
56
|
+
"@/val.config"
|
|
57
|
+
); // tsconfig maps @ to src
|
|
58
|
+
expect(baseCase).toBeDefined();
|
|
59
|
+
expect(baseCase).toEqual(pathsMapping);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("resolution on .d.ts files", () => {
|
|
63
|
+
const rootDir = path.resolve(
|
|
64
|
+
__dirname,
|
|
65
|
+
TestCaseDir,
|
|
66
|
+
"typescript-description-files"
|
|
67
|
+
);
|
|
68
|
+
const moduleLoader = createModuleLoader(rootDir);
|
|
69
|
+
|
|
70
|
+
const containingFile = "./pages/blogs.val.js";
|
|
71
|
+
expect(
|
|
72
|
+
moduleLoader.resolveModulePath(containingFile, "../val.config")
|
|
73
|
+
).toMatch(/val\.config\.js$/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { getCompilerOptions } from "./getCompilerOptions";
|
|
3
|
+
import type { IValFSHost } from "./ValFSHost";
|
|
4
|
+
import { ValSourceFileHandler } from "./ValSourceFileHandler";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
const JsFileLookupMapping: [resolvedFileExt: string, replacements: string[]][] =
|
|
8
|
+
[
|
|
9
|
+
// NOTE: first one matching will be used
|
|
10
|
+
[".cjs.d.ts", [".esm.js", ".mjs.js"]],
|
|
11
|
+
[".cjs.js", [".esm.js", ".mjs.js"]],
|
|
12
|
+
[".cjs", [".mjs"]],
|
|
13
|
+
[".d.ts", [".js"]],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const createModuleLoader = (
|
|
17
|
+
rootDir: string,
|
|
18
|
+
host: IValFSHost = {
|
|
19
|
+
...ts.sys,
|
|
20
|
+
writeFile: fs.writeFileSync,
|
|
21
|
+
}
|
|
22
|
+
): ValModuleLoader => {
|
|
23
|
+
const compilerOptions = getCompilerOptions(rootDir, host);
|
|
24
|
+
const sourceFileHandler = new ValSourceFileHandler(
|
|
25
|
+
rootDir,
|
|
26
|
+
compilerOptions,
|
|
27
|
+
host
|
|
28
|
+
);
|
|
29
|
+
const loader = new ValModuleLoader(
|
|
30
|
+
rootDir,
|
|
31
|
+
compilerOptions,
|
|
32
|
+
sourceFileHandler,
|
|
33
|
+
host
|
|
34
|
+
);
|
|
35
|
+
return loader;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class ValModuleLoader {
|
|
39
|
+
constructor(
|
|
40
|
+
public readonly projectRoot: string,
|
|
41
|
+
private readonly compilerOptions: ts.CompilerOptions,
|
|
42
|
+
private readonly sourceFileHandler: ValSourceFileHandler,
|
|
43
|
+
private readonly host: IValFSHost = {
|
|
44
|
+
...ts.sys,
|
|
45
|
+
writeFile: fs.writeFileSync,
|
|
46
|
+
}
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
getModule(modulePath: string): string {
|
|
50
|
+
if (!modulePath) {
|
|
51
|
+
throw Error(`Illegal module path: "${modulePath}"`);
|
|
52
|
+
}
|
|
53
|
+
const code = this.host.readFile(modulePath);
|
|
54
|
+
if (!code) {
|
|
55
|
+
throw Error(`Could not read file "${modulePath}"`);
|
|
56
|
+
}
|
|
57
|
+
return ts.transpile(code, {
|
|
58
|
+
...this.compilerOptions,
|
|
59
|
+
// allowJs: true,
|
|
60
|
+
// rootDir: this.compilerOptions.rootDir,
|
|
61
|
+
module: ts.ModuleKind.ESNext,
|
|
62
|
+
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
|
+
// moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
64
|
+
// target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resolveModulePath(
|
|
69
|
+
containingFilePath: string,
|
|
70
|
+
requestedModuleName: string
|
|
71
|
+
): string {
|
|
72
|
+
const sourceFileName = this.sourceFileHandler.resolveSourceModulePath(
|
|
73
|
+
containingFilePath,
|
|
74
|
+
requestedModuleName
|
|
75
|
+
);
|
|
76
|
+
const matches = this.findMatchingJsFile(sourceFileName);
|
|
77
|
+
if (matches.match === false) {
|
|
78
|
+
throw Error(
|
|
79
|
+
`Could not find matching js file for module "${requestedModuleName}". Tried:\n${matches.tried.join(
|
|
80
|
+
"\n"
|
|
81
|
+
)}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const filePath = matches.match;
|
|
85
|
+
// resolve all symlinks (preconstruct for example symlinks the dist folder)
|
|
86
|
+
const followedPath = this.host.realpath?.(filePath) ?? filePath;
|
|
87
|
+
if (!followedPath) {
|
|
88
|
+
throw Error(
|
|
89
|
+
`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return followedPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private findMatchingJsFile(
|
|
96
|
+
filePath: string
|
|
97
|
+
): { match: string } | { match: false; tried: string[] } {
|
|
98
|
+
let requiresReplacements = false;
|
|
99
|
+
for (const [currentEnding] of JsFileLookupMapping) {
|
|
100
|
+
if (filePath.endsWith(currentEnding)) {
|
|
101
|
+
requiresReplacements = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// avoid unnecessary calls to fileExists if we don't need to replace anything
|
|
106
|
+
if (!requiresReplacements) {
|
|
107
|
+
if (this.host.fileExists(filePath)) {
|
|
108
|
+
return { match: filePath };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const tried = [];
|
|
112
|
+
for (const [currentEnding, replacements] of JsFileLookupMapping) {
|
|
113
|
+
if (filePath.endsWith(currentEnding)) {
|
|
114
|
+
for (const replacement of replacements) {
|
|
115
|
+
const newFilePath =
|
|
116
|
+
filePath.slice(0, -currentEnding.length) + replacement;
|
|
117
|
+
if (this.host.fileExists(newFilePath)) {
|
|
118
|
+
return { match: newFilePath };
|
|
119
|
+
} else {
|
|
120
|
+
tried.push(newFilePath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { match: false, tried: tried.concat(filePath) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { JSModuleNormalizeResult, QuickJSWASMModule } from "quickjs-emscripten";
|
|
2
|
+
import { ValModuleLoader } from "./ValModuleLoader";
|
|
3
|
+
|
|
4
|
+
export async function newValQuickJSRuntime(
|
|
5
|
+
quickJSModule: Pick<QuickJSWASMModule, "newRuntime">,
|
|
6
|
+
moduleLoader: ValModuleLoader,
|
|
7
|
+
{
|
|
8
|
+
maxStackSize = 1024 * 640, // TODO: these were randomly chosen, we should figure out what the right values are:
|
|
9
|
+
memoryLimit = 1024 * 640,
|
|
10
|
+
}: {
|
|
11
|
+
maxStackSize?: number;
|
|
12
|
+
memoryLimit?: number;
|
|
13
|
+
} = {}
|
|
14
|
+
) {
|
|
15
|
+
const runtime = quickJSModule.newRuntime();
|
|
16
|
+
|
|
17
|
+
runtime.setMaxStackSize(maxStackSize);
|
|
18
|
+
runtime.setMemoryLimit(memoryLimit);
|
|
19
|
+
|
|
20
|
+
runtime.setModuleLoader(
|
|
21
|
+
(modulePath) => {
|
|
22
|
+
try {
|
|
23
|
+
return { value: moduleLoader.getModule(modulePath) };
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return {
|
|
26
|
+
error: Error(`Could not resolve module: ${modulePath}'`),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
(baseModuleName, requestedName): JSModuleNormalizeResult => {
|
|
31
|
+
try {
|
|
32
|
+
const modulePath = moduleLoader.resolveModulePath(
|
|
33
|
+
baseModuleName,
|
|
34
|
+
requestedName
|
|
35
|
+
);
|
|
36
|
+
return { value: modulePath };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.debug(
|
|
39
|
+
`Could not resolve ${requestedName} in ${baseModuleName}`,
|
|
40
|
+
e
|
|
41
|
+
);
|
|
42
|
+
return { value: requestedName };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
return runtime;
|
|
47
|
+
}
|
package/src/ValServer.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
|
|
3
|
+
export interface ValServer {
|
|
4
|
+
authorize(req: express.Request, res: express.Response): Promise<void>;
|
|
5
|
+
|
|
6
|
+
callback(req: express.Request, res: express.Response): Promise<void>;
|
|
7
|
+
|
|
8
|
+
logout(req: express.Request, res: express.Response): Promise<void>;
|
|
9
|
+
|
|
10
|
+
session(req: express.Request, res: express.Response): Promise<void>;
|
|
11
|
+
|
|
12
|
+
getIds(
|
|
13
|
+
req: express.Request<{ 0: string }>,
|
|
14
|
+
res: express.Response
|
|
15
|
+
): Promise<void>;
|
|
16
|
+
|
|
17
|
+
patchIds(
|
|
18
|
+
req: express.Request<{ 0: string }>,
|
|
19
|
+
res: express.Response
|
|
20
|
+
): Promise<void>;
|
|
21
|
+
|
|
22
|
+
commit(req: express.Request, res: express.Response): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { IValFSHost } from "./ValFSHost";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
export class ValSourceFileHandler {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly projectRoot: string,
|
|
9
|
+
private readonly compilerOptions: ts.CompilerOptions,
|
|
10
|
+
private readonly host: IValFSHost = {
|
|
11
|
+
...ts.sys,
|
|
12
|
+
writeFile: fs.writeFileSync,
|
|
13
|
+
}
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
getSourceFile(filePath: string): ts.SourceFile | undefined {
|
|
17
|
+
const fileContent = this.host.readFile(filePath);
|
|
18
|
+
const scriptTarget = this.compilerOptions.target ?? ts.ScriptTarget.ES2020;
|
|
19
|
+
if (fileContent) {
|
|
20
|
+
return ts.createSourceFile(filePath, fileContent, scriptTarget);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
writeSourceFile(sourceFile: ts.SourceFile) {
|
|
25
|
+
return this.writeFile(sourceFile.fileName, sourceFile.text, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeFile(filePath: string, content: string, encoding: "binary" | "utf8") {
|
|
29
|
+
this.host.writeFile(filePath, content, encoding);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
resolveSourceModulePath(
|
|
33
|
+
containingFilePath: string,
|
|
34
|
+
requestedModuleName: string
|
|
35
|
+
): string {
|
|
36
|
+
const resolutionRes = ts.resolveModuleName(
|
|
37
|
+
requestedModuleName,
|
|
38
|
+
path.isAbsolute(containingFilePath)
|
|
39
|
+
? containingFilePath
|
|
40
|
+
: path.resolve(this.projectRoot, containingFilePath),
|
|
41
|
+
this.compilerOptions,
|
|
42
|
+
this.host,
|
|
43
|
+
undefined,
|
|
44
|
+
undefined,
|
|
45
|
+
ts.ModuleKind.ESNext
|
|
46
|
+
);
|
|
47
|
+
const resolvedModule = resolutionRes.resolvedModule;
|
|
48
|
+
if (!resolvedModule) {
|
|
49
|
+
throw Error(
|
|
50
|
+
`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(
|
|
51
|
+
resolutionRes
|
|
52
|
+
)}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return resolvedModule.resolvedFileName;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express, { RequestHandler, Router } from "express";
|
|
2
|
+
import { ValServer } from "./ValServer";
|
|
3
|
+
import { createRequestHandler as createUIRequestHandler } from "@valbuild/ui/server";
|
|
4
|
+
|
|
5
|
+
export function createRequestHandler(valServer: ValServer): RequestHandler {
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
router.use("/static", createUIRequestHandler());
|
|
9
|
+
router.get("/session", valServer.session.bind(valServer));
|
|
10
|
+
router.get("/authorize", valServer.authorize.bind(valServer));
|
|
11
|
+
router.get("/callback", valServer.callback.bind(valServer));
|
|
12
|
+
router.get("/logout", valServer.logout.bind(valServer));
|
|
13
|
+
router.get<{ 0: string }>("/ids/*", valServer.getIds.bind(valServer));
|
|
14
|
+
router.patch<{ 0: string }>(
|
|
15
|
+
"/ids/*",
|
|
16
|
+
express.json({
|
|
17
|
+
type: "application/json-patch+json",
|
|
18
|
+
limit: "10mb",
|
|
19
|
+
}),
|
|
20
|
+
valServer.patchIds.bind(valServer)
|
|
21
|
+
);
|
|
22
|
+
router.post("/commit", valServer.commit.bind(valServer));
|
|
23
|
+
return router;
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
export const getCompilerOptions = (
|
|
5
|
+
rootDir: string,
|
|
6
|
+
parseConfigHost: ts.ParseConfigHost
|
|
7
|
+
): ts.CompilerOptions => {
|
|
8
|
+
const tsConfigPath = path.resolve(rootDir, "tsconfig.json");
|
|
9
|
+
const jsConfigPath = path.resolve(rootDir, "jsconfig.json");
|
|
10
|
+
let configFilePath: string;
|
|
11
|
+
if (parseConfigHost.fileExists(jsConfigPath)) {
|
|
12
|
+
configFilePath = jsConfigPath;
|
|
13
|
+
} else if (parseConfigHost.fileExists(tsConfigPath)) {
|
|
14
|
+
configFilePath = tsConfigPath;
|
|
15
|
+
} else {
|
|
16
|
+
throw Error(
|
|
17
|
+
`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const { config, error } = ts.readConfigFile(
|
|
21
|
+
configFilePath,
|
|
22
|
+
parseConfigHost.readFile.bind(parseConfigHost)
|
|
23
|
+
);
|
|
24
|
+
if (error) {
|
|
25
|
+
if (typeof error.messageText === "string") {
|
|
26
|
+
throw Error(
|
|
27
|
+
`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
throw Error(
|
|
31
|
+
`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const optionsOverrides = undefined;
|
|
35
|
+
const parsedConfigFile = ts.parseJsonConfigFileContent(
|
|
36
|
+
config,
|
|
37
|
+
parseConfigHost,
|
|
38
|
+
rootDir,
|
|
39
|
+
optionsOverrides,
|
|
40
|
+
configFilePath
|
|
41
|
+
);
|
|
42
|
+
if (parsedConfigFile.errors.length > 0) {
|
|
43
|
+
throw Error(
|
|
44
|
+
`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors
|
|
45
|
+
.map((e) => e.messageText)
|
|
46
|
+
.join("\n")}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return parsedConfigFile.options;
|
|
50
|
+
};
|
package/src/hosting.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { RequestListener } from "node:http";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { createService, ServiceOptions } from "./Service";
|
|
4
|
+
import { createRequestHandler } from "./createRequestHandler";
|
|
5
|
+
import { ValServer } from "./ValServer";
|
|
6
|
+
import { LocalValServer, LocalValServerOptions } from "./LocalValServer";
|
|
7
|
+
import { ProxyValServer, ProxyValServerOptions } from "./ProxyValServer";
|
|
8
|
+
|
|
9
|
+
type Opts = ValServerOverrides & ServiceOptions;
|
|
10
|
+
|
|
11
|
+
type ValServerOverrides = Partial<{
|
|
12
|
+
/**
|
|
13
|
+
* Override the Val API key.
|
|
14
|
+
*
|
|
15
|
+
* Typically this is set using VAL_API_KEY env var.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: if this is set you must also set valSecret or VAL_SECRET env var.
|
|
18
|
+
*/
|
|
19
|
+
apiKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Override the Val session key.
|
|
22
|
+
*
|
|
23
|
+
* This can be any randomly generated string.
|
|
24
|
+
* It will be used for authentication between the frontend and val api
|
|
25
|
+
* endpoints in this app.
|
|
26
|
+
*
|
|
27
|
+
* Typically this is set using VAL_SECRET env var.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: if this is set you must also set apiKey or VAL_API_KEY env var.
|
|
30
|
+
*/
|
|
31
|
+
valSecret: string;
|
|
32
|
+
/**
|
|
33
|
+
* Override the default the mode of operation.
|
|
34
|
+
*
|
|
35
|
+
* Typically this should not be set.
|
|
36
|
+
*
|
|
37
|
+
* "local" means that changes will be written to the local filesystem,
|
|
38
|
+
* which is what you want when developing locally.
|
|
39
|
+
*
|
|
40
|
+
* "proxy" means that changes will proxied to https://app.val.build
|
|
41
|
+
* and eventually be committed in the Git repository.
|
|
42
|
+
*
|
|
43
|
+
* It will automatically be "proxy" if both VAL_API_KEY env var (or the apiKey property) and VAL_SECRET env var (or the valSecret property)
|
|
44
|
+
* is set.
|
|
45
|
+
*
|
|
46
|
+
* If both is missing, it will default to "local".
|
|
47
|
+
*/
|
|
48
|
+
mode: "proxy" | "local";
|
|
49
|
+
/**
|
|
50
|
+
* Current git commit.
|
|
51
|
+
*
|
|
52
|
+
* Required if mode is "proxy".
|
|
53
|
+
*
|
|
54
|
+
* @example "e83c5163316f89bfbde7d9ab23ca2e25604af290"
|
|
55
|
+
*/
|
|
56
|
+
gitCommit: string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Current git branch.
|
|
60
|
+
*
|
|
61
|
+
* Required if mode is "proxy".
|
|
62
|
+
*
|
|
63
|
+
* @example "main"
|
|
64
|
+
*/
|
|
65
|
+
gitBranch: string;
|
|
66
|
+
/**
|
|
67
|
+
* The base url of Val.
|
|
68
|
+
*
|
|
69
|
+
* Typically this should not be set.
|
|
70
|
+
*
|
|
71
|
+
* Can also be overridden using the VAL_BUILD_URL env var.
|
|
72
|
+
*
|
|
73
|
+
* @example "https://app.val.build"
|
|
74
|
+
*/
|
|
75
|
+
valBuildUrl: string;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
async function _createRequestListener(
|
|
79
|
+
route: string,
|
|
80
|
+
opts: Opts
|
|
81
|
+
): Promise<RequestListener> {
|
|
82
|
+
const serverOpts = await initHandlerOptions(route, opts);
|
|
83
|
+
let valServer: ValServer;
|
|
84
|
+
if (serverOpts.mode === "proxy") {
|
|
85
|
+
valServer = new ProxyValServer(serverOpts);
|
|
86
|
+
} else {
|
|
87
|
+
valServer = new LocalValServer(serverOpts);
|
|
88
|
+
}
|
|
89
|
+
const reqHandler = createRequestHandler(valServer);
|
|
90
|
+
return express().use(route, reqHandler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type ValServerOptions =
|
|
94
|
+
| ({ mode: "proxy" } & ProxyValServerOptions)
|
|
95
|
+
| ({ mode: "local" } & LocalValServerOptions);
|
|
96
|
+
async function initHandlerOptions(
|
|
97
|
+
route: string,
|
|
98
|
+
opts: ValServerOverrides & ServiceOptions
|
|
99
|
+
): Promise<ValServerOptions> {
|
|
100
|
+
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
|
101
|
+
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
|
102
|
+
const isProxyMode =
|
|
103
|
+
opts.mode === "proxy" ||
|
|
104
|
+
(opts.mode === undefined && (maybeApiKey || maybeValSecret));
|
|
105
|
+
|
|
106
|
+
if (isProxyMode) {
|
|
107
|
+
const valBuildUrl =
|
|
108
|
+
opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
|
109
|
+
if (!maybeApiKey || !maybeValSecret) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const maybeGitCommit = opts.gitCommit || process.env.VAL_GIT_COMMIT;
|
|
115
|
+
if (!maybeGitCommit) {
|
|
116
|
+
throw new Error("VAL_GIT_COMMIT env var must be set in proxy mode");
|
|
117
|
+
}
|
|
118
|
+
const maybeGitBranch = opts.gitBranch || process.env.VAL_GIT_BRANCH;
|
|
119
|
+
if (!maybeGitBranch) {
|
|
120
|
+
throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
mode: "proxy",
|
|
124
|
+
route,
|
|
125
|
+
apiKey: maybeApiKey,
|
|
126
|
+
valSecret: maybeValSecret,
|
|
127
|
+
valBuildUrl,
|
|
128
|
+
gitCommit: maybeGitCommit,
|
|
129
|
+
gitBranch: maybeGitBranch,
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
const service = await createService(process.cwd(), opts);
|
|
133
|
+
return {
|
|
134
|
+
mode: "local",
|
|
135
|
+
service,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// TODO: rename to createValApiHandlers?
|
|
141
|
+
export function createRequestListener(
|
|
142
|
+
route: string,
|
|
143
|
+
opts: Opts
|
|
144
|
+
): RequestListener {
|
|
145
|
+
const handler = _createRequestListener(route, opts);
|
|
146
|
+
return async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
return (await handler)(req, res);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
res.statusCode = 500;
|
|
151
|
+
res.write(e instanceof Error ? e.message : "Unknown error");
|
|
152
|
+
res.end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { ServiceOptions } from "./Service";
|
|
2
|
+
export { createService, Service } from "./Service";
|
|
3
|
+
export { createRequestHandler } from "./createRequestHandler";
|
|
4
|
+
export { createRequestListener } from "./hosting";
|
|
5
|
+
export { ValModuleLoader } from "./ValModuleLoader";
|
|
6
|
+
export { getCompilerOptions } from "./getCompilerOptions";
|
|
7
|
+
export { ValSourceFileHandler } from "./ValSourceFileHandler";
|
|
8
|
+
export { ValFSHost } from "./ValFSHost";
|
|
9
|
+
export type { IValFSHost } from "./ValFSHost";
|
|
10
|
+
export type { ValFS } from "./ValFS";
|
|
11
|
+
export { patchSourceFile } from "./patchValFile";
|
|
12
|
+
export { formatSyntaxErrorTree } from "./patch/ts/syntax";
|