Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/@rnw-scripts/warm-feed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ from the working-tree `vnext/package.json`; for `0.NN-stable`, the exact
scaffolds, and reads the generated manifests. Mirrors
`vnext/Scripts/creaternwlib.cmd`.

The second module, **`create-react-native-app`**, is the app counterpart: it
reproduces the CLI-init *app* test, which scaffolds a standalone app with
`@react-native-community/cli init` (pulling `@react-native-community/template`)
and installs *that generated app* — so its closure (e.g.
`react-native-safe-area-context`) is what the feed needs. It reuses the same
per-branch RN/CLI/RNW version resolution and mirrors
`vnext/Scripts/creaternwapp.cmd`, including that script's template selection: a
stable branch lets the CLI pick the template matching its React Native, while a
nightly RN overrides it to a fixed stable `@react-native-community/template`
(`nightlyTemplate`, default `@react-native-community/template@0.86.0`). Keep that
default in sync with the `#15124` override in `creaternwapp.cmd`.

The scheduled pipeline run warms every **enabled** configured module in addition
to the latest-patch sync, so brand-new closures stay warm automatically.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* create-react-native-app special module.
*
* The CLI-init *app* test doesn't install react-native-windows' own dependencies —
* it scaffolds a standalone app with `@react-native-community/cli init` (pulling
* `@react-native-community/template`) and installs *that generated app*. So the
* closure the feed needs is the generated app's dependency graph (e.g.
* `react-native-safe-area-context`), which enumeration can't discover. This module
* reproduces that scaffold and resolves the generated app's closure. It is the app
* counterpart to the create-react-native-library module (the lib test), and mirrors
* `vnext/Scripts/creaternwapp.cmd`.
*
* warm-feed runs only from `main`, but the test runs on every release branch, each
* pinned to a different React Native. The config manifest therefore lists all
* branches; per branch we derive the RN/CLI versions (shared with the lib module:
* nightly for `main` from the working-tree vnext/package.json; for a stable branch,
* the react-native it pins in its own vnext/package.json, read via git), scaffold,
* and read the generated app manifest.
*
* Template selection mirrors creaternwapp.cmd exactly: a stable branch lets the CLI
* pick the template that matches its React Native, while a nightly RN overrides the
* template to a fixed stable one (`@react-native-community/template@0.86.0`) so its
* inner CLI never requests a `nightly` template. Keep this in sync with the
* `#15124`-tagged override in creaternwapp.cmd.
*
* @format
*/

import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {runTool, type DepSpecSet} from '../closure';
import {manifestSpecs} from '../manifest';
// Branch config and version resolution are shared with the lib module, which owns
// them today; the app module reuses them rather than re-deriving RN/CLI/RNW versions.
import {
nightlyFixupSpecs,
readRnwWorkspaceSpecs,
resolveBranchVersions,
type BranchVersions,
type CrnlBranch,
} from './createReactNativeLibrary';
import type {SpecialModule, SpecialModuleContext} from './types';

/** The stable template creaternwapp.cmd pins when React Native is a nightly (#15124). */
const DEFAULT_NIGHTLY_TEMPLATE = '@react-native-community/template@0.86.0';

/** A space-free app name so the Windows shell launch needs no quoting. */
const DEFAULT_APP_NAME = 'warmapp';

interface CrnAppConfig {
/** `@react-native-community/template@<version>` used when the RN is a nightly. */
nightlyTemplate: string;
/** Name of the scaffolded app directory. */
appName: string;
branches: CrnlBranch[];
}

