delacour 0.5.0-alpha.20261021114607 → 0.5.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.js +871 -521
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4068,6 +4068,70 @@ ${r ? styleText("cyan", S_BAR_END) : ""}
|
|
|
4068
4068
|
}
|
|
4069
4069
|
}).prompt();
|
|
4070
4070
|
//#endregion
|
|
4071
|
+
//#region src/registry/test-runner.ts
|
|
4072
|
+
/**
|
|
4073
|
+
* The runner a project's copied tests are written for.
|
|
4074
|
+
*
|
|
4075
|
+
* The library's suites are Bun's, and the registry build holds every shipped one
|
|
4076
|
+
* to the API Bun, Jest and Vitest share — `describe` / `test` / `it` / `expect`
|
|
4077
|
+
* and the four hooks, a `node:*` read beside itself through
|
|
4078
|
+
* `import.meta.dirname`, and nothing else. So a suite moves between runners by
|
|
4079
|
+
* two substitutions and no parsing: the module the API is imported from, and,
|
|
4080
|
+
* for Jest, which cannot evaluate `import.meta` in CommonJS, `__dirname`.
|
|
4081
|
+
*/
|
|
4082
|
+
const TEST_RUNNERS = [
|
|
4083
|
+
"bun",
|
|
4084
|
+
"jest",
|
|
4085
|
+
"vitest"
|
|
4086
|
+
];
|
|
4087
|
+
/** Where each runner's shared API is imported from. */
|
|
4088
|
+
const RUNNER_MODULE = {
|
|
4089
|
+
bun: "bun:test",
|
|
4090
|
+
jest: "@jest/globals",
|
|
4091
|
+
vitest: "vitest"
|
|
4092
|
+
};
|
|
4093
|
+
/**
|
|
4094
|
+
* What a project needs to typecheck and run its copied tests. `node:*` imports
|
|
4095
|
+
* need Node's types everywhere but Bun, whose own types carry them.
|
|
4096
|
+
*/
|
|
4097
|
+
const RUNNER_PACKAGES = {
|
|
4098
|
+
bun: ["@types/bun"],
|
|
4099
|
+
jest: ["@jest/globals", "@types/node"],
|
|
4100
|
+
vitest: ["@types/node", "vitest"]
|
|
4101
|
+
};
|
|
4102
|
+
const BUN_TEST_IMPORT = /(from\s*)(["'])bun:test\2/g;
|
|
4103
|
+
/** A copied test's text as `runner` runs it. */
|
|
4104
|
+
function adaptTest(content, runner) {
|
|
4105
|
+
if (runner === "bun") return content;
|
|
4106
|
+
const imported = content.replace(BUN_TEST_IMPORT, (_match, from, quote) => {
|
|
4107
|
+
return `${from}${quote}${RUNNER_MODULE[runner]}${quote}`;
|
|
4108
|
+
});
|
|
4109
|
+
return runner === "jest" ? imported.replaceAll("import.meta.dirname", "__dirname") : imported;
|
|
4110
|
+
}
|
|
4111
|
+
function testRunnerPackages(runner) {
|
|
4112
|
+
return [...RUNNER_PACKAGES[runner]];
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* The runner a `package.json` tests with, or `null` when it names none.
|
|
4116
|
+
*
|
|
4117
|
+
* The `test` script is read first: it is the runner the project actually
|
|
4118
|
+
* invokes, where a dependency may be a leftover. `jest-expo` is Expo's own
|
|
4119
|
+
* preset, so it counts as Jest.
|
|
4120
|
+
*/
|
|
4121
|
+
function detectTestRunner(manifest) {
|
|
4122
|
+
const script = manifest.scripts?.test ?? "";
|
|
4123
|
+
if (/\bvitest\b/.test(script)) return "vitest";
|
|
4124
|
+
if (/\bjest\b/.test(script)) return "jest";
|
|
4125
|
+
if (/\bbun\s+test\b/.test(script)) return "bun";
|
|
4126
|
+
const packages = {
|
|
4127
|
+
...manifest.dependencies,
|
|
4128
|
+
...manifest.devDependencies
|
|
4129
|
+
};
|
|
4130
|
+
if ("vitest" in packages) return "vitest";
|
|
4131
|
+
if ("jest" in packages || "jest-expo" in packages || manifest.jest !== void 0) return "jest";
|
|
4132
|
+
return null;
|
|
4133
|
+
}
|
|
4134
|
+
//#endregion
|
|
4071
4135
|
//#region ../../node_modules/zod/v4/core/util.js
|
|
4072
4136
|
function getEnumValues(entries) {
|
|
4073
4137
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -10016,6 +10080,12 @@ const registrySchema = object$1({
|
|
|
10016
10080
|
const packageSchema = object$1({
|
|
10017
10081
|
/** The package's npm name, and therefore the app's import prefix. */
|
|
10018
10082
|
name: string().min(1) });
|
|
10083
|
+
/**
|
|
10084
|
+
* How copied tests are written. Without a `runner`, `add --tests` reads the one
|
|
10085
|
+
* the project's `package.json` tests with, and falls back to Bun's, which the
|
|
10086
|
+
* library's suites are written for.
|
|
10087
|
+
*/
|
|
10088
|
+
const testsSchema = object$1({ runner: _enum(TEST_RUNNERS).optional() });
|
|
10019
10089
|
const configSchema = object$1({
|
|
10020
10090
|
$schema: string().optional(),
|
|
10021
10091
|
framework: _enum(["expo", "react-native"]).default("expo"),
|
|
@@ -10027,7 +10097,8 @@ const configSchema = object$1({
|
|
|
10027
10097
|
aliases: aliasesSchema.default({}),
|
|
10028
10098
|
/** Absent when the components live in the app itself. */
|
|
10029
10099
|
package: packageSchema.optional(),
|
|
10030
|
-
app: appSchema.prefault({})
|
|
10100
|
+
app: appSchema.prefault({}),
|
|
10101
|
+
tests: testsSchema.optional()
|
|
10031
10102
|
});
|
|
10032
10103
|
const CONFIG_FILENAME = "native-components.json";
|
|
10033
10104
|
const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/delacournz/delacour-ui/main/registry/config.schema.json";
|
|
@@ -10069,14 +10140,35 @@ async function readConfig(configPath) {
|
|
|
10069
10140
|
const issues = result.error.issues.map((issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`);
|
|
10070
10141
|
throw new Error(`${configPath} is not a valid config:\n${issues.join("\n")}`);
|
|
10071
10142
|
}
|
|
10072
|
-
|
|
10143
|
+
const root = dirname(configPath);
|
|
10144
|
+
const detected = result.data.tests?.runner ? null : await readTestRunner([root, absolute(root, result.data.app.root)]);
|
|
10145
|
+
return resolveConfig(result.data, root, configPath, detected);
|
|
10146
|
+
}
|
|
10147
|
+
/**
|
|
10148
|
+
* The runner the first of `directories` to name one tests with. The config's
|
|
10149
|
+
* own `package.json` comes first — in a monorepo that is the shared package the
|
|
10150
|
+
* tests land in — then the app's.
|
|
10151
|
+
*/
|
|
10152
|
+
async function readTestRunner(directories) {
|
|
10153
|
+
for (const directory of new Set(directories)) {
|
|
10154
|
+
let manifest;
|
|
10155
|
+
try {
|
|
10156
|
+
manifest = JSON.parse(await readFile(join(directory, "package.json"), "utf-8"));
|
|
10157
|
+
} catch {
|
|
10158
|
+
continue;
|
|
10159
|
+
}
|
|
10160
|
+
const runner = detectTestRunner(manifest);
|
|
10161
|
+
if (runner) return runner;
|
|
10162
|
+
}
|
|
10163
|
+
return null;
|
|
10073
10164
|
}
|
|
10074
|
-
function resolveConfig(config, root, configPath) {
|
|
10165
|
+
function resolveConfig(config, root, configPath, detectedRunner = null) {
|
|
10075
10166
|
const appRoot = absolute(root, config.app.root);
|
|
10076
10167
|
return {
|
|
10077
10168
|
...config,
|
|
10078
10169
|
root,
|
|
10079
10170
|
configPath,
|
|
10171
|
+
testRunner: config.tests?.runner ?? detectedRunner ?? "bun",
|
|
10080
10172
|
directories: {
|
|
10081
10173
|
ui: absolute(root, config.paths.ui),
|
|
10082
10174
|
lib: absolute(root, config.paths.lib),
|
|
@@ -10405,10 +10497,10 @@ const LOCKFILES = [
|
|
|
10405
10497
|
async function detectProject(cwd) {
|
|
10406
10498
|
const packageJsonPath = findUp(cwd, "package.json");
|
|
10407
10499
|
const packageRoot = packageJsonPath ? dirname(packageJsonPath) : null;
|
|
10408
|
-
const packageJson = packageJsonPath ? await readJson$
|
|
10500
|
+
const packageJson = packageJsonPath ? await readJson$2(packageJsonPath) : null;
|
|
10409
10501
|
const workspaceRoot = await findWorkspaceRoot(packageRoot ?? cwd);
|
|
10410
10502
|
const appRoot = await findAppRoot(cwd, workspaceRoot);
|
|
10411
|
-
const appPackage = appRoot ? await readJson$
|
|
10503
|
+
const appPackage = appRoot ? await readJson$2(join(appRoot, "package.json")) : null;
|
|
10412
10504
|
const tsconfigPath = findUp(cwd, "tsconfig.json");
|
|
10413
10505
|
const tsconfig = tsconfigPath ? await readJsonc(tsconfigPath) : null;
|
|
10414
10506
|
return {
|
|
@@ -10444,7 +10536,7 @@ async function findWorkspaceRoot(from) {
|
|
|
10444
10536
|
let directory = resolve(from);
|
|
10445
10537
|
while (true) {
|
|
10446
10538
|
if (existsSync(join(directory, "pnpm-workspace.yaml"))) return directory;
|
|
10447
|
-
if ((await readJson$
|
|
10539
|
+
if ((await readJson$2(join(directory, "package.json")))?.workspaces) return directory;
|
|
10448
10540
|
const parent = dirname(directory);
|
|
10449
10541
|
if (parent === directory) return null;
|
|
10450
10542
|
directory = parent;
|
|
@@ -10460,7 +10552,7 @@ async function findWorkspaceRoot(from) {
|
|
|
10460
10552
|
async function findAppRoot(cwd, workspaceRoot) {
|
|
10461
10553
|
let directory = resolve(cwd);
|
|
10462
10554
|
while (true) {
|
|
10463
|
-
const packageJson = await readJson$
|
|
10555
|
+
const packageJson = await readJson$2(join(directory, "package.json"));
|
|
10464
10556
|
if (packageJson && versionOf(packageJson, "expo")) return directory;
|
|
10465
10557
|
const parent = dirname(directory);
|
|
10466
10558
|
if (parent === directory || workspaceRoot && directory === workspaceRoot) break;
|
|
@@ -10479,7 +10571,7 @@ async function findExpoPackageIn(directory) {
|
|
|
10479
10571
|
}
|
|
10480
10572
|
for (const entry of entries.sort()) {
|
|
10481
10573
|
const candidate = join(directory, entry);
|
|
10482
|
-
const packageJson = await readJson$
|
|
10574
|
+
const packageJson = await readJson$2(join(candidate, "package.json"));
|
|
10483
10575
|
if (packageJson && versionOf(packageJson, "expo")) return candidate;
|
|
10484
10576
|
}
|
|
10485
10577
|
return null;
|
|
@@ -10503,7 +10595,7 @@ function findUp(from, filename) {
|
|
|
10503
10595
|
directory = parent;
|
|
10504
10596
|
}
|
|
10505
10597
|
}
|
|
10506
|
-
async function readJson$
|
|
10598
|
+
async function readJson$2(path) {
|
|
10507
10599
|
try {
|
|
10508
10600
|
return JSON.parse(await readFile(path, "utf-8"));
|
|
10509
10601
|
} catch {
|
|
@@ -10863,7 +10955,7 @@ const exec = x;
|
|
|
10863
10955
|
function channelFor(version) {
|
|
10864
10956
|
return /-alpha\b/.test(version) ? "alpha" : "latest";
|
|
10865
10957
|
}
|
|
10866
|
-
const CLI_CHANNEL = channelFor("0.5.0
|
|
10958
|
+
const CLI_CHANNEL = channelFor("0.5.0");
|
|
10867
10959
|
//#endregion
|
|
10868
10960
|
//#region src/project/package-manager.ts
|
|
10869
10961
|
const ADD = {
|
|
@@ -10971,9 +11063,9 @@ function installCommands(request) {
|
|
|
10971
11063
|
function planDependencies(request, packageJson) {
|
|
10972
11064
|
const groups = installCommands({
|
|
10973
11065
|
packageManager: request.packageManager,
|
|
10974
|
-
expoDependencies: missingPackages(packageJson, request.expoDependencies),
|
|
10975
|
-
dependencies: missingPackages(packageJson, request.dependencies),
|
|
10976
|
-
devDependencies: missingPackages(packageJson, request.devDependencies),
|
|
11066
|
+
expoDependencies: missingPackages$1(packageJson, request.expoDependencies),
|
|
11067
|
+
dependencies: missingPackages$1(packageJson, request.dependencies),
|
|
11068
|
+
devDependencies: missingPackages$1(packageJson, request.devDependencies),
|
|
10977
11069
|
channel: request.channel
|
|
10978
11070
|
});
|
|
10979
11071
|
const wanted = unique([
|
|
@@ -10998,7 +11090,7 @@ function unique(names) {
|
|
|
10998
11090
|
return [...new Set(names)].sort();
|
|
10999
11091
|
}
|
|
11000
11092
|
/** The packages in `wanted` that the project does not already have. */
|
|
11001
|
-
function missingPackages(packageJson, wanted) {
|
|
11093
|
+
function missingPackages$1(packageJson, wanted) {
|
|
11002
11094
|
const installed = /* @__PURE__ */ new Set([...Object.keys(packageJson?.dependencies ?? {}), ...Object.keys(packageJson?.devDependencies ?? {})]);
|
|
11003
11095
|
return wanted.filter((name) => !installed.has(name));
|
|
11004
11096
|
}
|
|
@@ -11117,7 +11209,7 @@ function sharedTsconfig() {
|
|
|
11117
11209
|
async function syncSharedPackage(config, peers) {
|
|
11118
11210
|
if (!config.package) return null;
|
|
11119
11211
|
const path = join(config.root, "package.json");
|
|
11120
|
-
const existing = await readJson(path);
|
|
11212
|
+
const existing = await readJson$1(path);
|
|
11121
11213
|
const merged = mergePackageJson(existing, {
|
|
11122
11214
|
name: config.package.name,
|
|
11123
11215
|
exports: buildExportsMap(config.paths, await collectWritten(config)),
|
|
@@ -11144,7 +11236,7 @@ async function linkPackageToApp(config) {
|
|
|
11144
11236
|
if (!config.package) return false;
|
|
11145
11237
|
if (config.app.resolved.root === config.root) return false;
|
|
11146
11238
|
const path = join(config.app.resolved.root, "package.json");
|
|
11147
|
-
const app = await readJson(path);
|
|
11239
|
+
const app = await readJson$1(path);
|
|
11148
11240
|
if (!app) return false;
|
|
11149
11241
|
const name = config.package.name;
|
|
11150
11242
|
if (app.dependencies?.[name] || app.peerDependencies?.[name]) return false;
|
|
@@ -11170,7 +11262,7 @@ async function listFiles(root, recursive) {
|
|
|
11170
11262
|
withFileTypes: true
|
|
11171
11263
|
})).filter((entry) => entry.isFile()).map((entry) => toPosix$6(relative(root, join(entry.parentPath, entry.name)))).sort();
|
|
11172
11264
|
}
|
|
11173
|
-
async function readJson(path) {
|
|
11265
|
+
async function readJson$1(path) {
|
|
11174
11266
|
try {
|
|
11175
11267
|
return JSON.parse(await readFile(path, "utf-8"));
|
|
11176
11268
|
} catch {
|
|
@@ -11184,6 +11276,212 @@ function toPosix$6(path) {
|
|
|
11184
11276
|
return sep === "/" ? path : path.split(sep).join("/");
|
|
11185
11277
|
}
|
|
11186
11278
|
//#endregion
|
|
11279
|
+
//#region src/registry/classify.ts
|
|
11280
|
+
/**
|
|
11281
|
+
* Maps a path inside `packages/react-native-ui/src` onto the registry item it
|
|
11282
|
+
* belongs to and where it lands in a consumer's project.
|
|
11283
|
+
*
|
|
11284
|
+
* The rules here are `react-native-ui`'s own conventions, restated — one component
|
|
11285
|
+
* per folder with an `index.ts` entry point, flat files under `lib/`, `hooks/`
|
|
11286
|
+
* and `icons/`. `packages/react-native-ui/scripts/gen-exports.ts` reads the same
|
|
11287
|
+
* shape to build the package's `exports` map, so a component folder that is
|
|
11288
|
+
* malformed fails there first.
|
|
11289
|
+
*
|
|
11290
|
+
* Pure and filesystem-free, so the whole mapping is reachable from `bun test`.
|
|
11291
|
+
*/
|
|
11292
|
+
const REGISTRY_TYPES = [
|
|
11293
|
+
"registry:ui",
|
|
11294
|
+
"registry:lib",
|
|
11295
|
+
"registry:hook",
|
|
11296
|
+
"registry:style"
|
|
11297
|
+
];
|
|
11298
|
+
/** A test, or a file that exists only to serve tests — `*.test-helpers.ts`, `*.fixtures.ts`. */
|
|
11299
|
+
function isTest(path) {
|
|
11300
|
+
return /\.(test|test-helpers|fixtures)\.tsx?$/.test(path);
|
|
11301
|
+
}
|
|
11302
|
+
//#endregion
|
|
11303
|
+
//#region src/project/test-setup.ts
|
|
11304
|
+
/** Expo's base config, which SDK 58's `exports` map hides from Vite's tsconfig loader. */
|
|
11305
|
+
const EXPO_TSCONFIG = "expo/tsconfig.base";
|
|
11306
|
+
function checkTestRunner(setup) {
|
|
11307
|
+
const name = "Test runner";
|
|
11308
|
+
if (setup.copiedTests === 0) return {
|
|
11309
|
+
name,
|
|
11310
|
+
status: "skip",
|
|
11311
|
+
detail: "no copied tests"
|
|
11312
|
+
};
|
|
11313
|
+
const problems = [
|
|
11314
|
+
...missingPackages(setup),
|
|
11315
|
+
...mismatchedScript(setup),
|
|
11316
|
+
...runnerProblems(setup)
|
|
11317
|
+
];
|
|
11318
|
+
const count = `${setup.copiedTests} copied test${setup.copiedTests === 1 ? "" : "s"}`;
|
|
11319
|
+
if (problems.length === 0) return {
|
|
11320
|
+
name,
|
|
11321
|
+
status: "pass",
|
|
11322
|
+
detail: `${count} set up for ${setup.runner}`
|
|
11323
|
+
};
|
|
11324
|
+
return {
|
|
11325
|
+
name,
|
|
11326
|
+
status: "warn",
|
|
11327
|
+
detail: `${count} for ${setup.runner}, but ${problems.map((problem) => problem.detail).join("; ")}`,
|
|
11328
|
+
fix: problems.map((problem) => problem.fix).join(" ")
|
|
11329
|
+
};
|
|
11330
|
+
}
|
|
11331
|
+
function missingPackages(setup) {
|
|
11332
|
+
const missing = [...setup.runner === "jest" ? ["jest"] : [], ...testRunnerPackages(setup.runner)].filter((name) => !(name in setup.dependencies));
|
|
11333
|
+
if (missing.length === 0) return [];
|
|
11334
|
+
return [{
|
|
11335
|
+
detail: `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not installed`,
|
|
11336
|
+
fix: `Install ${missing.map((name) => `\`${name}\``).join(", ")} as dev dependencies.`
|
|
11337
|
+
}];
|
|
11338
|
+
}
|
|
11339
|
+
function mismatchedScript(setup) {
|
|
11340
|
+
if (setup.scripted === null || setup.scripted === setup.runner) return [];
|
|
11341
|
+
return [{
|
|
11342
|
+
detail: `the project runs ${setup.scripted}`,
|
|
11343
|
+
fix: `Set \`tests.runner\` to "${setup.scripted}" in native-components.json and run \`update --tests\` to rewrite them, or point the \`test\` script at ${setup.runner}.`
|
|
11344
|
+
}];
|
|
11345
|
+
}
|
|
11346
|
+
function runnerProblems(setup) {
|
|
11347
|
+
switch (setup.runner) {
|
|
11348
|
+
case "bun": return [];
|
|
11349
|
+
case "jest": return jestProblems(setup);
|
|
11350
|
+
case "vitest": return vitestProblems(setup);
|
|
11351
|
+
}
|
|
11352
|
+
}
|
|
11353
|
+
/**
|
|
11354
|
+
* Bare Jest compiles neither TypeScript nor React Native, and `jest-expo` is
|
|
11355
|
+
* the preset that does both — and resolves the `tsconfig` aliases the copies
|
|
11356
|
+
* import through. Any other preset has to map them itself.
|
|
11357
|
+
*/
|
|
11358
|
+
function jestProblems(setup) {
|
|
11359
|
+
if (setup.jestConfig === null) return [{
|
|
11360
|
+
detail: "there is no Jest config",
|
|
11361
|
+
fix: "Add `\"jest\": { \"preset\": \"jest-expo\" }` to package.json, beside `jest-expo` installed at the app's SDK."
|
|
11362
|
+
}];
|
|
11363
|
+
if (setup.jestConfig.includes("jest-expo")) return [];
|
|
11364
|
+
const unmapped = setup.aliasPrefixes.filter((prefix) => !(setup.jestConfig?.includes("moduleNameMapper") && setup.jestConfig.includes(trimSlash(prefix))));
|
|
11365
|
+
if (unmapped.length === 0) return [];
|
|
11366
|
+
return [{
|
|
11367
|
+
detail: `Jest does not resolve ${unmapped.join(", ")}`,
|
|
11368
|
+
fix: `Use the \`jest-expo\` preset, or map ${unmapped.map((prefix) => `\`${prefix}\``).join(", ")} in \`moduleNameMapper\`.`
|
|
11369
|
+
}];
|
|
11370
|
+
}
|
|
11371
|
+
/**
|
|
11372
|
+
* Vitest reads no `tsconfig` paths, so an alias needs its own entry or the
|
|
11373
|
+
* `vite-tsconfig-paths` plugin. And Vite's own tsconfig loader cannot follow
|
|
11374
|
+
* `extends: "expo/tsconfig.base"` on SDK 58, which fails every suite before one
|
|
11375
|
+
* runs; `esbuild.tsconfigRaw` stops it reading `tsconfig.json` at all.
|
|
11376
|
+
*/
|
|
11377
|
+
function vitestProblems(setup) {
|
|
11378
|
+
const config = setup.vitestConfig ?? "";
|
|
11379
|
+
const problems = [];
|
|
11380
|
+
const unmapped = config.includes("vite-tsconfig-paths") || config.includes("tsconfigPaths") ? [] : setup.aliasPrefixes.filter((prefix) => !quotes(config, trimSlash(prefix)));
|
|
11381
|
+
if (unmapped.length > 0) problems.push({
|
|
11382
|
+
detail: `Vitest does not resolve ${unmapped.join(", ")}`,
|
|
11383
|
+
fix: `Map ${unmapped.map((prefix) => `\`${prefix}\``).join(", ")} in \`resolve.alias\` in vitest.config.ts.`
|
|
11384
|
+
});
|
|
11385
|
+
if (setup.tsconfigExtends === EXPO_TSCONFIG && !config.includes("tsconfigRaw")) problems.push({
|
|
11386
|
+
detail: `Vite cannot follow tsconfig.json's extends of ${EXPO_TSCONFIG}`,
|
|
11387
|
+
fix: "Set `esbuild: { tsconfigRaw: \"{}\" }` in vitest.config.ts."
|
|
11388
|
+
});
|
|
11389
|
+
return problems;
|
|
11390
|
+
}
|
|
11391
|
+
function trimSlash(prefix) {
|
|
11392
|
+
return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
11393
|
+
}
|
|
11394
|
+
/** Whether `text` holds `name`, or `name/`, as a whole quoted string. */
|
|
11395
|
+
function quotes(text, name) {
|
|
11396
|
+
return [
|
|
11397
|
+
"\"",
|
|
11398
|
+
"'",
|
|
11399
|
+
"`"
|
|
11400
|
+
].some((quote) => text.includes(`${quote}${name}${quote}`) || text.includes(`${quote}${name}/`));
|
|
11401
|
+
}
|
|
11402
|
+
const JEST_CONFIG_FILES = [
|
|
11403
|
+
"jest.config.js",
|
|
11404
|
+
"jest.config.ts",
|
|
11405
|
+
"jest.config.cjs",
|
|
11406
|
+
"jest.config.mjs",
|
|
11407
|
+
"jest.config.json"
|
|
11408
|
+
];
|
|
11409
|
+
const VITEST_CONFIG_FILES = [
|
|
11410
|
+
"vitest.config.ts",
|
|
11411
|
+
"vitest.config.mts",
|
|
11412
|
+
"vitest.config.js",
|
|
11413
|
+
"vitest.config.mjs",
|
|
11414
|
+
"vitest.config.cjs",
|
|
11415
|
+
"vite.config.ts",
|
|
11416
|
+
"vite.config.mts",
|
|
11417
|
+
"vite.config.js",
|
|
11418
|
+
"vite.config.mjs"
|
|
11419
|
+
];
|
|
11420
|
+
/** Reads what `checkTestRunner` and `checkJestTransform` need, from the config's package and then the app. */
|
|
11421
|
+
async function readTestSetup(config, project) {
|
|
11422
|
+
const roots = [.../* @__PURE__ */ new Set([config.root, config.app.resolved.root])];
|
|
11423
|
+
const manifests = await Promise.all(roots.map((root) => readJson(join(root, "package.json"))));
|
|
11424
|
+
const dependencies = Object.assign({}, project.packageJson?.dependencies, project.packageJson?.devDependencies, ...manifests.flatMap((manifest) => [manifest?.dependencies, manifest?.devDependencies]));
|
|
11425
|
+
return {
|
|
11426
|
+
runner: config.testRunner,
|
|
11427
|
+
scripted: manifests.map((manifest) => manifest ? detectTestRunner(manifest) : null).find(Boolean) ?? null,
|
|
11428
|
+
copiedTests: await countCopiedTests(config.root),
|
|
11429
|
+
dependencies,
|
|
11430
|
+
jestConfig: await readJestConfig(roots, manifests),
|
|
11431
|
+
vitestConfig: await readFirst(roots, VITEST_CONFIG_FILES),
|
|
11432
|
+
tsconfigExtends: await readTsconfigExtends(roots),
|
|
11433
|
+
aliasPrefixes: aliasPrefixes(config)
|
|
11434
|
+
};
|
|
11435
|
+
}
|
|
11436
|
+
function aliasPrefixes(config) {
|
|
11437
|
+
const prefixes = Object.values(config.aliases).flatMap((alias) => alias?.match(/^[^/]+\//)?.[0] ?? []);
|
|
11438
|
+
return [...new Set(prefixes)];
|
|
11439
|
+
}
|
|
11440
|
+
async function countCopiedTests(root) {
|
|
11441
|
+
try {
|
|
11442
|
+
const lock = await readLock(root);
|
|
11443
|
+
return Object.values(lock.items).flatMap((item) => Object.keys(item.files)).filter(isTest).length;
|
|
11444
|
+
} catch {
|
|
11445
|
+
return 0;
|
|
11446
|
+
}
|
|
11447
|
+
}
|
|
11448
|
+
async function readJestConfig(roots, manifests) {
|
|
11449
|
+
for (const [index, root] of roots.entries()) {
|
|
11450
|
+
const file = await readFirst([root], JEST_CONFIG_FILES);
|
|
11451
|
+
if (file !== null) return file;
|
|
11452
|
+
const inline = manifests[index]?.jest;
|
|
11453
|
+
if (inline !== void 0) return JSON.stringify(inline);
|
|
11454
|
+
}
|
|
11455
|
+
return null;
|
|
11456
|
+
}
|
|
11457
|
+
async function readFirst(roots, names) {
|
|
11458
|
+
for (const root of roots) for (const name of names) {
|
|
11459
|
+
const path = join(root, name);
|
|
11460
|
+
if (existsSync(path)) return readFile(path, "utf-8");
|
|
11461
|
+
}
|
|
11462
|
+
return null;
|
|
11463
|
+
}
|
|
11464
|
+
async function readTsconfigExtends(roots) {
|
|
11465
|
+
for (const root of roots) {
|
|
11466
|
+
const text = await readFile(join(root, "tsconfig.json"), "utf-8").catch(() => null);
|
|
11467
|
+
if (text === null) continue;
|
|
11468
|
+
try {
|
|
11469
|
+
const { extends: base } = parseJsonc(text);
|
|
11470
|
+
return typeof base === "string" ? base : null;
|
|
11471
|
+
} catch {
|
|
11472
|
+
return null;
|
|
11473
|
+
}
|
|
11474
|
+
}
|
|
11475
|
+
return null;
|
|
11476
|
+
}
|
|
11477
|
+
async function readJson(path) {
|
|
11478
|
+
try {
|
|
11479
|
+
return JSON.parse(await readFile(path, "utf-8"));
|
|
11480
|
+
} catch {
|
|
11481
|
+
return null;
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
//#endregion
|
|
11187
11485
|
//#region src/registry/transform.ts
|
|
11188
11486
|
/**
|
|
11189
11487
|
* Anchored on the surrounding quote or backtick.
|
|
@@ -11238,13 +11536,17 @@ function destination$1(config, namespace, target) {
|
|
|
11238
11536
|
displayPath: toPosix$4(relative(config.root, path))
|
|
11239
11537
|
};
|
|
11240
11538
|
}
|
|
11241
|
-
/**
|
|
11539
|
+
/**
|
|
11540
|
+
* A fetched file's text as this project would hold it at `path`: placeholders
|
|
11541
|
+
* resolved to its aliases, and a test written for the runner it tests with.
|
|
11542
|
+
*/
|
|
11242
11543
|
function transformFile(content, path, config) {
|
|
11243
|
-
|
|
11544
|
+
const transformed = transformContent(content, {
|
|
11244
11545
|
fileDirectory: dirname(path),
|
|
11245
11546
|
directories: config.directories,
|
|
11246
11547
|
aliases: config.aliases
|
|
11247
11548
|
});
|
|
11549
|
+
return isTest(path) ? adaptTest(transformed, config.testRunner) : transformed;
|
|
11248
11550
|
}
|
|
11249
11551
|
async function writeFiles(files) {
|
|
11250
11552
|
for (const file of files) {
|
|
@@ -11296,30 +11598,6 @@ function escapePattern(value) {
|
|
|
11296
11598
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11297
11599
|
}
|
|
11298
11600
|
//#endregion
|
|
11299
|
-
//#region src/registry/classify.ts
|
|
11300
|
-
/**
|
|
11301
|
-
* Maps a path inside `packages/react-native-ui/src` onto the registry item it
|
|
11302
|
-
* belongs to and where it lands in a consumer's project.
|
|
11303
|
-
*
|
|
11304
|
-
* The rules here are `react-native-ui`'s own conventions, restated — one component
|
|
11305
|
-
* per folder with an `index.ts` entry point, flat files under `lib/`, `hooks/`
|
|
11306
|
-
* and `icons/`. `packages/react-native-ui/scripts/gen-exports.ts` reads the same
|
|
11307
|
-
* shape to build the package's `exports` map, so a component folder that is
|
|
11308
|
-
* malformed fails there first.
|
|
11309
|
-
*
|
|
11310
|
-
* Pure and filesystem-free, so the whole mapping is reachable from `bun test`.
|
|
11311
|
-
*/
|
|
11312
|
-
const REGISTRY_TYPES = [
|
|
11313
|
-
"registry:ui",
|
|
11314
|
-
"registry:lib",
|
|
11315
|
-
"registry:hook",
|
|
11316
|
-
"registry:style"
|
|
11317
|
-
];
|
|
11318
|
-
/** A test, or a file that exists only to serve tests — `*.test-helpers.ts`, `*.fixtures.ts`. */
|
|
11319
|
-
function isTest(path) {
|
|
11320
|
-
return /\.(test|test-helpers|fixtures)\.tsx?$/.test(path);
|
|
11321
|
-
}
|
|
11322
|
-
//#endregion
|
|
11323
11601
|
//#region src/registry/schema.ts
|
|
11324
11602
|
/**
|
|
11325
11603
|
* The shape of the JSON the builder emits and the `add` command consumes.
|
|
@@ -11446,13 +11724,14 @@ const registryIndexSchema = object$1({
|
|
|
11446
11724
|
* then treats a test like any other file, which is the point: a copied test is
|
|
11447
11725
|
* the consumer's from then on, recorded and updated exactly as the source is.
|
|
11448
11726
|
*/
|
|
11449
|
-
function withTests(item) {
|
|
11727
|
+
function withTests(item, runner = "bun") {
|
|
11450
11728
|
const tests = "tests" in item ? item.tests : [];
|
|
11729
|
+
const testDependencies = item.testDependencies.flatMap((name) => name === "@types/bun" ? testRunnerPackages(runner) : [name]);
|
|
11451
11730
|
return {
|
|
11452
11731
|
...item,
|
|
11453
11732
|
..."files" in item && "tests" in item ? { files: [...item.files, ...tests] } : {},
|
|
11454
11733
|
registryDependencies: union(item.registryDependencies, item.testRegistryDependencies),
|
|
11455
|
-
devDependencies: union(item.devDependencies,
|
|
11734
|
+
devDependencies: union(item.devDependencies, testDependencies)
|
|
11456
11735
|
};
|
|
11457
11736
|
}
|
|
11458
11737
|
function union(a, b) {
|
|
@@ -11564,13 +11843,13 @@ const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/delacournz/delac
|
|
|
11564
11843
|
* Replaced at build time with the git tag being published (see `tsdown.config.ts`).
|
|
11565
11844
|
* `main` in a working tree, where there is no tag yet.
|
|
11566
11845
|
*/
|
|
11567
|
-
const DEFAULT_REGISTRY_REF = "
|
|
11846
|
+
const DEFAULT_REGISTRY_REF = "713984b74d21a6bb6b0e1f719a947cce7a2d847e";
|
|
11568
11847
|
const RAW_GITHUB = "https://raw.githubusercontent.com";
|
|
11569
11848
|
const GITHUB_SHORTHAND = /^github:([^/#]+)\/([^/#]+)(?:#(.+))?$/;
|
|
11570
11849
|
/** `https://raw.githubusercontent.com/owner/repo` and nothing more. */
|
|
11571
11850
|
const BARE_RAW_REPO = new RegExp(`^${RAW_GITHUB}/([^/]+)/([^/]+)$`);
|
|
11572
11851
|
function resolveRegistrySource(options) {
|
|
11573
|
-
const ref = options.ref || "
|
|
11852
|
+
const ref = options.ref || "713984b74d21a6bb6b0e1f719a947cce7a2d847e";
|
|
11574
11853
|
const url = options.url?.trim();
|
|
11575
11854
|
if (!url) return remote(`${DEFAULT_REGISTRY_URL}/${ref}`);
|
|
11576
11855
|
const shorthand = GITHUB_SHORTHAND.exec(url);
|
|
@@ -11984,477 +12263,90 @@ const style = {
|
|
|
11984
12263
|
path: (value) => import_picocolors.default.dim(value)
|
|
11985
12264
|
};
|
|
11986
12265
|
//#endregion
|
|
11987
|
-
//#region src/
|
|
12266
|
+
//#region ../design-system/src/styles.ts
|
|
11988
12267
|
/**
|
|
11989
|
-
*
|
|
11990
|
-
*
|
|
11991
|
-
* Two things have to happen and only one of them is obvious. The `@import`
|
|
11992
|
-
* pulls in the tokens and the theme. The `@source` lines are what make the
|
|
11993
|
-
* classes inside the copied components survive a production build: Tailwind
|
|
11994
|
-
* scans source text, and a class it never saw is a class it never compiled.
|
|
12268
|
+
* The three control heights, the type paired with each, and the icon scale.
|
|
11995
12269
|
*
|
|
11996
|
-
*
|
|
11997
|
-
*
|
|
11998
|
-
*
|
|
11999
|
-
*
|
|
12000
|
-
* nothing, and the components render unstyled in release and fine in dev. The
|
|
12001
|
-
* comment in `apps/playground/src/styles/global.css` is this bug, already paid
|
|
12002
|
-
* for once.
|
|
12270
|
+
* A style names one density and everything steps from it, rather than listing
|
|
12271
|
+
* two dozen numbers per style and inviting them to drift out of proportion.
|
|
12272
|
+
* `heights` are the button and input scales, which the library keeps separate
|
|
12273
|
+
* but which no style has yet had a reason to disagree on.
|
|
12003
12274
|
*/
|
|
12004
|
-
|
|
12005
|
-
|
|
12275
|
+
function geometry({ radius, heights, text, icons, navbar, gutter, buttonRadius }) {
|
|
12276
|
+
return {
|
|
12277
|
+
radius,
|
|
12278
|
+
"spacing-button-sm": heights[0],
|
|
12279
|
+
"spacing-button-md": heights[1],
|
|
12280
|
+
"spacing-button-lg": heights[2],
|
|
12281
|
+
"text-button-sm": text[0],
|
|
12282
|
+
"text-button-md": text[1],
|
|
12283
|
+
"text-button-lg": text[2],
|
|
12284
|
+
"radius-button-sm": Math.min(buttonRadius[0], heights[0] / 2),
|
|
12285
|
+
"radius-button-md": Math.min(buttonRadius[1], heights[1] / 2),
|
|
12286
|
+
"radius-button-lg": Math.min(buttonRadius[2], heights[2] / 2),
|
|
12287
|
+
"spacing-input-sm": heights[0],
|
|
12288
|
+
"spacing-input-md": heights[1],
|
|
12289
|
+
"spacing-input-lg": heights[2],
|
|
12290
|
+
"text-input-sm": text[0],
|
|
12291
|
+
"text-input-md": text[1],
|
|
12292
|
+
"text-input-lg": text[2],
|
|
12293
|
+
"spacing-icon-xs": icons[0],
|
|
12294
|
+
"spacing-icon-sm": icons[1],
|
|
12295
|
+
"spacing-icon-md": icons[2],
|
|
12296
|
+
"spacing-icon-lg": icons[3],
|
|
12297
|
+
"spacing-icon-xl": icons[4],
|
|
12298
|
+
"spacing-icon-2xl": icons[5],
|
|
12299
|
+
"spacing-navbar-row": navbar,
|
|
12300
|
+
"spacing-screen-gutter": gutter,
|
|
12301
|
+
"spacing-chart-sm": chartHeight(LIBRARY_CHART[0], heights[1]),
|
|
12302
|
+
"spacing-chart-md": chartHeight(LIBRARY_CHART[1], heights[1]),
|
|
12303
|
+
"spacing-chart-lg": chartHeight(LIBRARY_CHART[2], heights[1]),
|
|
12304
|
+
"spacing-fab-sm": LIBRARY_FAB[0],
|
|
12305
|
+
"spacing-fab-md": LIBRARY_FAB[1],
|
|
12306
|
+
"spacing-fab-lg": LIBRARY_FAB[2]
|
|
12307
|
+
};
|
|
12308
|
+
}
|
|
12309
|
+
const LIBRARY_HEIGHTS = [
|
|
12310
|
+
36,
|
|
12311
|
+
44,
|
|
12312
|
+
52
|
|
12313
|
+
];
|
|
12314
|
+
const LIBRARY_TEXT = [
|
|
12315
|
+
14,
|
|
12316
|
+
16,
|
|
12317
|
+
18
|
|
12318
|
+
];
|
|
12319
|
+
const LIBRARY_ICONS = [
|
|
12320
|
+
14,
|
|
12321
|
+
16,
|
|
12322
|
+
18,
|
|
12323
|
+
20,
|
|
12324
|
+
24,
|
|
12325
|
+
32
|
|
12326
|
+
];
|
|
12327
|
+
const CAPSULE = [
|
|
12328
|
+
18,
|
|
12329
|
+
22,
|
|
12330
|
+
26
|
|
12331
|
+
];
|
|
12332
|
+
const LIBRARY_CHART = [
|
|
12333
|
+
160,
|
|
12334
|
+
220,
|
|
12335
|
+
280
|
|
12336
|
+
];
|
|
12006
12337
|
/**
|
|
12007
|
-
*
|
|
12008
|
-
*
|
|
12338
|
+
* A floating action button's footprint, the same under every style. It floats
|
|
12339
|
+
* over content rather than sitting in a row of controls, so a denser style's
|
|
12340
|
+
* shorter buttons are no reason to shrink the one target above the list — and
|
|
12341
|
+
* 44 is the floor a touch target cannot go under.
|
|
12009
12342
|
*/
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
""
|
|
12016
|
-
];
|
|
12017
|
-
const scanned = new Set(NAMESPACES.filter((namespace) => namespace !== "styles").map((namespace) => context.directories[namespace]));
|
|
12018
|
-
for (const directory of [...scanned].sort()) {
|
|
12019
|
-
if ([...scanned].some((other) => other !== directory && isInside(directory, other))) continue;
|
|
12020
|
-
lines.push(`@source "${toSpecifier$1(from, directory)}";`);
|
|
12021
|
-
}
|
|
12022
|
-
lines.push(END);
|
|
12023
|
-
return lines.join("\n");
|
|
12024
|
-
}
|
|
12343
|
+
const LIBRARY_FAB = [
|
|
12344
|
+
44,
|
|
12345
|
+
56,
|
|
12346
|
+
64
|
|
12347
|
+
];
|
|
12025
12348
|
/**
|
|
12026
|
-
*
|
|
12027
|
-
*
|
|
12028
|
-
* Bounded by markers so a second `init` replaces the block rather than
|
|
12029
|
-
* appending a second copy, and so everything a developer adds around it is left
|
|
12030
|
-
* alone.
|
|
12031
|
-
*/
|
|
12032
|
-
function patchGlobalCss(existing, block) {
|
|
12033
|
-
if (existing === null) return {
|
|
12034
|
-
content: `${DEFAULT_ENTRY}\n${block}\n`,
|
|
12035
|
-
changed: true
|
|
12036
|
-
};
|
|
12037
|
-
const start = existing.indexOf(START);
|
|
12038
|
-
const end = existing.indexOf(END);
|
|
12039
|
-
if (start !== -1 && end > start) {
|
|
12040
|
-
const content = `${existing.slice(0, start)}${block}${existing.slice(end + 18)}`;
|
|
12041
|
-
return {
|
|
12042
|
-
content,
|
|
12043
|
-
changed: content !== existing
|
|
12044
|
-
};
|
|
12045
|
-
}
|
|
12046
|
-
return {
|
|
12047
|
-
content: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${block}\n`,
|
|
12048
|
-
changed: true
|
|
12049
|
-
};
|
|
12050
|
-
}
|
|
12051
|
-
const DEFAULT_ENTRY = `@import "tailwindcss";
|
|
12052
|
-
@import "uniwind";
|
|
12053
|
-
|
|
12054
|
-
@source "../**/*.{ts,tsx}";
|
|
12055
|
-
`;
|
|
12056
|
-
/**
|
|
12057
|
-
* The directories a Tailwind entry actually scans.
|
|
12058
|
-
*
|
|
12059
|
-
* Read back rather than assumed, because this is the check that matters: a
|
|
12060
|
-
* `@source` that does not cover where the components landed produces a build
|
|
12061
|
-
* with no error and no styles. Only directory sources are returned — a glob
|
|
12062
|
-
* like `../**\/*.{ts,tsx}` is reduced to the directory it starts from, which is
|
|
12063
|
-
* what decides coverage.
|
|
12064
|
-
*/
|
|
12065
|
-
function parseSources(css, cssPath) {
|
|
12066
|
-
const from = dirname(cssPath);
|
|
12067
|
-
const sources = [];
|
|
12068
|
-
for (const match of css.matchAll(/@source\s+(?:not\s+)?["']([^"']+)["']/g)) {
|
|
12069
|
-
const value = match[1];
|
|
12070
|
-
if (value.startsWith("inline(")) continue;
|
|
12071
|
-
const wildcard = value.search(/[*?[]/);
|
|
12072
|
-
const directory = wildcard === -1 ? value : value.slice(0, wildcard);
|
|
12073
|
-
sources.push(resolve(from, directory));
|
|
12074
|
-
}
|
|
12075
|
-
return sources;
|
|
12076
|
-
}
|
|
12077
|
-
/** Whether any source directory contains `directory`. */
|
|
12078
|
-
function isCovered(directory, sources) {
|
|
12079
|
-
return sources.some((source) => {
|
|
12080
|
-
const suffix = relative(source, directory);
|
|
12081
|
-
return suffix === "" || !suffix.startsWith("..");
|
|
12082
|
-
});
|
|
12083
|
-
}
|
|
12084
|
-
/** A path Tailwind can resolve from the entry file, always POSIX and always relative. */
|
|
12085
|
-
function toSpecifier$1(from, target) {
|
|
12086
|
-
const path = toPosix$3(relative(from, target));
|
|
12087
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
12088
|
-
}
|
|
12089
|
-
function isInside(directory, parent) {
|
|
12090
|
-
const suffix = relative(parent, directory);
|
|
12091
|
-
return suffix !== "" && !suffix.startsWith("..");
|
|
12092
|
-
}
|
|
12093
|
-
function toPosix$3(path) {
|
|
12094
|
-
return sep === "/" ? path : path.split(sep).join("/");
|
|
12095
|
-
}
|
|
12096
|
-
//#endregion
|
|
12097
|
-
//#region src/project/css-entry.ts
|
|
12098
|
-
/**
|
|
12099
|
-
* Finds the Tailwind entry an app already has.
|
|
12100
|
-
*
|
|
12101
|
-
* Creating a second one is the failure this exists to prevent. Metro compiles
|
|
12102
|
-
* the file its `cssEntryFile` names and no other; the app imports whichever
|
|
12103
|
-
* file its root layout imports. Write a new entry beside an existing one and
|
|
12104
|
-
* those become two different files — Tailwind compiles the one with the
|
|
12105
|
-
* `@source` globs, the app loads the one without them, and every component
|
|
12106
|
-
* renders unstyled with nothing logged.
|
|
12107
|
-
*
|
|
12108
|
-
* The Metro config wins when it names one, because that is what actually
|
|
12109
|
-
* compiles — see `readUniwindPaths`. This is the fallback for a project that
|
|
12110
|
-
* has an entry but has not wired Metro to it yet, which is every app that
|
|
12111
|
-
* installed Uniwind and stopped there.
|
|
12112
|
-
*
|
|
12113
|
-
* Shallowest first: an app that ran an older version of this CLI has both
|
|
12114
|
-
* `src/global.css` and `src/styles/global.css`, and the one it imports is the
|
|
12115
|
-
* one it had first.
|
|
12116
|
-
*/
|
|
12117
|
-
const CANDIDATES$1 = [
|
|
12118
|
-
"global.css",
|
|
12119
|
-
"src/global.css",
|
|
12120
|
-
"app/global.css",
|
|
12121
|
-
"src/app/global.css",
|
|
12122
|
-
"styles/global.css",
|
|
12123
|
-
"src/styles/global.css",
|
|
12124
|
-
"src/styles/index.css"
|
|
12125
|
-
];
|
|
12126
|
-
/** `@import "tailwindcss"`, however it is quoted and whatever follows it. */
|
|
12127
|
-
const TAILWIND_IMPORT = /@import\s+["']tailwindcss["']/;
|
|
12128
|
-
function findTailwindEntry(appRoot) {
|
|
12129
|
-
for (const candidate of CANDIDATES$1) {
|
|
12130
|
-
const path = join(appRoot, candidate);
|
|
12131
|
-
if (!existsSync(path)) continue;
|
|
12132
|
-
try {
|
|
12133
|
-
if (TAILWIND_IMPORT.test(readFileSync(path, "utf-8"))) return path;
|
|
12134
|
-
} catch {}
|
|
12135
|
-
}
|
|
12136
|
-
return null;
|
|
12137
|
-
}
|
|
12138
|
-
//#endregion
|
|
12139
|
-
//#region src/project/metro.ts
|
|
12140
|
-
/**
|
|
12141
|
-
* Wraps the app's Metro config with Uniwind's.
|
|
12142
|
-
*
|
|
12143
|
-
* `withUniwindConfig` has to stay the outermost wrapper — it installs the
|
|
12144
|
-
* transformer that compiles `className` into styles, and a wrapper applied
|
|
12145
|
-
* after it can replace the `transformer` key and leave every component
|
|
12146
|
-
* unstyled with nothing logged. So the patch rewrites the export itself rather
|
|
12147
|
-
* than inserting a line somewhere in the middle.
|
|
12148
|
-
*
|
|
12149
|
-
* When the export is not in a shape this can safely rewrite, it says so and
|
|
12150
|
-
* prints what to write. Guessing at an unfamiliar config file earns nothing —
|
|
12151
|
-
* the failure it causes is a Metro error at bundle time in someone else's app.
|
|
12152
|
-
*/
|
|
12153
|
-
/** Modules that must resolve to exactly one copy across a workspace. */
|
|
12154
|
-
const PINNED_MODULES = [
|
|
12155
|
-
"@shopify/react-native-skia",
|
|
12156
|
-
"react",
|
|
12157
|
-
"react-native",
|
|
12158
|
-
"react-native-gesture-handler",
|
|
12159
|
-
"react-native-keyboard-controller",
|
|
12160
|
-
"react-native-reanimated",
|
|
12161
|
-
"react-native-safe-area-context",
|
|
12162
|
-
"react-native-svg",
|
|
12163
|
-
"react-native-teleport",
|
|
12164
|
-
"react-native-worklets"
|
|
12165
|
-
];
|
|
12166
|
-
const REQUIRE = "const { withUniwindConfig } = require(\"uniwind/metro\");";
|
|
12167
|
-
const IMPORT = "import { withUniwindConfig } from \"uniwind/metro\";";
|
|
12168
|
-
/** `module.exports = config;` or `export default config;`, with nothing after it. */
|
|
12169
|
-
const COMMONJS_EXPORT = /module\.exports\s*=\s*([\s\S]+?);?\s*$/;
|
|
12170
|
-
const ESM_EXPORT = /export default\s+([\s\S]+?);?\s*$/;
|
|
12171
|
-
const CSS_ENTRY = /cssEntryFile\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
12172
|
-
const DTS_FILE = /dtsFile\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
12173
|
-
/**
|
|
12174
|
-
* Reads the paths a wrapped Metro config already names.
|
|
12175
|
-
*
|
|
12176
|
-
* A project that arrives with Uniwind set up — Expo's `with-router-uniwind`
|
|
12177
|
-
* example is the common one — has chosen its own CSS entry, and Metro compiles
|
|
12178
|
-
* **that file and no other**. Writing the `@source` block into the path this
|
|
12179
|
-
* CLI would have picked leaves Tailwind scanning a file with no globs in it, so
|
|
12180
|
-
* every class the components use is dropped and every one of them renders
|
|
12181
|
-
* unstyled. Nothing errors; the app just looks wrong.
|
|
12182
|
-
*
|
|
12183
|
-
* So the wired config wins. Returns nothing when the config is not wired, in
|
|
12184
|
-
* which case `init` is about to wrap it with paths of its own.
|
|
12185
|
-
*/
|
|
12186
|
-
function readUniwindPaths(existing) {
|
|
12187
|
-
if (!existing?.includes("withUniwindConfig")) return {};
|
|
12188
|
-
return {
|
|
12189
|
-
css: CSS_ENTRY.exec(existing)?.[1],
|
|
12190
|
-
types: DTS_FILE.exec(existing)?.[1]
|
|
12191
|
-
};
|
|
12192
|
-
}
|
|
12193
|
-
function patchMetroConfig(existing, options) {
|
|
12194
|
-
const wrapper = optionsLiteral(options);
|
|
12195
|
-
const monorepo = options.workspaceRoot && options.workspaceRoot !== options.metroConfigPath ? monorepoResolverBlock(dirname(options.metroConfigPath), options.workspaceRoot) : "";
|
|
12196
|
-
if (existing === null) return {
|
|
12197
|
-
status: "created",
|
|
12198
|
-
content: template(wrapper, monorepo)
|
|
12199
|
-
};
|
|
12200
|
-
if (existing.includes("withUniwindConfig")) return { status: "already-wired" };
|
|
12201
|
-
const isEsm = /^\s*(import|export default)/m.test(existing);
|
|
12202
|
-
const match = (isEsm ? ESM_EXPORT : COMMONJS_EXPORT).exec(existing);
|
|
12203
|
-
if (!match?.[1]) return {
|
|
12204
|
-
status: "manual",
|
|
12205
|
-
snippet: manualSnippet(wrapper, isEsm)
|
|
12206
|
-
};
|
|
12207
|
-
const keyword = isEsm ? "export default" : "module.exports =";
|
|
12208
|
-
const head = existing.slice(0, match.index);
|
|
12209
|
-
const wrapped = `${keyword} withUniwindConfig(${match[1].trim()}, ${wrapper});\n`;
|
|
12210
|
-
const body = monorepo ? `${head}${monorepo}\n` : head;
|
|
12211
|
-
return {
|
|
12212
|
-
status: "patched",
|
|
12213
|
-
content: `${isEsm ? IMPORT : REQUIRE}\n${body}${wrapped}`
|
|
12214
|
-
};
|
|
12215
|
-
}
|
|
12216
|
-
function optionsLiteral(options) {
|
|
12217
|
-
const from = dirname(options.metroConfigPath);
|
|
12218
|
-
return `{\n\tcssEntryFile: "${toSpecifier(from, options.cssPath)}",\n\tdtsFile: "${toSpecifier(from, options.typesPath)}",\n}`;
|
|
12219
|
-
}
|
|
12220
|
-
/**
|
|
12221
|
-
* The resolver settings an app needs to reach a package outside its own tree.
|
|
12222
|
-
*
|
|
12223
|
-
* Metro resolves by walking `node_modules` from the app downwards, so without
|
|
12224
|
-
* `watchFolders` and `nodeModulesPaths` it simply cannot see a sibling package —
|
|
12225
|
-
* the import fails at bundle time with a missing module, pointing at the app
|
|
12226
|
-
* rather than at the workspace.
|
|
12227
|
-
*
|
|
12228
|
-
* `extraNodeModules` is the other half, and it is about runtime rather than
|
|
12229
|
-
* resolution: a package manager can materialise a second copy of a native module
|
|
12230
|
-
* under the app, and two registrations of one native module break. This is the
|
|
12231
|
-
* block `apps/playground/metro.config.js` in this repository carries for exactly
|
|
12232
|
-
* this reason.
|
|
12233
|
-
*/
|
|
12234
|
-
function monorepoResolverBlock(appRoot, workspaceRoot) {
|
|
12235
|
-
const pins = PINNED_MODULES.map((name) => `\t${JSON.stringify(name)}: path.resolve(workspaceRoot, "node_modules/${name}"),`).join("\n");
|
|
12236
|
-
return `const path = require("node:path");
|
|
12237
|
-
|
|
12238
|
-
const workspaceRoot = path.resolve(__dirname, "${toSpecifier(appRoot, workspaceRoot)}");
|
|
12239
|
-
|
|
12240
|
-
// The components live in a package outside this app, so Metro has to watch and
|
|
12241
|
-
// resolve beyond the app's own directory.
|
|
12242
|
-
config.watchFolders = [workspaceRoot];
|
|
12243
|
-
config.resolver.nodeModulesPaths = [
|
|
12244
|
-
\tpath.resolve(__dirname, "node_modules"),
|
|
12245
|
-
\tpath.resolve(workspaceRoot, "node_modules"),
|
|
12246
|
-
];
|
|
12247
|
-
config.resolver.disableHierarchicalLookup = true;
|
|
12248
|
-
|
|
12249
|
-
// One copy of each native module for the whole bundle: two registrations of a
|
|
12250
|
-
// native module break at runtime.
|
|
12251
|
-
config.resolver.extraNodeModules = {
|
|
12252
|
-
${pins}
|
|
12253
|
-
};
|
|
12254
|
-
`;
|
|
12255
|
-
}
|
|
12256
|
-
function template(wrapper, monorepo) {
|
|
12257
|
-
return `const { getDefaultConfig } = require("expo/metro-config");
|
|
12258
|
-
${REQUIRE}
|
|
12259
|
-
|
|
12260
|
-
const config = getDefaultConfig(__dirname);
|
|
12261
|
-
${monorepo ? `\n${monorepo}` : ""}
|
|
12262
|
-
// withUniwindConfig must stay the outermost wrapper.
|
|
12263
|
-
module.exports = withUniwindConfig(config, ${wrapper});
|
|
12264
|
-
`;
|
|
12265
|
-
}
|
|
12266
|
-
function manualSnippet(wrapper, isEsm) {
|
|
12267
|
-
return `${isEsm ? IMPORT : REQUIRE}\n\n// Keep this the outermost wrapper.\n${isEsm ? "export default" : "module.exports ="} withUniwindConfig(config, ${wrapper});`;
|
|
12268
|
-
}
|
|
12269
|
-
function toSpecifier(from, target) {
|
|
12270
|
-
const path = toPosix$2(relative(from, target));
|
|
12271
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
12272
|
-
}
|
|
12273
|
-
function toPosix$2(path) {
|
|
12274
|
-
return sep === "/" ? path : path.split(sep).join("/");
|
|
12275
|
-
}
|
|
12276
|
-
//#endregion
|
|
12277
|
-
//#region src/project/root-layout.ts
|
|
12278
|
-
/**
|
|
12279
|
-
* Router layouts first: an app holding both is an Expo Router app whose
|
|
12280
|
-
* `App.tsx` is left over, and the layout is the file that actually renders.
|
|
12281
|
-
*/
|
|
12282
|
-
const CANDIDATES = [
|
|
12283
|
-
{
|
|
12284
|
-
path: "src/app/_layout.tsx",
|
|
12285
|
-
router: true
|
|
12286
|
-
},
|
|
12287
|
-
{
|
|
12288
|
-
path: "app/_layout.tsx",
|
|
12289
|
-
router: true
|
|
12290
|
-
},
|
|
12291
|
-
{
|
|
12292
|
-
path: "src/app/_layout.jsx",
|
|
12293
|
-
router: true
|
|
12294
|
-
},
|
|
12295
|
-
{
|
|
12296
|
-
path: "app/_layout.jsx",
|
|
12297
|
-
router: true
|
|
12298
|
-
},
|
|
12299
|
-
{
|
|
12300
|
-
path: "App.tsx",
|
|
12301
|
-
router: false
|
|
12302
|
-
},
|
|
12303
|
-
{
|
|
12304
|
-
path: "src/App.tsx",
|
|
12305
|
-
router: false
|
|
12306
|
-
},
|
|
12307
|
-
{
|
|
12308
|
-
path: "App.jsx",
|
|
12309
|
-
router: false
|
|
12310
|
-
},
|
|
12311
|
-
{
|
|
12312
|
-
path: "index.tsx",
|
|
12313
|
-
router: false
|
|
12314
|
-
}
|
|
12315
|
-
];
|
|
12316
|
-
function findRootLayout(appRoot) {
|
|
12317
|
-
return CANDIDATES.find((candidate) => existsSync(join(appRoot, candidate.path))) ?? null;
|
|
12318
|
-
}
|
|
12319
|
-
/**
|
|
12320
|
-
* The root layout, whole, ready to paste.
|
|
12321
|
-
*
|
|
12322
|
-
* The CSS import is the first line because that is the rule it exists to
|
|
12323
|
-
* teach — anywhere else and every component renders unstyled with nothing
|
|
12324
|
-
* logged.
|
|
12325
|
-
*
|
|
12326
|
-
* `<Slot />` is Expo Router's minimal root. A layout already rendering a
|
|
12327
|
-
* `<Stack>` keeps it; the provider goes around whatever is there, which is
|
|
12328
|
-
* what the line printed beside this says.
|
|
12329
|
-
*/
|
|
12330
|
-
function renderRootLayout(layout, specifiers) {
|
|
12331
|
-
const { css, provider } = specifiers;
|
|
12332
|
-
if (!layout.router) return [
|
|
12333
|
-
`import "${css}";`,
|
|
12334
|
-
`import { DelacourProvider } from "${provider}";`,
|
|
12335
|
-
"",
|
|
12336
|
-
"export default function App() {",
|
|
12337
|
-
" return (",
|
|
12338
|
-
" <DelacourProvider>",
|
|
12339
|
-
" <YourApp />",
|
|
12340
|
-
" </DelacourProvider>",
|
|
12341
|
-
" );",
|
|
12342
|
-
"}"
|
|
12343
|
-
].join("\n");
|
|
12344
|
-
return [
|
|
12345
|
-
`import "${css}";`,
|
|
12346
|
-
`import { DelacourProvider } from "${provider}";`,
|
|
12347
|
-
"import { Slot } from \"expo-router\";",
|
|
12348
|
-
"",
|
|
12349
|
-
"export default function RootLayout() {",
|
|
12350
|
-
" return (",
|
|
12351
|
-
" <DelacourProvider>",
|
|
12352
|
-
" <Slot />",
|
|
12353
|
-
" </DelacourProvider>",
|
|
12354
|
-
" );",
|
|
12355
|
-
"}"
|
|
12356
|
-
].join("\n");
|
|
12357
|
-
}
|
|
12358
|
-
//#endregion
|
|
12359
|
-
//#region src/project/uniwind-env.ts
|
|
12360
|
-
/**
|
|
12361
|
-
* The one line that makes `className` a real prop in TypeScript.
|
|
12362
|
-
*
|
|
12363
|
-
* Uniwind augments React Native's types through a global `/// <reference>`, and
|
|
12364
|
-
* a declaration file is only loaded if it is part of the program — either
|
|
12365
|
-
* matched by `tsconfig`'s `include` or reachable by import. This file is neither
|
|
12366
|
-
* importable nor, in a shared package, inside the app's `include`, which is why
|
|
12367
|
-
* the app needs a copy of its own.
|
|
12368
|
-
*
|
|
12369
|
-
* Kept here rather than inlined so `init` and `doctor` agree on the contents.
|
|
12370
|
-
*/
|
|
12371
|
-
const UNIWIND_ENV_FILENAME = "uniwind-env.d.ts";
|
|
12372
|
-
const UNIWIND_ENV_REFERENCE = "/// <reference types=\"uniwind/types\" />";
|
|
12373
|
-
//#endregion
|
|
12374
|
-
//#region ../design-system/src/styles.ts
|
|
12375
|
-
/**
|
|
12376
|
-
* The three control heights, the type paired with each, and the icon scale.
|
|
12377
|
-
*
|
|
12378
|
-
* A style names one density and everything steps from it, rather than listing
|
|
12379
|
-
* two dozen numbers per style and inviting them to drift out of proportion.
|
|
12380
|
-
* `heights` are the button and input scales, which the library keeps separate
|
|
12381
|
-
* but which no style has yet had a reason to disagree on.
|
|
12382
|
-
*/
|
|
12383
|
-
function geometry({ radius, heights, text, icons, navbar, gutter, buttonRadius }) {
|
|
12384
|
-
return {
|
|
12385
|
-
radius,
|
|
12386
|
-
"spacing-button-sm": heights[0],
|
|
12387
|
-
"spacing-button-md": heights[1],
|
|
12388
|
-
"spacing-button-lg": heights[2],
|
|
12389
|
-
"text-button-sm": text[0],
|
|
12390
|
-
"text-button-md": text[1],
|
|
12391
|
-
"text-button-lg": text[2],
|
|
12392
|
-
"radius-button-sm": Math.min(buttonRadius[0], heights[0] / 2),
|
|
12393
|
-
"radius-button-md": Math.min(buttonRadius[1], heights[1] / 2),
|
|
12394
|
-
"radius-button-lg": Math.min(buttonRadius[2], heights[2] / 2),
|
|
12395
|
-
"spacing-input-sm": heights[0],
|
|
12396
|
-
"spacing-input-md": heights[1],
|
|
12397
|
-
"spacing-input-lg": heights[2],
|
|
12398
|
-
"text-input-sm": text[0],
|
|
12399
|
-
"text-input-md": text[1],
|
|
12400
|
-
"text-input-lg": text[2],
|
|
12401
|
-
"spacing-icon-xs": icons[0],
|
|
12402
|
-
"spacing-icon-sm": icons[1],
|
|
12403
|
-
"spacing-icon-md": icons[2],
|
|
12404
|
-
"spacing-icon-lg": icons[3],
|
|
12405
|
-
"spacing-icon-xl": icons[4],
|
|
12406
|
-
"spacing-icon-2xl": icons[5],
|
|
12407
|
-
"spacing-navbar-row": navbar,
|
|
12408
|
-
"spacing-screen-gutter": gutter,
|
|
12409
|
-
"spacing-chart-sm": chartHeight(LIBRARY_CHART[0], heights[1]),
|
|
12410
|
-
"spacing-chart-md": chartHeight(LIBRARY_CHART[1], heights[1]),
|
|
12411
|
-
"spacing-chart-lg": chartHeight(LIBRARY_CHART[2], heights[1]),
|
|
12412
|
-
"spacing-fab-sm": LIBRARY_FAB[0],
|
|
12413
|
-
"spacing-fab-md": LIBRARY_FAB[1],
|
|
12414
|
-
"spacing-fab-lg": LIBRARY_FAB[2]
|
|
12415
|
-
};
|
|
12416
|
-
}
|
|
12417
|
-
const LIBRARY_HEIGHTS = [
|
|
12418
|
-
36,
|
|
12419
|
-
44,
|
|
12420
|
-
52
|
|
12421
|
-
];
|
|
12422
|
-
const LIBRARY_TEXT = [
|
|
12423
|
-
14,
|
|
12424
|
-
16,
|
|
12425
|
-
18
|
|
12426
|
-
];
|
|
12427
|
-
const LIBRARY_ICONS = [
|
|
12428
|
-
14,
|
|
12429
|
-
16,
|
|
12430
|
-
18,
|
|
12431
|
-
20,
|
|
12432
|
-
24,
|
|
12433
|
-
32
|
|
12434
|
-
];
|
|
12435
|
-
const CAPSULE = [
|
|
12436
|
-
18,
|
|
12437
|
-
22,
|
|
12438
|
-
26
|
|
12439
|
-
];
|
|
12440
|
-
const LIBRARY_CHART = [
|
|
12441
|
-
160,
|
|
12442
|
-
220,
|
|
12443
|
-
280
|
|
12444
|
-
];
|
|
12445
|
-
/**
|
|
12446
|
-
* A floating action button's footprint, the same under every style. It floats
|
|
12447
|
-
* over content rather than sitting in a row of controls, so a denser style's
|
|
12448
|
-
* shorter buttons are no reason to shrink the one target above the list — and
|
|
12449
|
-
* 44 is the floor a touch target cannot go under.
|
|
12450
|
-
*/
|
|
12451
|
-
const LIBRARY_FAB = [
|
|
12452
|
-
44,
|
|
12453
|
-
56,
|
|
12454
|
-
64
|
|
12455
|
-
];
|
|
12456
|
-
/**
|
|
12457
|
-
* A chart's height, scaled to a style's control density.
|
|
12349
|
+
* A chart's height, scaled to a style's control density.
|
|
12458
12350
|
*
|
|
12459
12351
|
* A plot is a region rather than a control, so none of the axes a style
|
|
12460
12352
|
* actually sets — a corner, an icon, a button's height — describes it directly.
|
|
@@ -13205,6 +13097,213 @@ function escapeRegExp(value) {
|
|
|
13205
13097
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13206
13098
|
}
|
|
13207
13099
|
//#endregion
|
|
13100
|
+
//#region src/project/css.ts
|
|
13101
|
+
/**
|
|
13102
|
+
* Wires the copied styles into the app's Tailwind entry.
|
|
13103
|
+
*
|
|
13104
|
+
* Two things have to happen and only one of them is obvious. The `@import`
|
|
13105
|
+
* pulls in the tokens and the theme. The `@source` lines are what make the
|
|
13106
|
+
* classes inside the copied components survive a production build: Tailwind
|
|
13107
|
+
* scans source text, and a class it never saw is a class it never compiled.
|
|
13108
|
+
*
|
|
13109
|
+
* The paths are computed from where the files actually landed, which resolves
|
|
13110
|
+
* the symlink trap on its own. In a monorepo the components live in a package
|
|
13111
|
+
* that Bun links into `node_modules`, and Tailwind's scanner does not follow
|
|
13112
|
+
* symlinks — a `@source` pointing at the linked copy silently contributes
|
|
13113
|
+
* nothing, and the components render unstyled in release and fine in dev. The
|
|
13114
|
+
* comment in `apps/playground/src/styles/global.css` is this bug, already paid
|
|
13115
|
+
* for once.
|
|
13116
|
+
*/
|
|
13117
|
+
const START = "/* delacour:start — managed by `delacour init`, edit outside this block */";
|
|
13118
|
+
const END = "/* delacour:end */";
|
|
13119
|
+
/**
|
|
13120
|
+
* The managed block: one import for the theme, one source glob per directory
|
|
13121
|
+
* holding copied code.
|
|
13122
|
+
*/
|
|
13123
|
+
function buildStylesBlock(context) {
|
|
13124
|
+
const from = dirname(context.cssPath);
|
|
13125
|
+
const lines = [
|
|
13126
|
+
START,
|
|
13127
|
+
`@import "${toSpecifier$1(from, `${context.directories.styles}/index.css`)}";`,
|
|
13128
|
+
""
|
|
13129
|
+
];
|
|
13130
|
+
const scanned = new Set(NAMESPACES.filter((namespace) => namespace !== "styles").map((namespace) => context.directories[namespace]));
|
|
13131
|
+
for (const directory of [...scanned].sort()) {
|
|
13132
|
+
if ([...scanned].some((other) => other !== directory && isInside(directory, other))) continue;
|
|
13133
|
+
lines.push(`@source "${toSpecifier$1(from, directory)}";`);
|
|
13134
|
+
}
|
|
13135
|
+
lines.push(END);
|
|
13136
|
+
return lines.join("\n");
|
|
13137
|
+
}
|
|
13138
|
+
/**
|
|
13139
|
+
* Writes the block into an existing entry, or creates one.
|
|
13140
|
+
*
|
|
13141
|
+
* Bounded by markers so a second `init` replaces the block rather than
|
|
13142
|
+
* appending a second copy, and so everything a developer adds around it is left
|
|
13143
|
+
* alone.
|
|
13144
|
+
*/
|
|
13145
|
+
function patchGlobalCss(existing, block) {
|
|
13146
|
+
if (existing === null) return {
|
|
13147
|
+
content: `${DEFAULT_ENTRY}\n${block}\n`,
|
|
13148
|
+
changed: true
|
|
13149
|
+
};
|
|
13150
|
+
const start = existing.indexOf(START);
|
|
13151
|
+
const end = existing.indexOf(END);
|
|
13152
|
+
if (start !== -1 && end > start) {
|
|
13153
|
+
const content = `${existing.slice(0, start)}${block}${existing.slice(end + 18)}`;
|
|
13154
|
+
return {
|
|
13155
|
+
content,
|
|
13156
|
+
changed: content !== existing
|
|
13157
|
+
};
|
|
13158
|
+
}
|
|
13159
|
+
return {
|
|
13160
|
+
content: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${block}\n`,
|
|
13161
|
+
changed: true
|
|
13162
|
+
};
|
|
13163
|
+
}
|
|
13164
|
+
const DEFAULT_ENTRY = `@import "tailwindcss";
|
|
13165
|
+
@import "uniwind";
|
|
13166
|
+
|
|
13167
|
+
@source "../**/*.{ts,tsx}";
|
|
13168
|
+
`;
|
|
13169
|
+
/**
|
|
13170
|
+
* The directories a Tailwind entry actually scans.
|
|
13171
|
+
*
|
|
13172
|
+
* Read back rather than assumed, because this is the check that matters: a
|
|
13173
|
+
* `@source` that does not cover where the components landed produces a build
|
|
13174
|
+
* with no error and no styles. Only directory sources are returned — a glob
|
|
13175
|
+
* like `../**\/*.{ts,tsx}` is reduced to the directory it starts from, which is
|
|
13176
|
+
* what decides coverage.
|
|
13177
|
+
*/
|
|
13178
|
+
function parseSources(css, cssPath) {
|
|
13179
|
+
const from = dirname(cssPath);
|
|
13180
|
+
const sources = [];
|
|
13181
|
+
for (const match of css.matchAll(/@source\s+(?:not\s+)?["']([^"']+)["']/g)) {
|
|
13182
|
+
const value = match[1];
|
|
13183
|
+
if (value.startsWith("inline(")) continue;
|
|
13184
|
+
const wildcard = value.search(/[*?[]/);
|
|
13185
|
+
const directory = wildcard === -1 ? value : value.slice(0, wildcard);
|
|
13186
|
+
sources.push(resolve(from, directory));
|
|
13187
|
+
}
|
|
13188
|
+
return sources;
|
|
13189
|
+
}
|
|
13190
|
+
/** Whether any source directory contains `directory`. */
|
|
13191
|
+
function isCovered(directory, sources) {
|
|
13192
|
+
return sources.some((source) => {
|
|
13193
|
+
const suffix = relative(source, directory);
|
|
13194
|
+
return suffix === "" || !suffix.startsWith("..");
|
|
13195
|
+
});
|
|
13196
|
+
}
|
|
13197
|
+
/** A path Tailwind can resolve from the entry file, always POSIX and always relative. */
|
|
13198
|
+
function toSpecifier$1(from, target) {
|
|
13199
|
+
const path = toPosix$3(relative(from, target));
|
|
13200
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
13201
|
+
}
|
|
13202
|
+
function isInside(directory, parent) {
|
|
13203
|
+
const suffix = relative(parent, directory);
|
|
13204
|
+
return suffix !== "" && !suffix.startsWith("..");
|
|
13205
|
+
}
|
|
13206
|
+
function toPosix$3(path) {
|
|
13207
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
13208
|
+
}
|
|
13209
|
+
//#endregion
|
|
13210
|
+
//#region src/project/root-layout.ts
|
|
13211
|
+
/**
|
|
13212
|
+
* Router layouts first: an app holding both is an Expo Router app whose
|
|
13213
|
+
* `App.tsx` is left over, and the layout is the file that actually renders.
|
|
13214
|
+
*/
|
|
13215
|
+
const CANDIDATES$1 = [
|
|
13216
|
+
{
|
|
13217
|
+
path: "src/app/_layout.tsx",
|
|
13218
|
+
router: true
|
|
13219
|
+
},
|
|
13220
|
+
{
|
|
13221
|
+
path: "app/_layout.tsx",
|
|
13222
|
+
router: true
|
|
13223
|
+
},
|
|
13224
|
+
{
|
|
13225
|
+
path: "src/app/_layout.jsx",
|
|
13226
|
+
router: true
|
|
13227
|
+
},
|
|
13228
|
+
{
|
|
13229
|
+
path: "app/_layout.jsx",
|
|
13230
|
+
router: true
|
|
13231
|
+
},
|
|
13232
|
+
{
|
|
13233
|
+
path: "App.tsx",
|
|
13234
|
+
router: false
|
|
13235
|
+
},
|
|
13236
|
+
{
|
|
13237
|
+
path: "src/App.tsx",
|
|
13238
|
+
router: false
|
|
13239
|
+
},
|
|
13240
|
+
{
|
|
13241
|
+
path: "App.jsx",
|
|
13242
|
+
router: false
|
|
13243
|
+
},
|
|
13244
|
+
{
|
|
13245
|
+
path: "index.tsx",
|
|
13246
|
+
router: false
|
|
13247
|
+
}
|
|
13248
|
+
];
|
|
13249
|
+
function findRootLayout(appRoot) {
|
|
13250
|
+
return CANDIDATES$1.find((candidate) => existsSync(join(appRoot, candidate.path))) ?? null;
|
|
13251
|
+
}
|
|
13252
|
+
/**
|
|
13253
|
+
* The root layout, whole, ready to paste.
|
|
13254
|
+
*
|
|
13255
|
+
* The CSS import is the first line because that is the rule it exists to
|
|
13256
|
+
* teach — anywhere else and every component renders unstyled with nothing
|
|
13257
|
+
* logged.
|
|
13258
|
+
*
|
|
13259
|
+
* `<Slot />` is Expo Router's minimal root. A layout already rendering a
|
|
13260
|
+
* `<Stack>` keeps it; the provider goes around whatever is there, which is
|
|
13261
|
+
* what the line printed beside this says.
|
|
13262
|
+
*/
|
|
13263
|
+
function renderRootLayout(layout, specifiers) {
|
|
13264
|
+
const { css, provider } = specifiers;
|
|
13265
|
+
if (!layout.router) return [
|
|
13266
|
+
`import "${css}";`,
|
|
13267
|
+
`import { DelacourProvider } from "${provider}";`,
|
|
13268
|
+
"",
|
|
13269
|
+
"export default function App() {",
|
|
13270
|
+
" return (",
|
|
13271
|
+
" <DelacourProvider>",
|
|
13272
|
+
" <YourApp />",
|
|
13273
|
+
" </DelacourProvider>",
|
|
13274
|
+
" );",
|
|
13275
|
+
"}"
|
|
13276
|
+
].join("\n");
|
|
13277
|
+
return [
|
|
13278
|
+
`import "${css}";`,
|
|
13279
|
+
`import { DelacourProvider } from "${provider}";`,
|
|
13280
|
+
"import { Slot } from \"expo-router\";",
|
|
13281
|
+
"",
|
|
13282
|
+
"export default function RootLayout() {",
|
|
13283
|
+
" return (",
|
|
13284
|
+
" <DelacourProvider>",
|
|
13285
|
+
" <Slot />",
|
|
13286
|
+
" </DelacourProvider>",
|
|
13287
|
+
" );",
|
|
13288
|
+
"}"
|
|
13289
|
+
].join("\n");
|
|
13290
|
+
}
|
|
13291
|
+
//#endregion
|
|
13292
|
+
//#region src/project/uniwind-env.ts
|
|
13293
|
+
/**
|
|
13294
|
+
* The one line that makes `className` a real prop in TypeScript.
|
|
13295
|
+
*
|
|
13296
|
+
* Uniwind augments React Native's types through a global `/// <reference>`, and
|
|
13297
|
+
* a declaration file is only loaded if it is part of the program — either
|
|
13298
|
+
* matched by `tsconfig`'s `include` or reachable by import. This file is neither
|
|
13299
|
+
* importable nor, in a shared package, inside the app's `include`, which is why
|
|
13300
|
+
* the app needs a copy of its own.
|
|
13301
|
+
*
|
|
13302
|
+
* Kept here rather than inlined so `init` and `doctor` agree on the contents.
|
|
13303
|
+
*/
|
|
13304
|
+
const UNIWIND_ENV_FILENAME = "uniwind-env.d.ts";
|
|
13305
|
+
const UNIWIND_ENV_REFERENCE = "/// <reference types=\"uniwind/types\" />";
|
|
13306
|
+
//#endregion
|
|
13208
13307
|
//#region src/commands/doctor.ts
|
|
13209
13308
|
async function doctor(options) {
|
|
13210
13309
|
const output = createOutput(options);
|
|
@@ -13233,10 +13332,65 @@ async function runChecks(options) {
|
|
|
13233
13332
|
await checkCssEntryImported(config),
|
|
13234
13333
|
await checkGestureHandlerRoot(config),
|
|
13235
13334
|
await checkDuplicateNativeModules(project),
|
|
13335
|
+
...testChecks(await readTestSetup(config, project)),
|
|
13236
13336
|
checkLock(existsSync(lockPath(config.root)), await hasEntries(config.directories.ui))
|
|
13237
13337
|
];
|
|
13238
13338
|
}
|
|
13239
13339
|
/**
|
|
13340
|
+
* What `transformIgnorePatterns` has to let through for each engine. Both
|
|
13341
|
+
* publish TypeScript source; the charts engine also imports d3, whose packages
|
|
13342
|
+
* ship ES modules only.
|
|
13343
|
+
*/
|
|
13344
|
+
const ENGINE_TRANSFORMS = {
|
|
13345
|
+
"@delacour/react-native-bottom-sheet": ["@delacour/.*"],
|
|
13346
|
+
"@delacour/react-native-charts": [
|
|
13347
|
+
"@delacour/.*",
|
|
13348
|
+
"d3-.*",
|
|
13349
|
+
"internmap"
|
|
13350
|
+
]
|
|
13351
|
+
};
|
|
13352
|
+
/** Whether copied tests can run: the runner's setup, then what Jest has to transform. */
|
|
13353
|
+
function testChecks(setup) {
|
|
13354
|
+
return [checkTestRunner(setup), checkJestTransform(setup)];
|
|
13355
|
+
}
|
|
13356
|
+
/**
|
|
13357
|
+
* A Jest project that imports an engine has to transform it.
|
|
13358
|
+
*
|
|
13359
|
+
* Jest skips `node_modules` by default and `jest-expo`'s list does not name
|
|
13360
|
+
* these, so every suite reaching `chart` or `bottom-sheet` dies with "Must use
|
|
13361
|
+
* import to load ES Module" and a path inside `node_modules`. Nothing in the app
|
|
13362
|
+
* is wrong, so it is a warning: only the tests are broken. The config is read as
|
|
13363
|
+
* text, so a pattern built in code may be missed — the fix names exactly what
|
|
13364
|
+
* the pattern has to contain.
|
|
13365
|
+
*/
|
|
13366
|
+
function checkJestTransform(setup) {
|
|
13367
|
+
const name = "Jest transforms";
|
|
13368
|
+
if (setup.runner !== "jest") return {
|
|
13369
|
+
name,
|
|
13370
|
+
status: "pass",
|
|
13371
|
+
detail: `tests run on ${setup.runner}, not Jest`
|
|
13372
|
+
};
|
|
13373
|
+
const engines = Object.keys(ENGINE_TRANSFORMS).filter((engine) => engine in setup.dependencies);
|
|
13374
|
+
if (engines.length === 0) return {
|
|
13375
|
+
name,
|
|
13376
|
+
status: "pass",
|
|
13377
|
+
detail: "no engine package to transform"
|
|
13378
|
+
};
|
|
13379
|
+
const patterns = setup.jestConfig?.includes("transformIgnorePatterns") ? setup.jestConfig : "";
|
|
13380
|
+
const missing = [...new Set(engines.flatMap((engine) => ENGINE_TRANSFORMS[engine] ?? []))].filter((pattern) => !patterns.includes(pattern.replace(".*", "")));
|
|
13381
|
+
if (missing.length === 0) return {
|
|
13382
|
+
name,
|
|
13383
|
+
status: "pass",
|
|
13384
|
+
detail: `${engines.join(", ")} transformed`
|
|
13385
|
+
};
|
|
13386
|
+
return {
|
|
13387
|
+
name,
|
|
13388
|
+
status: "warn",
|
|
13389
|
+
detail: `${engines.join(", ")} ship${engines.length === 1 ? "s" : ""} untranspiled, and transformIgnorePatterns does not let ${missing.join(", ")} through`,
|
|
13390
|
+
fix: `Add ${missing.map((pattern) => `\`${pattern}\``).join(", ")} to the packages transformIgnorePatterns lets through, beside jest-expo's own.`
|
|
13391
|
+
};
|
|
13392
|
+
}
|
|
13393
|
+
/**
|
|
13240
13394
|
* Components copied in before the lock existed have nothing to merge from.
|
|
13241
13395
|
*
|
|
13242
13396
|
* A warning, never a failure: the components work exactly as they did. What is
|
|
@@ -13696,10 +13850,10 @@ function cssImportSpecifier(config) {
|
|
|
13696
13850
|
}
|
|
13697
13851
|
/** A relative path as an import specifier: `../styles/global.css`, `./global.css`. */
|
|
13698
13852
|
function relativeSpecifier(from, target) {
|
|
13699
|
-
const path = toPosix$
|
|
13853
|
+
const path = toPosix$2(relative(from, target));
|
|
13700
13854
|
return path.startsWith(".") ? path : `./${path}`;
|
|
13701
13855
|
}
|
|
13702
|
-
function toPosix$
|
|
13856
|
+
function toPosix$2(path) {
|
|
13703
13857
|
return sep === "/" ? path : path.split(sep).join("/");
|
|
13704
13858
|
}
|
|
13705
13859
|
/**
|
|
@@ -13865,6 +14019,186 @@ async function read$1(path) {
|
|
|
13865
14019
|
}
|
|
13866
14020
|
}
|
|
13867
14021
|
//#endregion
|
|
14022
|
+
//#region src/project/css-entry.ts
|
|
14023
|
+
/**
|
|
14024
|
+
* Finds the Tailwind entry an app already has.
|
|
14025
|
+
*
|
|
14026
|
+
* Creating a second one is the failure this exists to prevent. Metro compiles
|
|
14027
|
+
* the file its `cssEntryFile` names and no other; the app imports whichever
|
|
14028
|
+
* file its root layout imports. Write a new entry beside an existing one and
|
|
14029
|
+
* those become two different files — Tailwind compiles the one with the
|
|
14030
|
+
* `@source` globs, the app loads the one without them, and every component
|
|
14031
|
+
* renders unstyled with nothing logged.
|
|
14032
|
+
*
|
|
14033
|
+
* The Metro config wins when it names one, because that is what actually
|
|
14034
|
+
* compiles — see `readUniwindPaths`. This is the fallback for a project that
|
|
14035
|
+
* has an entry but has not wired Metro to it yet, which is every app that
|
|
14036
|
+
* installed Uniwind and stopped there.
|
|
14037
|
+
*
|
|
14038
|
+
* Shallowest first: an app that ran an older version of this CLI has both
|
|
14039
|
+
* `src/global.css` and `src/styles/global.css`, and the one it imports is the
|
|
14040
|
+
* one it had first.
|
|
14041
|
+
*/
|
|
14042
|
+
const CANDIDATES = [
|
|
14043
|
+
"global.css",
|
|
14044
|
+
"src/global.css",
|
|
14045
|
+
"app/global.css",
|
|
14046
|
+
"src/app/global.css",
|
|
14047
|
+
"styles/global.css",
|
|
14048
|
+
"src/styles/global.css",
|
|
14049
|
+
"src/styles/index.css"
|
|
14050
|
+
];
|
|
14051
|
+
/** `@import "tailwindcss"`, however it is quoted and whatever follows it. */
|
|
14052
|
+
const TAILWIND_IMPORT = /@import\s+["']tailwindcss["']/;
|
|
14053
|
+
function findTailwindEntry(appRoot) {
|
|
14054
|
+
for (const candidate of CANDIDATES) {
|
|
14055
|
+
const path = join(appRoot, candidate);
|
|
14056
|
+
if (!existsSync(path)) continue;
|
|
14057
|
+
try {
|
|
14058
|
+
if (TAILWIND_IMPORT.test(readFileSync(path, "utf-8"))) return path;
|
|
14059
|
+
} catch {}
|
|
14060
|
+
}
|
|
14061
|
+
return null;
|
|
14062
|
+
}
|
|
14063
|
+
//#endregion
|
|
14064
|
+
//#region src/project/metro.ts
|
|
14065
|
+
/**
|
|
14066
|
+
* Wraps the app's Metro config with Uniwind's.
|
|
14067
|
+
*
|
|
14068
|
+
* `withUniwindConfig` has to stay the outermost wrapper — it installs the
|
|
14069
|
+
* transformer that compiles `className` into styles, and a wrapper applied
|
|
14070
|
+
* after it can replace the `transformer` key and leave every component
|
|
14071
|
+
* unstyled with nothing logged. So the patch rewrites the export itself rather
|
|
14072
|
+
* than inserting a line somewhere in the middle.
|
|
14073
|
+
*
|
|
14074
|
+
* When the export is not in a shape this can safely rewrite, it says so and
|
|
14075
|
+
* prints what to write. Guessing at an unfamiliar config file earns nothing —
|
|
14076
|
+
* the failure it causes is a Metro error at bundle time in someone else's app.
|
|
14077
|
+
*/
|
|
14078
|
+
/** Modules that must resolve to exactly one copy across a workspace. */
|
|
14079
|
+
const PINNED_MODULES = [
|
|
14080
|
+
"@shopify/react-native-skia",
|
|
14081
|
+
"react",
|
|
14082
|
+
"react-native",
|
|
14083
|
+
"react-native-gesture-handler",
|
|
14084
|
+
"react-native-keyboard-controller",
|
|
14085
|
+
"react-native-reanimated",
|
|
14086
|
+
"react-native-safe-area-context",
|
|
14087
|
+
"react-native-svg",
|
|
14088
|
+
"react-native-teleport",
|
|
14089
|
+
"react-native-worklets"
|
|
14090
|
+
];
|
|
14091
|
+
const REQUIRE = "const { withUniwindConfig } = require(\"uniwind/metro\");";
|
|
14092
|
+
const IMPORT = "import { withUniwindConfig } from \"uniwind/metro\";";
|
|
14093
|
+
/** `module.exports = config;` or `export default config;`, with nothing after it. */
|
|
14094
|
+
const COMMONJS_EXPORT = /module\.exports\s*=\s*([\s\S]+?);?\s*$/;
|
|
14095
|
+
const ESM_EXPORT = /export default\s+([\s\S]+?);?\s*$/;
|
|
14096
|
+
const CSS_ENTRY = /cssEntryFile\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
14097
|
+
const DTS_FILE = /dtsFile\s*:\s*["'`]([^"'`]+)["'`]/;
|
|
14098
|
+
/**
|
|
14099
|
+
* Reads the paths a wrapped Metro config already names.
|
|
14100
|
+
*
|
|
14101
|
+
* A project that arrives with Uniwind set up — Expo's `with-router-uniwind`
|
|
14102
|
+
* example is the common one — has chosen its own CSS entry, and Metro compiles
|
|
14103
|
+
* **that file and no other**. Writing the `@source` block into the path this
|
|
14104
|
+
* CLI would have picked leaves Tailwind scanning a file with no globs in it, so
|
|
14105
|
+
* every class the components use is dropped and every one of them renders
|
|
14106
|
+
* unstyled. Nothing errors; the app just looks wrong.
|
|
14107
|
+
*
|
|
14108
|
+
* So the wired config wins. Returns nothing when the config is not wired, in
|
|
14109
|
+
* which case `init` is about to wrap it with paths of its own.
|
|
14110
|
+
*/
|
|
14111
|
+
function readUniwindPaths(existing) {
|
|
14112
|
+
if (!existing?.includes("withUniwindConfig")) return {};
|
|
14113
|
+
return {
|
|
14114
|
+
css: CSS_ENTRY.exec(existing)?.[1],
|
|
14115
|
+
types: DTS_FILE.exec(existing)?.[1]
|
|
14116
|
+
};
|
|
14117
|
+
}
|
|
14118
|
+
function patchMetroConfig(existing, options) {
|
|
14119
|
+
const wrapper = optionsLiteral(options);
|
|
14120
|
+
const monorepo = options.workspaceRoot && options.workspaceRoot !== options.metroConfigPath ? monorepoResolverBlock(dirname(options.metroConfigPath), options.workspaceRoot) : "";
|
|
14121
|
+
if (existing === null) return {
|
|
14122
|
+
status: "created",
|
|
14123
|
+
content: template(wrapper, monorepo)
|
|
14124
|
+
};
|
|
14125
|
+
if (existing.includes("withUniwindConfig")) return { status: "already-wired" };
|
|
14126
|
+
const isEsm = /^\s*(import|export default)/m.test(existing);
|
|
14127
|
+
const match = (isEsm ? ESM_EXPORT : COMMONJS_EXPORT).exec(existing);
|
|
14128
|
+
if (!match?.[1]) return {
|
|
14129
|
+
status: "manual",
|
|
14130
|
+
snippet: manualSnippet(wrapper, isEsm)
|
|
14131
|
+
};
|
|
14132
|
+
const keyword = isEsm ? "export default" : "module.exports =";
|
|
14133
|
+
const head = existing.slice(0, match.index);
|
|
14134
|
+
const wrapped = `${keyword} withUniwindConfig(${match[1].trim()}, ${wrapper});\n`;
|
|
14135
|
+
const body = monorepo ? `${head}${monorepo}\n` : head;
|
|
14136
|
+
return {
|
|
14137
|
+
status: "patched",
|
|
14138
|
+
content: `${isEsm ? IMPORT : REQUIRE}\n${body}${wrapped}`
|
|
14139
|
+
};
|
|
14140
|
+
}
|
|
14141
|
+
function optionsLiteral(options) {
|
|
14142
|
+
const from = dirname(options.metroConfigPath);
|
|
14143
|
+
return `{\n\tcssEntryFile: "${toSpecifier(from, options.cssPath)}",\n\tdtsFile: "${toSpecifier(from, options.typesPath)}",\n}`;
|
|
14144
|
+
}
|
|
14145
|
+
/**
|
|
14146
|
+
* The resolver settings an app needs to reach a package outside its own tree.
|
|
14147
|
+
*
|
|
14148
|
+
* Metro resolves by walking `node_modules` from the app downwards, so without
|
|
14149
|
+
* `watchFolders` and `nodeModulesPaths` it simply cannot see a sibling package —
|
|
14150
|
+
* the import fails at bundle time with a missing module, pointing at the app
|
|
14151
|
+
* rather than at the workspace.
|
|
14152
|
+
*
|
|
14153
|
+
* `extraNodeModules` is the other half, and it is about runtime rather than
|
|
14154
|
+
* resolution: a package manager can materialise a second copy of a native module
|
|
14155
|
+
* under the app, and two registrations of one native module break. This is the
|
|
14156
|
+
* block `apps/playground/metro.config.js` in this repository carries for exactly
|
|
14157
|
+
* this reason.
|
|
14158
|
+
*/
|
|
14159
|
+
function monorepoResolverBlock(appRoot, workspaceRoot) {
|
|
14160
|
+
const pins = PINNED_MODULES.map((name) => `\t${JSON.stringify(name)}: path.resolve(workspaceRoot, "node_modules/${name}"),`).join("\n");
|
|
14161
|
+
return `const path = require("node:path");
|
|
14162
|
+
|
|
14163
|
+
const workspaceRoot = path.resolve(__dirname, "${toSpecifier(appRoot, workspaceRoot)}");
|
|
14164
|
+
|
|
14165
|
+
// The components live in a package outside this app, so Metro has to watch and
|
|
14166
|
+
// resolve beyond the app's own directory.
|
|
14167
|
+
config.watchFolders = [workspaceRoot];
|
|
14168
|
+
config.resolver.nodeModulesPaths = [
|
|
14169
|
+
\tpath.resolve(__dirname, "node_modules"),
|
|
14170
|
+
\tpath.resolve(workspaceRoot, "node_modules"),
|
|
14171
|
+
];
|
|
14172
|
+
config.resolver.disableHierarchicalLookup = true;
|
|
14173
|
+
|
|
14174
|
+
// One copy of each native module for the whole bundle: two registrations of a
|
|
14175
|
+
// native module break at runtime.
|
|
14176
|
+
config.resolver.extraNodeModules = {
|
|
14177
|
+
${pins}
|
|
14178
|
+
};
|
|
14179
|
+
`;
|
|
14180
|
+
}
|
|
14181
|
+
function template(wrapper, monorepo) {
|
|
14182
|
+
return `const { getDefaultConfig } = require("expo/metro-config");
|
|
14183
|
+
${REQUIRE}
|
|
14184
|
+
|
|
14185
|
+
const config = getDefaultConfig(__dirname);
|
|
14186
|
+
${monorepo ? `\n${monorepo}` : ""}
|
|
14187
|
+
// withUniwindConfig must stay the outermost wrapper.
|
|
14188
|
+
module.exports = withUniwindConfig(config, ${wrapper});
|
|
14189
|
+
`;
|
|
14190
|
+
}
|
|
14191
|
+
function manualSnippet(wrapper, isEsm) {
|
|
14192
|
+
return `${isEsm ? IMPORT : REQUIRE}\n\n// Keep this the outermost wrapper.\n${isEsm ? "export default" : "module.exports ="} withUniwindConfig(config, ${wrapper});`;
|
|
14193
|
+
}
|
|
14194
|
+
function toSpecifier(from, target) {
|
|
14195
|
+
const path = toPosix$1(relative(from, target));
|
|
14196
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
14197
|
+
}
|
|
14198
|
+
function toPosix$1(path) {
|
|
14199
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
14200
|
+
}
|
|
14201
|
+
//#endregion
|
|
13868
14202
|
//#region src/commands/init.ts
|
|
13869
14203
|
async function init(components, options) {
|
|
13870
14204
|
const output = createOutput(options);
|
|
@@ -14213,7 +14547,7 @@ async function add(names, options) {
|
|
|
14213
14547
|
output.warn("Nothing to add. Name a component, or pass --all.");
|
|
14214
14548
|
return null;
|
|
14215
14549
|
}
|
|
14216
|
-
const scope = (item) => options.tests ? withTests(item) : item;
|
|
14550
|
+
const scope = (item) => options.tests ? withTests(item, config.testRunner) : item;
|
|
14217
14551
|
const byName = new Map(index.items.map((item) => [item.name, item]));
|
|
14218
14552
|
const order = resolveItemGraph(requested, (name) => {
|
|
14219
14553
|
const entry = byName.get(name);
|
|
@@ -14227,16 +14561,31 @@ async function add(names, options) {
|
|
|
14227
14561
|
const toWrite = await resolveConflicts(planned, options, output);
|
|
14228
14562
|
if (toWrite === null) return null;
|
|
14229
14563
|
await writeFiles(toWrite);
|
|
14230
|
-
await recordLock(config, planned, toWrite, options.ref || config.registry.ref || "
|
|
14564
|
+
await recordLock(config, planned, toWrite, options.ref || config.registry.ref || "713984b74d21a6bb6b0e1f719a947cce7a2d847e");
|
|
14231
14565
|
report$1(toWrite, planned, requested, output);
|
|
14232
14566
|
const settled = await settleDependencies(items, config, project, options, output);
|
|
14567
|
+
const testSetup = options.tests ? await checkTestSetup(config, project, output) : [];
|
|
14233
14568
|
return {
|
|
14234
14569
|
items: items.map((item) => item.name),
|
|
14235
14570
|
written: toWrite.length,
|
|
14236
|
-
...settled
|
|
14571
|
+
...settled,
|
|
14572
|
+
testSetup
|
|
14237
14573
|
};
|
|
14238
14574
|
}
|
|
14239
14575
|
/**
|
|
14576
|
+
* Says what the runner still needs before the copied tests will run.
|
|
14577
|
+
*
|
|
14578
|
+
* After the install, so a package it just added is not reported missing. The
|
|
14579
|
+
* CLI writes no runner config — that file is the project's — so a suite that
|
|
14580
|
+
* cannot load is said here, where it was copied, rather than found at the first
|
|
14581
|
+
* test run with an error naming neither the cause nor the fix.
|
|
14582
|
+
*/
|
|
14583
|
+
async function checkTestSetup(config, project, output) {
|
|
14584
|
+
const checks = testChecks(await readTestSetup(config, project));
|
|
14585
|
+
for (const check of checks.filter((entry) => entry.status === "warn" || entry.status === "fail")) output.warn([`${check.name}: ${check.detail}`, ...check.fix ? [` ${check.fix}`] : []].join("\n"));
|
|
14586
|
+
return checks;
|
|
14587
|
+
}
|
|
14588
|
+
/**
|
|
14240
14589
|
* Everything that follows a write: the shared package's manifest, the report of
|
|
14241
14590
|
* what the components need from npm, and the install if it is wanted.
|
|
14242
14591
|
*
|
|
@@ -14389,7 +14738,7 @@ function printSkippedInstall(plan, appPath, output) {
|
|
|
14389
14738
|
* true.
|
|
14390
14739
|
*/
|
|
14391
14740
|
function warnAboutNativeRebuild(items, project, output) {
|
|
14392
|
-
const added = missingPackages(project.packageJson, collect(items, "expoDependencies"));
|
|
14741
|
+
const added = missingPackages$1(project.packageJson, collect(items, "expoDependencies"));
|
|
14393
14742
|
if (added.length === 0) return;
|
|
14394
14743
|
output.warn([
|
|
14395
14744
|
`Added native modules: ${added.join(", ")}`,
|
|
@@ -15260,7 +15609,7 @@ async function planUpdate(input) {
|
|
|
15260
15609
|
const byName = new Map(index.items.map((item) => [item.name, item]));
|
|
15261
15610
|
const candidates = input.names.length > 0 ? [...input.names] : [.../* @__PURE__ */ new Set([...Object.keys(lock.items), ...await installedItems(index.items, config)])];
|
|
15262
15611
|
const dropped = candidates.filter((name) => !byName.has(name) && lock.items[name] !== void 0);
|
|
15263
|
-
const scope = (item) => input.tests || hasTests(lock, item.name) ? withTests(item) : item;
|
|
15612
|
+
const scope = (item) => input.tests || hasTests(lock, item.name) ? withTests(item, config.testRunner) : item;
|
|
15264
15613
|
const order = resolveItemGraph(candidates.filter((name) => !dropped.includes(name)), (name) => {
|
|
15265
15614
|
const entry = byName.get(name);
|
|
15266
15615
|
return entry && scope(entry);
|
|
@@ -15518,7 +15867,7 @@ async function promptUntracked(files) {
|
|
|
15518
15867
|
async function readPlan(names, options, output, clients) {
|
|
15519
15868
|
const config = await loadConfig(options.cwd);
|
|
15520
15869
|
const lock = await readLock(config.root);
|
|
15521
|
-
const ref = options.ref || config.registry.ref || "
|
|
15870
|
+
const ref = options.ref || config.registry.ref || "713984b74d21a6bb6b0e1f719a947cce7a2d847e";
|
|
15522
15871
|
const url = options.registry ?? config.registry.url;
|
|
15523
15872
|
const open = (at) => createRegistryClient({
|
|
15524
15873
|
cwd: options.cwd,
|
|
@@ -31669,6 +32018,7 @@ function describeAdd(result) {
|
|
|
31669
32018
|
} else if (result.installed) lines.push(`Installed: ${dependencies.missing.join(", ")}.`);
|
|
31670
32019
|
else lines.push("Not installed. These have to be run from the app before it will build:", ...dependencies.groups.map((group) => ` ${commandLine(group)}`), "Re-run with install: true to have them run for you.");
|
|
31671
32020
|
if (dependencies.groups.some((group) => group.label === "expo install")) lines.push("A native module changed — rebuild the dev client; a JS reload will not pick it up.");
|
|
32021
|
+
for (const check of result.testSetup.filter((entry) => entry.status === "warn" || entry.status === "fail")) lines.push(`The copied tests will not run yet — ${check.name}: ${check.detail}.${check.fix ? ` ${check.fix}` : ""}`);
|
|
31672
32022
|
return lines;
|
|
31673
32023
|
}
|
|
31674
32024
|
//#endregion
|
|
@@ -32784,7 +33134,7 @@ function report(result, output) {
|
|
|
32784
33134
|
if (result.derived.length > 0) output.info(`${result.derived.length} filled in, derived from tokens the theme does define: ${result.derived.map((name) => name.slice(2)).join(", ")}.`);
|
|
32785
33135
|
for (const warning of result.warnings) output.warn(warning);
|
|
32786
33136
|
}
|
|
32787
|
-
const program = new Command().name("delacour").description("Add React Native components to your Expo app. You own the code.").version("0.5.0
|
|
33137
|
+
const program = new Command().name("delacour").description("Add React Native components to your Expo app. You own the code.").version("0.5.0");
|
|
32788
33138
|
/** Shared by every command that reads the registry. */
|
|
32789
33139
|
function withRegistryOptions(command) {
|
|
32790
33140
|
return command.addOption(new Option("--registry <url>", "registry to read from: a URL, github:owner/repo, or a local path")).addOption(new Option("--ref <ref>", "git ref to read the registry at").default(DEFAULT_REGISTRY_REF)).addOption(new Option("--offline", "use only what is already cached")).addOption(new Option("-c, --cwd <path>", "directory to work in").default(process.cwd()));
|