@fulcro/transform-core 0.2.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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 diguu <rodrigogeribola@hotmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @fulcro/transform-core
2
+
3
+ Shared machinery behind the Fulcro compile time transformers.
4
+
5
+ **You do not install this.** `@fulcro/reflect` depends on it, and any other
6
+ package in the library that grows a transformer will too. It is published
7
+ because they depend on it, not because it is meant to be used directly, and its
8
+ shape is theirs to change without notice.
9
+
10
+ If you are looking for the compiler plugin, it lives in the package whose calls
11
+ it rewrites:
12
+
13
+ ```json
14
+ { "plugins": [{ "transform": "@fulcro/reflect/transformer" }] }
15
+ ```
16
+
17
+ ## Why it exists
18
+
19
+ The library used to ship one transformer, `@fulcro/transformer`, installed
20
+ beside `@fulcro/reflect` as a peer dependency. Two things were wrong with that.
21
+
22
+ **It could go missing.** npm installs peer dependencies and Yarn does not, so a
23
+ project could end up with the utilities and no transformer. The failure was
24
+ quiet, because the utilities are built to degrade rather than crash: `defaultOf`
25
+ throws, `nameOf` falls back to parsing closures, `typeOf(…).declared` reads
26
+ `null`. Nothing says _your build is missing a plugin_.
27
+
28
+ **It could go out of step.** `@fulcro/reflect@0.3` with
29
+ `@fulcro/transformer@0.2` was an installable, broken combination, because what a
30
+ call means and what it compiles to were versioned separately.
31
+
32
+ Both disappear when the transformer ships inside the package whose calls it
33
+ rewrites. What is left over is the part that belongs to no package in
34
+ particular — and that is this one.
35
+
36
+ ## What is in here
37
+
38
+ Everything with nothing to do with any particular utility:
39
+
40
+ | Piece | Does |
41
+ | --------------------------- | --------------------------------------------------------------------------- |
42
+ | `isOwnedCall` | Follows a call back to the declaration that owns it, rather than by name |
43
+ | `createTransformer` | Walks a source file once and hands each call to the rewriter that claims it |
44
+ | `createFileTransformer` | Builds and keeps a program, for the bundlers that have no checker |
45
+ | `createTransformerUnplugin` | The adapter surface Vite, Rollup, Webpack, Rspack, esbuild and Farm expect |
46
+
47
+ What lives in each library instead is the part that knows what to **emit** — how
48
+ `defaultOf` fills a tuple, how a type argument becomes a runtime test. That
49
+ split is why a package can own its compile time behaviour without a second
50
+ install, and why two of them can do so without knowing about each other.
51
+
52
+ ## TypeScript 5 and 6 only
53
+
54
+ ```json
55
+ { "typescript": ">=5.3.3 <7" }
56
+ ```
57
+
58
+ 7.x is the native port, and its package no longer exposes the compiler API this
59
+ is built on. Declared as an **optional** peer dependency, so that a project only
60
+ using the runtime half of a library never has to install a compiler it is not
61
+ going to run.
62
+
63
+ ## Licence
64
+
65
+ ISC.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared machinery behind the Fulcro compile time transformers.
3
+ *
4
+ * **Not a package to depend on directly.** It is installed for you by
5
+ * `@fulcro/reflect` and `@fulcro/collections`, each of which ships its own
6
+ * transformer built on this, and its shape is theirs to change.
7
+ *
8
+ * What lives here is everything that has nothing to do with any particular
9
+ * utility: following a call back to the declaration that owns it, walking a
10
+ * source file once, building and keeping a program for the bundlers that have
11
+ * no checker of their own, and the adapter surface `unplugin` expects.
12
+ *
13
+ * What lives in each library instead is the part that knows what to emit — how
14
+ * `defaultOf` fills a tuple, how `ofType` turns a type argument into a runtime
15
+ * test. That split is why a package can own its compile time behaviour without
16
+ * a second install, and why two of them can do so without knowing about each
17
+ * other.
18
+ */
19
+ export { createFileTransformer, type FileTransformer, type TransformCoreOptions, } from './program/index.js';
20
+ export { type CallRewriter, IDENTIFIER_PATTERN, isOwnedCall, isTupleType, type RewriteContext, utilityModuleSegment, } from './shared/index.js';
21
+ export { createTransformer, type TransformerFactory, type TransformerOptions, } from './transformer/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Shared machinery behind the Fulcro compile time transformers.
4
+ *
5
+ * **Not a package to depend on directly.** It is installed for you by
6
+ * `@fulcro/reflect` and `@fulcro/collections`, each of which ships its own
7
+ * transformer built on this, and its shape is theirs to change.
8
+ *
9
+ * What lives here is everything that has nothing to do with any particular
10
+ * utility: following a call back to the declaration that owns it, walking a
11
+ * source file once, building and keeping a program for the bundlers that have
12
+ * no checker of their own, and the adapter surface `unplugin` expects.
13
+ *
14
+ * What lives in each library instead is the part that knows what to emit — how
15
+ * `defaultOf` fills a tuple, how `ofType` turns a type argument into a runtime
16
+ * test. That split is why a package can own its compile time behaviour without
17
+ * a second install, and why two of them can do so without knowing about each
18
+ * other.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.createTransformer = exports.utilityModuleSegment = exports.isTupleType = exports.isOwnedCall = exports.IDENTIFIER_PATTERN = exports.createFileTransformer = void 0;
22
+ var program_1 = require("./program/index.js");
23
+ Object.defineProperty(exports, "createFileTransformer", { enumerable: true, get: function () { return program_1.createFileTransformer; } });
24
+ var shared_1 = require("./shared/index.js");
25
+ Object.defineProperty(exports, "IDENTIFIER_PATTERN", { enumerable: true, get: function () { return shared_1.IDENTIFIER_PATTERN; } });
26
+ Object.defineProperty(exports, "isOwnedCall", { enumerable: true, get: function () { return shared_1.isOwnedCall; } });
27
+ Object.defineProperty(exports, "isTupleType", { enumerable: true, get: function () { return shared_1.isTupleType; } });
28
+ Object.defineProperty(exports, "utilityModuleSegment", { enumerable: true, get: function () { return shared_1.utilityModuleSegment; } });
29
+ var transformer_1 = require("./transformer/index.js");
30
+ Object.defineProperty(exports, "createTransformer", { enumerable: true, get: function () { return transformer_1.createTransformer; } });
@@ -0,0 +1,44 @@
1
+ import { CallRewriter } from '../shared/index.js';
2
+ import { TransformerOptions } from '../transformer/index.js';
3
+ /** Options accepted by the transformer core. */
4
+ export interface TransformCoreOptions extends TransformerOptions {
5
+ /** Path of the tsconfig driving the program. Defaults to the nearest one. */
6
+ readonly tsconfig?: string;
7
+ /** Directory the tsconfig search and the program resolve against. */
8
+ readonly root?: string;
9
+ }
10
+ /** Transforms one file, or declines it. */
11
+ export interface FileTransformer {
12
+ /**
13
+ * Tells whether a file is one this transformer is responsible for.
14
+ *
15
+ * Decided from the path alone, because the bundlers that filter before
16
+ * reading a file have nothing else to offer at that point. Whether the file
17
+ * actually calls anything is settled later, inside
18
+ * {@link FileTransformer.transform}.
19
+ *
20
+ * @param id Identifier of the file, as the bundler spells it.
21
+ * @returns `true` when the file is a TypeScript source of the project.
22
+ */
23
+ readonly handles: (id: string) => boolean;
24
+ /**
25
+ * Applies the transformer to one file.
26
+ *
27
+ * @param id Identifier of the file, as the bundler spells it.
28
+ * @param code Current content of the file.
29
+ * @returns The rewritten TypeScript, or `null` when nothing was done and the
30
+ * bundler should keep the original.
31
+ */
32
+ readonly transform: (id: string, code: string) => string | null;
33
+ }
34
+ /**
35
+ * Creates the transformer core.
36
+ *
37
+ * The program is built on the first file that actually needs it, so a project
38
+ * never calling the utilities pays nothing.
39
+ *
40
+ * @param rewriters Rewriters of the package this core is serving.
41
+ * @param options Options of the core.
42
+ * @returns A transformer usable by any bundler adapter.
43
+ */
44
+ export declare const createFileTransformer: (rewriters: readonly CallRewriter[], options?: TransformCoreOptions) => FileTransformer;
@@ -0,0 +1,269 @@
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.createFileTransformer = 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 transformer_1 = require("../transformer/index.js");
44
+ /**
45
+ * Type checking core shared by every bundler integration.
46
+ *
47
+ * The transformer needs a type checker, and no bundler has one: esbuild and swc
48
+ * reach TypeScript by erasing its types, never by resolving them, so a plugin
49
+ * cannot simply hand them a transformer and expect `defaultOf<Order>()` to mean
50
+ * anything. This module therefore builds and keeps its own program, and
51
+ * transforms each file itself.
52
+ *
53
+ * Nothing here knows about any bundler: the adapters built on top only have to
54
+ * call {@link createFileTransformer} and hand back what it returns. Kept as
55
+ * CommonJS as well, because the compiler plugin loads it through `require`.
56
+ */
57
+ /** Extensions carrying TypeScript this core is responsible for. */
58
+ const HANDLED_EXTENSIONS = ['.ts', '.mts', '.cts'];
59
+ /**
60
+ * Builds the cheap pre-filter that skips files with nothing to rewrite.
61
+ *
62
+ * Reading a file is far cheaper than type checking it, so a source mentioning
63
+ * none of the owned function names never reaches the program. Derived from the
64
+ * rewriters rather than written out, because each package brings its own names
65
+ * and this core belongs to none of them.
66
+ *
67
+ * @param rewriters Rewriters the core was built with.
68
+ * @returns A pattern matching any of their function names as a whole word.
69
+ */
70
+ const buildUtilityPattern = (rewriters) => {
71
+ const names = rewriters
72
+ .map((rewriter) => rewriter.functionName)
73
+ .join('|');
74
+ return new RegExp(`\\b(${names})\\b`);
75
+ };
76
+ /**
77
+ * Rewrites a path the way the compiler spells it.
78
+ *
79
+ * TypeScript keeps every file name with forward slashes, on every platform,
80
+ * while the bundler and `path.normalize` hand back the native separator. Mixing
81
+ * the two silently defeats every lookup against the files of the program: a
82
+ * file already in it looks new, gets added as another root, and invalidates the
83
+ * whole program — turning one startup cost into one per file.
84
+ *
85
+ * @param fileName Path in whatever spelling it arrived.
86
+ * @returns The path as the compiler spells it.
87
+ */
88
+ const toCompilerPath = (fileName) => path.resolve(fileName).split(path.sep).join('/');
89
+ /**
90
+ * Keeps a TypeScript program alive across recompilations.
91
+ *
92
+ * A language service rather than a one shot program: a watch run recompiles the
93
+ * same files repeatedly, and rebuilding the whole program on each edit would
94
+ * cost seconds every keystroke. The service reuses everything that did not
95
+ * change, and the version counters below are what tell it what did.
96
+ */
97
+ class ProgramHost {
98
+ /** Files of the program, as resolved from the tsconfig. */
99
+ rootNames;
100
+ /** Compiler options resolved from the tsconfig. */
101
+ options;
102
+ /** Content handed over by the bundler, which may be ahead of the disk. */
103
+ overlays = new Map();
104
+ /** Bumped whenever a file changes, which is how the service invalidates. */
105
+ versions = new Map();
106
+ /** Directory the relative paths of the program resolve against. */
107
+ currentDirectory;
108
+ /**
109
+ * Initializes the host from a parsed tsconfig.
110
+ *
111
+ * @param parsed Parsed contents of the tsconfig.
112
+ * @param currentDirectory Directory the program resolves against.
113
+ */
114
+ constructor(parsed, currentDirectory) {
115
+ this.rootNames = parsed.fileNames.map(toCompilerPath);
116
+ this.options = parsed.options;
117
+ this.currentDirectory = currentDirectory;
118
+ }
119
+ /**
120
+ * Records the content of a file as the bundler sees it.
121
+ *
122
+ * @param fileName File being compiled.
123
+ * @param content Current content of the file.
124
+ */
125
+ update(fileName, content) {
126
+ if (this.overlays.get(fileName) === content)
127
+ return;
128
+ // On a cold run the bundler hands over exactly what is on disk, for
129
+ // every file of the project. Recording those as changes would
130
+ // invalidate and re-check the program once per file, turning a single
131
+ // startup cost into a quadratic one — so an unchanged file is left
132
+ // alone and stays at the version the program was built with.
133
+ if (!this.overlays.has(fileName) &&
134
+ this.rootNames.includes(fileName) &&
135
+ this.matchesDisk(fileName, content)) {
136
+ return;
137
+ }
138
+ this.overlays.set(fileName, content);
139
+ this.versions.set(fileName, (this.versions.get(fileName) ?? 0) + 1);
140
+ // A file the tsconfig never listed — created after startup, or simply
141
+ // excluded — still has to enter the program to be transformed.
142
+ if (!this.rootNames.includes(fileName))
143
+ this.rootNames.push(fileName);
144
+ }
145
+ /**
146
+ * Tells whether the content handed over matches what is on disk.
147
+ *
148
+ * @param fileName File being compiled.
149
+ * @param content Content handed over by the bundler.
150
+ * @returns `true` when the two are identical.
151
+ */
152
+ matchesDisk(fileName, content) {
153
+ try {
154
+ return fs.readFileSync(fileName, 'utf8') === content;
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ getScriptFileNames() {
161
+ return this.rootNames;
162
+ }
163
+ getScriptVersion(fileName) {
164
+ return String(this.versions.get(fileName) ?? 0);
165
+ }
166
+ getScriptSnapshot(fileName) {
167
+ const overlay = this.overlays.get(fileName);
168
+ if (overlay !== undefined) {
169
+ return typescript_1.default.ScriptSnapshot.fromString(overlay);
170
+ }
171
+ if (!fs.existsSync(fileName))
172
+ return undefined;
173
+ return typescript_1.default.ScriptSnapshot.fromString(fs.readFileSync(fileName, 'utf8'));
174
+ }
175
+ getCurrentDirectory() {
176
+ return this.currentDirectory;
177
+ }
178
+ getCompilationSettings() {
179
+ return this.options;
180
+ }
181
+ getDefaultLibFileName(options) {
182
+ return typescript_1.default.getDefaultLibFilePath(options);
183
+ }
184
+ readFile(fileName, encoding) {
185
+ return (this.overlays.get(fileName) ?? typescript_1.default.sys.readFile(fileName, encoding));
186
+ }
187
+ fileExists(fileName) {
188
+ return this.overlays.has(fileName) || typescript_1.default.sys.fileExists(fileName);
189
+ }
190
+ readDirectory = typescript_1.default.sys.readDirectory;
191
+ directoryExists = typescript_1.default.sys.directoryExists;
192
+ getDirectories = typescript_1.default.sys.getDirectories;
193
+ realpath = typescript_1.default.sys.realpath;
194
+ }
195
+ /**
196
+ * Locates and parses the tsconfig driving the program.
197
+ *
198
+ * @param root Directory the search starts from.
199
+ * @param explicit Path given in the options, when there is one.
200
+ * @returns The parsed tsconfig.
201
+ * @throws {Error} When no tsconfig can be found or it cannot be read.
202
+ */
203
+ const parseTsconfig = (root, explicit) => {
204
+ const configPath = explicit !== undefined
205
+ ? path.resolve(root, explicit)
206
+ : typescript_1.default.findConfigFile(root, typescript_1.default.sys.fileExists);
207
+ if (configPath === undefined) {
208
+ throw new Error(`No tsconfig.json found from ${root}. The transformer needs one to ` +
209
+ 'know which files belong to the program.');
210
+ }
211
+ const read = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
212
+ if (read.error !== undefined) {
213
+ throw new Error(typescript_1.default.flattenDiagnosticMessageText(read.error.messageText, '\n'));
214
+ }
215
+ return typescript_1.default.parseJsonConfigFileContent(read.config, typescript_1.default.sys, path.dirname(configPath));
216
+ };
217
+ /**
218
+ * Creates the transformer core.
219
+ *
220
+ * The program is built on the first file that actually needs it, so a project
221
+ * never calling the utilities pays nothing.
222
+ *
223
+ * @param rewriters Rewriters of the package this core is serving.
224
+ * @param options Options of the core.
225
+ * @returns A transformer usable by any bundler adapter.
226
+ */
227
+ const createFileTransformer = (rewriters, options = {}) => {
228
+ const root = options.root ?? process.cwd();
229
+ const utilityPattern = buildUtilityPattern(rewriters);
230
+ const transformer = (0, transformer_1.createTransformer)(rewriters);
231
+ let host;
232
+ let service;
233
+ const handles = (id) => {
234
+ const fileName = id.split('?')[0];
235
+ if (!HANDLED_EXTENSIONS.includes(path.extname(fileName)))
236
+ return false;
237
+ return !fileName.includes('node_modules');
238
+ };
239
+ const transform = (id, code) => {
240
+ if (!handles(id))
241
+ return null;
242
+ // Cheap rejection before anything expensive: most files of a project
243
+ // mention none of the utilities, and building the program for them
244
+ // would cost seconds for nothing.
245
+ if (!utilityPattern.test(code))
246
+ return null;
247
+ const fileName = toCompilerPath(id.split('?')[0]);
248
+ if (host === undefined || service === undefined) {
249
+ host = new ProgramHost(parseTsconfig(root, options.tsconfig), root);
250
+ service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry());
251
+ }
252
+ host.update(fileName, code);
253
+ const program = service.getProgram();
254
+ const sourceFile = program?.getSourceFile(fileName);
255
+ if (program === undefined || sourceFile === undefined)
256
+ return null;
257
+ const result = typescript_1.default.transform(sourceFile, [transformer(program, { projectRoot: options.projectRoot ?? root })], program.getCompilerOptions());
258
+ const [transformed] = result.transformed;
259
+ const printed = typescript_1.default
260
+ .createPrinter({ newLine: typescript_1.default.NewLineKind.LineFeed })
261
+ .printFile(transformed);
262
+ result.dispose();
263
+ // Still TypeScript, only with the calls resolved: the bundler goes on to
264
+ // erase the types as it would have anyway.
265
+ return printed;
266
+ };
267
+ return { handles, transform };
268
+ };
269
+ exports.createFileTransformer = createFileTransformer;
@@ -0,0 +1,84 @@
1
+ import typescript from 'typescript';
2
+ /**
3
+ * Contract shared by the rewriters of the transformer.
4
+ *
5
+ * Each utility owns one module here, so that a change to how `defaultOf` fills
6
+ * a tuple never has to be made inside the same file that decides how `typeOf`
7
+ * reports a declaration site. The composition root walks the tree once and asks
8
+ * each rewriter whether the call in hand belongs to it.
9
+ */
10
+ /** Everything a rewriter needs from the compilation in progress. */
11
+ export interface RewriteContext {
12
+ /** Checker of the program being compiled. */
13
+ readonly checker: typescript.TypeChecker;
14
+ /** Node factory of the current transformation. */
15
+ readonly factory: typescript.NodeFactory;
16
+ /** Root that reported declaration paths are made relative to. */
17
+ readonly projectRoot: string;
18
+ /**
19
+ * Visits a node with the whole transformer.
20
+ *
21
+ * Handed over so that a rewriter keeping part of the original call — as
22
+ * `typeOf` does with its argument — still has the other utilities applied
23
+ * inside whatever it keeps.
24
+ */
25
+ readonly visit: (node: typescript.Node) => typescript.Node;
26
+ }
27
+ /** Rewrites the calls of a single utility. */
28
+ export interface CallRewriter {
29
+ /** Name the exported function is called by. */
30
+ readonly functionName: string;
31
+ /** Path segment identifying the module that declares it. */
32
+ readonly moduleSegment: string;
33
+ /**
34
+ * Rewrites one call.
35
+ *
36
+ * @param call Call already known to belong to this rewriter.
37
+ * @param context Compilation in progress.
38
+ * @returns The replacement node, or `null` to leave the call untouched so
39
+ * that the runtime implementation stays in charge.
40
+ */
41
+ readonly rewrite: (call: typescript.CallExpression, context: RewriteContext) => typescript.Node | null;
42
+ }
43
+ /** Property names that can be emitted unquoted in an object literal. */
44
+ export declare const IDENTIFIER_PATTERN: RegExp;
45
+ /**
46
+ * Builds the path segment identifying a utility module.
47
+ *
48
+ * These segments tie this package to the folder layout `@fulcro/reflect`
49
+ * publishes — a call is recognised by the module that declares it, and after
50
+ * resolution that module is `functions/utils/<name>` inside the built output of
51
+ * that package. Moving those folders there silently stops the rewriting here,
52
+ * because a mismatch does not fail loudly on its own: the transformer simply
53
+ * leaves the calls alone and the runtime fallbacks take over. The compile
54
+ * fixture is what catches it, and it imports `@fulcro/reflect` by name rather
55
+ * than by path precisely so that it resolves the same way a consumer would.
56
+ *
57
+ * Matched exactly, casing included.
58
+ *
59
+ * @param name Folder of the utility inside the output of `@fulcro/reflect`.
60
+ * @returns The segment to look for in a declaration path.
61
+ */
62
+ export declare const utilityModuleSegment: (name: string) => string;
63
+ /**
64
+ * Tells whether a call resolves to the function a rewriter owns.
65
+ *
66
+ * The symbol is followed back to its declaration rather than matched by name,
67
+ * so an unrelated local `nameOf` is never rewritten.
68
+ *
69
+ * @param call Call being inspected.
70
+ * @param checker Checker of the program being compiled.
71
+ * @param rewriter Rewriter claiming the call.
72
+ * @returns `true` when the call targets the function of that rewriter.
73
+ */
74
+ export declare const isOwnedCall: (call: typescript.CallExpression, checker: typescript.TypeChecker, rewriter: CallRewriter) => boolean;
75
+ /**
76
+ * Tells whether an object type is a tuple.
77
+ *
78
+ * Shared because both the description built by `typeOf` and the value built by
79
+ * `defaultOf` have to tell a tuple apart from a plain array.
80
+ *
81
+ * @param type Type being inspected.
82
+ * @returns `true` when the type is a tuple.
83
+ */
84
+ export declare const isTupleType: (type: typescript.ObjectType) => boolean;
@@ -0,0 +1,102 @@
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.isTupleType = exports.isOwnedCall = exports.utilityModuleSegment = exports.IDENTIFIER_PATTERN = void 0;
40
+ const path = __importStar(require("node:path"));
41
+ const typescript_1 = __importDefault(require("typescript"));
42
+ /** Property names that can be emitted unquoted in an object literal. */
43
+ exports.IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
44
+ /**
45
+ * Builds the path segment identifying a utility module.
46
+ *
47
+ * These segments tie this package to the folder layout `@fulcro/reflect`
48
+ * publishes — a call is recognised by the module that declares it, and after
49
+ * resolution that module is `functions/utils/<name>` inside the built output of
50
+ * that package. Moving those folders there silently stops the rewriting here,
51
+ * because a mismatch does not fail loudly on its own: the transformer simply
52
+ * leaves the calls alone and the runtime fallbacks take over. The compile
53
+ * fixture is what catches it, and it imports `@fulcro/reflect` by name rather
54
+ * than by path precisely so that it resolves the same way a consumer would.
55
+ *
56
+ * Matched exactly, casing included.
57
+ *
58
+ * @param name Folder of the utility inside the output of `@fulcro/reflect`.
59
+ * @returns The segment to look for in a declaration path.
60
+ */
61
+ const utilityModuleSegment = (name) => path.join('functions', 'utils', name);
62
+ exports.utilityModuleSegment = utilityModuleSegment;
63
+ /**
64
+ * Tells whether a call resolves to the function a rewriter owns.
65
+ *
66
+ * The symbol is followed back to its declaration rather than matched by name,
67
+ * so an unrelated local `nameOf` is never rewritten.
68
+ *
69
+ * @param call Call being inspected.
70
+ * @param checker Checker of the program being compiled.
71
+ * @param rewriter Rewriter claiming the call.
72
+ * @returns `true` when the call targets the function of that rewriter.
73
+ */
74
+ const isOwnedCall = (call, checker, rewriter) => {
75
+ if (!typescript_1.default.isIdentifier(call.expression))
76
+ return false;
77
+ if (call.expression.text !== rewriter.functionName)
78
+ return false;
79
+ const symbol = checker.getSymbolAtLocation(call.expression);
80
+ const resolved = symbol !== undefined && (symbol.flags & typescript_1.default.SymbolFlags.Alias) !== 0
81
+ ? checker.getAliasedSymbol(symbol)
82
+ : symbol;
83
+ const declarations = resolved?.declarations ?? [];
84
+ return declarations.some((declaration) => path
85
+ .normalize(declaration.getSourceFile().fileName)
86
+ .includes(rewriter.moduleSegment));
87
+ };
88
+ exports.isOwnedCall = isOwnedCall;
89
+ /**
90
+ * Tells whether an object type is a tuple.
91
+ *
92
+ * Shared because both the description built by `typeOf` and the value built by
93
+ * `defaultOf` have to tell a tuple apart from a plain array.
94
+ *
95
+ * @param type Type being inspected.
96
+ * @returns `true` when the type is a tuple.
97
+ */
98
+ const isTupleType = (type) => (type.objectFlags & typescript_1.default.ObjectFlags.Reference) !== 0 &&
99
+ (type.target.objectFlags &
100
+ typescript_1.default.ObjectFlags.Tuple) !==
101
+ 0;
102
+ exports.isTupleType = isTupleType;
@@ -0,0 +1,42 @@
1
+ import typescript from 'typescript';
2
+ import { CallRewriter } from '../shared/index.js';
3
+ /**
4
+ * Composition root shared by every Fulcro transformer.
5
+ *
6
+ * TypeScript erases its own type system on the way to JavaScript: interfaces,
7
+ * type aliases, generic arguments and the file a type was declared in leave no
8
+ * trace in the emitted code. A runtime function can therefore only report what
9
+ * the value in front of it shows, which is why these libraries need a compiler
10
+ * plugin to answer properly at all.
11
+ *
12
+ * This module holds the checker, walks every source file once, and hands each
13
+ * call to the rewriter that owns it. What any particular utility emits lives in
14
+ * the package that owns that utility — this one knows only how to walk and how
15
+ * to ask.
16
+ *
17
+ * Which is the whole reason it is a package of its own: `@fulcro/reflect` and
18
+ * `@fulcro/collections` each ship their own transformer, so neither has to be
19
+ * installed for the other to work, and neither can drift out of version with
20
+ * the runtime code it rewrites. The walking is the only part they share.
21
+ *
22
+ * Calls no rewriter can resolve are left untouched, so the runtime
23
+ * implementations stay in charge and a project compiling without the
24
+ * transformer keeps working, in whatever reduced form each utility documents.
25
+ */
26
+ /** Options accepted from the `plugins` entry of the tsconfig. */
27
+ export interface TransformerOptions {
28
+ /** Root the reported declaration paths are made relative to. */
29
+ readonly projectRoot?: string;
30
+ }
31
+ /** The factory shape `ts-patch` expects from a plugin module. */
32
+ export type TransformerFactory = (program: typescript.Program, options?: TransformerOptions) => typescript.TransformerFactory<typescript.SourceFile>;
33
+ /**
34
+ * Builds a transformer from a set of rewriters.
35
+ *
36
+ * @param rewriters Rewriters consulted for every call, in order. Each one
37
+ * claims a single exported function of a single module, so the order carries no
38
+ * meaning beyond the one a reader gives it.
39
+ * @returns The factory a package exports as the default of its transformer
40
+ * entry point.
41
+ */
42
+ export declare const createTransformer: (rewriters: readonly CallRewriter[]) => TransformerFactory;
@@ -0,0 +1,43 @@
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.createTransformer = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const shared_1 = require("../shared/index.js");
9
+ /**
10
+ * Builds a transformer from a set of rewriters.
11
+ *
12
+ * @param rewriters Rewriters consulted for every call, in order. Each one
13
+ * claims a single exported function of a single module, so the order carries no
14
+ * meaning beyond the one a reader gives it.
15
+ * @returns The factory a package exports as the default of its transformer
16
+ * entry point.
17
+ */
18
+ const createTransformer = (rewriters) => (program, options = {}) => {
19
+ const checker = program.getTypeChecker();
20
+ const projectRoot = options.projectRoot ?? program.getCurrentDirectory();
21
+ return (context) => {
22
+ const visit = (node) => {
23
+ if (typescript_1.default.isCallExpression(node)) {
24
+ const rewritten = rewriteCall(node);
25
+ if (rewritten !== null)
26
+ return rewritten;
27
+ }
28
+ return typescript_1.default.visitEachChild(node, visit, context);
29
+ };
30
+ const rewriteContext = {
31
+ checker,
32
+ factory: context.factory,
33
+ projectRoot,
34
+ visit,
35
+ };
36
+ const rewriteCall = (call) => {
37
+ const owner = rewriters.find((rewriter) => (0, shared_1.isOwnedCall)(call, checker, rewriter));
38
+ return owner === undefined ? null : owner.rewrite(call, rewriteContext);
39
+ };
40
+ return (sourceFile) => typescript_1.default.visitNode(sourceFile, visit);
41
+ };
42
+ };
43
+ exports.createTransformer = createTransformer;
@@ -0,0 +1,57 @@
1
+ import { type UnpluginFactory, type UnpluginInstance } from 'unplugin';
2
+ import { type TransformCoreOptions } from '../program/index.js';
3
+ import { type CallRewriter } from '../shared/index.js';
4
+ /**
5
+ * Bundler adapters for the Fulcro transformers.
6
+ *
7
+ * Built on `unplugin`, so the same integration serves Vite, Rollup, Webpack,
8
+ * esbuild, Rspack and Farm — and, through them, the frameworks layered on top.
9
+ * Everything specific to type checking lives in the core this wraps; what is
10
+ * left here is the shape each bundler expects.
11
+ *
12
+ * Nothing here knows which utilities are being rewritten. The owning package
13
+ * passes its own rewriters in, which is what lets `@fulcro/reflect` and
14
+ * `@fulcro/collections` each publish a plugin of their own without either
15
+ * having to know the other exists.
16
+ *
17
+ * Emitted as an ES module because `unplugin` ships as one. The core stays
18
+ * CommonJS, since the compiler plugin loads it through `require`, and an ES
19
+ * module importing CommonJS is the direction that works.
20
+ */
21
+ /** Options accepted by every adapter. */
22
+ export type PluginOptions = TransformCoreOptions;
23
+ /**
24
+ * Every form `unplugin` produces for these options.
25
+ *
26
+ * Named through `UnpluginInstance` rather than `ReturnType<typeof
27
+ * createUnplugin>`, which infers its option type as `unknown` and makes every
28
+ * adapter below reject the options it is actually given.
29
+ */
30
+ type Adapters = UnpluginInstance<PluginOptions | undefined>;
31
+ /** The adapters a package re-exports from its own `/unplugin` entry point. */
32
+ export interface TransformerUnplugin {
33
+ /** The factory itself, for a bundler not covered below. */
34
+ readonly unpluginFactory: UnpluginFactory<PluginOptions | undefined>;
35
+ /** Adapter for Vite, which is also what vitest runs on. */
36
+ readonly vite: Adapters['vite'];
37
+ /** Adapter for Rollup. */
38
+ readonly rollup: Adapters['rollup'];
39
+ /** Adapter for Webpack. */
40
+ readonly webpack: Adapters['webpack'];
41
+ /** Adapter for Rspack. */
42
+ readonly rspack: Adapters['rspack'];
43
+ /** Adapter for esbuild. */
44
+ readonly esbuild: Adapters['esbuild'];
45
+ /** Adapter for Farm. */
46
+ readonly farm: Adapters['farm'];
47
+ }
48
+ /**
49
+ * Builds the bundler adapters for one set of rewriters.
50
+ *
51
+ * @param rewriters Rewriters of the package publishing the plugin.
52
+ * @param name Name the plugin reports to the bundler, which is what shows up
53
+ * in its logs and timings.
54
+ * @returns Every adapter `unplugin` can produce.
55
+ */
56
+ export declare const createTransformerUnplugin: (rewriters: readonly CallRewriter[], name: string) => TransformerUnplugin;
57
+ export {};
@@ -0,0 +1,61 @@
1
+ import { createUnplugin, } from 'unplugin';
2
+ import { createFileTransformer, } from '../program/index.js';
3
+ /**
4
+ * Builds the bundler adapters for one set of rewriters.
5
+ *
6
+ * @param rewriters Rewriters of the package publishing the plugin.
7
+ * @param name Name the plugin reports to the bundler, which is what shows up
8
+ * in its logs and timings.
9
+ * @returns Every adapter `unplugin` can produce.
10
+ */
11
+ export const createTransformerUnplugin = (rewriters, name) => {
12
+ const unpluginFactory = (options = {}) => {
13
+ let transformer;
14
+ /**
15
+ * Resolves the core lazily, so that the root reported by the bundler is
16
+ * already known when the program is configured.
17
+ *
18
+ * @returns The transformer core.
19
+ */
20
+ const resolve = () => {
21
+ transformer ??= createFileTransformer(rewriters, options);
22
+ return transformer;
23
+ };
24
+ return {
25
+ name,
26
+ // Ahead of the TypeScript handling of the bundler: once esbuild or swc
27
+ // has erased the types, there is nothing left for the transformer to
28
+ // read.
29
+ enforce: 'pre',
30
+ transformInclude(id) {
31
+ return resolve().handles(id);
32
+ },
33
+ transform(code, id) {
34
+ const transformed = resolve().transform(id, code);
35
+ return transformed === null ? null : { code: transformed, map: null };
36
+ },
37
+ vite: {
38
+ configResolved(config) {
39
+ // The root of the bundler wins over the working directory, which
40
+ // is what makes the plugin behave inside a monorepo.
41
+ if (config.root !== undefined && options.root === undefined) {
42
+ transformer = createFileTransformer(rewriters, {
43
+ ...options,
44
+ root: config.root,
45
+ });
46
+ }
47
+ },
48
+ },
49
+ };
50
+ };
51
+ const unplugin = createUnplugin(unpluginFactory);
52
+ return {
53
+ unpluginFactory,
54
+ vite: unplugin.vite,
55
+ rollup: unplugin.rollup,
56
+ webpack: unplugin.webpack,
57
+ rspack: unplugin.rspack,
58
+ esbuild: unplugin.esbuild,
59
+ farm: unplugin.farm,
60
+ };
61
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@fulcro/transform-core",
3
+ "version": "0.2.1",
4
+ "description": "Shared machinery behind the Fulcro compile time transformers. Installed for you; not meant to be depended on directly.",
5
+ "keywords": [
6
+ "typescript",
7
+ "transformer",
8
+ "ts-patch",
9
+ "unplugin",
10
+ "compiler"
11
+ ],
12
+ "license": "ISC",
13
+ "author": "diguu <rodrigogeribola@hotmail.com>",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./unplugin": {
22
+ "types": "./dist/unplugin/index.d.mts",
23
+ "default": "./dist/unplugin/index.mjs"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "sideEffects": false,
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
33
+ "typecheck": "tsc --noEmit -p tsconfig.json",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "dependencies": {
37
+ "unplugin": "^3.3.0"
38
+ },
39
+ "peerDependencies": {
40
+ "typescript": ">=5.3.3 <7"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "typescript": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/DigUu-RL/fulcro.git",
56
+ "directory": "packages/transform-core"
57
+ },
58
+ "homepage": "https://github.com/DigUu-RL/fulcro/tree/main/packages/transform-core#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/DigUu-RL/fulcro/issues"
61
+ }
62
+ }