function parseBranch(raw: unknown): CrnlBranch {
const rec = (raw ?? {}) as Record<string, unknown>;
if (typeof rec.name !== 'string' || !rec.name) {
throw new Error("each cRNApp branch requires a 'name'");
}
return {
name: rec.name,
nightly: rec.nightly === true,
reactNativeVersion:
typeof rec.reactNativeVersion === 'string'
? rec.reactNativeVersion
: undefined,
reactNativeCliVersion:
typeof rec.reactNativeCliVersion === 'string'
? rec.reactNativeCliVersion
: undefined,
reactNativeWindowsSpec:
typeof rec.reactNativeWindowsSpec === 'string'
? rec.reactNativeWindowsSpec
: undefined,
ref: typeof rec.ref === 'string' ? rec.ref : undefined,
};
}

export function parseCrnAppConfig(
config: Record<string, unknown>,
): CrnAppConfig {
const branchesRaw = config.branches;
if (!Array.isArray(branchesRaw) || branchesRaw.length === 0) {
throw new Error(
"create-react-native-app module requires a non-empty 'branches' list",
);
}
return {
nightlyTemplate:
typeof config.nightlyTemplate === 'string' && config.nightlyTemplate
? config.nightlyTemplate
: DEFAULT_NIGHTLY_TEMPLATE,
appName:
typeof config.appName === 'string' && config.appName
? config.appName
: DEFAULT_APP_NAME,
branches: branchesRaw.map(parseBranch),
};
}

/**
* The `--template` args passed to `cli init`, mirroring creaternwapp.cmd: none for a
* stable RN (the CLI's default template matches the version), the fixed stable
* template for a nightly RN (so the inner CLI never requests a nightly template).
*/
export function templateArgs(cfg: CrnAppConfig, nightly: boolean): string[] {
return nightly ? ['--template', cfg.nightlyTemplate] : [];
}

/**
* The generated app manifest as a single spec set. For a nightly branch the
* generated RN-family/CLI specs are rewritten to the nightly versions (mirroring
* vnext/Scripts/UpdateNightlyDependencies.ps1, as the lib module does), and the
* resolved react-native-windows spec is added (the test installs it too).
*/
export function appSpecsFromManifest(
manifest: unknown,
versions: BranchVersions,
branchName: string,
): DepSpecSet[] {
const specs = manifestSpecs(manifest);
if (versions.nightly) {
nightlyFixupSpecs(specs, versions.reactNative, versions.reactNativeCli);
}
if (versions.reactNativeWindowsSpec) {
specs['react-native-windows'] = versions.reactNativeWindowsSpec;
}
return [{label: `crnapp:${branchName}:app`, specs}];
}

function scaffold(
mctx: SpecialModuleContext,
cfg: CrnAppConfig,
branch: CrnlBranch,
versions: BranchVersions,
): {workDir: string; projectDir: string} {
if (!versions.reactNativeCli) {
throw new Error(
`cRNApp: could not determine a @react-native-community/cli version for branch '${branch.name}' (set reactNativeCliVersion)`,
);
}
const workDir = mkdtempSync(join(tmpdir(), 'warm-crnapp-'));
const args = [
'--yes',
`@react-native-community/cli@${versions.reactNativeCli}`,
'init',
cfg.appName,
'--version',
versions.reactNative,
...templateArgs(cfg, versions.nightly),
// Match creaternwapp.cmd: resolve metadata only, no install/pods/git in the warmer.
'--skip-install',
'--install-pods',
'false',
'--skip-git-init',
'true',
];
try {
runTool(
'npx',
args,
workDir,
{
npm_config_userconfig: mctx.npmrcPath,
npm_config_registry: mctx.npmRegistryUrl,
},
`scaffold cRNApp ${branch.name} (react-native ${versions.reactNative})`,
);
} catch (err) {
// npx failed before we returned workDir, so the caller's cleanup never runs;
// remove the temp tree here to avoid leaking warm-crnapp-* on every failed run.
rmSync(workDir, {recursive: true, force: true});
throw err;
}
return {workDir, projectDir: join(workDir, cfg.appName)};
}

