Skip to content

Commit a7dec4c

Browse files
committed
respond to PR feedback
1 parent 9c20aa2 commit a7dec4c

File tree

9 files changed

+168
-98
lines changed

9 files changed

+168
-98
lines changed

incubator/tools-typescript/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ typescript itself is particularly restrictive with module resolution. This
3535
addresses the ability to build react-native better right now, but also creates a
3636
framework for experimenting with different resolvers.
3737

38+
## Things to do
39+
40+
- Look at watch mode behavior, add invalidation for caching layers
41+
- Look at routing host creation in metro-plugin-typescript through here
42+
- Evaluate alternative resolvers
43+
3844
## Installation
3945

4046
```sh

incubator/tools-typescript/src/build.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { AllPlatforms } from "@rnx-kit/tools-react-native";
33
import { findConfigFile, readConfigFile } from "@rnx-kit/typescript-service";
44
import path from "node:path";
55
import ts from "typescript";
6-
import { loadPkgPlatformInfo } from "./platforms";
6+
import { loadPackagePlatformInfo } from "./platforms";
77
import { createReporter } from "./reporter";
88
import { createBuildTasks } from "./task";
99
import type { BuildContext, BuildOptions, PlatformInfo } from "./types";
@@ -66,7 +66,11 @@ export async function buildTypeScript(options: BuildOptions) {
6666
// load/detect the platforms
6767
let targetPlatforms: PlatformInfo[] | undefined = undefined;
6868
if (options.platforms || options.reactNative) {
69-
const platformInfo = loadPkgPlatformInfo(root, manifest, options.platforms);
69+
const platformInfo = loadPackagePlatformInfo(
70+
root,
71+
manifest,
72+
options.platforms
73+
);
7074
const platforms = Object.keys(platformInfo);
7175
if (platforms.length > 0) {
7276
options.platforms = platforms as AllPlatforms[];
@@ -94,6 +98,8 @@ export async function buildTypeScript(options: BuildOptions) {
9498
cmdLine,
9599
root,
96100
reporter,
101+
build: [],
102+
check: [],
97103
};
98104

99105
// create the set of tasks to run then resolve all the tasks

incubator/tools-typescript/src/host.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ const platformHosts: Record<string, PlatformHost> = {};
1313
* Get the platform host for a given platform, will be cached for that platform
1414
* @returns platform specific host implementation
1515
*/
16-
export function getPlatformHost(platformInfo?: PlatformInfo): PlatformHost {
17-
const platformName = platformInfo?.name ?? "none";
18-
const remap = platformInfo?.remapReactNative;
19-
const key = platformName + remap ? "-remap" : "";
16+
export function getPlatformHost(
17+
platformInfo: Partial<PlatformInfo> = {}
18+
): PlatformHost {
19+
const { name = "", remapReactNative } = platformInfo;
20+
const key = name + remapReactNative ? "-remap" : "";
2021
platformHosts[key] ??= new PlatformHost(platformInfo);
2122
return platformHosts[key];
2223
}
@@ -38,7 +39,7 @@ export class PlatformHost {
3839
private service = new Service();
3940
private newResolvers = ts.versionMajorMinor >= "5.0";
4041

41-
constructor(platform: PlatformInfo = { name: "" } as PlatformInfo) {
42+
constructor(platform: Partial<PlatformInfo>) {
4243
this.suffixes = platform.suffixes;
4344
const { remapReactNative, pkgName } = platform;
4445

@@ -47,13 +48,27 @@ export class PlatformHost {
4748
}
4849
}
4950

51+
/**
52+
* @param context project settings used to create the Project
53+
* @returns a Project initialized with as much caching as possible
54+
*/
5055
createProject(context: ProjectContext): Project {
5156
// create the host enhancer and open the project
5257
const enhancer = this.createHostEnhancer(context);
5358
return this.service.openProject(context.cmdLine, enhancer);
5459
}
5560

56-
private createHostEnhancer(context: ProjectContext) {
61+
/**
62+
* The language service host interface is what allows us to hook various parts of the typescript build.
63+
* The typescript-service package provides a default implementation of this interface, but this does the work
64+
* to modify it for react-native projects
65+
*
66+
* @param context project context used to create the host enhancer
67+
* @returns a function which will be called by the Project upon intialization that will modify the host
68+
*/
69+
private createHostEnhancer(
70+
context: ProjectContext
71+
): (host: ts.LanguageServiceHost) => void {
5772
const { reporter, cmdLine, writer, root } = context;
5873
const options = cmdLine.options;
5974

incubator/tools-typescript/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
export { buildTypeScript } from "./build";
33
export { createAsyncThrottler, createAsyncWriter } from "./files";
44
export { openProject } from "./host";
5-
export { loadPkgPlatformInfo, parseSourceFileDetails } from "./platforms";
5+
export {
6+
loadPackagePlatformInfo,
7+
parseSourceFileReference as parseSourceFileDetails,
8+
} from "./platforms";
69
export { createReporter } from "./reporter";
710
export type {
811
AsyncThrottler,
912
AsyncWriter,
1013
BuildOptions,
14+
ParsedFileReference,
1115
PlatformInfo,
1216
Reporter,
1317
} from "./types";

incubator/tools-typescript/src/platforms.ts

Lines changed: 84 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
getAvailablePlatforms,
88
platformExtensions,
99
} from "@rnx-kit/tools-react-native";
10+
import path from "node:path";
1011
import type ts from "typescript";
11-
import type { BuildContext, ParsedFileName, PlatformInfo } from "./types";
12+
import type { BuildContext, ParsedFileReference, PlatformInfo } from "./types";
1213

1314
// quick helper for converting a value to an array
1415
function coerceArray<T>(value: T | T[] | undefined): T[] {
@@ -18,22 +19,29 @@ function coerceArray<T>(value: T | T[] | undefined): T[] {
1819
return Array.isArray(value) ? value : [value];
1920
}
2021

22+
// a list of built-in platforms to use as a fallback
23+
const fallbackPackages: Record<string, string> = {
24+
android: "react-native",
25+
ios: "react-native",
26+
macos: "react-native-macos",
27+
win32: "@office-iss/react-native-win32",
28+
windows: "react-native-windows",
29+
visionos: "@callstack/react-native-visionos",
30+
};
31+
2132
/**
2233
* @returns a PlatformInfo with a package name and the module suffixes set
2334
*/
2435
function createPlatformInfo(platform: string, npmName?: string): PlatformInfo {
25-
// a list of built-in platforms to use as a fallback
26-
const fallbackPackages: Record<string, string> = {
27-
android: "react-native",
28-
ios: "react-native",
29-
macos: "react-native-macos",
30-
win32: "@office-iss/react-native-win32",
31-
windows: "react-native-windows",
32-
visionos: "@callstack/react-native-visionos",
33-
};
36+
const pkgName = npmName || fallbackPackages[platform];
37+
if (!pkgName) {
38+
throw new Error(
39+
`Unable to determine package name for platform ${platform}`
40+
);
41+
}
3442
return {
3543
name: platform,
36-
pkgName: npmName || fallbackPackages[platform] || "",
44+
pkgName: npmName || fallbackPackages[platform],
3745
suffixes: platformExtensions(platform)
3846
.concat("")
3947
.map((s) => `.${s}`),
@@ -73,7 +81,7 @@ export function platformsFromKitConfig(
7381
* @param platformOverride override platform detection with a specific set of platforms
7482
* @returns a map of platform name to PlatformInfo
7583
*/
76-
export function loadPkgPlatformInfo(
84+
export function loadPackagePlatformInfo(
7785
pkgRoot: string,
7886
manifest: PackageManifest,
7987
platformOverride?: string[]
@@ -96,50 +104,74 @@ export function loadPkgPlatformInfo(
96104
return result;
97105
}
98106

107+
export function parseFileReference(
108+
file: string,
109+
extensions: Set<string>,
110+
ignoreSuffix?: boolean
111+
): ParsedFileReference {
112+
let ext = path.extname(file);
113+
let suffix = "";
114+
let base = file;
115+
116+
if (extensions.has(ext)) {
117+
if (ext === ".ts" || ext === ".tsx") {
118+
const dtsExt = ".d" + ext;
119+
if (extensions.has(dtsExt) && file.endsWith(dtsExt)) {
120+
ext = dtsExt;
121+
}
122+
}
123+
base = base.slice(0, -ext.length);
124+
suffix = path.extname(base);
125+
} else {
126+
suffix = ext;
127+
ext = "";
128+
}
129+
130+
if (suffix && !ignoreSuffix) {
131+
base = base.slice(0, -suffix.length);
132+
} else {
133+
suffix = "";
134+
}
135+
136+
return { base, ext, suffix };
137+
}
138+
139+
/**
140+
* Standard source file extensions. The reason this is a discrete list rather than allowing all extensions
141+
* is that the references being parsed can be in ./folder/file.js or ./folder/file format, depending on how they
142+
* are declared. We want to parse both and don't want to accidentally treat ./folder/file.types as having an
143+
* extension of .types
144+
*/
145+
const sourceExtensions = new Set([
146+
".js",
147+
".jsx",
148+
".cjs",
149+
".mjs",
150+
".ts",
151+
".tsx",
152+
".d.ts",
153+
".d.tsx",
154+
".json",
155+
]);
156+
99157
/**
100158
* Take a file path and return the base file name, the platform extension if one exists, and the file extension.
101159
*
102-
* @param file file path to parse, can be a source file or a module reference
160+
* @param file file path to parse, can be a source file (./src/foo.js) or a module style reference (./src/foo)
103161
* @param ignoreSuffix don't split out the module suffix, leaving it attached to the base
104162
* @returns an object with the base file name, the platform extension if one exists, and the file extension
105163
*/
106-
export function parseSourceFileDetails(
164+
export function parseSourceFileReference(
107165
file: string,
108166
ignoreSuffix?: boolean
109-
): ParsedFileName {
110-
// valid last extensions, will manually check for .d.ts and .d.tsx
111-
const validExtensions = [".js", ".jsx", ".ts", ".tsx", ".json"];
112-
const match = /^(.*?)(\.[^./\\]+)?(\.[^./\\]+)?(\.[^./\\]+)?$/
113-
.exec(file)
114-
?.slice(1)
115-
.filter((s) => s);
116-
117-
const result: ParsedFileName = { base: file };
118-
if (match && match.length > 1) {
119-
if (validExtensions.includes(match[match.length - 1])) {
120-
result.ext = match.pop();
121-
if (
122-
match.length > 1 &&
123-
match[match.length - 1] === ".d" &&
124-
result.ext?.startsWith(".ts")
125-
) {
126-
result.ext = match.pop() + result.ext;
127-
}
128-
}
129-
// if there is an extra term treat it as the suffix
130-
if (match.length > 1 && !ignoreSuffix) {
131-
result.suffix = match.pop();
132-
}
133-
result.base = match.join("");
134-
}
135-
return result;
167+
): ParsedFileReference {
168+
return parseFileReference(file, sourceExtensions, ignoreSuffix);
136169
}
137170

138-
export type FoundSuffixes = Set<string>;
139171
export type FileEntry = {
140172
file: string;
141173
suffix?: string;
142-
allSuffixes: FoundSuffixes;
174+
allSuffixes: Set<string>;
143175
built?: boolean;
144176
};
145177

@@ -162,14 +194,14 @@ export function isBestMatch(entry: FileEntry, suffixes: string[]): boolean {
162194
* @returns a match array of FileEntry structures
163195
*/
164196
function processFileList(files: string[]): FileEntry[] {
165-
const lookup = new Map<string, FoundSuffixes>();
197+
const lookup = new Map<string, Set<string>>();
166198

167199
const ensureSuffixes = (base: string) => {
168200
return lookup.get(base) || lookup.set(base, new Set<string>()).get(base)!;
169201
};
170202

171203
return files.map((file) => {
172-
const { base, suffix } = parseSourceFileDetails(file);
204+
const { base, suffix } = parseSourceFileReference(file);
173205
const allSuffixes = ensureSuffixes(base);
174206
if (suffix) {
175207
allSuffixes.add(suffix);
@@ -181,31 +213,31 @@ function processFileList(files: string[]): FileEntry[] {
181213
/**
182214
* Given the parsed file entries, go through adding the files to the task based on the platform.
183215
* @param files processed file entry list
184-
* @param defaultTask is this the task which should build any unbuilt files
216+
* @param isDefaultTask is this the task which should build any unbuilt files
185217
* @param context task to add files to
186218
*/
187219
function addFilesToContext(
188220
files: FileEntry[],
189-
defaultTask: boolean,
221+
isDefaultTask: boolean,
190222
context: BuildContext
191223
) {
192224
const fileNames: string[] = [];
193225

194-
const { suffixes } = context.platform || {};
226+
const { suffixes = [] } = context.platform || {};
195227

196228
// if we are in checkOnly mode then task.build will be null and we will fall back to the check array
197229
const pushToBuild = (file: string, checkOnly: boolean) => {
198230
if (context.build && !checkOnly) {
199231
context.build.push(file);
200232
} else {
201-
context.check!.push(file);
233+
context.check?.push(file);
202234
}
203235
fileNames.push(file);
204236
};
205237

206238
for (const entry of files) {
207-
const bestMatch = isBestMatch(entry, suffixes!);
208-
if (!entry.built && (bestMatch || defaultTask)) {
239+
const bestMatch = isBestMatch(entry, suffixes);
240+
if (!entry.built && (bestMatch || isDefaultTask)) {
209241
pushToBuild(entry.file, false);
210242
entry.built = true;
211243
} else if (bestMatch) {
@@ -239,7 +271,7 @@ export function multiplexForPlatforms(
239271
const checkOnly = Boolean(cmdLine.options.noEmit);
240272
// no platforms then we have nothing to do
241273
if (!platforms || platforms.length === 0) {
242-
context.platform = platforms?.[0];
274+
context.platform = undefined;
243275
context.check = checkOnly ? cmdLine.fileNames : [];
244276
context.build = checkOnly ? [] : cmdLine.fileNames;
245277
return [context];

incubator/tools-typescript/src/reporter.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function getNullTimer(): Timer {
4040
return fn();
4141
},
4242
async timeAsync<T>(_s: string, fn: () => Promise<T>): Promise<T> {
43-
return await fn();
43+
return fn();
4444
},
4545
results() {
4646
return {};
@@ -66,14 +66,18 @@ export function createReporter(
6666
// set up the logging functions
6767
const warn = (...args: unknown[]) => console.warn(`${name}:`, ...args);
6868
const log = logging
69-
? (...args: unknown[]) => { console.log(`${name}:`, ...args); }
69+
? (...args: unknown[]) => {
70+
console.log(`${name}:`, ...args);
71+
}
7072
: () => undefined;
7173
const error = (...args: unknown[]) => {
7274
errors++;
7375
console.error(...args);
7476
};
7577
const trace = tracing
76-
? (...args: unknown[]) => { console.log(`${name}:`, ...args); }
78+
? (...args: unknown[]) => {
79+
console.log(`${name}:`, ...args);
80+
}
7781
: () => undefined;
7882

7983
// create the reporter we will return

0 commit comments

Comments
 (0)