1const fs = require("node:fs");
2const path = require("node:path");
3const { pathToFileURL } = require("node:url");
4const { getDefaultConfig } = require("expo/metro-config");
5const { withUniwindConfig } = require("uniwind/metro");
6const extraThemes = require("./generated-uniwind-theme-names.json");
7
8/** @type {import("expo/metro-config").MetroConfig} */
9const config = getDefaultConfig(__dirname);
10const workspaceRoot = path.resolve(__dirname, "../..");
11const generatedLicenseModuleRoot = path.join(__dirname, ".generated", "third-party-licenses");
12const licenseGeneratorSource = path.join(
13 workspaceRoot,
14 "scripts",
15 "lib",
16 "third-party-licenses.ts",
17);
18const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] }));
20const generatedDeviceStreamRoot = path.join(__dirname, ".generated", "device-stream");
21const generatedPreviewStreamRoot = path.join(__dirname, ".generated", "preview-stream");
22const resolveShikiDependencyRoot = (packageName) => {
23 const entryPath = require.resolve(packageName, { paths: [mobileShikiRoot] });
24 let currentDir = path.dirname(entryPath);
25
26 while (!fs.existsSync(path.join(currentDir, "package.json"))) {
27 const parentDir = path.dirname(currentDir);
28 if (parentDir === currentDir) {
29 throw new Error(`Could not resolve package root for ${packageName}`);
30 }
31 currentDir = parentDir;
32 }
33
34 return currentDir;
35};
36
37config.watchFolders = [...new Set([...(config.watchFolders ?? []), workspaceRoot])];
38config.resolver = {
39 ...config.resolver,
40 blockList: [
41 ...(Array.isArray(config.resolver?.blockList)
42 ? config.resolver.blockList
43 : config.resolver?.blockList
44 ? [config.resolver.blockList]
45 : []),
46 new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`),
47 ],
48 extraNodeModules: {
49 ...config.resolver?.extraNodeModules,
50 "@t3tools/mobile-third-party-licenses": generatedLicenseModuleRoot,
51 "@t3tools/mobile-device-stream": generatedDeviceStreamRoot,
52 "@t3tools/mobile-preview-stream": generatedPreviewStreamRoot,
53 shiki: mobileShikiRoot,
54 "@shikijs/core": resolveShikiDependencyRoot("@shikijs/core"),
55 "@shikijs/engine-javascript": resolveShikiDependencyRoot("@shikijs/engine-javascript"),
56 "@shikijs/engine-oniguruma": resolveShikiDependencyRoot("@shikijs/engine-oniguruma"),
57 "@shikijs/langs": resolveShikiDependencyRoot("@shikijs/langs"),
58 "@shikijs/themes": resolveShikiDependencyRoot("@shikijs/themes"),
59 "@shikijs/types": resolveShikiDependencyRoot("@shikijs/types"),
60 "@shikijs/vscode-textmate": resolveShikiDependencyRoot("@shikijs/vscode-textmate"),
61 },
62};
63
64async function writeFileIfChanged(filePath, contents) {
65 try {
66 if ((await fs.promises.readFile(filePath, "utf8")) === contents) return;
67 } catch (error) {
68 if (error?.code !== "ENOENT") throw error;
69 }
70 await fs.promises.writeFile(filePath, contents, "utf8");
71}
72
73async function generateMobileThirdPartyLicenses() {
74 await fs.promises.mkdir(generatedLicenseModuleRoot, { recursive: true });
75 const generatorVersion = (await fs.promises.stat(licenseGeneratorSource)).mtimeMs;
76 const { generateThirdPartyLicenseManifest } = await import(
77 `${pathToFileURL(licenseGeneratorSource).href}?version=${String(generatorVersion)}`
78 );
79 const manifest = await generateThirdPartyLicenseManifest({
80 configFile: path.join(workspaceRoot, "third-party-licenses.config.json"),
81 packageManifests: [{ bundle: "mobile", path: path.join(__dirname, "package.json") }],
82 allowMissingGeneratedNotices:
83 process.env.NODE_ENV !== "production" &&
84 process.env.EAS_BUILD !== "true" &&
85 process.env.T3CODE_LICENSES_STRICT !== "1",
86 });
87
88 await Promise.all([
89 writeFileIfChanged(
90 path.join(generatedLicenseModuleRoot, "index.js"),
91 `module.exports = ${JSON.stringify(manifest)};\n`,
92 ),
93 writeFileIfChanged(
94 path.join(generatedLicenseModuleRoot, "package.json"),
95 '{"main":"index.js"}\n',
96 ),
97 ]);
98}
99
100async function prepareStreamScripts() {
101 const { generateDeviceStreamScript, generatePreviewStreamScript } = await import(
102 pathToFileURL(path.join(__dirname, "scripts", "generate-device-stream.mts")).href
103 );
104 const generateAll = () =>
105 Promise.all([generateDeviceStreamScript(), generatePreviewStreamScript()]);
106 await generateAll();
107 if (process.env.NODE_ENV !== "production") {
108 let rebuild = Promise.resolve();
109 for (const [directory, files, generate] of [
110 [
111 "apps/mobile/src/features/devices",
112 ["device-stream.browser.ts"],
113 generateDeviceStreamScript,
114 ],
115 [
116 "apps/mobile/src/features/browser",
117 ["preview-stream.browser.ts"],
118 generatePreviewStreamScript,
119 ],
120 // The preview transport also imports `hubAccess.ts`.
121 ["packages/client-runtime/src/device", ["stream.ts", "hubAccess.ts"], generateAll],
122 [
123 "packages/client-runtime/src/preview",
124 ["serverBrowserStream.ts"],
125 generatePreviewStreamScript,
126 ],
127 ]) {
128 // The generated modules participate in Metro's normal Fast Refresh.
129 fs.watch(path.join(workspaceRoot, directory), { persistent: false }, (_event, filename) => {
130 if (filename && !files.includes(String(filename))) return;
131 rebuild = rebuild
132 .then(generate)
133 .then(() => undefined)
134 .catch((error) => {
135 console.error("Could not rebuild a WebView stream script:", error);
136 });
137 });
138 }
139 }
140}
141
142module.exports = Promise.all([generateMobileThirdPartyLicenses(), prepareStreamScripts()]).then(
143 () =>
144 withUniwindConfig(config, {
145 cssEntryFile: "./global.css",
146 extraThemes,
147 polyfills: { rem: 14 },
148 }),
149);