@typed/virtual-modules-compiler 1.0.0-beta.1
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 +130 -0
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +69 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +103 -0
- package/dist/commandLine.d.ts +8 -0
- package/dist/commandLine.d.ts.map +1 -0
- package/dist/commandLine.js +43 -0
- package/dist/compile.d.ts +16 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +96 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +34 -0
- package/dist/resolverLoader.d.ts +15 -0
- package/dist/resolverLoader.d.ts.map +1 -0
- package/dist/resolverLoader.js +26 -0
- package/dist/watch.d.ts +15 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +61 -0
- package/package.json +34 -0
- package/src/build.ts +127 -0
- package/src/cli.integration.test.ts +207 -0
- package/src/cli.ts +117 -0
- package/src/commandLine.ts +54 -0
- package/src/compile.ts +150 -0
- package/src/index.ts +9 -0
- package/src/init.ts +50 -0
- package/src/resolverLoader.ts +44 -0
- package/src/watch.ts +116 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @typed/virtual-modules-compiler
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for `tsc` that uses the custom compiler host from `@typed/virtual-modules`, enabling virtual module resolution during type-checking and compilation.
|
|
4
|
+
|
|
5
|
+
## Why vmc?
|
|
6
|
+
|
|
7
|
+
Virtual modules (e.g. `router:./routes`, `api:./endpoints`) exist at runtime via Vite or the TS plugin, but `tsc` cannot resolve them. vmc uses the same resolver so CI, `pnpm run typecheck`, and non-Vite workflows get correct type-checking and emit. A single config (`vmc.config.ts`) is shared with the TS plugin and `typedVitePlugin`.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @typed/virtual-modules-compiler typescript
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
The `vmc` CLI mirrors `tsc` 100%:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Initialize vmc.config.ts in project root
|
|
21
|
+
vmc init
|
|
22
|
+
|
|
23
|
+
# Single-shot compile (same as tsc)
|
|
24
|
+
vmc
|
|
25
|
+
|
|
26
|
+
# With options
|
|
27
|
+
vmc --noEmit -p tsconfig.json
|
|
28
|
+
|
|
29
|
+
# Watch mode
|
|
30
|
+
vmc --watch
|
|
31
|
+
|
|
32
|
+
# Build mode (project references)
|
|
33
|
+
vmc --build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Capabilities
|
|
37
|
+
|
|
38
|
+
- Single-shot compile, `--noEmit`, `--watch`, `--build` (project references)
|
|
39
|
+
- `vmc init` — scaffolds `vmc.config.ts` with a starter plugin
|
|
40
|
+
- Inline plugins (name, shouldResolve, build)
|
|
41
|
+
- Plugin modules (string paths to sync ESM)
|
|
42
|
+
- Custom resolver
|
|
43
|
+
- TypeInfo API in `build(id, importer, api)` for file/directory snapshots and assignability checks
|
|
44
|
+
- Type target specs for structural assignability (auto-injected bootstrap file)
|
|
45
|
+
|
|
46
|
+
### `vmc init`
|
|
47
|
+
|
|
48
|
+
Creates an initial `vmc.config.ts` in the current directory with a starter plugin. Use `--force` to overwrite an existing config:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
vmc init
|
|
52
|
+
vmc init --force
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Place a `vmc.config.ts` in your project root (preferred) to configure virtual module plugins:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// vmc.config.ts
|
|
61
|
+
export default {
|
|
62
|
+
plugins: [
|
|
63
|
+
{
|
|
64
|
+
name: "virtual-types",
|
|
65
|
+
shouldResolve: (id) => id.startsWith("virtual:"),
|
|
66
|
+
build: (id, _importer, api) => {
|
|
67
|
+
// Return generated TypeScript source
|
|
68
|
+
return `export type ${id.replace("virtual:", "")} = { value: string };`;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You can also provide plugin module specifiers (including sync ESM modules):
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
export default {
|
|
79
|
+
plugins: ["./plugins/virtual-types.mjs"],
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`vmc` plugin modules must be synchronous (no top-level await).
|
|
84
|
+
|
|
85
|
+
Or provide a custom resolver:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
export default {
|
|
89
|
+
resolver: myCustomResolver,
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Plugin `build` callback and TypeInfoApi
|
|
94
|
+
|
|
95
|
+
The `build(id, importer, api)` callback receives a `TypeInfoApi` instance:
|
|
96
|
+
|
|
97
|
+
- `api.file(relativePath, options)` — file snapshot with exports and imports
|
|
98
|
+
- `api.directory(relativeGlobs, options)` — directory snapshots
|
|
99
|
+
- `api.resolveExport(baseDir, filePath, exportName)` — resolve an export by name
|
|
100
|
+
- `api.isAssignableTo(node, targetId, projection?)` — structural assignability check
|
|
101
|
+
|
|
102
|
+
Plugins that need assignability checks (e.g. Route, Effect) should declare `typeTargetSpecs`.
|
|
103
|
+
|
|
104
|
+
## Type target specs
|
|
105
|
+
|
|
106
|
+
Plugins can export `typeTargetSpecs` for structural assignability. vmc injects a bootstrap file into the program’s `rootNames` when present so type targets resolve from the same TypeScript program.
|
|
107
|
+
|
|
108
|
+
For typed-smol apps using router and HttpApi virtual modules, use `createTypeInfoApiSessionForApp` from `@typed/app` to supply the session with router and HttpApi type targets.
|
|
109
|
+
|
|
110
|
+
## How it works
|
|
111
|
+
|
|
112
|
+
`vmc` parses the same command-line arguments as `tsc` via `ts.parseCommandLine`, then:
|
|
113
|
+
|
|
114
|
+
1. Loads the resolver and plugins from `vmc.config.ts` (or returns an empty PluginManager)
|
|
115
|
+
2. When `typeTargetSpecs` exist, writes a bootstrap file and adds it to `rootNames`
|
|
116
|
+
3. Creates a standard `CompilerHost` with `ts.createCompilerHost`
|
|
117
|
+
4. Wraps it with `attachCompilerHostAdapter` from `@typed/virtual-modules`
|
|
118
|
+
5. Runs the compilation (single-shot, watch, or build) using the adapted host
|
|
119
|
+
|
|
120
|
+
Virtual modules resolved by your plugins are injected into the compilation graph and type-checked alongside real files.
|
|
121
|
+
|
|
122
|
+
## Errors
|
|
123
|
+
|
|
124
|
+
vmc does **not** throw itself. Errors are surfaced via exit code (1) and TypeScript diagnostics. If the underlying adapter or resolver throws (e.g. invalid `projectRoot`), that may propagate uncaught. See [virtual-modules-errors-and-gotchas](../virtual-modules/.docs/virtual-modules-errors-and-gotchas.md) for full reference.
|
|
125
|
+
|
|
126
|
+
## Ecosystem
|
|
127
|
+
|
|
128
|
+
`vmc.config.ts` is shared by vmc, `@typed/virtual-modules-ts-plugin`, and `typedVitePlugin` (via `@typed/virtual-modules-vite`). The VS Code integration (`@typed/virtual-modules-vscode`) can use the same config via the `vmcConfigPath` option.
|
|
129
|
+
|
|
130
|
+
For agent-facing guidance (exports, constraints, pointers), see [AGENTS.md](./AGENTS.md).
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import type { TypeTargetSpec, VirtualModuleResolver } from "@typed/virtual-modules";
|
|
3
|
+
export interface BuildParams {
|
|
4
|
+
readonly ts: typeof import("typescript");
|
|
5
|
+
readonly buildCommand: ts.ParsedBuildCommand;
|
|
6
|
+
readonly resolver: VirtualModuleResolver;
|
|
7
|
+
readonly reportDiagnostic: ts.DiagnosticReporter;
|
|
8
|
+
readonly reportSolutionBuilderStatus?: ts.DiagnosticReporter;
|
|
9
|
+
readonly typeTargetSpecs?: readonly TypeTargetSpec[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run the compiler in build mode (tsc -b). Mirrors tsc --build.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runBuild(params: BuildParams): number;
|
|
15
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAkBpF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,cAAc,YAAY,CAAC,CAAC;IACzC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,kBAAkB,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,kBAAkB,CAAC;IACjD,QAAQ,CAAC,2BAA2B,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC;IAC7D,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACtD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAmFpD"}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { attachCompilerHostAdapter, createTypeInfoApiSessionFactory, ensureTypeTargetBootstrapFile, } from "@typed/virtual-modules";
|
|
4
|
+
function inferProjectRoot(sys, rootNames, fallback) {
|
|
5
|
+
if (rootNames && rootNames.length > 0) {
|
|
6
|
+
return dirname(rootNames[0]);
|
|
7
|
+
}
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run the compiler in build mode (tsc -b). Mirrors tsc --build.
|
|
12
|
+
*/
|
|
13
|
+
export function runBuild(params) {
|
|
14
|
+
const { ts, buildCommand, resolver, reportDiagnostic, reportSolutionBuilderStatus, typeTargetSpecs } = params;
|
|
15
|
+
const { projects, buildOptions } = buildCommand;
|
|
16
|
+
const sys = ts.sys;
|
|
17
|
+
if (!sys) {
|
|
18
|
+
reportDiagnostic(createDiagnostic(ts, ts.DiagnosticCategory.Error, 0, 0, "ts.sys is not available."));
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const projectRoot = sys.getCurrentDirectory();
|
|
22
|
+
const createProgramForSession = (rootNames, opts) => {
|
|
23
|
+
const h = ts.createCompilerHost(opts ?? {});
|
|
24
|
+
return ts.createProgram(rootNames, opts ?? {}, h);
|
|
25
|
+
};
|
|
26
|
+
const createProgram = (rootNames, opts, host, oldProgram, configFileParsingDiagnostics, refs) => {
|
|
27
|
+
if (!host) {
|
|
28
|
+
host = ts.createCompilerHost(opts ?? {});
|
|
29
|
+
}
|
|
30
|
+
const root = inferProjectRoot(sys, rootNames, projectRoot);
|
|
31
|
+
let effectiveRootNames = rootNames ?? [];
|
|
32
|
+
if (typeTargetSpecs && typeTargetSpecs.length > 0) {
|
|
33
|
+
const bootstrapPath = ensureTypeTargetBootstrapFile(root, typeTargetSpecs, {
|
|
34
|
+
mkdirSync,
|
|
35
|
+
writeFile: (path, content) => sys.writeFile(path, content),
|
|
36
|
+
});
|
|
37
|
+
effectiveRootNames = effectiveRootNames.includes(bootstrapPath)
|
|
38
|
+
? [...effectiveRootNames]
|
|
39
|
+
: [...effectiveRootNames, bootstrapPath];
|
|
40
|
+
}
|
|
41
|
+
const preliminaryProgram = createProgramForSession(effectiveRootNames, opts ?? {});
|
|
42
|
+
const createTypeInfoApiSession = createTypeInfoApiSessionFactory({
|
|
43
|
+
ts,
|
|
44
|
+
program: preliminaryProgram,
|
|
45
|
+
...(typeTargetSpecs?.length ? { typeTargetSpecs } : {}),
|
|
46
|
+
});
|
|
47
|
+
const adapter = attachCompilerHostAdapter({
|
|
48
|
+
ts,
|
|
49
|
+
compilerHost: host,
|
|
50
|
+
resolver,
|
|
51
|
+
projectRoot: root,
|
|
52
|
+
createTypeInfoApiSession,
|
|
53
|
+
reportDiagnostic,
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
return ts.createEmitAndSemanticDiagnosticsBuilderProgram(effectiveRootNames, opts ?? {}, host, oldProgram, configFileParsingDiagnostics, refs);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
adapter.dispose();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const host = ts.createSolutionBuilderHost(sys, createProgram, reportDiagnostic, reportSolutionBuilderStatus);
|
|
63
|
+
const builder = ts.createSolutionBuilder(host, projects, buildOptions);
|
|
64
|
+
const exitCode = builder.build();
|
|
65
|
+
return exitCode === ts.ExitStatus.Success ? 0 : 1;
|
|
66
|
+
}
|
|
67
|
+
function createDiagnostic(ts, category, code, length, messageText) {
|
|
68
|
+
return { category, code, file: undefined, start: 0, length, messageText };
|
|
69
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { resolveCommandLine } from "./commandLine.js";
|
|
4
|
+
import { loadResolver } from "./resolverLoader.js";
|
|
5
|
+
import { compile } from "./compile.js";
|
|
6
|
+
import { runWatch } from "./watch.js";
|
|
7
|
+
import { runBuild } from "./build.js";
|
|
8
|
+
import { runInit } from "./init.js";
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const sys = ts.sys;
|
|
11
|
+
if (!sys) {
|
|
12
|
+
console.error("vmc: ts.sys is not available.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const reportDiagnostic = (diagnostic) => {
|
|
16
|
+
const message = ts.formatDiagnostic(diagnostic, {
|
|
17
|
+
getCanonicalFileName: (f) => f,
|
|
18
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
19
|
+
getCurrentDirectory: sys.getCurrentDirectory,
|
|
20
|
+
getNewLine: () => sys.newLine,
|
|
21
|
+
});
|
|
22
|
+
if (diagnostic.category === ts.DiagnosticCategory.Error) {
|
|
23
|
+
sys.write(message);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
sys.write(message);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function main() {
|
|
30
|
+
if (args[0] === "init") {
|
|
31
|
+
const force = args.includes("--force");
|
|
32
|
+
const result = runInit({
|
|
33
|
+
projectRoot: sys.getCurrentDirectory(),
|
|
34
|
+
force,
|
|
35
|
+
});
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
sys.write(result.message + sys.newLine);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
sys.write(result.message + sys.newLine);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const buildIndex = args.findIndex((a) => a === "--build" || a === "-b");
|
|
44
|
+
const watchIndex = args.findIndex((a) => a === "--watch" || a === "-w");
|
|
45
|
+
if (buildIndex >= 0) {
|
|
46
|
+
const buildArgs = args.filter((_, i) => i !== buildIndex);
|
|
47
|
+
const parsed = ts.parseBuildCommand(buildArgs);
|
|
48
|
+
for (const d of parsed.errors) {
|
|
49
|
+
reportDiagnostic(d);
|
|
50
|
+
}
|
|
51
|
+
if (parsed.errors.length > 0) {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const projectRoot = sys.getCurrentDirectory();
|
|
55
|
+
const { resolver, typeTargetSpecs } = loadResolver(projectRoot);
|
|
56
|
+
return runBuild({
|
|
57
|
+
ts,
|
|
58
|
+
buildCommand: parsed,
|
|
59
|
+
resolver,
|
|
60
|
+
typeTargetSpecs,
|
|
61
|
+
reportDiagnostic,
|
|
62
|
+
reportSolutionBuilderStatus: reportDiagnostic,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const watchArgs = watchIndex >= 0 ? args.filter((_, i) => i !== watchIndex) : args;
|
|
66
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
67
|
+
let commandLine = ts.parseCommandLine(watchArgs, sys.readFile);
|
|
68
|
+
commandLine = resolveCommandLine(ts, commandLine, sys);
|
|
69
|
+
for (const d of commandLine.errors) {
|
|
70
|
+
reportDiagnostic(d);
|
|
71
|
+
}
|
|
72
|
+
if (commandLine.errors.length > 0) {
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const projectRoot = sys.getCurrentDirectory();
|
|
76
|
+
const { resolver, typeTargetSpecs } = loadResolver(projectRoot);
|
|
77
|
+
if (watchIndex >= 0) {
|
|
78
|
+
runWatch({
|
|
79
|
+
ts,
|
|
80
|
+
commandLine,
|
|
81
|
+
resolver,
|
|
82
|
+
typeTargetSpecs,
|
|
83
|
+
reportDiagnostic,
|
|
84
|
+
reportWatchStatus: (diag, newLine, _opts, _errorCount) => {
|
|
85
|
+
sys.write(ts.formatDiagnostic(diag, {
|
|
86
|
+
getCanonicalFileName: (f) => f,
|
|
87
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
88
|
+
getCurrentDirectory: sys.getCurrentDirectory,
|
|
89
|
+
getNewLine: () => newLine,
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
return compile({
|
|
96
|
+
ts,
|
|
97
|
+
commandLine,
|
|
98
|
+
resolver,
|
|
99
|
+
reportDiagnostic,
|
|
100
|
+
typeTargetSpecs,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
process.exit(main());
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the parsed command line. When fileNames is empty (e.g. "tsc" with no args),
|
|
4
|
+
* find tsconfig.json and use getParsedCommandLineOfConfigFile to populate fileNames.
|
|
5
|
+
* This mirrors tsc behavior.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveCommandLine(ts: typeof import("typescript"), commandLine: ts.ParsedCommandLine, sys: ts.System): ts.ParsedCommandLine;
|
|
8
|
+
//# sourceMappingURL=commandLine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commandLine.d.ts","sourceRoot":"","sources":["../src/commandLine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,cAAc,YAAY,CAAC,EAC/B,WAAW,EAAE,EAAE,CAAC,iBAAiB,EACjC,GAAG,EAAE,EAAE,CAAC,MAAM,GACb,EAAE,CAAC,iBAAiB,CA0CtB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the parsed command line. When fileNames is empty (e.g. "tsc" with no args),
|
|
3
|
+
* find tsconfig.json and use getParsedCommandLineOfConfigFile to populate fileNames.
|
|
4
|
+
* This mirrors tsc behavior.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveCommandLine(ts, commandLine, sys) {
|
|
7
|
+
if (commandLine.fileNames.length > 0) {
|
|
8
|
+
return commandLine;
|
|
9
|
+
}
|
|
10
|
+
const project = commandLine.options.project;
|
|
11
|
+
const cwd = sys.getCurrentDirectory();
|
|
12
|
+
let configPath;
|
|
13
|
+
if (project) {
|
|
14
|
+
configPath = sys.fileExists(project)
|
|
15
|
+
? project
|
|
16
|
+
: ts.findConfigFile(project, (p) => sys.fileExists(p));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
configPath = ts.findConfigFile(cwd, (p) => sys.fileExists(p));
|
|
20
|
+
}
|
|
21
|
+
if (!configPath) {
|
|
22
|
+
return commandLine;
|
|
23
|
+
}
|
|
24
|
+
const configHost = {
|
|
25
|
+
getCurrentDirectory: () => sys.getCurrentDirectory(),
|
|
26
|
+
useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames,
|
|
27
|
+
readDirectory: sys.readDirectory,
|
|
28
|
+
fileExists: sys.fileExists,
|
|
29
|
+
readFile: (p) => sys.readFile(p),
|
|
30
|
+
onUnRecoverableConfigFileDiagnostic: (_d) => {
|
|
31
|
+
// Will be reported by caller
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const resolved = ts.getParsedCommandLineOfConfigFile(configPath, commandLine.options, configHost);
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
return commandLine;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...commandLine,
|
|
40
|
+
...resolved,
|
|
41
|
+
options: { ...commandLine.options, ...resolved.options },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import type { TypeTargetSpec, VirtualModuleResolver } from "@typed/virtual-modules";
|
|
3
|
+
export interface CompileParams {
|
|
4
|
+
readonly ts: typeof import("typescript");
|
|
5
|
+
readonly commandLine: ts.ParsedCommandLine;
|
|
6
|
+
readonly resolver: VirtualModuleResolver;
|
|
7
|
+
readonly reportDiagnostic: ts.DiagnosticReporter;
|
|
8
|
+
/** Type target specs for structural assignability in TypeInfo API. From vmc.config when using loadResolver. */
|
|
9
|
+
readonly typeTargetSpecs?: readonly TypeTargetSpec[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Perform a single compile pass using the adapted compiler host.
|
|
13
|
+
* Mirrors tsc behavior: create program, emit, report diagnostics, return exit code.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compile(params: CompileParams): number;
|
|
16
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../src/compile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAOpF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,cAAc,YAAY,CAAC,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,kBAAkB,CAAC;IACjD,+GAA+G;IAC/G,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACtD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA8GrD"}
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { attachCompilerHostAdapter, createTypeInfoApiSessionFactory, ensureTypeTargetBootstrapFile, } from "@typed/virtual-modules";
|
|
3
|
+
/**
|
|
4
|
+
* Perform a single compile pass using the adapted compiler host.
|
|
5
|
+
* Mirrors tsc behavior: create program, emit, report diagnostics, return exit code.
|
|
6
|
+
*/
|
|
7
|
+
export function compile(params) {
|
|
8
|
+
const { ts, commandLine, resolver, reportDiagnostic, typeTargetSpecs } = params;
|
|
9
|
+
const { options, fileNames, projectReferences } = commandLine;
|
|
10
|
+
const configFileParsingDiagnostics = commandLine.configFileParsingDiagnostics;
|
|
11
|
+
const configParseDiags = ts.getConfigFileParsingDiagnostics?.(commandLine) ?? commandLine.errors;
|
|
12
|
+
const allConfigErrors = [...(configFileParsingDiagnostics ?? []), ...configParseDiags];
|
|
13
|
+
for (const d of allConfigErrors) {
|
|
14
|
+
reportDiagnostic(d);
|
|
15
|
+
}
|
|
16
|
+
if (allConfigErrors.length > 0) {
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
if (fileNames.length === 0) {
|
|
20
|
+
reportDiagnostic(createDiagnostic(ts, ts.DiagnosticCategory.Message, 0, 0, "No inputs were found in config file."));
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
const sys = ts.sys;
|
|
24
|
+
if (!sys) {
|
|
25
|
+
reportDiagnostic(createDiagnostic(ts, ts.DiagnosticCategory.Error, 0, 0, "ts.sys is not available."));
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const projectRoot = sys.getCurrentDirectory();
|
|
29
|
+
let effectiveRootNames = fileNames;
|
|
30
|
+
if (typeTargetSpecs && typeTargetSpecs.length > 0) {
|
|
31
|
+
const bootstrapPath = ensureTypeTargetBootstrapFile(projectRoot, typeTargetSpecs, {
|
|
32
|
+
mkdirSync,
|
|
33
|
+
writeFile: (path, content) => sys.writeFile(path, content),
|
|
34
|
+
});
|
|
35
|
+
effectiveRootNames = fileNames.includes(bootstrapPath)
|
|
36
|
+
? fileNames
|
|
37
|
+
: [...fileNames, bootstrapPath];
|
|
38
|
+
}
|
|
39
|
+
const host = ts.createCompilerHost(options);
|
|
40
|
+
// Preliminary program for TypeInfo API (plugins that use api.file()/api.directory() need it).
|
|
41
|
+
const preliminaryProgram = ts.createProgram({
|
|
42
|
+
rootNames: effectiveRootNames,
|
|
43
|
+
options,
|
|
44
|
+
host,
|
|
45
|
+
projectReferences,
|
|
46
|
+
configFileParsingDiagnostics: allConfigErrors,
|
|
47
|
+
});
|
|
48
|
+
const createTypeInfoApiSession = createTypeInfoApiSessionFactory({
|
|
49
|
+
ts,
|
|
50
|
+
program: preliminaryProgram,
|
|
51
|
+
...(typeTargetSpecs?.length ? { typeTargetSpecs } : {}),
|
|
52
|
+
});
|
|
53
|
+
const adapter = attachCompilerHostAdapter({
|
|
54
|
+
ts,
|
|
55
|
+
compilerHost: host,
|
|
56
|
+
resolver,
|
|
57
|
+
projectRoot,
|
|
58
|
+
createTypeInfoApiSession,
|
|
59
|
+
reportDiagnostic,
|
|
60
|
+
});
|
|
61
|
+
let exitCode = 0;
|
|
62
|
+
try {
|
|
63
|
+
const program = ts.createProgram({
|
|
64
|
+
rootNames: effectiveRootNames,
|
|
65
|
+
options,
|
|
66
|
+
host,
|
|
67
|
+
projectReferences,
|
|
68
|
+
configFileParsingDiagnostics: allConfigErrors,
|
|
69
|
+
});
|
|
70
|
+
const preEmit = ts.getPreEmitDiagnostics(program);
|
|
71
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
72
|
+
const emitResult = program.emit(undefined, sys.writeFile);
|
|
73
|
+
const allDiagnostics = [...preEmit, ...emitResult.diagnostics];
|
|
74
|
+
for (const d of allDiagnostics) {
|
|
75
|
+
reportDiagnostic(d);
|
|
76
|
+
}
|
|
77
|
+
if (emitResult.emitSkipped ||
|
|
78
|
+
allDiagnostics.some((d) => d.category === ts.DiagnosticCategory.Error)) {
|
|
79
|
+
exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
adapter.dispose();
|
|
84
|
+
}
|
|
85
|
+
return exitCode;
|
|
86
|
+
}
|
|
87
|
+
function createDiagnostic(ts, category, code, length, messageText) {
|
|
88
|
+
return {
|
|
89
|
+
category,
|
|
90
|
+
code,
|
|
91
|
+
file: undefined,
|
|
92
|
+
start: 0,
|
|
93
|
+
length,
|
|
94
|
+
messageText,
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { loadResolver, type LoadResolverResult, type VmcConfig, } from "./resolverLoader.js";
|
|
2
|
+
export { resolveCommandLine } from "./commandLine.js";
|
|
3
|
+
export { compile } from "./compile.js";
|
|
4
|
+
export { runWatch } from "./watch.js";
|
|
5
|
+
export { runBuild } from "./build.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
readonly projectRoot: string;
|
|
3
|
+
readonly force?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface InitResult {
|
|
6
|
+
readonly ok: boolean;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runInit(options: InitOptions): InitResult;
|
|
11
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAkBxD"}
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const VMC_CONFIG_FILENAME = "vmc.config.ts";
|
|
4
|
+
const INITIAL_VMC_CONFIG = `export default {
|
|
5
|
+
plugins: [
|
|
6
|
+
{
|
|
7
|
+
name: "example",
|
|
8
|
+
shouldResolve: (id) => id.startsWith("virtual:"),
|
|
9
|
+
build: (id) => {
|
|
10
|
+
// Return generated TypeScript source for the virtual module
|
|
11
|
+
const name = id.replace("virtual:", "");
|
|
12
|
+
return \`export type \${name} = { value: string };\`;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
`;
|
|
18
|
+
export function runInit(options) {
|
|
19
|
+
const { projectRoot, force = false } = options;
|
|
20
|
+
const configPath = join(projectRoot, VMC_CONFIG_FILENAME);
|
|
21
|
+
if (existsSync(configPath) && !force) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
path: configPath,
|
|
25
|
+
message: `vmc.config.ts already exists at ${configPath}. Use --force to overwrite.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(configPath, INITIAL_VMC_CONFIG.trim() + "\n", "utf8");
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
path: configPath,
|
|
32
|
+
message: `Created ${configPath}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TypeTargetSpec, VirtualModuleResolver, VmcPluginEntry } from "@typed/virtual-modules";
|
|
2
|
+
export interface VmcConfig {
|
|
3
|
+
readonly resolver?: VirtualModuleResolver;
|
|
4
|
+
readonly plugins?: readonly VmcPluginEntry[];
|
|
5
|
+
}
|
|
6
|
+
export interface LoadResolverResult {
|
|
7
|
+
readonly resolver: VirtualModuleResolver;
|
|
8
|
+
readonly typeTargetSpecs?: readonly TypeTargetSpec[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Load the resolver from vmc.config.* in projectRoot, or return an empty PluginManager.
|
|
12
|
+
* Also returns typeTargetSpecs when configured for structural assignability in TypeInfo API.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadResolver(projectRoot: string): LoadResolverResult;
|
|
15
|
+
//# sourceMappingURL=resolverLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolverLoader.d.ts","sourceRoot":"","sources":["../src/resolverLoader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACf,MAAM,wBAAwB,CAAC;AAGhC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACtD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAqBpE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { loadResolverFromVmcConfig, PluginManager } from "@typed/virtual-modules";
|
|
3
|
+
/**
|
|
4
|
+
* Load the resolver from vmc.config.* in projectRoot, or return an empty PluginManager.
|
|
5
|
+
* Also returns typeTargetSpecs when configured for structural assignability in TypeInfo API.
|
|
6
|
+
*/
|
|
7
|
+
export function loadResolver(projectRoot) {
|
|
8
|
+
const loaded = loadResolverFromVmcConfig({ projectRoot, ts });
|
|
9
|
+
if (loaded.status === "not-found") {
|
|
10
|
+
return { resolver: new PluginManager() };
|
|
11
|
+
}
|
|
12
|
+
if (loaded.status === "error") {
|
|
13
|
+
console.error(`[vmc] ${loaded.message}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (loaded.pluginLoadErrors.length > 0) {
|
|
17
|
+
for (const error of loaded.pluginLoadErrors) {
|
|
18
|
+
console.error(`[vmc] Failed to load plugin "${error.specifier}": ${error.message}`);
|
|
19
|
+
}
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
resolver: loaded.resolver ?? new PluginManager(),
|
|
24
|
+
...(loaded.typeTargetSpecs ? { typeTargetSpecs: loaded.typeTargetSpecs } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
import type { TypeTargetSpec, VirtualModuleResolver } from "@typed/virtual-modules";
|
|
3
|
+
export interface WatchParams {
|
|
4
|
+
readonly ts: typeof import("typescript");
|
|
5
|
+
readonly commandLine: ts.ParsedCommandLine;
|
|
6
|
+
readonly resolver: VirtualModuleResolver;
|
|
7
|
+
readonly reportDiagnostic: ts.DiagnosticReporter;
|
|
8
|
+
readonly reportWatchStatus?: ts.WatchStatusReporter;
|
|
9
|
+
readonly typeTargetSpecs?: readonly TypeTargetSpec[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run the compiler in watch mode. Mirrors tsc --watch.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runWatch(params: WatchParams): void;
|
|
15
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAOpF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,cAAc,YAAY,CAAC,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAC,kBAAkB,CAAC;IACjD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC;IACpD,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CACtD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAoFlD"}
|