@fulcro/transform-core 0.9.0 → 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.
package/dist/index.d.ts CHANGED
@@ -17,6 +17,11 @@
17
17
  * other.
18
18
  */
19
19
  export { createFileTransformer, type FileTransformer, type TransformCoreOptions, } from './program/index.js';
20
+ export { type ExpressionContext, type ExpressionRewriter, type Replacement, rewriteSourceFile, type RewriteStatistics, } from './rewrite/file/index.js';
21
+ export { isRewritable, type ProgramRewrite, type ProgramSource, rewriteToFixpoint, } from './rewrite/fixpoint/index.js';
22
+ export { createLanguageServicePlugin, type LanguageServicePlugin, type PluginCreateInfo, } from './rewrite/languageService/index.js';
23
+ export { createProgramTransformer, type ProgramTransformer, rewriteProgram, } from './rewrite/programTransformer/index.js';
24
+ export { type RewrittenText } from './rewrite/text/index.js';
20
25
  export { type CallForm, type CallRewriter, IDENTIFIER_PATTERN, isOwnedCall, isTupleType, type RewriteContext, utilityModuleSegment, } from './shared/index.js';
21
26
  export { buildStructuralTest, type StructuralOptions, type TypeTest, } from './structural/index.js';
22
27
  export { createTransformer, type TransformerFactory, type TransformerOptions, } from './transformer/index.js';
package/dist/index.js CHANGED
@@ -18,9 +18,19 @@
18
18
  * other.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.createTransformer = exports.buildStructuralTest = exports.utilityModuleSegment = exports.isTupleType = exports.isOwnedCall = exports.IDENTIFIER_PATTERN = exports.createFileTransformer = void 0;
21
+ exports.createTransformer = exports.buildStructuralTest = exports.utilityModuleSegment = exports.isTupleType = exports.isOwnedCall = exports.IDENTIFIER_PATTERN = exports.rewriteProgram = exports.createProgramTransformer = exports.createLanguageServicePlugin = exports.rewriteToFixpoint = exports.isRewritable = exports.rewriteSourceFile = exports.createFileTransformer = void 0;
22
22
  var program_1 = require("./program/index.js");
23
23
  Object.defineProperty(exports, "createFileTransformer", { enumerable: true, get: function () { return program_1.createFileTransformer; } });
24
+ var file_1 = require("./rewrite/file/index.js");
25
+ Object.defineProperty(exports, "rewriteSourceFile", { enumerable: true, get: function () { return file_1.rewriteSourceFile; } });
26
+ var fixpoint_1 = require("./rewrite/fixpoint/index.js");
27
+ Object.defineProperty(exports, "isRewritable", { enumerable: true, get: function () { return fixpoint_1.isRewritable; } });
28
+ Object.defineProperty(exports, "rewriteToFixpoint", { enumerable: true, get: function () { return fixpoint_1.rewriteToFixpoint; } });
29
+ var languageService_1 = require("./rewrite/languageService/index.js");
30
+ Object.defineProperty(exports, "createLanguageServicePlugin", { enumerable: true, get: function () { return languageService_1.createLanguageServicePlugin; } });
31
+ var programTransformer_1 = require("./rewrite/programTransformer/index.js");
32
+ Object.defineProperty(exports, "createProgramTransformer", { enumerable: true, get: function () { return programTransformer_1.createProgramTransformer; } });
33
+ Object.defineProperty(exports, "rewriteProgram", { enumerable: true, get: function () { return programTransformer_1.rewriteProgram; } });
24
34
  var shared_1 = require("./shared/index.js");
25
35
  Object.defineProperty(exports, "IDENTIFIER_PATTERN", { enumerable: true, get: function () { return shared_1.IDENTIFIER_PATTERN; } });
26
36
  Object.defineProperty(exports, "isOwnedCall", { enumerable: true, get: function () { return shared_1.isOwnedCall; } });
@@ -1,5 +1,82 @@
1
+ import typescript from 'typescript';
1
2
  import { CallRewriter } from '../shared/index.js';
2
3
  import { TransformerOptions } from '../transformer/index.js';
