@bleedingdev/modern-js-renderer-octane 0.0.0-bootstrap → 3.9.3-ultramodern.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/LICENSE +21 -0
- package/bin/octane-tsc.mjs +4 -0
- package/dist/cjs/bootstrap.js +68 -0
- package/dist/cjs/client.js +371 -0
- package/dist/cjs/entry-application.js +83 -0
- package/dist/cjs/entry-client.js +92 -0
- package/dist/cjs/entry-server.js +127 -0
- package/dist/cjs/federation-context.js +72 -0
- package/dist/cjs/federation.js +103 -0
- package/dist/cjs/i18n/I18nProvider.js +67 -0
- package/dist/cjs/i18n/LocalizedLink.js +89 -0
- package/dist/cjs/i18n/context.js +41 -0
- package/dist/cjs/i18n/index.js +86 -0
- package/dist/cjs/i18n/languageSync.js +99 -0
- package/dist/cjs/i18n/types.js +18 -0
- package/dist/cjs/i18n/useI18n.js +110 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/manifest.js +167 -0
- package/dist/cjs/plugin/compiler.js +125 -0
- package/dist/cjs/plugin/create.js +265 -0
- package/dist/cjs/plugin/index.js +151 -0
- package/dist/cjs/plugin/profile.js +89 -0
- package/dist/cjs/plugin/source.js +56 -0
- package/dist/cjs/router-client.js +139 -0
- package/dist/cjs/router-handler.js +168 -0
- package/dist/cjs/router-injection.js +139 -0
- package/dist/cjs/router-server-cleanup.js +46 -0
- package/dist/cjs/router-server-snapshot.js +405 -0
- package/dist/cjs/router-server.js +108 -0
- package/dist/cjs/router-snapshot.js +270 -0
- package/dist/cjs/router.js +218 -0
- package/dist/cjs/routes.js +187 -0
- package/dist/cjs/server.js +224 -0
- package/dist/cjs/typecheck.js +615 -0
- package/dist/esm/bootstrap.mjs +24 -0
- package/dist/esm/client.mjs +327 -0
- package/dist/esm/entry-application.mjs +39 -0
- package/dist/esm/entry-client.mjs +54 -0
- package/dist/esm/entry-server.mjs +86 -0
- package/dist/esm/federation-context.mjs +25 -0
- package/dist/esm/federation.mjs +65 -0
- package/dist/esm/i18n/I18nProvider.mjs +29 -0
- package/dist/esm/i18n/LocalizedLink.mjs +48 -0
- package/dist/esm/i18n/context.mjs +3 -0
- package/dist/esm/i18n/index.mjs +8 -0
- package/dist/esm/i18n/languageSync.mjs +61 -0
- package/dist/esm/i18n/types.mjs +0 -0
- package/dist/esm/i18n/useI18n.mjs +72 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/manifest.mjs +117 -0
- package/dist/esm/router-client.mjs +98 -0
- package/dist/esm/router-handler.mjs +130 -0
- package/dist/esm/router-injection.mjs +101 -0
- package/dist/esm/router-server-cleanup.mjs +8 -0
- package/dist/esm/router-server-snapshot.mjs +367 -0
- package/dist/esm/router-server.mjs +12 -0
- package/dist/esm/router-snapshot.mjs +199 -0
- package/dist/esm/router.mjs +94 -0
- package/dist/esm/routes.mjs +134 -0
- package/dist/esm/server.mjs +183 -0
- package/dist/esm-node/bootstrap.mjs +24 -0
- package/dist/esm-node/manifest.mjs +117 -0
- package/dist/esm-node/plugin/compiler.mjs +70 -0
- package/dist/esm-node/plugin/create.mjs +227 -0
- package/dist/esm-node/plugin/index.mjs +103 -0
- package/dist/esm-node/plugin/profile.mjs +51 -0
- package/dist/esm-node/plugin/source.mjs +7 -0
- package/dist/esm-node/typecheck.mjs +560 -0
- package/dist/types/bootstrap.d.ts +10 -0
- package/dist/types/client.d.ts +36 -0
- package/dist/types/entry-application.d.ts +19 -0
- package/dist/types/entry-client.d.ts +8 -0
- package/dist/types/entry-server.d.ts +11 -0
- package/dist/types/federation-context.d.ts +28 -0
- package/dist/types/federation.d.ts +18 -0
- package/dist/types/i18n/I18nProvider.d.ts +8 -0
- package/dist/types/i18n/LocalizedLink.d.ts +41 -0
- package/dist/types/i18n/context.d.ts +9 -0
- package/dist/types/i18n/index.d.ts +9 -0
- package/dist/types/i18n/languageSync.d.ts +26 -0
- package/dist/types/i18n/types.d.ts +34 -0
- package/dist/types/i18n/useI18n.d.ts +21 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/manifest.d.ts +34 -0
- package/dist/types/plugin/compiler-manifest.d.cts +10 -0
- package/dist/types/plugin/compiler.d.ts +5 -0
- package/dist/types/plugin/create.d.ts +23 -0
- package/dist/types/plugin/index.d.ts +3 -0
- package/dist/types/plugin/profile.d.ts +2 -0
- package/dist/types/plugin/source.d.ts +1 -0
- package/dist/types/plugin/svg-component-template.d.cts +6 -0
- package/dist/types/router-client.d.ts +12 -0
- package/dist/types/router-handler.d.ts +13 -0
- package/dist/types/router-injection.d.ts +8 -0
- package/dist/types/router-server-cleanup.d.ts +3 -0
- package/dist/types/router-server-snapshot.d.ts +15 -0
- package/dist/types/router-server.d.ts +13 -0
- package/dist/types/router-snapshot.d.ts +14 -0
- package/dist/types/router.d.ts +30 -0
- package/dist/types/routes.d.ts +18 -0
- package/dist/types/server.d.ts +41 -0
- package/dist/types/typecheck.d.ts +31 -0
- package/package.json +211 -4
- package/src/bootstrap.ts +60 -0
- package/src/client.ts +485 -0
- package/src/entry-application.ts +72 -0
- package/src/entry-client.ts +80 -0
- package/src/entry-server.ts +142 -0
- package/src/federation-context.ts +53 -0
- package/src/federation.ts +158 -0
- package/src/i18n/I18nProvider.ts +56 -0
- package/src/i18n/LocalizedLink.ts +112 -0
- package/src/i18n/context.ts +12 -0
- package/src/i18n/index.ts +35 -0
- package/src/i18n/languageSync.ts +132 -0
- package/src/i18n/types.ts +36 -0
- package/src/i18n/useI18n.ts +119 -0
- package/src/index.ts +10 -0
- package/src/manifest.ts +253 -0
- package/src/plugin/compiler-manifest.cjs +178 -0
- package/src/plugin/compiler.ts +112 -0
- package/src/plugin/create.ts +272 -0
- package/src/plugin/index.ts +105 -0
- package/src/plugin/profile.ts +46 -0
- package/src/plugin/source.ts +16 -0
- package/src/plugin/svg-component-template.cjs +17 -0
- package/src/router-client.ts +144 -0
- package/src/router-handler.ts +182 -0
- package/src/router-injection.ts +145 -0
- package/src/router-server-cleanup.ts +11 -0
- package/src/router-server-snapshot.ts +564 -0
- package/src/router-server.ts +23 -0
- package/src/router-snapshot.ts +251 -0
- package/src/router.ts +192 -0
- package/src/routes.ts +263 -0
- package/src/server.ts +300 -0
- package/src/typecheck.ts +723 -0
package/src/typecheck.ts
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
type CodeInformation,
|
|
6
|
+
type Mapping,
|
|
7
|
+
SourceMap,
|
|
8
|
+
shouldReportDiagnostics,
|
|
9
|
+
} from '@volar/language-core';
|
|
10
|
+
import {
|
|
11
|
+
findNodeAtLocation,
|
|
12
|
+
getNodeValue,
|
|
13
|
+
type ParseError,
|
|
14
|
+
parse,
|
|
15
|
+
parseTree,
|
|
16
|
+
} from 'jsonc-parser';
|
|
17
|
+
import { compileToVolarMappings } from 'octane/compiler/volar';
|
|
18
|
+
import { version } from 'typescript';
|
|
19
|
+
import {
|
|
20
|
+
createVirtualFileSystem,
|
|
21
|
+
type FileSystem,
|
|
22
|
+
} from 'typescript/unstable/fs';
|
|
23
|
+
import { API, type Diagnostic } from 'typescript/unstable/sync';
|
|
24
|
+
|
|
25
|
+
export interface OctaneTypeDiagnostic {
|
|
26
|
+
fileName?: string;
|
|
27
|
+
pos: number;
|
|
28
|
+
end: number;
|
|
29
|
+
line?: number;
|
|
30
|
+
column?: number;
|
|
31
|
+
code: number | string;
|
|
32
|
+
category: 'error' | 'warning' | 'message';
|
|
33
|
+
text: string;
|
|
34
|
+
origin: 'typescript' | 'octane' | 'projection';
|
|
35
|
+
relatedInformation?: OctaneTypeDiagnostic[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OctaneTypecheckResult {
|
|
39
|
+
compilerVersion: string;
|
|
40
|
+
files: string[];
|
|
41
|
+
diagnostics: OctaneTypeDiagnostic[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface OctaneTypecheckOptions {
|
|
45
|
+
project: string;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
compilerOptions?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TextProjection {
|
|
51
|
+
fileName: string;
|
|
52
|
+
source: string;
|
|
53
|
+
code: string;
|
|
54
|
+
map: SourceMap<CodeInformation>;
|
|
55
|
+
}
|
|
56
|
+
type Projection = Pick<
|
|
57
|
+
ReturnType<typeof compileToVolarMappings>,
|
|
58
|
+
'errors' | 'diagnostics'
|
|
59
|
+
> &
|
|
60
|
+
TextProjection;
|
|
61
|
+
|
|
62
|
+
const nativeExtension = /\.tsrx$/u;
|
|
63
|
+
const projectedExtension = /\.tsrx\.tsx$/u;
|
|
64
|
+
|
|
65
|
+
function location(source: string, pos: number) {
|
|
66
|
+
const prefix = source.slice(0, pos);
|
|
67
|
+
const lines = prefix.split(/\r\n|\r|\n/u);
|
|
68
|
+
return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFile(file: string): string | undefined {
|
|
72
|
+
try {
|
|
73
|
+
return fs.readFileSync(file, 'utf8');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (
|
|
76
|
+
error instanceof Error &&
|
|
77
|
+
'code' in error &&
|
|
78
|
+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
79
|
+
)
|
|
80
|
+
return undefined;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Only filename fields change; native TypeScript owns config inheritance and globs. */
|
|
86
|
+
function projectConfig(
|
|
87
|
+
source: string,
|
|
88
|
+
fileName: string,
|
|
89
|
+
discovery: 'ordinary' | 'native' | 'complete',
|
|
90
|
+
roots?: readonly string[],
|
|
91
|
+
): TextProjection | undefined {
|
|
92
|
+
const errors: ParseError[] = [];
|
|
93
|
+
const config = parseTree(source.replace(/^\uFEFF/u, ' '), errors, {
|
|
94
|
+
allowTrailingComma: true,
|
|
95
|
+
});
|
|
96
|
+
if (errors.length || !config || config.type !== 'object') return undefined;
|
|
97
|
+
const edits: { offset: number; length: number; content: string }[] = [];
|
|
98
|
+
const additions: string[] = [];
|
|
99
|
+
for (const field of ['files', 'include', 'exclude']) {
|
|
100
|
+
const array = findNodeAtLocation(config, [field]);
|
|
101
|
+
if (roots) {
|
|
102
|
+
const content = JSON.stringify(field === 'files' ? roots : []);
|
|
103
|
+
if (!array) additions.push(`${JSON.stringify(field)}:${content}`);
|
|
104
|
+
else if (
|
|
105
|
+
array.type === 'array' &&
|
|
106
|
+
(array.children ?? []).every(node => node.type === 'string')
|
|
107
|
+
)
|
|
108
|
+
edits.push({ offset: array.offset, length: array.length, content });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (array?.type !== 'array') continue;
|
|
112
|
+
for (const node of array.children ?? []) {
|
|
113
|
+
const file = getNodeValue(node);
|
|
114
|
+
if (typeof file !== 'string') continue;
|
|
115
|
+
const projected =
|
|
116
|
+
field === 'files' || discovery === 'complete'
|
|
117
|
+
? nativeExtension.test(file)
|
|
118
|
+
? `${file}.tsx`
|
|
119
|
+
: file
|
|
120
|
+
: discovery === 'native' &&
|
|
121
|
+
/[.*?]/u.test(file.replaceAll('\\', '/').split('/').at(-1) ?? '')
|
|
122
|
+
? `${file.endsWith('/**') ? `${file}/*` : file}.tsx`
|
|
123
|
+
: file;
|
|
124
|
+
if (projected !== file)
|
|
125
|
+
edits.push({
|
|
126
|
+
offset: node.offset,
|
|
127
|
+
length: node.length,
|
|
128
|
+
content: JSON.stringify(projected),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (additions.length)
|
|
133
|
+
edits.push({
|
|
134
|
+
offset: config.offset + 1,
|
|
135
|
+
length: 0,
|
|
136
|
+
content: `${additions.join(',')}${config.children?.length ? ',' : ''}`,
|
|
137
|
+
});
|
|
138
|
+
if (!edits.length) return undefined;
|
|
139
|
+
const mappings: Mapping<CodeInformation>[] = [];
|
|
140
|
+
let sourceOffset = 0;
|
|
141
|
+
let code = '';
|
|
142
|
+
for (const edit of edits.sort((left, right) => left.offset - right.offset)) {
|
|
143
|
+
const unchanged = source.slice(sourceOffset, edit.offset);
|
|
144
|
+
mappings.push({
|
|
145
|
+
sourceOffsets: [sourceOffset],
|
|
146
|
+
generatedOffsets: [code.length],
|
|
147
|
+
lengths: [unchanged.length],
|
|
148
|
+
data: { verification: true },
|
|
149
|
+
});
|
|
150
|
+
code += unchanged;
|
|
151
|
+
mappings.push({
|
|
152
|
+
sourceOffsets: [edit.offset],
|
|
153
|
+
generatedOffsets: [code.length],
|
|
154
|
+
lengths: [edit.length],
|
|
155
|
+
generatedLengths: [edit.content.length],
|
|
156
|
+
data: { verification: true },
|
|
157
|
+
});
|
|
158
|
+
code += edit.content;
|
|
159
|
+
sourceOffset = edit.offset + edit.length;
|
|
160
|
+
}
|
|
161
|
+
mappings.push({
|
|
162
|
+
sourceOffsets: [sourceOffset],
|
|
163
|
+
generatedOffsets: [code.length],
|
|
164
|
+
lengths: [source.length - sourceOffset],
|
|
165
|
+
data: { verification: true },
|
|
166
|
+
});
|
|
167
|
+
code += source.slice(sourceOffset);
|
|
168
|
+
return { source, fileName, code, map: new SourceMap(mappings) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function diagnosticText(diagnostic: Diagnostic): string {
|
|
172
|
+
return [
|
|
173
|
+
diagnostic.text,
|
|
174
|
+
...(diagnostic.messageChain?.map(diagnosticText) ?? []),
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Checks authored TSRX through Octane's public language projection and native TS7. */
|
|
179
|
+
export function checkOctaneProject(
|
|
180
|
+
options: OctaneTypecheckOptions,
|
|
181
|
+
): OctaneTypecheckResult {
|
|
182
|
+
if (version !== '7.0.2')
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Octane typechecking requires TypeScript 7.0.2; found ${version}`,
|
|
185
|
+
);
|
|
186
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
187
|
+
let project = path.resolve(cwd, options.project);
|
|
188
|
+
if (fs.existsSync(project) && fs.statSync(project).isDirectory())
|
|
189
|
+
project = path.join(project, 'tsconfig.json');
|
|
190
|
+
if (!fs.existsSync(project))
|
|
191
|
+
throw new Error(`Cannot find project ${project}`);
|
|
192
|
+
if (
|
|
193
|
+
options.compilerOptions?.noEmit === false ||
|
|
194
|
+
options.compilerOptions?.noCheck === true ||
|
|
195
|
+
options.compilerOptions?.skipLibCheck === true ||
|
|
196
|
+
options.compilerOptions?.skipDefaultLibCheck === true
|
|
197
|
+
)
|
|
198
|
+
throw new Error(
|
|
199
|
+
'Octane typechecking requires noEmit, full checking, and library checking',
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const effectiveConfig = path.join(
|
|
203
|
+
path.dirname(project),
|
|
204
|
+
`.octane-typecheck-${randomUUID()}.json`,
|
|
205
|
+
);
|
|
206
|
+
const authoredConfig = parse(
|
|
207
|
+
(readFile(project) ?? '').replace(/^\uFEFF/u, ' '),
|
|
208
|
+
[],
|
|
209
|
+
{ allowTrailingComma: true },
|
|
210
|
+
);
|
|
211
|
+
const effectiveOptions = {
|
|
212
|
+
extends: project,
|
|
213
|
+
...(Array.isArray(authoredConfig?.references)
|
|
214
|
+
? { references: authoredConfig.references }
|
|
215
|
+
: {}),
|
|
216
|
+
compilerOptions: {
|
|
217
|
+
...options.compilerOptions,
|
|
218
|
+
noEmit: true,
|
|
219
|
+
noCheck: false,
|
|
220
|
+
skipLibCheck: false,
|
|
221
|
+
skipDefaultLibCheck: false,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const virtual = createVirtualFileSystem({
|
|
225
|
+
[effectiveConfig]: JSON.stringify(effectiveOptions),
|
|
226
|
+
});
|
|
227
|
+
const projections = new Map<string, Projection>();
|
|
228
|
+
const configurations = new Map<string, TextProjection>();
|
|
229
|
+
const configFiles = new Set([project]);
|
|
230
|
+
const referencedConfigs = new Set([project]);
|
|
231
|
+
const referenceRoots = new Map<string, Set<string>>();
|
|
232
|
+
let discovery: 'ordinary' | 'native' | 'complete' = 'ordinary';
|
|
233
|
+
function authoredName(file: string): string {
|
|
234
|
+
if (!projectedExtension.test(file)) return file;
|
|
235
|
+
const authored = file.slice(0, -4);
|
|
236
|
+
if (!fs.existsSync(authored)) return file;
|
|
237
|
+
if (fs.existsSync(file))
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Octane projection collision: ${authored} conflicts with physical ${file}`,
|
|
240
|
+
);
|
|
241
|
+
return authored;
|
|
242
|
+
}
|
|
243
|
+
function projection(file: string): Projection | undefined {
|
|
244
|
+
const fileName = authoredName(file);
|
|
245
|
+
if (fileName === file) return undefined;
|
|
246
|
+
const known = projections.get(file);
|
|
247
|
+
if (known) return known;
|
|
248
|
+
const source = readFile(fileName);
|
|
249
|
+
if (source === undefined) return undefined;
|
|
250
|
+
let compiled: Pick<
|
|
251
|
+
ReturnType<typeof compileToVolarMappings>,
|
|
252
|
+
'code' | 'mappings' | 'errors' | 'diagnostics'
|
|
253
|
+
>;
|
|
254
|
+
try {
|
|
255
|
+
compiled = compileToVolarMappings(source, fileName, { loose: true });
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
258
|
+
compiled = {
|
|
259
|
+
code: 'export {};',
|
|
260
|
+
mappings: [],
|
|
261
|
+
diagnostics: [],
|
|
262
|
+
errors: [
|
|
263
|
+
{
|
|
264
|
+
name: error.name,
|
|
265
|
+
message: error.message,
|
|
266
|
+
fileName,
|
|
267
|
+
type: 'fatal',
|
|
268
|
+
...('pos' in error && typeof error.pos === 'number'
|
|
269
|
+
? { pos: error.pos }
|
|
270
|
+
: {}),
|
|
271
|
+
...('end' in error && typeof error.end === 'number'
|
|
272
|
+
? { end: error.end }
|
|
273
|
+
: {}),
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const result = {
|
|
279
|
+
...compiled,
|
|
280
|
+
source,
|
|
281
|
+
fileName,
|
|
282
|
+
map: new SourceMap(compiled.mappings),
|
|
283
|
+
};
|
|
284
|
+
projections.set(file, result);
|
|
285
|
+
virtual.writeFile?.(file, compiled.code);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
const overlay: FileSystem = {
|
|
289
|
+
readFile(file) {
|
|
290
|
+
const content = virtual.readFile?.(file);
|
|
291
|
+
if (content !== undefined) return content;
|
|
292
|
+
const compiled = projection(file);
|
|
293
|
+
if (compiled) return compiled.code;
|
|
294
|
+
if (file.endsWith('.json')) {
|
|
295
|
+
const source = readFile(file);
|
|
296
|
+
if (source !== undefined) {
|
|
297
|
+
// Config parsing reads extends through the native resolver. Package
|
|
298
|
+
// manifests encountered there are metadata, not configuration files.
|
|
299
|
+
if (
|
|
300
|
+
!configFiles.has(file) &&
|
|
301
|
+
(discovery === 'complete' || path.basename(file) === 'package.json')
|
|
302
|
+
)
|
|
303
|
+
return source;
|
|
304
|
+
configFiles.add(file);
|
|
305
|
+
const configData = parse(source.replace(/^\uFEFF/u, ' '), [], {
|
|
306
|
+
allowTrailingComma: true,
|
|
307
|
+
});
|
|
308
|
+
// References are not inherited from extends. Follow only the
|
|
309
|
+
// requested config and actual reference targets, never their bases.
|
|
310
|
+
for (const reference of referencedConfigs.has(file) &&
|
|
311
|
+
Array.isArray(configData?.references)
|
|
312
|
+
? configData.references
|
|
313
|
+
: []) {
|
|
314
|
+
if (typeof reference?.path !== 'string') continue;
|
|
315
|
+
const target = path.resolve(path.dirname(file), reference.path);
|
|
316
|
+
const referenceFile = target.endsWith('.json')
|
|
317
|
+
? target
|
|
318
|
+
: path.join(target, 'tsconfig.json');
|
|
319
|
+
configFiles.add(referenceFile);
|
|
320
|
+
referencedConfigs.add(referenceFile);
|
|
321
|
+
}
|
|
322
|
+
const roots =
|
|
323
|
+
discovery === 'complete' ? referenceRoots.get(file) : undefined;
|
|
324
|
+
const config = projectConfig(
|
|
325
|
+
source,
|
|
326
|
+
file,
|
|
327
|
+
discovery,
|
|
328
|
+
roots ? [...roots] : undefined,
|
|
329
|
+
);
|
|
330
|
+
if (config) configurations.set(file, config);
|
|
331
|
+
else configurations.delete(file);
|
|
332
|
+
return config?.code ?? source;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
},
|
|
337
|
+
fileExists(file) {
|
|
338
|
+
if (virtual.fileExists?.(file)) return true;
|
|
339
|
+
if (authoredName(file) !== file) return true;
|
|
340
|
+
return undefined;
|
|
341
|
+
},
|
|
342
|
+
directoryExists(directory) {
|
|
343
|
+
return virtual.directoryExists?.(directory) || undefined;
|
|
344
|
+
},
|
|
345
|
+
getAccessibleEntries(directory) {
|
|
346
|
+
const files = new Set(virtual.getAccessibleEntries?.(directory)?.files);
|
|
347
|
+
const directories = new Set(
|
|
348
|
+
virtual.getAccessibleEntries?.(directory)?.directories,
|
|
349
|
+
);
|
|
350
|
+
try {
|
|
351
|
+
for (const entry of fs.readdirSync(directory, {
|
|
352
|
+
withFileTypes: true,
|
|
353
|
+
})) {
|
|
354
|
+
let isDirectory = entry.isDirectory();
|
|
355
|
+
if (entry.isSymbolicLink()) {
|
|
356
|
+
try {
|
|
357
|
+
isDirectory = fs
|
|
358
|
+
.statSync(path.join(directory, entry.name))
|
|
359
|
+
.isDirectory();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (
|
|
362
|
+
error instanceof Error &&
|
|
363
|
+
'code' in error &&
|
|
364
|
+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
365
|
+
)
|
|
366
|
+
continue;
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (isDirectory) directories.add(entry.name);
|
|
371
|
+
else {
|
|
372
|
+
files.add(entry.name);
|
|
373
|
+
if (nativeExtension.test(entry.name)) {
|
|
374
|
+
const projected = path.join(directory, `${entry.name}.tsx`);
|
|
375
|
+
authoredName(projected);
|
|
376
|
+
files.add(`${entry.name}.tsx`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (
|
|
382
|
+
!(
|
|
383
|
+
error instanceof Error &&
|
|
384
|
+
'code' in error &&
|
|
385
|
+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
files: [...files]
|
|
392
|
+
.filter(
|
|
393
|
+
file =>
|
|
394
|
+
discovery !== 'ordinary' ||
|
|
395
|
+
authoredName(path.join(directory, file)) ===
|
|
396
|
+
path.join(directory, file),
|
|
397
|
+
)
|
|
398
|
+
.sort(),
|
|
399
|
+
directories: [...directories].sort(),
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
realpath(file) {
|
|
403
|
+
const authored = authoredName(file);
|
|
404
|
+
if (authored !== file) {
|
|
405
|
+
try {
|
|
406
|
+
return `${fs.realpathSync(authored)}.tsx`;
|
|
407
|
+
} catch {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return virtual.fileExists?.(file) ? file : undefined;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
// The native matcher runs over original names and suffix-transposed TSRX
|
|
415
|
+
// names separately. A '*.tsx' include/exclude therefore keeps its authored
|
|
416
|
+
// meaning instead of accidentally matching every '.tsrx.tsx' projection.
|
|
417
|
+
const roots = new Set<string>();
|
|
418
|
+
for (const mode of ['ordinary', 'native'] as const) {
|
|
419
|
+
discovery = mode;
|
|
420
|
+
const parser = new API({ cwd, fs: overlay });
|
|
421
|
+
try {
|
|
422
|
+
for (const file of parser.parseConfigFile(effectiveConfig).fileNames)
|
|
423
|
+
if (mode === 'ordinary' || authoredName(file) !== file) roots.add(file);
|
|
424
|
+
// Native parsing discovers each referenced config's inherited files.
|
|
425
|
+
// Their roots belong to the referenced project, not this project's roots.
|
|
426
|
+
const parsedReferences = new Set([project]);
|
|
427
|
+
for (const reference of referencedConfigs) {
|
|
428
|
+
if (parsedReferences.has(reference)) continue;
|
|
429
|
+
parsedReferences.add(reference);
|
|
430
|
+
const selected = referenceRoots.get(reference) ?? new Set<string>();
|
|
431
|
+
referenceRoots.set(reference, selected);
|
|
432
|
+
for (const file of parser.parseConfigFile(reference).fileNames)
|
|
433
|
+
if (mode === 'ordinary' || authoredName(file) !== file)
|
|
434
|
+
selected.add(file);
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
parser.close();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
discovery = 'complete';
|
|
441
|
+
virtual.writeFile?.(
|
|
442
|
+
effectiveConfig,
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
...effectiveOptions,
|
|
445
|
+
files: [...roots],
|
|
446
|
+
include: [],
|
|
447
|
+
exclude: [],
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
const api = new API({ cwd, fs: overlay });
|
|
451
|
+
try {
|
|
452
|
+
const snapshot = api.updateSnapshot({
|
|
453
|
+
openProjects: [effectiveConfig],
|
|
454
|
+
openFiles: [...roots],
|
|
455
|
+
});
|
|
456
|
+
const program = snapshot.getProject(effectiveConfig)?.program;
|
|
457
|
+
if (!program)
|
|
458
|
+
throw new Error(`Native TypeScript did not load project ${project}`);
|
|
459
|
+
function remap(diagnostic: Diagnostic): OctaneTypeDiagnostic | undefined {
|
|
460
|
+
const relatedInformation = diagnostic.relatedInformation?.flatMap(
|
|
461
|
+
info => {
|
|
462
|
+
const mapped = remap(info);
|
|
463
|
+
return mapped ? [mapped] : [];
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
const text = diagnosticText(diagnostic);
|
|
467
|
+
const compiled = diagnostic.fileName
|
|
468
|
+
? (projections.get(diagnostic.fileName) ??
|
|
469
|
+
configurations.get(diagnostic.fileName))
|
|
470
|
+
: undefined;
|
|
471
|
+
let pos = diagnostic.pos;
|
|
472
|
+
let end = diagnostic.end;
|
|
473
|
+
let origin: OctaneTypeDiagnostic['origin'] = 'typescript';
|
|
474
|
+
let message = text;
|
|
475
|
+
if (compiled) {
|
|
476
|
+
const ranges = compiled.map.toSourceRange(pos, end, true, info =>
|
|
477
|
+
shouldReportDiagnostics(info, undefined, diagnostic.code),
|
|
478
|
+
);
|
|
479
|
+
const range = ranges.next().value;
|
|
480
|
+
if (range) [pos, end] = range;
|
|
481
|
+
else {
|
|
482
|
+
// A deliberately disabled verification range is distinct from a gap.
|
|
483
|
+
if (!compiled.map.toSourceRange(pos, end, true).next().done)
|
|
484
|
+
return undefined;
|
|
485
|
+
origin = 'projection';
|
|
486
|
+
message = `Cannot map native TS${diagnostic.code} at generated offsets ${pos}-${end}: ${text}`;
|
|
487
|
+
pos = 0;
|
|
488
|
+
end = 0;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const fileName =
|
|
492
|
+
compiled?.fileName ??
|
|
493
|
+
(diagnostic.fileName === effectiveConfig
|
|
494
|
+
? undefined
|
|
495
|
+
: diagnostic.fileName);
|
|
496
|
+
const source =
|
|
497
|
+
compiled?.source ?? (fileName ? readFile(fileName) : undefined);
|
|
498
|
+
return {
|
|
499
|
+
...(fileName ? { fileName } : {}),
|
|
500
|
+
pos,
|
|
501
|
+
end,
|
|
502
|
+
...(source !== undefined ? location(source, pos) : {}),
|
|
503
|
+
code: diagnostic.code,
|
|
504
|
+
category:
|
|
505
|
+
origin === 'projection' || diagnostic.category === 1
|
|
506
|
+
? 'error'
|
|
507
|
+
: diagnostic.category === 0
|
|
508
|
+
? 'warning'
|
|
509
|
+
: 'message',
|
|
510
|
+
text: message,
|
|
511
|
+
origin,
|
|
512
|
+
...(relatedInformation?.length ? { relatedInformation } : {}),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const diagnostics: OctaneTypeDiagnostic[] = [
|
|
516
|
+
...program.getConfigFileParsingDiagnostics(),
|
|
517
|
+
...program.getProgramDiagnostics(),
|
|
518
|
+
...program.getGlobalDiagnostics(),
|
|
519
|
+
...program.getSyntacticDiagnostics(),
|
|
520
|
+
...program.getBindDiagnostics(),
|
|
521
|
+
...program.getSemanticDiagnostics(),
|
|
522
|
+
].flatMap(diagnostic => {
|
|
523
|
+
const mapped = remap(diagnostic);
|
|
524
|
+
return mapped ? [mapped] : [];
|
|
525
|
+
});
|
|
526
|
+
const nativeFiles = program.getSourceFileNames();
|
|
527
|
+
for (const file of nativeFiles) {
|
|
528
|
+
const compiled = projections.get(file);
|
|
529
|
+
if (!compiled) continue;
|
|
530
|
+
for (const diagnostic of compiled.diagnostics) {
|
|
531
|
+
diagnostics.push({
|
|
532
|
+
fileName: compiled.fileName,
|
|
533
|
+
pos: diagnostic.start.offset,
|
|
534
|
+
end: diagnostic.end.offset,
|
|
535
|
+
...location(compiled.source, diagnostic.start.offset),
|
|
536
|
+
code: diagnostic.code,
|
|
537
|
+
category:
|
|
538
|
+
diagnostic.severity === 'error'
|
|
539
|
+
? 'error'
|
|
540
|
+
: diagnostic.severity === 'warning'
|
|
541
|
+
? 'warning'
|
|
542
|
+
: 'message',
|
|
543
|
+
text: diagnostic.message,
|
|
544
|
+
origin: 'octane',
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
for (const error of compiled.errors) {
|
|
548
|
+
const pos = error.pos ?? 0;
|
|
549
|
+
diagnostics.push({
|
|
550
|
+
fileName: compiled.fileName,
|
|
551
|
+
pos,
|
|
552
|
+
end: error.end ?? pos,
|
|
553
|
+
...location(compiled.source, pos),
|
|
554
|
+
code: 'OCTANE_PARSE',
|
|
555
|
+
category: 'error',
|
|
556
|
+
text: error.message,
|
|
557
|
+
origin: 'octane',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const unique = new Map(
|
|
562
|
+
diagnostics.map(diagnostic => [JSON.stringify(diagnostic), diagnostic]),
|
|
563
|
+
);
|
|
564
|
+
return {
|
|
565
|
+
compilerVersion: version,
|
|
566
|
+
files: [
|
|
567
|
+
...new Set(
|
|
568
|
+
nativeFiles.map(file => projections.get(file)?.fileName ?? file),
|
|
569
|
+
),
|
|
570
|
+
].sort(),
|
|
571
|
+
diagnostics: [...unique.values()].sort(
|
|
572
|
+
(left, right) =>
|
|
573
|
+
(left.fileName ?? '').localeCompare(right.fileName ?? '') ||
|
|
574
|
+
left.pos - right.pos ||
|
|
575
|
+
String(left.code).localeCompare(String(right.code)),
|
|
576
|
+
),
|
|
577
|
+
};
|
|
578
|
+
} finally {
|
|
579
|
+
api.close();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function formatOctaneDiagnostic(
|
|
584
|
+
diagnostic: OctaneTypeDiagnostic,
|
|
585
|
+
cwd = process.cwd(),
|
|
586
|
+
): string {
|
|
587
|
+
const file = diagnostic.fileName
|
|
588
|
+
? `${path.relative(cwd, diagnostic.fileName)}(${diagnostic.line ?? 1},${diagnostic.column ?? 1}): `
|
|
589
|
+
: '';
|
|
590
|
+
const code =
|
|
591
|
+
typeof diagnostic.code === 'number'
|
|
592
|
+
? `TS${diagnostic.code}`
|
|
593
|
+
: diagnostic.code;
|
|
594
|
+
const related = diagnostic.relatedInformation
|
|
595
|
+
?.map(info => ` ${formatOctaneDiagnostic(info, cwd)}`)
|
|
596
|
+
.join('\n');
|
|
597
|
+
return `${file}${diagnostic.category} ${code}: ${diagnostic.text}${related ? `\n${related}` : ''}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const booleanOptions = new Set([
|
|
601
|
+
'strict',
|
|
602
|
+
'isolatedModules',
|
|
603
|
+
'verbatimModuleSyntax',
|
|
604
|
+
'allowImportingTsExtensions',
|
|
605
|
+
'allowJs',
|
|
606
|
+
'esModuleInterop',
|
|
607
|
+
'noUncheckedIndexedAccess',
|
|
608
|
+
'exactOptionalPropertyTypes',
|
|
609
|
+
'noImplicitOverride',
|
|
610
|
+
'noFallthroughCasesInSwitch',
|
|
611
|
+
'noPropertyAccessFromIndexSignature',
|
|
612
|
+
'noImplicitReturns',
|
|
613
|
+
'resolveJsonModule',
|
|
614
|
+
'strictNullChecks',
|
|
615
|
+
'strictFunctionTypes',
|
|
616
|
+
'strictBindCallApply',
|
|
617
|
+
'strictPropertyInitialization',
|
|
618
|
+
'noImplicitAny',
|
|
619
|
+
'noImplicitThis',
|
|
620
|
+
'useUnknownInCatchVariables',
|
|
621
|
+
'alwaysStrict',
|
|
622
|
+
]);
|
|
623
|
+
const stringOptions = new Set([
|
|
624
|
+
'target',
|
|
625
|
+
'module',
|
|
626
|
+
'moduleResolution',
|
|
627
|
+
'moduleDetection',
|
|
628
|
+
'jsx',
|
|
629
|
+
'jsxImportSource',
|
|
630
|
+
]);
|
|
631
|
+
|
|
632
|
+
/** Node CLI entry. Unsupported options fail visibly instead of being ignored. */
|
|
633
|
+
export function runOctaneTypecheck(
|
|
634
|
+
argv: string[],
|
|
635
|
+
io: {
|
|
636
|
+
cwd?: string;
|
|
637
|
+
stdout?: (text: string) => void;
|
|
638
|
+
stderr?: (text: string) => void;
|
|
639
|
+
} = {},
|
|
640
|
+
): number {
|
|
641
|
+
const stdout = io.stdout ?? (text => process.stdout.write(text));
|
|
642
|
+
const stderr = io.stderr ?? (text => process.stderr.write(text));
|
|
643
|
+
try {
|
|
644
|
+
let project = 'tsconfig.json';
|
|
645
|
+
let listFiles = false;
|
|
646
|
+
const compilerOptions: Record<string, unknown> = {};
|
|
647
|
+
for (let index = 0; index < argv.length; index++) {
|
|
648
|
+
const argument = argv[index];
|
|
649
|
+
if (argument === '--version' || argument === '-v') {
|
|
650
|
+
stdout(`Version ${version}\n`);
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
if (argument === '--help' || argument === '-h') {
|
|
654
|
+
stdout('octane-tsc --project <tsconfig.json> [--noEmit] [--strict]\n');
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
if (argument === '--project' || argument === '-p') {
|
|
658
|
+
const value = argv[++index];
|
|
659
|
+
if (!value || value.startsWith('-'))
|
|
660
|
+
throw new Error('--project requires a configuration path');
|
|
661
|
+
project = value;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (!argument?.startsWith('--'))
|
|
665
|
+
throw new Error(`Unsupported Octane typecheck argument ${argument}`);
|
|
666
|
+
const name = argument.slice(2);
|
|
667
|
+
if (
|
|
668
|
+
booleanOptions.has(name) ||
|
|
669
|
+
[
|
|
670
|
+
'noEmit',
|
|
671
|
+
'noCheck',
|
|
672
|
+
'skipLibCheck',
|
|
673
|
+
'skipDefaultLibCheck',
|
|
674
|
+
'pretty',
|
|
675
|
+
'listFiles',
|
|
676
|
+
'explainFiles',
|
|
677
|
+
].includes(name)
|
|
678
|
+
) {
|
|
679
|
+
const explicit = argv[index + 1];
|
|
680
|
+
const value =
|
|
681
|
+
explicit === 'true' || explicit === 'false'
|
|
682
|
+
? argv[++index] === 'true'
|
|
683
|
+
: true;
|
|
684
|
+
if (
|
|
685
|
+
(name === 'noEmit' && !value) ||
|
|
686
|
+
((name === 'noCheck' ||
|
|
687
|
+
name === 'skipLibCheck' ||
|
|
688
|
+
name === 'skipDefaultLibCheck') &&
|
|
689
|
+
value)
|
|
690
|
+
)
|
|
691
|
+
throw new Error(
|
|
692
|
+
`--${name} ${value} is incompatible with full Octane typechecking`,
|
|
693
|
+
);
|
|
694
|
+
if (name === 'listFiles' || name === 'explainFiles')
|
|
695
|
+
listFiles ||= value;
|
|
696
|
+
else if (name !== 'pretty') compilerOptions[name] = value;
|
|
697
|
+
} else if (
|
|
698
|
+
stringOptions.has(name) ||
|
|
699
|
+
name === 'lib' ||
|
|
700
|
+
name === 'types'
|
|
701
|
+
) {
|
|
702
|
+
const value = argv[++index];
|
|
703
|
+
if (!value || value.startsWith('-'))
|
|
704
|
+
throw new Error(`--${name} requires a value`);
|
|
705
|
+
compilerOptions[name] =
|
|
706
|
+
name === 'lib' || name === 'types' ? value.split(',') : value;
|
|
707
|
+
} else throw new Error(`Unsupported Octane typecheck option --${name}`);
|
|
708
|
+
}
|
|
709
|
+
const cwd = path.resolve(io.cwd ?? process.cwd());
|
|
710
|
+
const result = checkOctaneProject({ project, cwd, compilerOptions });
|
|
711
|
+
for (const diagnostic of result.diagnostics)
|
|
712
|
+
stdout(`${formatOctaneDiagnostic(diagnostic, cwd)}\n`);
|
|
713
|
+
if (listFiles) for (const file of result.files) stdout(`${file}\n`);
|
|
714
|
+
return result.diagnostics.some(
|
|
715
|
+
diagnostic => diagnostic.category === 'error',
|
|
716
|
+
)
|
|
717
|
+
? 1
|
|
718
|
+
: 0;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
721
|
+
return 1;
|
|
722
|
+
}
|
|
723
|
+
}
|