Skip to content

Commit 5862539

Browse files
committed
feat: implement project and library compilation commands with validation and error handling
1 parent 8855fec commit 5862539

4 files changed

Lines changed: 151 additions & 50 deletions

File tree

packages/project/src/compiler/index.ts

Lines changed: 115 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,114 @@ function printMessage(message: string | ts.DiagnosticMessageChain, stream: Writa
1919
}
2020
}
2121

22+
function validateProjectTsconfig(compilerOptions: ts.CompilerOptions, outDir: string) {
23+
const forcedOptions: Record<string, any[]> = {
24+
target: [ts.ScriptTarget.ES2023, ts.ScriptTarget.ES2020],
25+
module: [ts.ModuleKind.ES2022, ts.ModuleKind.ES2020],
26+
moduleResolution: [ts.ModuleResolutionKind.NodeJs],
27+
resolveJsonModule: [false],
28+
esModuleInterop: [true],
29+
outDir: [outDir],
30+
};
31+
32+
const optionNames: Record<string, Record<any, string>> = {
33+
target: Object.entries(ts.ScriptTarget).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
34+
module: Object.entries(ts.ModuleKind).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
35+
moduleResolution: Object.entries(ts.ModuleResolutionKind).reduce(
36+
(acc, [k, v]) => ({ ...acc, [v]: k }),
37+
{}
38+
),
39+
};
40+
41+
for (const [key, values] of Object.entries(forcedOptions)) {
42+
const valueNames = values.map((v) => optionNames[key]?.[v] ?? v).join(", ");
43+
if (compilerOptions[key] && !values.includes(compilerOptions[key])) {
44+
throw new Error(`tsconfig.json must have ${key} set to one of: [ ${valueNames} ]`);
45+
} else if (!compilerOptions[key]) {
46+
compilerOptions[key] = values[0];
47+
}
48+
}
49+
}
50+
51+
export async function compileProject(
52+
fs: FSInterface,
53+
projectPath: string,
54+
err: Writable,
55+
out?: Writable,
56+
tsLibsPath: string = path.dirname(
57+
fileURLToPath(import.meta.resolve?.("typescript") ?? "typescript")
58+
)
59+
): Promise<boolean> {
60+
const outDir = "build";
61+
const system = tsvfs.createSystem(fs, projectPath);
62+
63+
const tsconfig = ts.findConfigFile("./", system.fileExists, "tsconfig.json");
64+
if (!tsconfig) {
65+
throw new Error(`Could not find tsconfig.json in directory: ${projectPath}`);
66+
}
67+
const configJsonFile = ts.readConfigFile(tsconfig, system.readFile);
68+
if (configJsonFile.error) {
69+
printMessage(configJsonFile.error.messageText, err);
70+
throw new Error("Error reading tsconfig.json");
71+
}
72+
73+
validateProjectTsconfig(configJsonFile.config, outDir);
74+
return await compile(
75+
fs,
76+
projectPath,
77+
outDir,
78+
configJsonFile.config,
79+
system,
80+
err,
81+
out,
82+
false,
83+
tsLibsPath
84+
);
85+
}
86+
87+
export async function compileLibrary(
88+
fs: FSInterface,
89+
libraryPath: string,
90+
err: Writable,
91+
out?: Writable,
92+
transpileOnly: boolean = false,
93+
tsLibsPath: string = path.dirname(
94+
fileURLToPath(import.meta.resolve?.("typescript") ?? "typescript")
95+
)
96+
): Promise<boolean> {
97+
const outDir = "dist";
98+
const system = tsvfs.createSystem(fs, libraryPath);
99+
100+
const configJson = {
101+
compilerOptions: {
102+
target: "ES2023",
103+
module: "ES2022",
104+
lib: ["es2023"],
105+
moduleResolution: "node",
106+
declaration: true,
107+
declarationDir: "dist/types",
108+
outDir: "dist/js",
109+
rootDir: "src",
110+
strict: true,
111+
baseUrl: ".",
112+
noEmitOnError: !transpileOnly,
113+
},
114+
include: ["src"],
115+
};
116+
117+
return await compile(
118+
fs,
119+
libraryPath,
120+
outDir,
121+
configJson,
122+
system,
123+
err,
124+
out,
125+
transpileOnly,
126+
tsLibsPath
127+
);
128+
}
129+
22130
/**
23131
* Compiles TypeScript files with custom FSInterface
24132
* @param fs - The file system interface (Node, zenfs, etc.)
@@ -34,68 +142,28 @@ export async function compile(
34142
fs: FSInterface,
35143
inputDir: string,
36144
outDir: string,
145+
configJson: Record<string, unknown>,
146+
system: ts.System,
37147
err: Writable,
38148
out?: Writable,
149+
transpileOnly: boolean = false,
39150
tsLibsPath: string = path.dirname(
40151
fileURLToPath(import.meta.resolve?.("typescript") ?? "typescript")
41152
)
42153
): Promise<boolean> {
43-
const system = tsvfs.createSystem(fs, inputDir);
44-
const tsconfig = ts.findConfigFile("./", system.fileExists, "tsconfig.json");
45-
if (!tsconfig) {
46-
throw new Error(`Could not find tsconfig.json in directory: ${inputDir}`);
47-
}
48-
const config = ts.readConfigFile(tsconfig, system.readFile);
49-
if (config.error) {
50-
printMessage(config.error.messageText, err);
51-
throw new Error("Error reading tsconfig.json");
52-
}
53-
54-
// convert enum values to names for better error messages
55-
const optionNames: Record<string, Record<any, string>> = {
56-
target: Object.entries(ts.ScriptTarget).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
57-
module: Object.entries(ts.ModuleKind).reduce((acc, [k, v]) => ({ ...acc, [v]: k }), {}),
58-
moduleResolution: Object.entries(ts.ModuleResolutionKind).reduce(
59-
(acc, [k, v]) => ({ ...acc, [v]: k }),
60-
{}
61-
),
62-
};
63-
64-
const forcedOptions: Record<string, any[]> = {
65-
target: [ts.ScriptTarget.ES2023, ts.ScriptTarget.ES2020],
66-
module: [ts.ModuleKind.ES2022, ts.ModuleKind.ES2020],
67-
moduleResolution: [ts.ModuleResolutionKind.NodeJs],
68-
resolveJsonModule: [false],
69-
esModuleInterop: [true],
70-
outDir: [outDir],
71-
};
72-
73-
const {
74-
options: compilerOptions,
75-
fileNames,
76-
errors,
77-
} = ts.parseJsonConfigFileContent(config.config, system, "./");
154+
const { options, fileNames, errors } = ts.parseJsonConfigFileContent(configJson, system, "./");
78155
if (errors.length > 0) {
79156
errors.forEach((error) => printMessage(error.messageText, err));
80157
throw new Error(`Error parsing tsconfig.json - ${errors.length} error(s) found`);
81158
}
82159

83-
for (const [key, values] of Object.entries(forcedOptions)) {
84-
const valueNames = values.map((v) => optionNames[key]?.[v] ?? v).join(", ");
85-
if (compilerOptions[key] && !values.includes(compilerOptions[key])) {
86-
throw new Error(`tsconfig.json must have ${key} set to one of: [ ${valueNames} ]`);
87-
} else if (!compilerOptions[key]) {
88-
compilerOptions[key] = values[0];
89-
}
90-
}
91-
92160
out?.write("Compiling files: [" + fileNames.join(", ") + "]\n");
93161

94-
const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, tsLibsPath);
162+
const host = tsvfs.createVirtualCompilerHost(system, options, tsLibsPath);
95163

96164
const program = ts.createProgram({
97165
rootNames: fileNames,
98-
options: compilerOptions,
166+
options,
99167
host: host.compilerHost,
100168
});
101169
const emitResult = program.emit();
@@ -125,5 +193,5 @@ export async function compile(
125193
throw new Error("Compilation failed");
126194
}
127195

128-
return !emitResult.emitSkipped && !error;
196+
return !emitResult.emitSkipped && (transpileOnly || !error);
129197
}

packages/tools/src/commands/build.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Command, Opt } from "./lib/command.js";
22
import * as path from "path";
3-
import { stderr } from "process";
4-
import { compile } from "@jaculus/project/compiler";
3+
import { stderr, stdout } from "process";
4+
import { compileProject } from "@jaculus/project/compiler";
55
import * as fs from "fs";
66

77
const cmd = new Command("Build TypeScript project", {
88
action: async (options: Record<string, string | boolean>) => {
99
const path_ = options["input"] as string;
1010
const inputDir = path.resolve(path_);
1111

12-
if (await compile(fs, inputDir, "build", stderr)) {
12+
if (await compileProject(fs, inputDir, stderr, stdout)) {
1313
stderr.write("Compiled successfully\n");
1414
} else {
1515
stderr.write("Compilation failed\n");

packages/tools/src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import serialSocket from "./serial-socket.js";
55
import install from "./install.js";
66
import build from "./build.js";
77
import flash from "./flash.js";
8+
import libBuild from "./lib-build.js";
89
import libInstall from "./lib-install.js";
910
import libLs from "./lib-ls.js";
1011
import libRemove from "./lib-remove.js";
@@ -37,6 +38,7 @@ export function registerJaculusCommands(jac: Program) {
3738

3839
jac.addCommand("flash", flash);
3940

41+
jac.addCommand("lib-build", libBuild);
4042
jac.addCommand("lib-install", libInstall);
4143
jac.addCommand("lib-ls", libLs);
4244
jac.addCommand("lib-remove", libRemove);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { stdout } from "process";
2+
import { Command, Opt } from "./lib/command.js";
3+
import fs from "fs";
4+
import path from "path";
5+
import { stderr } from "process";
6+
import { compileLibrary } from "@jaculus/project/compiler";
7+
8+
const cmd = new Command("List libraries from project package.json", {
9+
action: async (options: Record<string, string | boolean>) => {
10+
const path_ = options["input"] as string;
11+
const inputDir = path.resolve(path_);
12+
const transpileOnly = options["transpileOnly"] as boolean;
13+
14+
if (await compileLibrary(fs, inputDir, stderr, stdout, transpileOnly)) {
15+
stderr.write("Compiled successfully\n");
16+
} else {
17+
stderr.write("Compilation failed\n");
18+
throw 1;
19+
}
20+
},
21+
options: {
22+
input: new Opt("The input directory", { required: true, defaultValue: "./" }),
23+
transpileOnly: new Opt(
24+
"Transpile only, skip type validation (still emits JS/d.ts on type errors)",
25+
{ isFlag: true }
26+
),
27+
},
28+
chainable: true,
29+
});
30+
31+
export default cmd;

0 commit comments

Comments
 (0)