4
+ /**
5
+ * Rewrites a path the way the compiler spells it.
6
+ *
7
+ * TypeScript keeps every file name with forward slashes, on every platform,
8
+ * while the bundler and `path.normalize` hand back the native separator. Mixing
9
+ * the two silently defeats every lookup against the files of the program: a
10
+ * file already in it looks new, gets added as another root, and invalidates the
11
+ * whole program — turning one startup cost into one per file.
12
+ *
13
+ * @param fileName Path in whatever spelling it arrived.
14
+ * @returns The path as the compiler spells it.
15
+ */
16
+ export declare const toCompilerPath: (fileName: string) => string;
17
+ /**
18
+ * Keeps a TypeScript program alive across recompilations.
19
+ *
20
+ * A language service rather than a one shot program: a watch run recompiles the
21
+ * same files repeatedly, and rebuilding the whole program on each edit would
22
+ * cost seconds every keystroke. The service reuses everything that did not
23
+ * change, and the version counters below are what tell it what did.
24
+ */
25
+ export declare class ProgramHost implements typescript.LanguageServiceHost {
26
+ /** Files of the program, as resolved from the tsconfig. */
27
+ private readonly rootNames;
28
+ /** Compiler options resolved from the tsconfig. */
29
+ private readonly options;
30
+ /** Content handed over by the bundler, which may be ahead of the disk. */
31
+ private readonly overlays;
32
+ /** Bumped whenever a file changes, which is how the service invalidates. */
33
+ private readonly versions;
34
+ /** Directory the relative paths of the program resolve against. */
35
+ private readonly currentDirectory;
36
+ /**
37
+ * Initializes the host from a parsed tsconfig.
38
+ *
39
+ * @param parsed Parsed contents of the tsconfig.
40
+ * @param currentDirectory Directory the program resolves against.
41
+ */
42
+ constructor(parsed: typescript.ParsedCommandLine, currentDirectory: string);
43
+ /**
44
+ * Records the content of a file as the bundler sees it.
45
+ *
46
+ * @param fileName File being compiled.
47
+ * @param content Current content of the file.
48
+ */
49
+ update(fileName: string, content: string): void;
50
+ /**
51
+ * Tells whether the content handed over matches what is on disk.
52
+ *
53
+ * @param fileName File being compiled.
54
+ * @param content Content handed over by the bundler.
55
+ * @returns `true` when the two are identical.
56
+ */
57
+ private matchesDisk;
58
+ getScriptFileNames(): string[];
59
+ getScriptVersion(fileName: string): string;
60
+ getScriptSnapshot(fileName: string): typescript.IScriptSnapshot | undefined;
61
+ getCurrentDirectory(): string;
62
+ getCompilationSettings(): typescript.CompilerOptions;
63
+ getDefaultLibFileName(options: typescript.CompilerOptions): string;
64
+ readFile(fileName: string, encoding?: string): string | undefined;
65
+ fileExists(fileName: string): boolean;
66
+ readDirectory: (path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) => string[];
67
+ directoryExists: (path: string) => boolean;
68
+ getDirectories: (path: string) => string[];
69
+ realpath: ((path: string) => string) | undefined;
70
+ }
71
+ /**
72
+ * Locates and parses the tsconfig driving the program.
73
+ *
74
+ * @param root Directory the search starts from.
75
+ * @param explicit Path given in the options, when there is one.
76
+ * @returns The parsed tsconfig.
77
+ * @throws {Error} When no tsconfig can be found or it cannot be read.
78
+ */
79
+ export declare const parseTsconfig: (root: string, explicit: string | undefined) => typescript.ParsedCommandLine;
3
80
  /** Options accepted by the transformer core. */
