@fulcro/transform-core 0.8.1 → 0.10.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.
@@ -0,0 +1,286 @@
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;
@@ -0,0 +1,44 @@
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;
@@ -0,0 +1,109 @@
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;
@@ -0,0 +1,64 @@
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;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ /**
3
+ * Text produced by rewriting a source file, with the map back to the original.
4
+ *
5
+ * A rewrite that happens before type checking changes the text the checker,
6
+ * the emitter and the editor all read, and every one of them reports positions
7
+ * in it. For those positions to mean anything to the person who wrote the
8
+ * original, the rewrite keeps a record of where each piece of its output came
9
+ * from — the same idea as a source map, kept as offsets rather than lines,
10
+ * because the language service speaks in offsets.
11
+ *
12
+ * The output is made of two kinds of piece. A **copy** is original text moved
13
+ * verbatim, and maps offset for offset. A **region** is the output of one
14
+ * rewritten node; any position inside it that is not also inside a copy — the
15
+ * synthesized `T.add(` around two operands — maps to the start of the node it
16
+ * replaced, which is where an error about it belongs.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.composeRewrites = exports.createTextBuilder = void 0;
20
+ /**
21
+ * Finds the last entry whose start is at or before an offset.
22
+ *
23
+ * @param entries Entries sorted by the key.
24
+ * @param key The start of an entry.
25
+ * @param offset Offset looked up.
26
+ * @returns The index, or -1.
27
+ */
28
+ const floorIndex = (entries, key, offset) => {
29
+ let low = 0;
30
+ let high = entries.length - 1;
31
+ let found = -1;
32
+ while (low <= high) {
33
+ const middle = (low + high) >> 1;
34
+ if (key(entries[middle]) <= offset) {
35
+ found = middle;
36
+ low = middle + 1;
37
+ }
38
+ else {
39
+ high = middle - 1;
40
+ }
41
+ }
42
+ return found;
43
+ };
44
+ /**
45
+ * Starts the output of a rewrite over an original text.
46
+ *
47
+ * @param original The text being rewritten.
48
+ * @returns The builder.
49
+ */
50
+ const createTextBuilder = (original) => {
51
+ const pieces = [];
52
+ const copies = [];
53
+ const regions = [];
54
+ let position = 0;
55
+ const copy = (start, end) => {
56
+ if (end <= start)
57
+ return;
58
+ copies.push({
59
+ originalStart: start,
60
+ generatedStart: position,
61
+ length: end - start,
62
+ });
63
+ pieces.push(original.slice(start, end));
64
+ position += end - start;
65
+ };
66
+ const write = (text) => {
67
+ pieces.push(text);
68
+ position += text.length;
69
+ };
70
+ const open = (originalStart, originalEnd) => {
71
+ const generatedStart = position;
72
+ return () => {
73
+ regions.push({
74
+ originalStart,
75
+ originalEnd,
76
+ generatedStart,
77
+ generatedEnd: position,
78
+ });
79
+ };
80
+ };
81
+ const build = () => {
82
+ const text = pieces.join('');
83
+ // A region closes after the regions nested in it, so sorting by start —
84
+ // and, at equal starts, the wider first — puts the innermost last among
85
+ // those containing any given offset.
86
+ const byGenerated = [...regions].sort((left, right) => left.generatedStart - right.generatedStart ||
87
+ right.generatedEnd - left.generatedEnd);
88
+ const byOriginal = [...regions].sort((left, right) => left.originalStart - right.originalStart ||
89
+ right.originalEnd - left.originalEnd);
90
+ const innermost = (sorted, start, end, offset) => {
91
+ let best;
92
+ for (const region of sorted) {
93
+ if (start(region) > offset)
94
+ break;
95
+ if (offset < end(region))
96
+ best = region;
97
+ }
98
+ return best;
99
+ };
100
+ const toOriginal = (generated) => {
101
+ const index = floorIndex(copies, (entry) => entry.generatedStart, generated);
102
+ const candidate = copies[index];
103
+ if (candidate !== undefined &&
104
+ generated < candidate.generatedStart + candidate.length) {
105
+ return candidate.originalStart + (generated - candidate.generatedStart);
106
+ }
107
+ const region = innermost(byGenerated, (entry) => entry.generatedStart, (entry) => entry.generatedEnd, generated);
108
+ if (region !== undefined)
109
+ return region.originalStart;
110
+ // Past the last copy, or inside text written outside any region:
111
+ // the end of the copy before it is the nearest original position.
112
+ return candidate === undefined
113
+ ? 0
114
+ : candidate.originalStart + candidate.length;
115
+ };
116
+ const toGenerated = (originalOffset) => {
117
+ // Copies are in output order, so the first one holding the offset is
118
+ // the earliest in the output — which is the one that counts when a
119
+ // node was written twice, as the target of a compound assignment is.
120
+ //
121
+ // A copy that contains the offset wins over one that merely ends at
122
+ // it: the start of an operand is also the end of the copy before its
123
+ // operator, and answering with that copy's end would land on the
124
+ // synthesized text in front of the operand rather than on the operand.
125
+ // Only when nothing contains it — the position just after the last
126
+ // word of a file — does a copy ending there count.
127
+ const holder = copies.find((entry) => entry.originalStart <= originalOffset &&
128
+ originalOffset < entry.originalStart + entry.length) ??
129
+ copies.find((entry) => entry.originalStart <= originalOffset &&
130
+ originalOffset === entry.originalStart + entry.length);
131
+ if (holder !== undefined) {
132
+ return holder.generatedStart + (originalOffset - holder.originalStart);
133
+ }
134
+ const region = innermost(byOriginal, (entry) => entry.originalStart, (entry) => entry.originalEnd, originalOffset);
135
+ return region === undefined ? originalOffset : region.generatedStart;
136
+ };
137
+ return { text, toOriginal, toGenerated };
138
+ };
139
+ return {
140
+ copy,
141
+ write,
142
+ open,
143
+ rewrote: () => regions.length > 0,
144
+ build,
145
+ };
146
+ };
147
+ exports.createTextBuilder = createTextBuilder;
148
+ /**
149
+ * Chains two rewrites of the same file, the second applied to the output of the
150
+ * first, into one map from the last text back to the original.
151
+ *
152
+ * @param first The earlier rewrite.
153
+ * @param second The later one, over the earlier one's text.
154
+ * @returns The combined rewrite.
155
+ */
156
+ const composeRewrites = (first, second) => ({
157
+ text: second.text,
158
+ toOriginal: (generated) => first.toOriginal(second.toOriginal(generated)),
159
+ toGenerated: (original) => second.toGenerated(first.toGenerated(original)),
160
+ });
161
+ exports.composeRewrites = composeRewrites;
@@ -1,5 +1,6 @@
1
1
  import { type UnpluginFactory, type UnpluginInstance } from 'unplugin';
2
2
  import { type TransformCoreOptions } from '../program/index.js';
3
+ import { type ExpressionRewriter } from '../rewrite/file/index.js';
3
4
  import { type CallRewriter } from '../shared/index.js';
4
5
  /**
5
6
  * Bundler adapters for the Fulcro transformers.
@@ -54,4 +55,14 @@ export interface TransformerUnplugin {
54
55
  * @returns Every adapter `unplugin` can produce.
55
56
  */
56
57
  export declare const createTransformerUnplugin: (rewriters: readonly CallRewriter[], name: string) => TransformerUnplugin;
58
+ /**
59
+ * Builds the bundler adapters for a rewrite that happens before type checking
60
+ * — one whose output is what the checker would read, such as the operators of
61
+ * `@fulcro/types`.
62
+ *
63
+ * @param rewriter Rewriter of the package publishing the plugin.
64
+ * @param name Name the plugin reports to the bundler.
65
+ * @returns Every adapter `unplugin` can produce.
66
+ */
67
+ export declare const createRewriterUnplugin: (rewriter: ExpressionRewriter, name: string) => TransformerUnplugin;
57
68
  export {};