| 1 | const fs = require("node:fs"); |
| 2 | const path = require("node:path"); |
| 3 | const { pathToFileURL } = require("node:url"); |
| 4 | const { getDefaultConfig } = require("expo/metro-config"); |
| 5 | const { withUniwindConfig } = require("uniwind/metro"); |
| 6 | const extraThemes = require("./generated-uniwind-theme-names.json"); |
| 7 | |
| 8 | /** @type {import("expo/metro-config").MetroConfig} */ |
| 9 | const config = getDefaultConfig(__dirname); |
| 10 | const workspaceRoot = path.resolve(__dirname, "../.."); |
| 11 | const generatedLicenseModuleRoot = path.join(__dirname, ".generated", "third-party-licenses"); |
| 12 | const licenseGeneratorSource = path.join( |
| 13 | workspaceRoot, |
| 14 | "scripts", |
| 15 | "lib", |
| 16 | "third-party-licenses.ts", |
| 17 | ); |
| 18 | const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 19 | const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] })); |
| 20 | const generatedDeviceStreamRoot = path.join(__dirname, ".generated", "device-stream"); |
| 21 | const generatedPreviewStreamRoot = path.join(__dirname, ".generated", "preview-stream"); |
| 22 | const 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 | |
| 37 | config.watchFolders = [...new Set([...(config.watchFolders ?? []), workspaceRoot])]; |
| 38 | config.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 | |
| 64 | async 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 | |
| 73 | async 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 | |
| 100 | async 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 | |
| 142 | module.exports = Promise.all([generateMobileThirdPartyLicenses(), prepareStreamScripts()]).then( |
| 143 | () => |
| 144 | withUniwindConfig(config, { |
| 145 | cssEntryFile: "./global.css", |
| 146 | extraThemes, |
| 147 | polyfills: { rem: 14 }, |
| 148 | }), |
| 149 | ); |