4
81
  export interface TransformCoreOptions extends TransformerOptions {
5
82
  /** Path of the tsconfig driving the program. Defaults to the nearest one. */
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createFileTransformer = void 0;
39
+ exports.createFileTransformer = exports.parseTsconfig = exports.ProgramHost = exports.toCompilerPath = void 0;
40
40
  const fs = __importStar(require("node:fs"));
41
41
  const path = __importStar(require("node:path"));
42
42
  const typescript_1 = __importDefault(require("typescript"));
@@ -86,6 +86,7 @@ const buildUtilityPattern = (rewriters) => {
86
86
  * @returns The path as the compiler spells it.
87
87
  */
88
88
  const toCompilerPath = (fileName) => path.resolve(fileName).split(path.sep).join('/');
89
+ exports.toCompilerPath = toCompilerPath;
89
90
  /**
90
91
  * Keeps a TypeScript program alive across recompilations.
91
92
  *
@@ -112,7 +113,7 @@ class ProgramHost {
112
113
  * @param currentDirectory Directory the program resolves against.
113
114
  */
114
115
  constructor(parsed, currentDirectory) {
115
- this.rootNames = parsed.fileNames.map(toCompilerPath);
116
+ this.rootNames = parsed.fileNames.map(exports.toCompilerPath);
116
117
  this.options = parsed.options;
117
118
  this.currentDirectory = currentDirectory;
118
119
  }
@@ -192,6 +193,7 @@ class ProgramHost {
192
193
  getDirectories = typescript_1.default.sys.getDirectories;
193
194
  realpath = typescript_1.default.sys.realpath;
194
195
  }
196
+ exports.ProgramHost = ProgramHost;
195
197
  /**
196
198
  * Locates and parses the tsconfig driving the program.
197
199
  *
@@ -214,6 +216,7 @@ const parseTsconfig = (root, explicit) => {
214
216
  }
215
217
  return typescript_1.default.parseJsonConfigFileContent(read.config, typescript_1.default.sys, path.dirname(configPath));
216
218
  };
219
+ exports.parseTsconfig = parseTsconfig;
217
220
  /**
218
221
  * Creates the transformer core.
219
222
  *
@@ -244,9 +247,9 @@ const createFileTransformer = (rewriters, options = {}) => {
244
247
  // would cost seconds for nothing.
245
248
  if (!utilityPattern.test(code))
246
249
  return null;
247
- const fileName = toCompilerPath(id.split('?')[0]);
250
+ const fileName = (0, exports.toCompilerPath)(id.split('?')[0]);
248
251
  if (host === undefined || service === undefined) {
249
- host = new ProgramHost(parseTsconfig(root, options.tsconfig), root);
252
+ host = new ProgramHost((0, exports.parseTsconfig)(root, options.tsconfig), root);
250
253
  service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry());
251
254
  }
252
255
  host.update(fileName, code);
@@ -0,0 +1,74 @@
1
+ import typescript from 'typescript';
2
+ import { type RewrittenText } from '../text/index.js';
3
+ /**
4
+ * One file rewritten before type checking.
5
+ *
6
+ * The call rewriters of `@/shared` run after the checker, on a tree whose types
7
+ * are already settled, and replace a call by a node. This is the other kind:
8
+ * it replaces **text**, before the checker has run on it, so that what it emits
9
+ * is what gets type checked. That is the only way to give a type a meaning the
10
+ * checker would otherwise refuse — `decimal * decimal` is a type error until it
11
+ * has become `decimal.multiply(decimal)`.
12
+ *
13
+ * The rewriter is asked about each node, outermost first, and answers with the
14
+ * text that replaces it, in which child nodes may appear to be rendered in turn.
15
+ * Everything it does not claim is copied verbatim, so the output differs from
16
+ * the input only where something was rewritten.
17
+ */
18
+ /** Everything a rewriter needs to decide about one node. */
19
+ export interface ExpressionContext {
20
+ /** Checker of the program the file belongs to. */
21
+ readonly checker: typescript.TypeChecker;
22
+ /** The file being rewritten. */
23
+ readonly sourceFile: typescript.SourceFile;
24
+ /**
25
+ * The line breaks the original text has between two offsets, as a string
26
+ * of newlines.
27
+ *
28
+ * Put into the synthesized text between two operands, it keeps every line
29
+ * of the file where it was — so an error the checker reports on the
30
+ * rewritten text still names the line the author wrote.
31
+ */
32
+ readonly lineBreaks: (start: number, end: number) => string;
33
+ }
34
+ /**
35
+ * What replaces a node: synthesized text and child nodes, in order. A node is
36
+ * rendered in turn — itself rewritten where the rewriter claims it — and may
37
+ * appear more than once.
38
+ */
39
+ export type Replacement = readonly (string | typescript.Node)[];
40
+ /** Rewrites the nodes of one kind of construct, before type checking. */
41
+ export interface ExpressionRewriter {
42
+ /**
43
+ * Module the rewritten code refers to, imported under
44
+ * {@link ExpressionRewriter.namespace} in every file that was rewritten.
45
+ */
46
+ readonly module: string;
47
+ /** Local name the module is imported under. */
48
+ readonly namespace: string;
49
+ /**
50
+ * Rewrites one node.
51
+ *
52
+ * @param node Node being considered.
53
+ * @param context The file and its checker.
54
+ * @returns The replacement, or `null` to keep the node and look inside it.
55
+ */
56
+ readonly rewrite: (node: typescript.Node, context: ExpressionContext) => Replacement | null;
57
+ }
58
+ /** How much a rewrite looked at, for the suites that count it. */
59
+ export interface RewriteStatistics {
60
+ /** Nodes the rewriter was asked about. */
61
+ visited: number;
62
+ /** Nodes it replaced. */
63
+ rewritten: number;
64
+ }
65
+ /**
66
+ * Rewrites one source file.
67
+ *
68
+ * @param sourceFile File to rewrite, from a program whose checker is given.
69
+ * @param checker Checker of that program.
70
+ * @param rewriter What to rewrite.
71
+ * @param statistics Counters to add to, when the caller keeps them.
72
+ * @returns The rewritten text and its map, or `null` when nothing was claimed.
73
+ */
74
+ export declare const rewriteSourceFile: (sourceFile: typescript.SourceFile, checker: typescript.TypeChecker, rewriter: ExpressionRewriter, statistics?: RewriteStatistics) => RewrittenText | null;
@@ -0,0 +1,124 @@
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.rewriteSourceFile = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const text_1 = require("../text/index.js");
9
+ /**
10
+ * Where the namespace import goes: on the first line of the first statement,
11
+ * before it and without a line break, so no line of the file moves. At the end
12
+ * of the file would keep the lines too, but a CommonJS emit leaves a `require`
13
+ * where the import was written, and code above it would run first.
14
+ *
15
+ * @param sourceFile File being rewritten.
16
+ * @returns The offset to insert at.
17
+ */
18
+ const importOffset = (sourceFile) => {
19
+ const [first] = sourceFile.statements;
20
+ return first === undefined ? sourceFile.end : first.getStart(sourceFile);
21
+ };
22
+ /**
23
+ * Rewrites one source file.
24
+ *
25
+ * @param sourceFile File to rewrite, from a program whose checker is given.
26
+ * @param checker Checker of that program.
27
+ * @param rewriter What to rewrite.
28
+ * @param statistics Counters to add to, when the caller keeps them.
29
+ * @returns The rewritten text and its map, or `null` when nothing was claimed.
30
+ */
31
+ const rewriteSourceFile = (sourceFile, checker, rewriter, statistics) => {
32
+ const original = sourceFile.text;
33
+ const output = (0, text_1.createTextBuilder)(original);
34
+ const context = {
35
+ checker,
36
+ sourceFile,
37
+ lineBreaks: (start, end) => '\n'.repeat(original.slice(start, end).split('\n').length - 1),
38
+ };
39
+ const importDeclaration = `import * as ${rewriter.namespace} from '${rewriter.module}'; `;
40
+ let cursor = 0;
41
+ let rewroteAny = false;
42
+ /**
43
+ * Copies original text from the cursor up to an offset.
44
+ *
45
+ * @param end Offset to copy up to.
46
+ */
47
+ const advance = (end) => {
48
+ output.copy(cursor, end);
49
+ cursor = Math.max(cursor, end);
50
+ };
51
+ const render = (node) => {
52
+ if (statistics !== undefined)
53
+ statistics.visited++;
54
+ const replacement = rewriter.rewrite(node, context);
55
+ if (replacement === null) {
56
+ typescript_1.default.forEachChild(node, (child) => {
57
+ advance(child.getStart(sourceFile));
58
+ render(child);
59
+ });
60
+ return;
61
+ }
62
+ rewroteAny = true;
63
+ if (statistics !== undefined)
64
+ statistics.rewritten++;
65
+ const start = node.getStart(sourceFile);
66
+ advance(start);
67
+ const close = output.open(start, node.end);
68
+ for (const part of replacement) {
69
+ if (typeof part === 'string') {
70
+ output.write(part);
71
+ }
72
+ else {
73
+ // A child is rendered from its own start, independently of the
74
+ // cursor: it may be written twice, and in any order.
75
+ const saved = cursor;
76
+ cursor = part.getStart(sourceFile);
77
+ render(part);
78
+ output.copy(cursor, part.end);
79
+ cursor = saved;
80
+ }
81
+ }
82
+ close();
83
+ cursor = node.end;
84
+ };
85
+ typescript_1.default.forEachChild(sourceFile, (child) => {
86
+ advance(child.getStart(sourceFile));
87
+ render(child);
88
+ });
89
+ advance(sourceFile.end);
90
+ if (!rewroteAny)
91
+ return null;
92
+ // A second pass over an already rewritten file finds the import there and
93
+ // must not add another. The import is only known to be needed once the
94
+ // walk is over, so it is spliced in afterwards rather than written in
95
+ // passing.
96
+ if (original.includes(importDeclaration))
97
+ return output.build();
98
+ return withImport(output.build(), importOffset(sourceFile), importDeclaration);
99
+ };
100
+ exports.rewriteSourceFile = rewriteSourceFile;
101
+ /**
102
+ * Inserts the namespace import into a rewritten text, extending its map.
103
+ *
104
+ * @param rewritten The rewritten text, without the import.
105
+ * @param originalOffset Where, in the original, the import goes.
106
+ * @param declaration The import declaration, with its trailing space.
107
+ * @returns The text with the import and a map that accounts for it.
108
+ */
109
+ const withImport = (rewritten, originalOffset, declaration) => {
110
+ const at = rewritten.toGenerated(originalOffset);
111
+ const length = declaration.length;
112
+ return {
113
+ text: rewritten.text.slice(0, at) + declaration + rewritten.text.slice(at),
114
+ toOriginal: (generated) => generated < at
115
+ ? rewritten.toOriginal(generated)
116
+ : generated < at + length
117
+ ? originalOffset
118
+ : rewritten.toOriginal(generated - length),
119
+ toGenerated: (original) => {
120
+ const position = rewritten.toGenerated(original);
121
+ return position < at ? position : position + length;
122
+ },
123
+ };
124
+ };
@@ -0,0 +1,10 @@
1
+ import { type FileTransformer, type TransformCoreOptions } from '../../program/index.js';
2
+ import { type ExpressionRewriter } from '../file/index.js';
3
+ /**
4
+ * Creates the bundler core of a rewriter.
5
+ *
6
+ * @param rewriter What to rewrite.
7
+ * @param options Options of the core.
8
+ * @returns A transformer any bundler adapter can drive.
9
+ */
10
+ export declare const createRewritingFileTransformer: (rewriter: ExpressionRewriter, options?: TransformCoreOptions) => FileTransformer;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createRewritingFileTransformer = void 0;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const typescript_1 = __importDefault(require("typescript"));
43
+ const program_1 = require("../../program/index.js");
44
+ const fixpoint_1 = require("../fixpoint/index.js");
45
+ /**
46
+ * The bundler integration of a rewrite that has to happen before type
47
+ * checking.
48
+ *
49
+ * A bundler never type checks — esbuild and swc erase the types — so an error
50
+ * the checker would raise on `decimal * decimal` never stops it. What it does
51
+ * need is the rewrite itself, and that needs the whole program: whether `c + d`
52
+ * in one file is claimable depends on how `c` was declared in another. So the
53
+ * first file to arrive rewrites the whole program to a fixed point, and every
54
+ * file after it is answered from that until some file changes.
55
+ */
56
+ /** Extensions carrying TypeScript this transformer is responsible for. */
57
+ const HANDLED_EXTENSIONS = ['.ts', '.mts', '.cts', '.tsx'];
58
+ /**
59
+ * Creates the bundler core of a rewriter.
60
+ *
61
+ * @param rewriter What to rewrite.
62
+ * @param options Options of the core.
63
+ * @returns A transformer any bundler adapter can drive.
64
+ */
65
+ const createRewritingFileTransformer = (rewriter, options = {}) => {
66
+ const root = options.root ?? process.cwd();
67
+ let host;
68
+ let service;
69
+ let rewrite;
70
+ /** What the bundler last handed over for each file, before any rewrite. */
71
+ const originals = new Map();
72
+ const handles = (id) => {
73
+ const fileName = id.split('?')[0];
74
+ if (!HANDLED_EXTENSIONS.includes(path.extname(fileName)))
75
+ return false;
76
+ return !fileName.includes('node_modules');
77
+ };
78
+ /**
79
+ * Puts every rewritten file back to its original text, so the next rewrite
80
+ * starts from what the author wrote rather than from its own output.
81
+ *
82
+ * @param programHost The host holding the texts.
83
+ */
84
+ const restore = (programHost) => {
85
+ for (const fileName of rewrite?.files.keys() ?? []) {
86
+ const text = originals.get(fileName) ??
87
+ (fs.existsSync(fileName)
88
+ ? fs.readFileSync(fileName, 'utf8')
89
+ : undefined);
90
+ if (text !== undefined)
91
+ programHost.update(fileName, text);
92
+ }
93
+ rewrite = undefined;
94
+ };
95
+ const transform = (id, code) => {
96
+ if (!handles(id))
97
+ return null;
98
+ const fileName = (0, program_1.toCompilerPath)(id.split('?')[0]);
99
+ if (host === undefined || service === undefined) {
100
+ host = new program_1.ProgramHost((0, program_1.parseTsconfig)(root, options.tsconfig), root);
101
+ service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry());
102
+ }
103
+ if (originals.get(fileName) !== code) {
104
+ originals.set(fileName, code);
105
+ restore(host);
106
+ host.update(fileName, code);
107
+ }
108
+ if (rewrite === undefined) {
109
+ const programHost = host;
110
+ const languageService = service;
111
+ rewrite = (0, fixpoint_1.rewriteToFixpoint)({
112
+ program: () => {
113
+ const program = languageService.getProgram();
114
+ if (program === undefined) {
115
+ throw new Error(`The language service produced no program for ${root}.`);
116
+ }
117
+ return program;
118
+ },
119
+ update: (name, text) => programHost.update(name, text),
120
+ }, rewriter);
121
+ }
122
+ const sourceFile = service
123
+ .getProgram()
124
+ ?.getSourceFile(fileName);
125
+ if (sourceFile !== undefined && !(0, fixpoint_1.isRewritable)(sourceFile))
126
+ return null;
127
+ return rewrite.files.get(fileName)?.text ?? null;
128
+ };
129
+ return { handles, transform };
130
+ };
131
+ exports.createRewritingFileTransformer = createRewritingFileTransformer;
@@ -0,0 +1,56 @@
1
+ import typescript from 'typescript';
2
+ import { type ExpressionRewriter, type RewriteStatistics } from '../file/index.js';
3
+ import { type RewrittenText } from '../text/index.js';
4
+ /**
5
+ * Rewriting a whole program until nothing more is claimed.
6
+ *
7
+ * One pass is not enough, because rewriting changes what the checker infers.
8
+ * `const c = a + b` types `c` as `number` in the original — the checker has no
9
+ * idea `+` means anything else — so a later `c + d` looks like arithmetic on a
10
+ * plain number and is left alone. Once the first line reads
11
+ * `const c = T.add(a, b)`, `c` has the type `T.add` returns, and the second
12
+ * line is claimable. So the program is rewritten, re-checked, and rewritten
13
+ * again until a pass claims nothing — each pass can only claim sites the one
14
+ * before could not, so the loop ends, and a bound on it makes sure it does.
15
+ *
16
+ * Where the program comes from is the caller's: a fresh `createProgram` for
17
+ * `tsc`, a language service for a bundler or an editor. This only needs to ask
18
+ * for the current program and to hand text back.
19
+ */
20
+ /** Something that holds a program and can have its files replaced. */
21
+ export interface ProgramSource {
22
+ /** The program as it stands, with every text handed back so far. */
23
+ readonly program: () => typescript.Program;
24
+ /**
25
+ * Replaces the text of a file for the next {@link ProgramSource.program}.
26
+ *
27
+ * @param fileName File, as the program spells it.
28
+ * @param text Its new text.
29
+ */
30
+ readonly update: (fileName: string, text: string) => void;
31
+ }
32
+ /** The outcome of rewriting a program. */
33
+ export interface ProgramRewrite {
34
+ /** Each rewritten file, mapped back to its original text. */
35
+ readonly files: ReadonlyMap<string, RewrittenText>;
36
+ /** Passes it took, the last one claiming nothing. */
37
+ readonly passes: number;
38
+ /** Nodes considered and replaced, over every pass. */
39
+ readonly statistics: RewriteStatistics;
40
+ }
41
+ /**
42
+ * Tells whether a file is the consumer's own source, which is all a rewrite
43
+ * touches: never a declaration file, never a dependency.
44
+ *
45
+ * @param sourceFile File of the program.
46
+ * @returns `true` when it may be rewritten.
47
+ */
48
+ export declare const isRewritable: (sourceFile: typescript.SourceFile) => boolean;
49
+ /**
50
+ * Rewrites a program to a fixed point.
51
+ *
52
+ * @param source Where the program comes from.
53
+ * @param rewriter What to rewrite.
54
+ * @returns The rewritten files and what it took.
55
+ */
56
+ export declare const rewriteToFixpoint: (source: ProgramSource, rewriter: ExpressionRewriter) => ProgramRewrite;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rewriteToFixpoint = exports.isRewritable = void 0;
4
+ const file_1 = require("../file/index.js");
5
+ const text_1 = require("../text/index.js");
6
+ /**
7
+ * Passes after which the loop stops whether or not it has settled. Each pass
8
+ * resolves one more step of inference through a chain of declarations, and a
9
+ * program needing more than this has a chain of `const` declarations longer
10
+ * than anyone writes — its remaining sites are left as the checker sees them,
11
+ * and report their own type errors.
12
+ */
13
+ const MAXIMUM_PASSES = 16;
14
+ /**
15
+ * Tells whether a file is the consumer's own source, which is all a rewrite
16
+ * touches: never a declaration file, never a dependency.
17
+ *
18
+ * @param sourceFile File of the program.
19
+ * @returns `true` when it may be rewritten.
20
+ */
21
+ const isRewritable = (sourceFile) => !sourceFile.isDeclarationFile &&
22
+ !sourceFile.fileName.includes('/node_modules/');
23
+ exports.isRewritable = isRewritable;
24
+ /**
25
+ * Rewrites a program to a fixed point.
26
+ *
27
+ * @param source Where the program comes from.
28
+ * @param rewriter What to rewrite.
29
+ * @returns The rewritten files and what it took.
30
+ */
31
+ const rewriteToFixpoint = (source, rewriter) => {
32
+ const files = new Map();
33
+ const statistics = { visited: 0, rewritten: 0 };
34
+ let passes = 0;
35
+ while (passes < MAXIMUM_PASSES) {
36
+ passes++;
37
+ const program = source.program();
38
+ const checker = program.getTypeChecker();
39
+ const changed = [];
40
+ for (const sourceFile of program.getSourceFiles()) {
41
+ if (!(0, exports.isRewritable)(sourceFile))
42
+ continue;
43
+ const rewritten = (0, file_1.rewriteSourceFile)(sourceFile, checker, rewriter, statistics);
44
+ if (rewritten !== null)
45
+ changed.push([sourceFile.fileName, rewritten]);
46
+ }
47
+ if (changed.length === 0)
48
+ break;
49
+ // Handed back only once the pass is over, so every file of a pass is
50
+ // read against the same program.
51
+ for (const [fileName, rewritten] of changed) {
52
+ const previous = files.get(fileName);
53
+ files.set(fileName, previous === undefined
54
+ ? rewritten
55
+ : (0, text_1.composeRewrites)(previous, rewritten));
56
+ source.update(fileName, rewritten.text);
57
+ }
58
+ }
59
+ return { files, passes, statistics };
60
+ };
61
+ exports.rewriteToFixpoint = rewriteToFixpoint;
@@ -0,0 +1,40 @@
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;