@fulcro/transform-core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,40 +0,0 @@
1
- import typescript from 'typescript';
2
- import { type ExpressionRewriter } from '../file/index.js';
3
- /**
4
- * The editor integration of a rewrite that has to happen before type checking.
5
- *
6
- * An editor asks `tsserver`, and `tsserver` checks the files as written: it has
7
- * never heard of the rewrite, so it underlines `decimal * decimal` and reports
8
- * `a + b` as a `number` even in a project that builds cleanly. A language
9
- * service plugin is the one place to change that.
10
- *
11
- * The plugin keeps a second language service — the shadow — over the same
12
- * project with every file rewritten, and answers the questions that depend on
13
- * types from it: the diagnostics, the hover, the completions, where a name is
14
- * defined, the signature being typed. Each position the editor sends is mapped
15
- * into the rewritten text before it is asked, and each span in the answer is
16
- * mapped back, so the editor never sees text it did not write. Everything else
17
- * — formatting, folding, the syntax — is the original service's, untouched.
18
- *
19
- * ```json
20
- * { "compilerOptions": { "plugins": [{ "name": "@fulcro/types/language-service" }] } }
21
- * ```
22
- */
23
- /** What `tsserver` hands a plugin's `create`. */
24
- export interface PluginCreateInfo {
25
- readonly languageService: typescript.LanguageService;
26
- readonly languageServiceHost: typescript.LanguageServiceHost;
27
- }
28
- /** The module shape `tsserver` loads a plugin from. */
29
- export type LanguageServicePlugin = (modules: {
30
- readonly typescript: typeof typescript;
31
- }) => {
32
- readonly create: (info: PluginCreateInfo) => typescript.LanguageService;
33
- };
34
- /**
35
- * Builds the `tsserver` plugin of a rewriter.
36
- *
37
- * @param rewriter What to rewrite.
38
- * @returns The plugin module, as `tsserver` loads it.
39
- */
40
- export declare const createLanguageServicePlugin: (rewriter: ExpressionRewriter) => LanguageServicePlugin;
@@ -1,286 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createLanguageServicePlugin = void 0;
7
- const typescript_1 = __importDefault(require("typescript"));
8
- const fixpoint_1 = require("../fixpoint/index.js");
9
- /**
10
- * A file name as the program spells it: forward slashes on every platform.
11
- * `tsserver` and a host may hand either spelling over, and a map keyed by one
12
- * would never be found by the other.
13
- *
14
- * @param fileName File name in either spelling.
15
- * @returns The program's spelling.
16
- */
17
- const normalized = (fileName) => fileName.replace(/\\/g, '/');
18
- /** The map of a file the rewrite did not touch: every offset is itself. */
19
- const UNCHANGED = {
20
- text: '',
21
- toOriginal: (generated) => generated,
22
- toGenerated: (original) => original,
23
- };
24
- /**
25
- * Maps a span of the rewritten text back to the original.
26
- *
27
- * @param span Span in the rewritten text.
28
- * @param map Map of the file.
29
- * @returns The span in the original text.
30
- */
31
- const spanToOriginal = (span, map) => {
32
- const start = map.toOriginal(span.start);
33
- if (span.length === 0)
34
- return { start, length: 0 };
35
- // The end is exclusive, so it can sit exactly where a copied piece stops
36
- // and synthesized text begins, and would map to the start of the rewritten
37
- // node. The last character of the span is inside the span; mapping it and
38
- // stepping one past lands where the span really ends.
39
- const end = map.toOriginal(span.start + span.length - 1) + 1;
40
- return { start, length: Math.max(0, end - start) };
41
- };
42
- /**
43
- * Removes the namespace the rewrite imports from text shown to the reader.
44
- *
45
- * The checker names a type by the shortest path it can reach it through, and
46
- * in a rewritten file that is often the namespace import the rewrite added —
47
- * `__fulcroTypes.SignedInteger<32>`, a name the author never wrote.
48
- *
49
- * @param namespace Local name of the import.
50
- * @returns The cleaner.
51
- */
52
- const withoutNamespace = (namespace) => (text) => text.split(`${namespace}.`).join('');
53
- /**
54
- * Builds the shadow of a language service: the same project, every file read
55
- * through the rewrite.
56
- *
57
- * @param info What `tsserver` handed the plugin.
58
- * @param rewriter What to rewrite.
59
- * @returns The shadow, and the rewrite it is currently built on.
60
- */
61
- const createShadow = (info, rewriter) => {
62
- const base = info.languageServiceHost;
63
- const texts = new Map();
64
- // Bumped on every text handed back, so the shadow re-reads exactly the
65
- // files the rewrite changed.
66
- let generation = 0;
67
- const host = Object.create(base, {
68
- getScriptSnapshot: {
69
- value: (fileName) => {
70
- const text = texts.get(normalized(fileName));
71
- return text === undefined
72
- ? base.getScriptSnapshot(fileName)
73
- : typescript_1.default.ScriptSnapshot.fromString(text);
74
- },
75
- },
76
- getScriptVersion: {
77
- value: (fileName) => texts.has(normalized(fileName))
78
- ? `${base.getScriptVersion(fileName)}:rewritten:${generation}`
79
- : base.getScriptVersion(fileName),
80
- },
81
- getProjectVersion: {
82
- value: () => `${base.getProjectVersion?.() ?? ''}:${generation}`,
83
- },
84
- });
85
- const service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry());
86
- let settledFor;
87
- let current;
88
- /**
89
- * The rewrite for the project as it stands, recomputed only when the
90
- * original project changed.
91
- *
92
- * @returns The rewrite.
93
- */
94
- const rewrite = () => {
95
- const version = base.getProjectVersion?.() ??
96
- base
97
- .getScriptFileNames()
98
- .map((fileName) => base.getScriptVersion(fileName))
99
- .join('|');
100
- if (current !== undefined && settledFor === version)
101
- return current;
102
- texts.clear();
103
- generation++;
104
- current = (0, fixpoint_1.rewriteToFixpoint)({
105
- program: () => {
106
- const program = service.getProgram();
107
- if (program === undefined) {
108
- throw new Error('The shadow language service produced no program.');
109
- }
110
- return program;
111
- },
112
- update: (fileName, text) => {
113
- texts.set(normalized(fileName), text);
114
- generation++;
115
- },
116
- }, rewriter);
117
- settledFor = version;
118
- return current;
119
- };
120
- return { service, rewrite };
121
- };
122
- /**
123
- * Builds the `tsserver` plugin of a rewriter.
124
- *
125
- * @param rewriter What to rewrite.
126
- * @returns The plugin module, as `tsserver` loads it.
127
- */
128
- const createLanguageServicePlugin = (rewriter) => () => ({
129
- create: (info) => {
130
- const original = info.languageService;
131
- const shadow = createShadow(info, rewriter);
132
- /**
133
- * The map of a file, when the rewrite touched it.
134
- *
135
- * @param fileName File being asked about.
136
- * @returns Its map, or `undefined` when it reads as written.
137
- */
138
- const mapOf = (fileName) => shadow.rewrite().files.get(normalized(fileName));
139
- const clean = withoutNamespace(rewriter.namespace);
140
- /**
141
- * Removes the rewrite's namespace from the parts of a display.
142
- *
143
- * @param parts Display parts, when there are any.
144
- * @returns The same parts, cleaned.
145
- */
146
- const cleanParts = (parts) =>
147
- // A display names the namespace as a part of its own, followed by
148
- // a `.` part; both go, and any text naming it inline is cleaned.
149
- parts
150
- ?.filter((part, index) => !(part.text === rewriter.namespace &&
151
- parts[index + 1]?.text === '.') &&
152
- !(part.text === '.' &&
153
- parts[index - 1]?.text === rewriter.namespace))
154
- .map((part) => ({ ...part, text: clean(part.text) }));
155
- /**
156
- * Removes the rewrite's namespace from a diagnostic message, however
157
- * deeply it is chained.
158
- *
159
- * @param message The message.
160
- * @returns The message, cleaned.
161
- */
162
- const cleanMessage = (message) => typeof message === 'string'
163
- ? clean(message)
164
- : {
165
- ...message,
166
- messageText: clean(message.messageText),
167
- next: message.next?.map((next) => cleanMessage(next)),
168
- };
169
- /**
170
- * Maps the diagnostics of one file back to its original text.
171
- *
172
- * @param fileName File the diagnostics are about.
173
- * @param diagnostics Diagnostics from the shadow.
174
- * @returns The same diagnostics, in the original's positions.
175
- */
176
- const mapDiagnostics = (fileName, diagnostics) => {
177
- const map = mapOf(fileName);
178
- const sourceFile = original
179
- .getProgram()
180
- ?.getSourceFile(fileName);
181
- return diagnostics.map((diagnostic) => {
182
- const messageText = cleanMessage(diagnostic.messageText);
183
- if (map === undefined || diagnostic.start === undefined) {
184
- return {
185
- ...diagnostic,
186
- messageText,
187
- file: sourceFile ?? diagnostic.file,
188
- };
189
- }
190
- const span = spanToOriginal({ start: diagnostic.start, length: diagnostic.length ?? 0 }, map);
191
- return {
192
- ...diagnostic,
193
- messageText,
194
- file: sourceFile ?? diagnostic.file,
195
- start: span.start,
196
- length: span.length,
197
- };
198
- });
199
- };
200
- /**
201
- * Maps a position into the shadow, asks it, and maps the answer back.
202
- *
203
- * @param fileName File asked about.
204
- * @param position Position in the original text.
205
- * @param ask The question, put to the shadow at the mapped position.
206
- * @param fromShadow Maps what the shadow answered back.
207
- * @returns The mapped answer.
208
- */
209
- const atPosition = (fileName, position, ask, fromShadow) => {
210
- // A file the rewrite left alone still gets its answer mapped: a
211
- // definition it points to may sit in a file that was rewritten.
212
- const map = mapOf(fileName) ?? UNCHANGED;
213
- return fromShadow(ask(shadow.service, map.toGenerated(position)), map);
214
- };
215
- /**
216
- * Maps a definition back when it points into a rewritten file.
217
- *
218
- * @param definition A definition or a reference.
219
- * @returns The same, in original positions.
220
- */
221
- const mapLocation = (definition) => {
222
- const map = mapOf(definition.fileName);
223
- return map === undefined
224
- ? definition
225
- : {
226
- ...definition,
227
- textSpan: spanToOriginal(definition.textSpan, map),
228
- };
229
- };
230
- const proxy = Object.create(original);
231
- // The rewrite is brought up to date before the shadow is asked, so the
232
- // shadow answers about the rewritten text and not the one before it.
233
- proxy.getSemanticDiagnostics = (fileName) => {
234
- shadow.rewrite();
235
- return mapDiagnostics(fileName, shadow.service.getSemanticDiagnostics(fileName));
236
- };
237
- proxy.getSuggestionDiagnostics = (fileName) => {
238
- shadow.rewrite();
239
- return mapDiagnostics(fileName, shadow.service.getSuggestionDiagnostics(fileName));
240
- };
241
- proxy.getQuickInfoAtPosition = (fileName, position, ...rest) => atPosition(fileName, position, (service, at) => service.getQuickInfoAtPosition(fileName, at, ...rest), (answer, map) => answer === undefined
242
- ? answer
243
- : {
244
- ...answer,
245
- textSpan: spanToOriginal(answer.textSpan, map),
246
- displayParts: cleanParts(answer.displayParts),
247
- documentation: cleanParts(answer.documentation),
248
- });
249
- proxy.getCompletionsAtPosition = (fileName, position, ...rest) => atPosition(fileName, position, (service, at) => service.getCompletionsAtPosition(fileName, at, ...rest), (answer, map) => answer === undefined
250
- ? answer
251
- : {
252
- ...answer,
253
- optionalReplacementSpan: answer.optionalReplacementSpan === undefined
254
- ? undefined
255
- : spanToOriginal(answer.optionalReplacementSpan, map),
256
- entries: answer.entries.map((entry) => entry.replacementSpan === undefined
257
- ? entry
258
- : {
259
- ...entry,
260
- replacementSpan: spanToOriginal(entry.replacementSpan, map),
261
- }),
262
- });
263
- proxy.getCompletionEntryDetails = (fileName, position, ...rest) => atPosition(fileName, position, (service, at) => service.getCompletionEntryDetails(fileName, at, ...rest), (answer) => answer === undefined
264
- ? answer
265
- : {
266
- ...answer,
267
- displayParts: cleanParts(answer.displayParts) ?? [],
268
- });
269
- proxy.getSignatureHelpItems = (fileName, position, ...rest) => atPosition(fileName, position, (service, at) => service.getSignatureHelpItems(fileName, at, ...rest), (answer, map) => answer === undefined
270
- ? answer
271
- : {
272
- ...answer,
273
- applicableSpan: spanToOriginal(answer.applicableSpan, map),
274
- });
275
- proxy.getDefinitionAtPosition = (fileName, position, ...rest) => atPosition(fileName, position, (service, at) => service.getDefinitionAtPosition(fileName, at, ...rest), (answer) => answer?.map(mapLocation));
276
- proxy.getDefinitionAndBoundSpan = (fileName, position) => atPosition(fileName, position, (service, at) => service.getDefinitionAndBoundSpan(fileName, at), (answer, map) => answer === undefined
277
- ? answer
278
- : {
279
- textSpan: spanToOriginal(answer.textSpan, map),
280
- definitions: answer.definitions?.map(mapLocation),
281
- });
282
- proxy.getTypeDefinitionAtPosition = (fileName, position) => atPosition(fileName, position, (service, at) => service.getTypeDefinitionAtPosition(fileName, at), (answer) => answer?.map(mapLocation));
283
- return proxy;
284
- },
285
- });
286
- exports.createLanguageServicePlugin = createLanguageServicePlugin;
@@ -1,44 +0,0 @@
1
- import typescript from 'typescript';
2
- import { type ExpressionRewriter } from '../file/index.js';
3
- import { type ProgramRewrite } from '../fixpoint/index.js';
4
- /**
5
- * The `tsc` integration of a rewrite that has to happen before type checking.
6
- *
7
- * `ts-patch` offers two kinds of plugin. The ordinary one, a `before`
8
- * transformer, runs on a program that has already been type checked, which is
9
- * too late: `decimal * decimal` has been reported as an error by then. The other
10
- * — `"transformProgram": true` in the tsconfig entry — is handed the program as
11
- * it is created and returns the one `tsc` goes on to check and emit. That is
12
- * where this runs: it rewrites the program to a fixed point and returns the
13
- * rewritten one, so what gets checked is what the rewrite produced.
14
- *
15
- * ```json
16
- * { "plugins": [{ "transform": "@fulcro/types/transformer", "transformProgram": true }] }
17
- * ```
18
- */
19
- /** The shape `ts-patch` expects from a program transformer. */
20
- export type ProgramTransformer = (program: typescript.Program, host?: typescript.CompilerHost, config?: unknown, extras?: unknown) => typescript.Program;
21
- /**
22
- * Rewrites an existing program, returning the rewritten program with what the
23
- * rewrite did.
24
- *
25
- * @param program Program to rewrite.
26
- * @param host Host it was created with; a default one when not given.
27
- * @param rewriter What to rewrite.
28
- * @returns The rewritten program, and the rewrite.
29
- */
30
- export declare const rewriteProgram: (program: typescript.Program, host: typescript.CompilerHost | undefined, rewriter: ExpressionRewriter) => {
31
- readonly program: typescript.Program;
32
- readonly rewrite: ProgramRewrite;
33
- };
34
- /**
35
- * Builds the program transformer of a rewriter.
36
- *
37
- * Guarded against itself: the new program is made with `createProgram`, which
38
- * `ts-patch` has patched to run program transformers, so without the guard the
39
- * transformer would be invoked on its own output, recursively.
40
- *
41
- * @param rewriter What to rewrite.
42
- * @returns The program transformer, as `ts-patch` expects it.
43
- */
44
- export declare const createProgramTransformer: (rewriter: ExpressionRewriter) => ProgramTransformer;
@@ -1,109 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createProgramTransformer = exports.rewriteProgram = void 0;
7
- const typescript_1 = __importDefault(require("typescript"));
8
- const fixpoint_1 = require("../fixpoint/index.js");
9
- /**
10
- * Serves the rewritten texts in place of the files on disk, and parses each
11
- * once per text.
12
- *
13
- * @param base Host the program was created with.
14
- * @param texts Rewritten texts by file name.
15
- * @param languageVersion Target the files are parsed for.
16
- * @param seed Program whose files are reused as they are.
17
- * @returns A host reading through the rewrite.
18
- */
19
- const overlayHost = (base, texts, languageVersion, seed) => {
20
- const parsed = new Map();
21
- // Every file the rewrite did not touch is handed back as the same object on
22
- // every pass. `createProgram` reuses an old program's file only when the
23
- // host returns the identical object, so a host that parsed the standard
24
- // library afresh each time would make every pass re-check it from nothing.
25
- const unchanged = new Map(seed.getSourceFiles().map((file) => [file.fileName, file]));
26
- return {
27
- ...base,
28
- getSourceFile: (fileName, options, onError, shouldCreate) => {
29
- const text = texts.get(fileName);
30
- if (text === undefined) {
31
- if (!unchanged.has(fileName)) {
32
- unchanged.set(fileName, base.getSourceFile(fileName, options, onError, shouldCreate));
33
- }
34
- return unchanged.get(fileName);
35
- }
36
- const cached = parsed.get(fileName);
37
- if (cached !== undefined && cached.text === text)
38
- return cached;
39
- const sourceFile = typescript_1.default.createSourceFile(fileName, text, typeof options === 'object' ? options : languageVersion, true);
40
- parsed.set(fileName, sourceFile);
41
- return sourceFile;
42
- },
43
- readFile: (fileName) => texts.get(fileName) ?? base.readFile(fileName),
44
- fileExists: (fileName) => texts.has(fileName) || base.fileExists(fileName),
45
- };
46
- };
47
- /**
48
- * Rewrites an existing program, returning the rewritten program with what the
49
- * rewrite did.
50
- *
51
- * @param program Program to rewrite.
52
- * @param host Host it was created with; a default one when not given.
53
- * @param rewriter What to rewrite.
54
- * @returns The rewritten program, and the rewrite.
55
- */
56
- const rewriteProgram = (program, host, rewriter) => {
57
- const options = program.getCompilerOptions();
58
- const base = host ?? typescript_1.default.createCompilerHost(options, true);
59
- const texts = new Map();
60
- const overlay = overlayHost(base, texts, options.target ?? typescript_1.default.ScriptTarget.ES2022, program);
61
- let current = program;
62
- let stale = false;
63
- const rewrite = (0, fixpoint_1.rewriteToFixpoint)({
64
- program: () => {
65
- if (stale) {
66
- current = typescript_1.default.createProgram({
67
- rootNames: program.getRootFileNames(),
68
- options,
69
- host: overlay,
70
- oldProgram: current,
71
- projectReferences: program.getProjectReferences(),
72
- });
73
- stale = false;
74
- }
75
- return current;
76
- },
77
- update: (fileName, text) => {
78
- texts.set(fileName, text);
79
- stale = true;
80
- },
81
- }, rewriter);
82
- return { program: current, rewrite };
83
- };
84
- exports.rewriteProgram = rewriteProgram;
85
- /**
86
- * Builds the program transformer of a rewriter.
87
- *
88
- * Guarded against itself: the new program is made with `createProgram`, which
89
- * `ts-patch` has patched to run program transformers, so without the guard the
90
- * transformer would be invoked on its own output, recursively.
91
- *
92
- * @param rewriter What to rewrite.
93
- * @returns The program transformer, as `ts-patch` expects it.
94
- */
95
- const createProgramTransformer = (rewriter) => {
96
- let running = false;
97
- return (program, host) => {
98
- if (running)
99
- return program;
100
- running = true;
101
- try {
102
- return (0, exports.rewriteProgram)(program, host, rewriter).program;
103
- }
104
- finally {
105
- running = false;
106
- }
107
- };
108
- };
109
- exports.createProgramTransformer = createProgramTransformer;
@@ -1,64 +0,0 @@
1
- /**
2
- * Text produced by rewriting a source file, with the map back to the original.
3
- *
4
- * A rewrite that happens before type checking changes the text the checker,
5
- * the emitter and the editor all read, and every one of them reports positions
6
- * in it. For those positions to mean anything to the person who wrote the
7
- * original, the rewrite keeps a record of where each piece of its output came
8
- * from — the same idea as a source map, kept as offsets rather than lines,
9
- * because the language service speaks in offsets.
10
- *
11
- * The output is made of two kinds of piece. A **copy** is original text moved
12
- * verbatim, and maps offset for offset. A **region** is the output of one
13
- * rewritten node; any position inside it that is not also inside a copy — the
14
- * synthesized `T.add(` around two operands — maps to the start of the node it
15
- * replaced, which is where an error about it belongs.
16
- */
17
- /** A rewritten text and the two directions of its position map. */
18
- export interface RewrittenText {
19
- /** The rewritten text. */
20
- readonly text: string;
21
- /**
22
- * Maps an offset in the rewritten text to the original.
23
- *
24
- * @param generated Offset in {@link RewrittenText.text}.
25
- * @returns The offset in the original text it came from.
26
- */
27
- readonly toOriginal: (generated: number) => number;
28
- /**
29
- * Maps an offset in the original text to the rewritten one.
30
- *
31
- * @param original Offset in the original text.
32
- * @returns The offset in {@link RewrittenText.text} it went to.
33
- */
34
- readonly toGenerated: (original: number) => number;
35
- }
36
- /** Accumulates the output of a rewrite and its map. */
37
- export interface TextBuilder {
38
- /** Copies a span of the original text verbatim. */
39
- readonly copy: (start: number, end: number) => void;
40
- /** Writes synthesized text, which maps to the enclosing region. */
41
- readonly write: (text: string) => void;
42
- /** Opens the region of a rewritten node; returns the handle to close it. */
43
- readonly open: (originalStart: number, originalEnd: number) => () => void;
44
- /** Whether any region was opened, which is whether anything was rewritten. */
45
- readonly rewrote: () => boolean;
46
- /** Finishes the text. */
47
- readonly build: () => RewrittenText;
48
- }
49
- /**
50
- * Starts the output of a rewrite over an original text.
51
- *
52
- * @param original The text being rewritten.
53
- * @returns The builder.
54
- */
55
- export declare const createTextBuilder: (original: string) => TextBuilder;
56
- /**
57
- * Chains two rewrites of the same file, the second applied to the output of the
58
- * first, into one map from the last text back to the original.
59
- *
60
- * @param first The earlier rewrite.
61
- * @param second The later one, over the earlier one's text.
62
- * @returns The combined rewrite.
63
- */
64
- export declare const composeRewrites: (first: RewrittenText, second: RewrittenText) => RewrittenText;