@valbuild/cli 0.97.6 → 0.98.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.
@@ -4,16 +4,13 @@ var meow = require('meow');
4
4
  var chalk = require('chalk');
5
5
  var path = require('path');
6
6
  var pc = require('picocolors');
7
- var fs = require('fs/promises');
7
+ var fs$1 = require('fs/promises');
8
8
  var fastGlob = require('fast-glob');
9
9
  var core = require('@valbuild/core');
10
10
  var server = require('@valbuild/server');
11
- var vm = require('node:vm');
12
- var ts = require('typescript');
13
- var z = require('zod');
14
- var node_module = require('node:module');
15
11
  var internal = require('@valbuild/shared/internal');
16
- var fs$1 = require('fs');
12
+ var ts = require('typescript');
13
+ var fs = require('fs');
17
14
  var JSZip = require('jszip');
18
15
  var readline = require('readline');
19
16
 
@@ -41,11 +38,9 @@ var meow__default = /*#__PURE__*/_interopDefault(meow);
41
38
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
42
39
  var path__default = /*#__PURE__*/_interopDefault(path);
43
40
  var pc__default = /*#__PURE__*/_interopDefault(pc);
44
- var fs__default = /*#__PURE__*/_interopDefault(fs);
45
- var vm__default = /*#__PURE__*/_interopDefault(vm);
46
- var ts__default = /*#__PURE__*/_interopDefault(ts);
47
- var z__default = /*#__PURE__*/_interopDefault(z);
48
41
  var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
42
+ var ts__default = /*#__PURE__*/_interopDefault(ts);
43
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
49
44
  var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
50
45
  var readline__default = /*#__PURE__*/_interopDefault(readline);
51
46
 
@@ -64,76 +59,6 @@ function info(message, opts = {}) {
64
59
  console.log(chalk__default["default"].blue("️ℹ️ : ") + message);
65
60
  }
66
61
 