export const createReactNativeAppModule: SpecialModule = {
name: 'create-react-native-app',
async collectDepSpecs(mctx, config) {
const cfg = parseCrnAppConfig(config);
const sets: DepSpecSet[] = [];
const failures: string[] = [];
for (const branch of cfg.branches) {
mctx.ctx.log.info(`cRNApp: preparing closure for branch ${branch.name}`);
try {
const versions = await resolveBranchVersions(mctx, branch);
const {workDir, projectDir} = scaffold(mctx, cfg, branch, versions);
try {
const manifest = JSON.parse(
readFileSync(join(projectDir, 'package.json'), 'utf8'),
);
sets.push(...appSpecsFromManifest(manifest, versions, branch.name));
} finally {
rmSync(workDir, {recursive: true, force: true});
}
if (versions.nightly) {
// The main app-init job installs the locally built RNW, whose manifest
// pins exact canary @react-native-windows/* versions the feed must have.
const rnwSpecs = await readRnwWorkspaceSpecs(mctx);
if (Object.keys(rnwSpecs).length > 0) {
sets.push({
label: `crnapp:${branch.name}:rnw-workspace`,
specs: rnwSpecs,
});
}
}
} catch (err) {
// One branch failing must not drop the others; record it so the caller can
// still surface a non-zero exit while the healthy branches are warmed.
mctx.ctx.log.error(
`cRNApp: branch ${branch.name} failed: ${(err as Error).message}`,
);
failures.push(branch.name);
}
}
return {sets, failures};
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {manifestSpecs, readManifestSpecs} from '../manifest';
import {compareSemver, isStable, parseSemver} from '../versions';
import type {SpecialModule, SpecialModuleContext} from './types';

interface CrnlBranch {
export interface CrnlBranch {
/** Branch name, e.g. 'main' or '0.85-stable'. */
name: string;
/** `main` uses a nightly RN and needs the generated deps rewritten. */
Expand All @@ -51,7 +51,7 @@ interface CrnlConfig {
branches: CrnlBranch[];
}

interface BranchVersions {
export interface BranchVersions {
reactNative: string;
reactNativeCli?: string;
reactNativeWindowsSpec?: string;
Expand Down Expand Up @@ -282,6 +282,20 @@ export async function resolveBranchVersions(
};
}

/**
* The react-native version to scaffold cRNL with. cRNL resolves a nightly RN
* through the short-lived `@react-native-community/template@nightly` tag, which
* rotates away and then fails to resolve (ETARGET). So for a nightly we scaffold
* from the matching stable base version (`0.87.0-nightly-...` -> `0.87.0`) and let
* nightlyFixupSpecs rewrite the generated deps back to the nightly. Mirrors
* vnext/Scripts/creaternwlib.cmd's RN_SCAFFOLD_VERSION.
*/
export function scaffoldReactNativeVersion(versions: BranchVersions): string {
return versions.nightly
? versions.reactNative.split('-')[0]
: versions.reactNative;
}

/**
* Rewrite generated nightly deps to the nightly RN/CLI, mirroring
* vnext/Scripts/UpdateNightlyDependencies.ps1 (cRNL can't pin nightly RN itself).
Expand Down Expand Up @@ -376,7 +390,7 @@ function scaffold(
cfg.type,
...cfg.tools.map(t => `--tools=${t}`),
'--react-native-version',
versions.reactNative,
scaffoldReactNativeVersion(versions),
'--example',
cfg.example,
'warmlib',
Expand Down
2 changes: 2 additions & 0 deletions packages/@rnw-scripts/warm-feed/src/specialModules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import type {SpecialModule} from './types';
import {createReactNativeLibraryModule} from './createReactNativeLibrary';
import {createReactNativeAppModule} from './createReactNativeApp';

const modules = new Map<string, SpecialModule>();

Expand All @@ -26,5 +27,6 @@ export function specialModuleNames(): string[] {
}

registerSpecialModule(createReactNativeLibraryModule);
registerSpecialModule(createReactNativeAppModule);

export type {SpecialModule, SpecialModuleContext} from './types';
Loading
Loading