@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/src/compile.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import type * as ts from "typescript";
3
+ import type { TypeTargetSpec, VirtualModuleResolver } from "@typed/virtual-modules";
4
+ import {
5
+ attachCompilerHostAdapter,
6
+ createTypeInfoApiSessionFactory,
7
+ ensureTypeTargetBootstrapFile,
8
+ } from "@typed/virtual-modules";
9
+
10
+ export interface CompileParams {
11
+ readonly ts: typeof import("typescript");
12
+ readonly commandLine: ts.ParsedCommandLine;
13
+ readonly resolver: VirtualModuleResolver;
14
+ readonly reportDiagnostic: ts.DiagnosticReporter;
15
+ /** Type target specs for structural assignability in TypeInfo API. From vmc.config when using loadResolver. */
16
+ readonly typeTargetSpecs?: readonly TypeTargetSpec[];
17
+ }
18
+
19
+ /**
20
+ * Perform a single compile pass using the adapted compiler host.
21
+ * Mirrors tsc behavior: create program, emit, report diagnostics, return exit code.
22
+ */
23
+ export function compile(params: CompileParams): number {
24
+ const { ts, commandLine, resolver, reportDiagnostic, typeTargetSpecs } = params;
25
+ const { options, fileNames, projectReferences } = commandLine;
26
+ const configFileParsingDiagnostics = (
27
+ commandLine as { configFileParsingDiagnostics?: readonly ts.Diagnostic[] }
28
+ ).configFileParsingDiagnostics;
29
+
30
+ const configParseDiags =
31
+ (
32
+ ts as {
33
+ getConfigFileParsingDiagnostics?: (p: ts.ParsedCommandLine) => readonly ts.Diagnostic[];
34
+ }
35
+ ).getConfigFileParsingDiagnostics?.(commandLine) ?? commandLine.errors;
36
+ const allConfigErrors = [...(configFileParsingDiagnostics ?? []), ...configParseDiags];
37
+ for (const d of allConfigErrors) {
38
+ reportDiagnostic(d);
39
+ }
40
+ if (allConfigErrors.length > 0) {
41
+ return 1;
42
+ }
43
+
44
+ if (fileNames.length === 0) {
45
+ reportDiagnostic(
46
+ createDiagnostic(
47
+ ts,
48
+ ts.DiagnosticCategory.Message,
49
+ 0,
50
+ 0,
51
+ "No inputs were found in config file.",
52
+ ),
53
+ );
54
+ return 0;
55
+ }
56
+
57
+ const sys = ts.sys;
58
+ if (!sys) {
59
+ reportDiagnostic(
60
+ createDiagnostic(ts, ts.DiagnosticCategory.Error, 0, 0, "ts.sys is not available."),
61
+ );
62
+ return 1;
63
+ }
64
+
65
+ const projectRoot = sys.getCurrentDirectory();
66
+
67
+ let effectiveRootNames = fileNames;
68
+ if (typeTargetSpecs && typeTargetSpecs.length > 0) {
69
+ const bootstrapPath = ensureTypeTargetBootstrapFile(projectRoot, typeTargetSpecs, {
70
+ mkdirSync,
71
+ writeFile: (path, content) => sys.writeFile(path, content),
72
+ });
73
+ effectiveRootNames = fileNames.includes(bootstrapPath)
74
+ ? fileNames
75
+ : [...fileNames, bootstrapPath];
76
+ }
77
+
78
+ const host = ts.createCompilerHost(options);
79
+
80
+ // Preliminary program for TypeInfo API (plugins that use api.file()/api.directory() need it).
81
+ const preliminaryProgram = ts.createProgram({
82
+ rootNames: effectiveRootNames,
83
+ options,
84
+ host,
85
+ projectReferences,
86
+ configFileParsingDiagnostics: allConfigErrors,
87
+ });
88
+ const createTypeInfoApiSession = createTypeInfoApiSessionFactory({
89
+ ts,
90
+ program: preliminaryProgram,
91
+ ...(typeTargetSpecs?.length ? { typeTargetSpecs } : {}),
92
+ });
93
+
94
+ const adapter = attachCompilerHostAdapter({
95
+ ts,
96
+ compilerHost: host,
97
+ resolver,
98
+ projectRoot,
99
+ createTypeInfoApiSession,
100
+ reportDiagnostic,
101
+ });
102
+
103
+ let exitCode = 0;
104
+ try {
105
+ const program = ts.createProgram({
106
+ rootNames: effectiveRootNames,
107
+ options,
108
+ host,
109
+ projectReferences,
110
+ configFileParsingDiagnostics: allConfigErrors,
111
+ });
112
+
113
+ const preEmit = ts.getPreEmitDiagnostics(program);
114
+ // oxlint-disable-next-line typescript/unbound-method
115
+ const emitResult = program.emit(undefined, sys.writeFile);
116
+ const allDiagnostics = [...preEmit, ...emitResult.diagnostics];
117
+
118
+ for (const d of allDiagnostics) {
119
+ reportDiagnostic(d);
120
+ }
121
+
122
+ if (
123
+ emitResult.emitSkipped ||
124
+ allDiagnostics.some((d) => d.category === ts.DiagnosticCategory.Error)
125
+ ) {
126
+ exitCode = 1;
127
+ }
128
+ } finally {
129
+ adapter.dispose();
130
+ }
131
+
132
+ return exitCode;
133
+ }
134
+
135
+ function createDiagnostic(
136
+ ts: typeof import("typescript"),
137
+ category: ts.DiagnosticCategory,
138
+ code: number,
139
+ length: number,
140
+ messageText: string,
141
+ ): ts.Diagnostic {
142
+ return {
143
+ category,
144
+ code,
145
+ file: undefined,
146
+ start: 0,
147
+ length,
148
+ messageText,
149
+ };
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ loadResolver,
3
+ type LoadResolverResult,
4
+ type VmcConfig,
5
+ } from "./resolverLoader.js";
6
+ export { resolveCommandLine } from "./commandLine.js";
7
+ export { compile } from "./compile.js";
8
+ export { runWatch } from "./watch.js";
9
+ export { runBuild } from "./build.js";
package/src/init.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const VMC_CONFIG_FILENAME = "vmc.config.ts";
5
+
6
+ const INITIAL_VMC_CONFIG = `export default {
7
+ plugins: [
8
+ {
9
+ name: "example",
10
+ shouldResolve: (id) => id.startsWith("virtual:"),
11
+ build: (id) => {
12
+ // Return generated TypeScript source for the virtual module
13
+ const name = id.replace("virtual:", "");
14
+ return \`export type \${name} = { value: string };\`;
15
+ },
16
+ },
17
+ ],
18
+ };
19
+ `;
20
+
21
+ export interface InitOptions {
22
+ readonly projectRoot: string;
23
+ readonly force?: boolean;
24
+ }
25
+
26
+ export interface InitResult {
27
+ readonly ok: boolean;
28
+ readonly path: string;
29
+ readonly message: string;
30
+ }
31
+
32
+ export function runInit(options: InitOptions): InitResult {
33
+ const { projectRoot, force = false } = options;
34
+ const configPath = join(projectRoot, VMC_CONFIG_FILENAME);
35
+
36
+ if (existsSync(configPath) && !force) {
37
+ return {
38
+ ok: false,
39
+ path: configPath,
40
+ message: `vmc.config.ts already exists at ${configPath}. Use --force to overwrite.`,
41
+ };
42
+ }
43
+
44
+ writeFileSync(configPath, INITIAL_VMC_CONFIG.trim() + "\n", "utf8");
45
+ return {
46
+ ok: true,
47
+ path: configPath,
48
+ message: `Created ${configPath}`,
49
+ };
50
+ }
@@ -0,0 +1,44 @@
1
+ import ts from "typescript";
2
+ import type {
3
+ TypeTargetSpec,
4
+ VirtualModuleResolver,
5
+ VmcPluginEntry,
6
+ } from "@typed/virtual-modules";
7
+ import { loadResolverFromVmcConfig, PluginManager } from "@typed/virtual-modules";
8
+
9
+ export interface VmcConfig {
10
+ readonly resolver?: VirtualModuleResolver;
11
+ readonly plugins?: readonly VmcPluginEntry[];
12
+ }
13
+
14
+ export interface LoadResolverResult {
15
+ readonly resolver: VirtualModuleResolver;
16
+ readonly typeTargetSpecs?: readonly TypeTargetSpec[];
17
+ }
18
+
19
+ /**
20
+ * Load the resolver from vmc.config.* in projectRoot, or return an empty PluginManager.
21
+ * Also returns typeTargetSpecs when configured for structural assignability in TypeInfo API.
22
+ */
23
+ export function loadResolver(projectRoot: string): LoadResolverResult {
24
+ const loaded = loadResolverFromVmcConfig({ projectRoot, ts });
25
+ if (loaded.status === "not-found") {
26
+ return { resolver: new PluginManager() };
27
+ }
28
+ if (loaded.status === "error") {
29
+ console.error(`[vmc] ${loaded.message}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ if (loaded.pluginLoadErrors.length > 0) {
34
+ for (const error of loaded.pluginLoadErrors) {
35
+ console.error(`[vmc] Failed to load plugin "${error.specifier}": ${error.message}`);
36
+ }
37
+ process.exit(1);
38
+ }
39
+
40
+ return {
41
+ resolver: loaded.resolver ?? new PluginManager(),
42
+ ...(loaded.typeTargetSpecs ? { typeTargetSpecs: loaded.typeTargetSpecs } : {}),
43
+ };
44
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import type * as ts from "typescript";
3
+ import type { TypeTargetSpec, VirtualModuleResolver } from "@typed/virtual-modules";
4
+ import {
5
+ attachCompilerHostAdapter,
6
+ createTypeInfoApiSessionFactory,
7
+ ensureTypeTargetBootstrapFile,
8
+ } from "@typed/virtual-modules";
9
+
10
+ export interface WatchParams {
11
+ readonly ts: typeof import("typescript");
12
+ readonly commandLine: ts.ParsedCommandLine;
13
+ readonly resolver: VirtualModuleResolver;
14
+ readonly reportDiagnostic: ts.DiagnosticReporter;
15
+ readonly reportWatchStatus?: ts.WatchStatusReporter;
16
+ readonly typeTargetSpecs?: readonly TypeTargetSpec[];
17
+ }
18
+
19
+ /**
20
+ * Run the compiler in watch mode. Mirrors tsc --watch.
21
+ */
22
+ export function runWatch(params: WatchParams): void {
23
+ const { ts, commandLine, resolver, reportDiagnostic, reportWatchStatus, typeTargetSpecs } =
24
+ params;
25
+ const { options, fileNames, projectReferences, watchOptions } = commandLine;
26
+
27
+ const sys = ts.sys;
28
+ if (!sys) {
29
+ reportDiagnostic(
30
+ createDiagnostic(ts, ts.DiagnosticCategory.Error, 0, 0, "ts.sys is not available."),
31
+ );
32
+ process.exit(1);
33
+ }
34
+
35
+ const projectRoot = sys.getCurrentDirectory();
36
+
37
+ let effectiveFileNames = fileNames;
38
+ if (typeTargetSpecs && typeTargetSpecs.length > 0) {
39
+ const bootstrapPath = ensureTypeTargetBootstrapFile(projectRoot, typeTargetSpecs, {
40
+ mkdirSync,
41
+ writeFile: (path, content) => sys.writeFile(path, content),
42
+ });
43
+ effectiveFileNames = fileNames.includes(bootstrapPath)
44
+ ? fileNames
45
+ : [...fileNames, bootstrapPath];
46
+ }
47
+
48
+ const preliminaryHost = ts.createCompilerHost(options);
49
+ const preliminaryProgram = ts.createProgram({
50
+ rootNames: effectiveFileNames,
51
+ options,
52
+ host: preliminaryHost,
53
+ projectReferences,
54
+ });
55
+ const createTypeInfoApiSession = createTypeInfoApiSessionFactory({
56
+ ts,
57
+ program: preliminaryProgram,
58
+ ...(typeTargetSpecs?.length ? { typeTargetSpecs } : {}),
59
+ });
60
+
61
+ const createProgram: ts.CreateProgram<ts.EmitAndSemanticDiagnosticsBuilderProgram> = (
62
+ rootNames,
63
+ opts,
64
+ host,
65
+ oldProgram,
66
+ configFileParsingDiagnostics,
67
+ refs,
68
+ ) => {
69
+ if (!host) {
70
+ host = ts.createCompilerHost(opts ?? options);
71
+ }
72
+ const adapter = attachCompilerHostAdapter({
73
+ ts,
74
+ compilerHost: host,
75
+ resolver,
76
+ projectRoot,
77
+ createTypeInfoApiSession,
78
+ reportDiagnostic,
79
+ });
80
+ try {
81
+ return ts.createEmitAndSemanticDiagnosticsBuilderProgram(
82
+ rootNames ?? fileNames,
83
+ opts ?? options,
84
+ host,
85
+ oldProgram,
86
+ configFileParsingDiagnostics,
87
+ refs ?? projectReferences,
88
+ );
89
+ } finally {
90
+ adapter.dispose();
91
+ }
92
+ };
93
+
94
+ const host = ts.createWatchCompilerHost(
95
+ effectiveFileNames,
96
+ options,
97
+ sys,
98
+ createProgram,
99
+ reportDiagnostic,
100
+ reportWatchStatus,
101
+ projectReferences,
102
+ watchOptions,
103
+ );
104
+
105
+ ts.createWatchProgram(host);
106
+ }
107
+
108
+ function createDiagnostic(
109
+ ts: typeof import("typescript"),
110
+ category: ts.DiagnosticCategory,
111
+ code: number,
112
+ length: number,
113
+ messageText: string,
114
+ ): ts.Diagnostic {
115
+ return { category, code, file: undefined, start: 0, length, messageText };
116
+ }