@decohere/workspace 0.0.1-dev.130 → 0.0.1-dev.20261105.46

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/README.md CHANGED
@@ -56,6 +56,8 @@ For each discovered project, the workspace package maps `package.json` `name` to
56
56
 
57
57
  Root `tsconfig.json` `paths` override the same keys. In this repo they come from `forge paths`, which writes this map to `tsconfig.paths.json` for `tsc`.
58
58
 
59
+ `tsconfig.json` is read with [get-tsconfig](https://github.com/privatenumber/get-tsconfig), which follows `extends`, not with the TypeScript compiler: loading the compiler takes longer than the rest of a `flux run`. Transpiling uses only `paths` and `baseUrl` (made absolute). Forge's type emission, which needs every option as tsc sees it, parses `Tsconfig.filePath` with the compiler itself.
60
+
59
61
  Forge debug and Flux consume this map (SWC `jsc.paths` / resolve hooks). Why debug uses it instead of `node_modules` workspace links: [Forge — debug path aliases](../forge/README.md#debug-path-aliases-not-node_modules-links).
60
62
 
61
63
  ## When to use
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Parses JSON with comments and trailing commas, as `tsconfig.json` allows. An empty file is
3
+ * `{}`, as tsc reads it. Throws a `SyntaxError` on anything else that isn't valid JSON.
4
+ */ "use strict";
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ Object.defineProperty(exports, "parseJsonc", {
9
+ enumerable: true,
10
+ get: function() {
11
+ return parseJsonc;
12
+ }
13
+ });
14
+ function parseJsonc(text) {
15
+ if (text.trim() === "") return {};
16
+ return JSON.parse(stripJsonc(text));
17
+ }
18
+ function stripJsonc(text) {
19
+ let out = "";
20
+ let i = 0;
21
+ while(i < text.length){
22
+ const char = text[i];
23
+ const next = text[i + 1];
24
+ if (char === '"') {
25
+ const end = stringEnd(text, i);
26
+ out += text.slice(i, end);
27
+ i = end;
28
+ } else if (char === "/" && next === "/") {
29
+ const end = text.indexOf("\n", i);
30
+ i = end === -1 ? text.length : end;
31
+ } else if (char === "/" && next === "*") {
32
+ const end = text.indexOf("*/", i + 2);
33
+ if (end === -1) throw new SyntaxError("Unterminated comment in JSON");
34
+ out += " ";
35
+ i = end + 2;
36
+ } else if (char === "}" || char === "]") {
37
+ // Drop a trailing comma before the closing bracket.
38
+ out = out.trimEnd();
39
+ if (out.endsWith(",")) out = out.slice(0, -1);
40
+ out += char;
41
+ i++;
42
+ } else {
43
+ out += char;
44
+ i++;
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ /** Index just past the string starting at `start` (a `"`); an unclosed string runs to the end. */ function stringEnd(text, start) {
50
+ let i = start + 1;
51
+ while(i < text.length){
52
+ if (text[i] === "\\") i += 2;
53
+ else if (text[i] === '"') return i + 1;
54
+ else i++;
55
+ }
56
+ return text.length;
57
+ }
@@ -14,7 +14,7 @@ const _caching = require("@decohere/core/caching");
14
14
  const _extensibility = require("@decohere/core/extensibility");
15
15
  const _io = require("@decohere/devkit/io");
16
16
  const _picomatch = /*#__PURE__*/ _interop_require_default(require("picomatch"));
17
- const _rxjs = require("rxjs");
17
+ const _system = require("@decohere/core/system");
18
18
  const _errors = require("../errors.js");
19
19
  const _haspackagejsonmixin = require("./has-package-json.mixin.js");
20
20
  const _hastsconfigmixin = require("./has-tsconfig.mixin.js");
@@ -36,7 +36,7 @@ function _ts_metadata(k, v) {
36
36
  }
37
37
  class Project extends (0, _extensibility.mixwith)(_haspackagejsonmixin.HasPackageJson, _hastsconfigmixin.HasTsConfig) {
38
38
  packageJson;
39
- #projectFileChangedSubject = new _rxjs.Subject();
39
+ #projectFileChangedSubject = new _system.Subject();
40
40
  #sourceFiles = new Map();
41
41
  #projectConfig;
42
42
  #name;
@@ -19,24 +19,14 @@ _export(exports, {
19
19
  const _nodefs = /*#__PURE__*/ _interop_require_default(require("node:fs"));
20
20
  const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path"));
21
21
  const _io = require("@decohere/core/io");
22
- const _typescript = /*#__PURE__*/ _interop_require_default(require("typescript"));
22
+ const _gettsconfig = require("get-tsconfig");
23
23
  const _errors = require("../errors.js");
24
+ const _jsonc = require("./jsonc.js");
24
25
  function _interop_require_default(obj) {
25
26
  return obj && obj.__esModule ? obj : {
26
27
  default: obj
27
28
  };
28
29
  }
29
- const host = _typescript.default.sys;
30
- host.readFile = (file)=>{
31
- return _nodefs.default.readFileSync(file, {
32
- encoding: "utf8"
33
- });
34
- };
35
- host.readDirectory = (directoryName)=>{
36
- return _nodefs.default.readdirSync(directoryName, {
37
- withFileTypes: true
38
- }).filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
39
- };
40
30
  const DEFAULT_COMPILER_OPTIONS = {
41
31
  emitDeclarationOnly: true,
42
32
  declaration: true,
@@ -50,34 +40,34 @@ class Tsconfig {
50
40
  if (!await _io.io.fileExists(filePath)) {
51
41
  throw new _errors.WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
52
42
  }
53
- const configFileText = await _io.io.readFile(filePath);
54
- const result = _typescript.default.parseConfigFileTextToJson(filePath, configFileText);
55
- if (result.error) {
56
- const errorMessage = result.error.messageText.toString();
57
- throw new _errors.WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
58
- }
59
- const dirName = _nodepath.default.resolve(_nodepath.default.dirname(filePath));
60
- const configParseResult = _typescript.default.parseJsonConfigFileContent(result.config, host, dirName);
61
- return new Tsconfig({
62
- compilerOptions: configParseResult.options,
63
- errors: configParseResult.errors
64
- });
43
+ return Tsconfig.read(filePath);
65
44
  }
66
45
  static parseSync(filePath) {
67
46
  if (!Tsconfig.fileExistsSync(filePath)) {
68
47
  throw new _errors.WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
69
48
  }
70
- const configFileText = _nodefs.default.readFileSync(filePath, "utf8");
71
- const result = _typescript.default.parseConfigFileTextToJson(filePath, configFileText);
72
- if (result.error) {
73
- const errorMessage = result.error.messageText.toString();
74
- throw new _errors.WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
49
+ return Tsconfig.read(filePath);
50
+ }
51
+ static read(filePath) {
52
+ const fullPath = _nodepath.default.resolve(filePath);
53
+ let compilerOptions;
54
+ try {
55
+ // get-tsconfig reads broken JSON as `{}`; check the file itself first.
56
+ (0, _jsonc.parseJsonc)(_nodefs.default.readFileSync(fullPath, "utf8"));
57
+ compilerOptions = (0, _gettsconfig.parseTsconfig)(fullPath).compilerOptions ?? {};
58
+ } catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new _errors.WorkspaceConfigurationError(`Error parsing ${filePath}. ${message}`, {
61
+ cause: error
62
+ });
75
63
  }
76
- const dirName = _nodepath.default.resolve(_nodepath.default.dirname(filePath));
77
- const configParseResult = _typescript.default.parseJsonConfigFileContent(result.config, host, dirName);
64
+ const { baseUrl } = compilerOptions;
78
65
  return new Tsconfig({
79
- compilerOptions: configParseResult.options,
80
- errors: configParseResult.errors
66
+ filePath: fullPath,
67
+ compilerOptions: baseUrl == null ? compilerOptions : {
68
+ ...compilerOptions,
69
+ baseUrl: _nodepath.default.resolve(_nodepath.default.dirname(fullPath), baseUrl)
70
+ }
81
71
  });
82
72
  }
83
73
  static fileExistsSync(filePath) {
@@ -92,6 +82,6 @@ class Tsconfig {
92
82
  }){
93
83
  Object.assign(this, data);
94
84
  }
85
+ /** Absolute path of the file read; undefined for {@link Tsconfig.default}. */ filePath;
95
86
  compilerOptions;
96
- errors;
97
87
  }
@@ -11,7 +11,7 @@ Object.defineProperty(exports, "Workspace", {
11
11
  const _decorators = require("@decohere/crucible/decorators");
12
12
  const _collections = require("@decohere/core/collections");
13
13
  const _extensibility = require("@decohere/core/extensibility");
14
- const _rxjs = require("rxjs");
14
+ const _system = require("@decohere/core/system");
15
15
  const _errors = require("../errors.js");
16
16
  const _haspackagejsonmixin = require("./has-package-json.mixin.js");
17
17
  const _hastsconfigmixin = require("./has-tsconfig.mixin.js");
@@ -38,7 +38,7 @@ class Workspace extends (0, _extensibility.mixwith)(_haspackagejsonmixin.HasPack
38
38
  * of every file: the Vitest plugin asks once per transformed file. Keyed by the list
39
39
  * `getAllFiles` returns, so a new list (after its cache is flushed) gets a new set.
40
40
  */ #sourcePaths = new WeakMap();
41
- #projectFileChangedSubject = new _rxjs.Subject();
41
+ #projectFileChangedSubject = new _system.Subject();
42
42
  #projectWatchSubscriptions = new Map();
43
43
  #watchedProjects = [];
44
44
  constructor(options = {}){
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Parses JSON with comments and trailing commas, as `tsconfig.json` allows. An empty file is
3
+ * `{}`, as tsc reads it. Throws a `SyntaxError` on anything else that isn't valid JSON.
4
+ */ export function parseJsonc(text) {
5
+ if (text.trim() === "") return {};
6
+ return JSON.parse(stripJsonc(text));
7
+ }
8
+ function stripJsonc(text) {
9
+ let out = "";
10
+ let i = 0;
11
+ while(i < text.length){
12
+ const char = text[i];
13
+ const next = text[i + 1];
14
+ if (char === '"') {
15
+ const end = stringEnd(text, i);
16
+ out += text.slice(i, end);
17
+ i = end;
18
+ } else if (char === "/" && next === "/") {
19
+ const end = text.indexOf("\n", i);
20
+ i = end === -1 ? text.length : end;
21
+ } else if (char === "/" && next === "*") {
22
+ const end = text.indexOf("*/", i + 2);
23
+ if (end === -1) throw new SyntaxError("Unterminated comment in JSON");
24
+ out += " ";
25
+ i = end + 2;
26
+ } else if (char === "}" || char === "]") {
27
+ // Drop a trailing comma before the closing bracket.
28
+ out = out.trimEnd();
29
+ if (out.endsWith(",")) out = out.slice(0, -1);
30
+ out += char;
31
+ i++;
32
+ } else {
33
+ out += char;
34
+ i++;
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ /** Index just past the string starting at `start` (a `"`); an unclosed string runs to the end. */ function stringEnd(text, start) {
40
+ let i = start + 1;
41
+ while(i < text.length){
42
+ if (text[i] === "\\") i += 2;
43
+ else if (text[i] === '"') return i + 1;
44
+ else i++;
45
+ }
46
+ return text.length;
47
+ }
@@ -13,7 +13,7 @@ import { cache, memoize } from "@decohere/core/caching";
13
13
  import { mixwith } from "@decohere/core/extensibility";
14
14
  import { findFiles, WatchEvents, watch as watchDirectory } from "@decohere/devkit/io";
15
15
  import picomatch from "picomatch";
16
- import { Subject } from "rxjs";
16
+ import { Subject } from "@decohere/core/system";
17
17
  import { WorkspaceConfigurationError } from "../errors.js";
18
18
  import { HasPackageJson } from "./has-package-json.mixin.js";
19
19
  import { HasTsConfig } from "./has-tsconfig.mixin.js";
@@ -1,25 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { io } from "@decohere/core/io";
4
- import ts from "typescript";
4
+ import { parseTsconfig } from "get-tsconfig";
5
5
  import { WorkspaceConfigurationError } from "../errors.js";
6
- const host = ts.sys;
7
- host.readFile = (file)=>{
8
- return fs.readFileSync(file, {
9
- encoding: "utf8"
10
- });
11
- };
12
- host.readDirectory = (directoryName)=>{
13
- return fs.readdirSync(directoryName, {
14
- withFileTypes: true
15
- }).filter((entry)=>entry.isDirectory()).map((entry)=>entry.name);
16
- };
6
+ import { parseJsonc } from "./jsonc.js";
17
7
  export const DEFAULT_COMPILER_OPTIONS = {
18
8
  emitDeclarationOnly: true,
19
9
  declaration: true,
20
10
  baseUrl: "./"
21
11
  };
22
- export class Tsconfig {
12
+ /**
13
+ * A `tsconfig.json`, read without the TypeScript compiler so running a file doesn't load it.
14
+ * Code that needs the compiler's own parse (type emission) parses `filePath` itself.
15
+ */ export class Tsconfig {
23
16
  static default() {
24
17
  return new Tsconfig();
25
18
  }
@@ -27,34 +20,34 @@ export class Tsconfig {
27
20
  if (!await io.fileExists(filePath)) {
28
21
  throw new WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
29
22
  }
30
- const configFileText = await io.readFile(filePath);
31
- const result = ts.parseConfigFileTextToJson(filePath, configFileText);
32
- if (result.error) {
33
- const errorMessage = result.error.messageText.toString();
34
- throw new WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
35
- }
36
- const dirName = path.resolve(path.dirname(filePath));
37
- const configParseResult = ts.parseJsonConfigFileContent(result.config, host, dirName);
38
- return new Tsconfig({
39
- compilerOptions: configParseResult.options,
40
- errors: configParseResult.errors
41
- });
23
+ return Tsconfig.read(filePath);
42
24
  }
43
25
  static parseSync(filePath) {
44
26
  if (!Tsconfig.fileExistsSync(filePath)) {
45
27
  throw new WorkspaceConfigurationError(`TSConfig not found ${filePath}`);
46
28
  }
47
- const configFileText = fs.readFileSync(filePath, "utf8");
48
- const result = ts.parseConfigFileTextToJson(filePath, configFileText);
49
- if (result.error) {
50
- const errorMessage = result.error.messageText.toString();
51
- throw new WorkspaceConfigurationError(`Error parsing ${filePath}. ${errorMessage}`);
29
+ return Tsconfig.read(filePath);
30
+ }
31
+ static read(filePath) {
32
+ const fullPath = path.resolve(filePath);
33
+ let compilerOptions;
34
+ try {
35
+ // get-tsconfig reads broken JSON as `{}`; check the file itself first.
36
+ parseJsonc(fs.readFileSync(fullPath, "utf8"));
37
+ compilerOptions = parseTsconfig(fullPath).compilerOptions ?? {};
38
+ } catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ throw new WorkspaceConfigurationError(`Error parsing ${filePath}. ${message}`, {
41
+ cause: error
42
+ });
52
43
  }
53
- const dirName = path.resolve(path.dirname(filePath));
54
- const configParseResult = ts.parseJsonConfigFileContent(result.config, host, dirName);
44
+ const { baseUrl } = compilerOptions;
55
45
  return new Tsconfig({
56
- compilerOptions: configParseResult.options,
57
- errors: configParseResult.errors
46
+ filePath: fullPath,
47
+ compilerOptions: baseUrl == null ? compilerOptions : {
48
+ ...compilerOptions,
49
+ baseUrl: path.resolve(path.dirname(fullPath), baseUrl)
50
+ }
58
51
  });
59
52
  }
60
53
  static fileExistsSync(filePath) {
@@ -69,6 +62,6 @@ export class Tsconfig {
69
62
  }){
70
63
  Object.assign(this, data);
71
64
  }
65
+ /** Absolute path of the file read; undefined for {@link Tsconfig.default}. */ filePath;
72
66
  compilerOptions;
73
- errors;
74
67
  }
@@ -10,7 +10,7 @@ function _ts_metadata(k, v) {
10
10
  import { injectable } from "@decohere/crucible/decorators";
11
11
  import { collections } from "@decohere/core/collections";
12
12
  import { mixwith } from "@decohere/core/extensibility";
13
- import { Subject } from "rxjs";
13
+ import { Subject } from "@decohere/core/system";
14
14
  import { WorkspaceConfigurationError } from "../errors.js";
15
15
  import { HasPackageJson } from "./has-package-json.mixin.js";
16
16
  import { HasTsConfig } from "./has-tsconfig.mixin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/workspace",
3
- "version": "0.0.1-dev.130",
3
+ "version": "0.0.1-dev.20261105.46",
4
4
  "description": "Workspace and project model primitives for monorepo tooling",
5
5
  "keywords": [
6
6
  "typescript",
@@ -33,12 +33,11 @@
33
33
  "test": "echo \"Error: no test specified\" && exit 1"
34
34
  },
35
35
  "dependencies": {
36
- "@decohere/core": "0.0.1-dev.130",
37
- "@decohere/crucible": "0.0.1-dev.130",
38
- "@decohere/devkit": "0.0.1-dev.130",
39
- "picomatch": "^4.0.4",
40
- "rxjs": "^7.8.2",
41
- "typescript": "^6.0.3"
36
+ "@decohere/core": "0.0.1-dev.20261105.46",
37
+ "@decohere/crucible": "0.0.1-dev.20261105.46",
38
+ "@decohere/devkit": "0.0.1-dev.20261105.46",
39
+ "get-tsconfig": "^4.14.3",
40
+ "picomatch": "^4.0.4"
42
41
  },
43
42
  "devDependencies": {}
44
43
  }
@@ -12,6 +12,7 @@ export type { ProjectConfig, ProjectSeed } from "./models/project.model.js";
12
12
  export { Project } from "./models/project.model.js";
13
13
  export { sourceExtensions } from "./models/source-extensions.js";
14
14
  export type { ProjectFileEvent } from "./models/project-file-event.js";
15
+ export type { CompilerOptions } from "./models/tsconfig.model.js";
15
16
  export { DEFAULT_COMPILER_OPTIONS, Tsconfig } from "./models/tsconfig.model.js";
16
17
  export { Workspace } from "./models/workspace.model.js";
17
18
  export type { Configuration, ProjectOptions, UiConfig, WorkspaceFactoryOptions, WorkspaceOptions, } from "./models/workspace-config.type.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Parses JSON with comments and trailing commas, as `tsconfig.json` allows. An empty file is
3
+ * `{}`, as tsc reads it. Throws a `SyntaxError` on anything else that isn't valid JSON.
4
+ */
5
+ export declare function parseJsonc(text: string): unknown;
@@ -33,7 +33,7 @@ export declare class Project extends Project_base {
33
33
  get dockerConfig(): import("./workspace-config.type.js").DockerConfig;
34
34
  get uiConfig(): import("./workspace-config.type.js").UiConfig;
35
35
  get configurations(): Configuration[];
36
- get projectFileChanged$(): import("rxjs").Observable<ProjectFileEvent>;
36
+ get projectFileChanged$(): import("@decohere/core/system").Observable<ProjectFileEvent>;
37
37
  getAllFiles(): Promise<FileEx[]>;
38
38
  isSourceFile(file: FileEx): boolean;
39
39
  watch(configuration: string): Promise<Watcher>;
@@ -1,11 +1,19 @@
1
- import ts, { type CompilerOptions } from "typescript";
1
+ import { type TsConfigJson } from "get-tsconfig";
2
+ /** `compilerOptions` as written in `tsconfig.json`, after `extends`; `baseUrl` is absolute. */
3
+ export type CompilerOptions = TsConfigJson.CompilerOptions;
2
4
  export declare const DEFAULT_COMPILER_OPTIONS: CompilerOptions;
5
+ /**
6
+ * A `tsconfig.json`, read without the TypeScript compiler so running a file doesn't load it.
7
+ * Code that needs the compiler's own parse (type emission) parses `filePath` itself.
8
+ */
3
9
  export declare class Tsconfig {
4
10
  static default(): Tsconfig;
5
11
  static parse(filePath: string): Promise<Tsconfig>;
6
12
  static parseSync(filePath: string): Tsconfig;
13
+ private static read;
7
14
  private static fileExistsSync;
8
15
  constructor(data?: Partial<Tsconfig>);
16
+ /** Absolute path of the file read; undefined for {@link Tsconfig.default}. */
17
+ readonly filePath?: string;
9
18
  readonly compilerOptions: CompilerOptions;
10
- readonly errors: readonly ts.Diagnostic[];
11
19
  }
@@ -37,7 +37,7 @@ export declare class Workspace extends Workspace_base {
37
37
  watch(configuration: string): Promise<void>;
38
38
  stopWatching(): void;
39
39
  [Symbol.dispose](): void;
40
- get projectFileChanged$(): import("rxjs").Observable<ProjectFileEvent>;
40
+ get projectFileChanged$(): import("@decohere/core/system").Observable<ProjectFileEvent>;
41
41
  findProject(filePath: string, buildConfig: string): Promise<Project | undefined>;
42
42
  private sourcePathsOf;
43
43
  private validateWorkspace;