67
- const ValConfigSchema = z__default["default"].object({
68
- project: z__default["default"].string().optional(),
69
- root: z__default["default"].string().optional(),
70
- files: z__default["default"].object({
71
- directory: z__default["default"].string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
72
- // Reject path traversal so the directory cannot escape /public
73
- !val.split("/").some(segment => segment === "." || segment === ".."), {
74
- message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
75
- })
76
- }).optional(),
77
- gitCommit: z__default["default"].string().optional(),
78
- gitBranch: z__default["default"].string().optional(),
79
- defaultTheme: z__default["default"].union([z__default["default"].literal("light"), z__default["default"].literal("dark")]).optional(),
80
- ai: z__default["default"].object({
81
- commitMessages: z__default["default"].object({
82
- disabled: z__default["default"].boolean().optional()
83
- }).optional()
84
- }).optional()
85
- });
86
- async function evalValConfigFile(projectRoot, configFileName) {
87
- const valConfigPath = path__default["default"].join(projectRoot, configFileName);
88
- let code = null;
89
- try {
90
- code = await fs__default["default"].readFile(valConfigPath, "utf-8");
91
- } catch {
92
- //
93
- }
94
- if (!code) {
95
- return null;
96
- }
97
- const transpiled = ts__default["default"].transpileModule(code, {
98
- compilerOptions: {
99
- target: ts__default["default"].ScriptTarget.ES2020,
100
- module: ts__default["default"].ModuleKind.CommonJS,
101
- esModuleInterop: true
102
- },
103
- fileName: valConfigPath
104
- });
105
- const projectRootRequire = node_module.createRequire(valConfigPath);
106
- const exportsObj = {};
107
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
- const sandbox = {
109
- exports: exportsObj,
110
- module: {
111
- exports: exportsObj
112
- },
113
- require: projectRootRequire,
114
- // NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
115
- __filename: valConfigPath,
116
- __dirname: projectRoot,
117
- console,
118
- process
119
- };
120
- sandbox.global = sandbox;
121
- const context = vm__default["default"].createContext(sandbox);
122
- const script = new vm__default["default"].Script(transpiled.outputText, {
123
- filename: valConfigPath
124
- });
125
- script.runInContext(context);
126
- const valConfig = sandbox.module.exports.config;
127
- if (!valConfig) {
128
- throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
129
- }
130
- const result = ValConfigSchema.safeParse(valConfig);
131
- if (!result.success) {
132
- throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
133
- }
134
- return result.data;
135
- }
136
-
137
62
  function getFileExt(filePath) {
138
63
  // NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
139
64
  return filePath.split(".").pop() || "";
@@ -511,15 +436,15 @@ function createDefaultValFSHost() {
511
436
  return {
512
437
  ...ts__default["default"].sys,
513
438
  writeFile: (fileName, data, encoding) => {
514
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(fileName), {
439
+ fs__default["default"].mkdirSync(path__default["default"].dirname(fileName), {
515
440
  recursive: true
516
441
  });
517
- fs__default$1["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
442
+ fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
518
443
  },
519
- rmFile: fs__default$1["default"].rmSync,
444
+ rmFile: fs__default["default"].rmSync,
520
445
  readBuffer: fileName => {
521
446
  try {
522
- return fs__default$1["default"].readFileSync(fileName);
447
+ return fs__default["default"].readFileSync(fileName);
523
448
  } catch {
524
449
  return undefined;
525
450
  }
@@ -860,7 +785,7 @@ function getCachedFile(moduleFilePath, projectRoot, cache) {
860
785
  const filePath = path__default["default"].join(projectRoot, moduleFilePath);
861
786
  let fileContent;
862
787
  try {
863
- fileContent = fs__default$1["default"].readFileSync(filePath, "utf-8");
788
+ fileContent = fs__default["default"].readFileSync(filePath, "utf-8");
864
789
  } catch {
865
790
  cache.set(moduleFilePath, null);
866
791
  return null;
@@ -891,7 +816,7 @@ async function validate({
891
816
  // errors found. Re-reads config and val files each call so it always reflects
892
817
  // the latest state on disk (used both for one-shot and watch mode).
893
818
  async function runOnce() {
894
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
819
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
895
820
  const resolvedValConfigFile = valConfigFile ? {
896
821
  ...valConfigFile,
897
822
  project: process.env.VAL_PROJECT || valConfigFile.project
@@ -1071,11 +996,11 @@ async function validate({
1071
996
  if (prettier) {
1072
997
  for (const file of fixedFiles) {
1073
998
  const filePath = path__default["default"].join(projectRoot, file);
1074
- const fileContent = await fs__default["default"].readFile(filePath, "utf-8");
999
+ const fileContent = await fs__default$1["default"].readFile(filePath, "utf-8");
1075
1000
  const formattedContent = await prettier.format(fileContent, {
1076
1001
  filepath: filePath
1077
1002
  });
1078
- await fs__default["default"].writeFile(filePath, formattedContent);
1003
+ await fs__default$1["default"].writeFile(filePath, formattedContent);
1079
1004
  }
1080
1005
  }
1081
1006
 
@@ -1196,7 +1121,7 @@ async function listUnusedFiles({
1196
1121
  }) {
1197
1122
  var _valConfigFile$files;
1198
1123
  const projectRoot = root ? path__default["default"].resolve(root) : process.cwd();
1199
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1124
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
1200
1125
  // Strip the leading "/" so it is relative to the project root (e.g. "public/val").
1201
1126
  const managedDir = ((valConfigFile === null || valConfigFile === void 0 || (_valConfigFile$files = valConfigFile.files) === null || _valConfigFile$files === void 0 ? void 0 : _valConfigFile$files.directory) ?? "/public/val").replace(/^\//, "");
1202
1127
  const service = await server.createService(projectRoot);
@@ -1285,7 +1210,7 @@ const getVersions = () => {
1285
1210
  };
1286
1211
  };
1287
1212
 
1288
- const host$1 = process.env.VAL_BUILD_URL || "https://admin.val.build";
1213
+ const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1289
1214
  async function connect(options) {
1290
1215
  const {
1291
1216
  root
@@ -1303,11 +1228,11 @@ async function connect(options) {
1303
1228
  if (maybeGitOwnerAndRepo) {
1304
1229
  params.set("github_repo", [maybeGitOwnerAndRepo.owner, maybeGitOwnerAndRepo.repo].join("/"));
1305
1230
  }
1306
- const url = `${host$1}/connect?${params.toString()}`;
1231
+ const url = `${host}/connect?${params.toString()}`;
1307
1232
  console.log(pc__default["default"].dim(`\nFollow the instructions in your browser to complete the setup:\n${pc__default["default"].cyan(url)}\n`));
1308
1233
  }
1309
1234
  async function tryGetProject(projectRoot) {
1310
- const valConfigFile = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1235
+ const valConfigFile = await server.findAndEvalValConfigFile(projectRoot);
1311
1236
  if (valConfigFile && valConfigFile.project) {
1312
1237
  const parts = valConfigFile.project.split("/");
1313
1238
  if (parts.length === 2) {
@@ -1360,8 +1285,8 @@ function tryGetGitConfig(root) {
1360
1285
  let lastDir = null;
1361
1286
  while (currentDir !== lastDir) {
1362
1287
  const gitConfigPath = path__default["default"].join(currentDir, ".git", "config");
1363
- if (fs__default$1["default"].existsSync(gitConfigPath)) {
1364
- return fs__default$1["default"].readFileSync(gitConfigPath, "utf-8");
1288
+ if (fs__default["default"].existsSync(gitConfigPath)) {
1289
+ return fs__default["default"].readFileSync(gitConfigPath, "utf-8");
1365
1290
  }
1366
1291
  lastDir = currentDir;
1367
1292
  currentDir = path__default["default"].dirname(currentDir);
@@ -1372,83 +1297,54 @@ function tryGetGitConfig(root) {
1372
1297
  return null;
1373
1298
  }
1374
1299
 
1375
- const host = process.env.VAL_BUILD_URL || "https://admin.val.build";
1300
+ /**
1301
+ * Terminal wrapper around the login device flow in `@valbuild/server`.
1302
+ * The flow itself (polling, token persistence) is shared with
1303
+ * `@valbuild/language-server`.
1304
+ */
1376
1305
  async function login(options) {
1377
1306
  try {
1378
- var _response$headers$get;
1379
1307
  console.log(pc__default["default"].cyan("\nStarting login process...\n"));
1380
-
1381
- // Step 1: Initiate login and get token and URL
1382
- const response = await fetch(`${host}/api/login`, {
1383
- method: "POST",
1384
- headers: {
1385
- "Content-Type": "application/json"
1386
- }
1387
- });
1388
- let token;
1389
- let url;
1390
- if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
1391
- const text = await response.text();
1392
- console.error(pc__default["default"].red("Unexpected failure while trying to login (content type was not JSON). "), text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
1393
- process.exit(1);
1394
- }
1395
- const json = await response.json();
1396
- if (json) {
1397
- token = json.nonce;
1398
- url = json.url;
1399
- }
1400
- if (!token || !url) {
1401
- console.error(pc__default["default"].red("Unexpected response from the server."), json);
1402
- process.exit(1);
1403
- }
1308
+ const {
1309
+ nonce,
1310
+ url
1311
+ } = await server.startValLogin();
1404
1312
  console.log(pc__default["default"].green("Open the following URL in your browser to log in:"));
1405
1313
  console.log(pc__default["default"].underline(pc__default["default"].blue(url)));
1406
1314
  console.log(pc__default["default"].dim("\nWaiting for login confirmation...\n"));
1407
-
1408
- // Step 2: Poll for login confirmation
1409
- const result = await pollForConfirmation(token);
1410
-
1411
- // Step 3: Save the token
1412
- const filePath = server.getPersonalAccessTokenPath(options.root || process.cwd());
1413
- saveToken(result, filePath);
1315
+ const result = await server.awaitValLoginConfirmation(nonce);
1316
+ const filePath = server.persistPersonalAccessToken(options.root || process.cwd(), result);
1317
+ console.log(pc__default["default"].green(`Token for ${pc__default["default"].cyan(result.profile.email)} saved to ${pc__default["default"].cyan(filePath)}`));
1414
1318
  } catch (error) {
1415
- console.error(pc__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1416
- process.exit(1);
1417
- }
1418
- }
1419
- const MAX_DURATION = 5 * 60 * 1000; // 5 minutes
1420
- async function pollForConfirmation(token) {
1421
- const start = Date.now();
1422
- while (Date.now() - start < MAX_DURATION) {
1423
- await new Promise(resolve => setTimeout(resolve, 1000));
1424
- const response = await fetch(`${host}/api/login?token=${token}&consume=true`, {
1425
- method: "POST"
1426
- });
1427
- if (response.status === 500) {
1428
- console.error(pc__default["default"].red("An error occurred on the server."));
1319
+ if (error instanceof server.ValLoginError) {
1320
+ console.error(pc__default["default"].red(error.message), error.details ?? "");
1429
1321
  process.exit(1);
1430
1322
  }
1431
- if (response.status === 200) {
1432
- const json = await response.json();
1433
- if (json) {
1434
- if (typeof json.profile.email === "string" && typeof json.pat === "string") {
1435
- return json;
1436
- } else {
1437
- console.error(pc__default["default"].red("Unexpected response from the server."));
1438
- process.exit(1);
1439
- }
1440
- }
1441
- }
1323
+ console.error(pc__default["default"].red("An error occurred during the login process. Check your internet connection. Details:"), error instanceof Error ? error.message : JSON.stringify(error, null, 2));
1324
+ process.exit(1);
1442
1325
  }
1443
- console.error(pc__default["default"].red("Login confirmation timed out."));
1444
- process.exit(1);
1445
1326
  }
1446
- function saveToken(result, filePath) {
1447
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(filePath), {
1448
- recursive: true
1449
- });
1450
- fs__default$1["default"].writeFileSync(filePath, JSON.stringify(result, null, 2));
1451
- console.log(pc__default["default"].green(`Token for ${pc__default["default"].cyan(result.profile.email)} saved to ${pc__default["default"].cyan(filePath)}`));
1327
+
1328
+ /**
1329
+ * Start the Val language server.
1330
+ *
1331
+ * Editors normally resolve `@valbuild/language-server` from the project's
1332
+ * `node_modules` and launch its `val-language-server` binary directly. This
1333
+ * subcommand exists so the server is discoverable from the CLI and easy to
1334
+ * start by hand when debugging an editor integration:
1335
+ *
1336
+ * npx val lsp --stdio
1337
+ *
1338
+ * The import is dynamic so that `vscode-languageserver` is not loaded on every
1339
+ * `val` invocation.
1340
+ */
1341
+ async function lsp() {
1342
+ const {
1343
+ main
1344
+ } = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('@valbuild/language-server')); });
1345
+ // Transport (`--stdio`, `--node-ipc`, `--socket=`) is read from argv by the
1346
+ // server itself, so the flags pass straight through.
1347
+ main();
1452
1348
  }
1453
1349
 
1454
1350
  /**
@@ -1465,10 +1361,10 @@ class DebugContextError extends Error {}
1465
1361
  async function createDebugContext(options) {
1466
1362
  var _config$files;
1467
1363
  const projectRoot = options.root ? path__default["default"].resolve(options.root) : process.cwd();
1468
- if (!fs__default$1["default"].existsSync(projectRoot)) {
1364
+ if (!fs__default["default"].existsSync(projectRoot)) {
1469
1365
  throw new DebugContextError(`Project root does not exist: ${projectRoot}`);
1470
1366
  }
1471
- const config = (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
1367
+ const config = await server.findAndEvalValConfigFile(projectRoot);
1472
1368
  if (!config) {
1473
1369
  throw new DebugContextError(`Could not find val.config.ts nor val.config.js in: ${projectRoot}`);
1474
1370
  }
@@ -1535,8 +1431,8 @@ async function createDebugContext(options) {
1535
1431
  */
1536
1432
  function readAuth(projectRoot) {
1537
1433
  const patFile = server.getPersonalAccessTokenPath(projectRoot);
1538
- if (fs__default$1["default"].existsSync(patFile)) {
1539
- const contents = fs__default$1["default"].readFileSync(patFile, "utf-8");
1434
+ if (fs__default["default"].existsSync(patFile)) {
1435
+ const contents = fs__default["default"].readFileSync(patFile, "utf-8");
1540
1436
  const parsed = server.parsePersonalAccessTokenFile(contents);
1541
1437
  if (parsed.success) {
1542
1438
  return {
@@ -2083,7 +1979,7 @@ function readProjectValVersions(projectRoot) {
2083
1979
  function readLocalFile(projectRoot, projectRelativePath) {
2084
1980
  const absPath = path__default["default"].join(projectRoot, projectRelativePath);
2085
1981
  try {
2086
- return fs__default$1["default"].readFileSync(absPath, "utf-8");
1982
+ return fs__default["default"].readFileSync(absPath, "utf-8");
2087
1983
  } catch {
2088
1984
  return null;
2089
1985
  }
@@ -2225,10 +2121,10 @@ async function writeZip(outPath, entries) {
2225
2121
  type: "nodebuffer",
2226
2122
  compression: "DEFLATE"
2227
2123
  });
2228
- fs__default$1["default"].mkdirSync(path__default["default"].dirname(outPath), {
2124
+ fs__default["default"].mkdirSync(path__default["default"].dirname(outPath), {
2229
2125
  recursive: true
2230
2126
  });
2231
- fs__default$1["default"].writeFileSync(outPath, buffer);
2127
+ fs__default["default"].writeFileSync(outPath, buffer);
2232
2128
  }
2233
2129
  function timestamp() {
2234
2130
  return new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
@@ -2365,6 +2261,7 @@ async function main() {
2365
2261
  files
2366
2262
  connect
2367
2263
  versions
2264
+ lsp
2368
2265
  debug
2369
2266
  delete-unappliable-patches
2370
2267
 
@@ -2394,6 +2291,13 @@ async function main() {
2394
2291
  Options:
2395
2292
  --root [root], -r [root] Set project root directory (default process.cwd())
2396
2293
 
2294
+ Command: lsp
2295
+ Description: start the Val language server (used by editor integrations)
2296
+ Options:
2297
+ --stdio Communicate over stdin/stdout
2298
+ --node-ipc Communicate over Node IPC
2299
+ --socket=[port] Communicate over a socket
2300
+
2397
2301
  Command: debug
2398
2302
  Description: Create a self-contained snapshot (the pending patches plus the modules they
2399
2303
  touch or reference) that Val developers can replay to reproduce validation and save
@@ -2423,11 +2327,6 @@ async function main() {
2423
2327
  --verbose List every pending patch, not just the failing ones
2424
2328
  `, {
2425
2329
  flags: {
2426
- port: {
2427
- type: "number",
2428
- alias: "p",
2429
- default: 4123
2430
- },
2431
2330
  root: {
2432
2331
  type: "string",
2433
2332
  alias: "r"
@@ -2489,6 +2388,8 @@ async function main() {
2489
2388
  });
2490
2389
  case "versions":
2491
2390
  return versions();
2391
+ case "lsp":
2392
+ return lsp();
2492
2393
  case "debug":
2493
2394
  return debug({
2494
2395
  root: flags.root,