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
8 changes: 4 additions & 4 deletions .github/workflows/generate-sandboxes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ jobs:
with:
node-version-file: '.nvmrc'

# Node's bundled npm is older than 11.10.0 and silently ignores
# NPM_CONFIG_MIN_RELEASE_AGE during scaffold. Keep in sync with
# BEFORE_SANDBOX_NPM_MIN_VERSION in scripts/sandbox/utils/yarn.ts.
# Node's bundled npm is older than 11.17.0 and silently ignores
# NPM_CONFIG_MIN_RELEASE_AGE / min-release-age-exclude during scaffold.
# Keep in sync with BEFORE_SANDBOX_NPM_MIN_VERSION in scripts/sandbox/utils/yarn.ts.
- name: Install npm with min-release-age support
working-directory: ${{ github.workspace }}
run: npm install -g npm@11.10.0
run: npm install -g npm@11.17.0
Comment thread
ndelangen marked this conversation as resolved.
Dismissed

- name: Setup git user
run: |
Expand Down
13 changes: 5 additions & 8 deletions code/lib/cli-storybook/src/sandbox-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,11 +990,13 @@ export const baseTemplates = {
// create-expo-app pins the current SDK, whose packages are released
// together and are routinely younger than the gate window.
// `babel-preset-expo` is part of that set despite not matching `expo-*`.
// `multitars` is pulled in transitively by the Expo CLI toolchain.
minAgeGateExemptions: [
'expo',
'expo-*',
'@expo/*',
'babel-preset-expo',
'multitars',
'react-native',
'@react-native/*',
],
Expand All @@ -1021,17 +1023,12 @@ export const baseTemplates = {
},
},
'react-native-web-vite/rn-cli-ts': {
// NOTE: create-expo-app installs React 18.2.0. But yarn portal
// expects 18.3.1 (dunno why). Therefore to run this in dev you
// must either:
// - edit the sandbox package.json to depend on react 18.3.1, OR
// - build/run the sandbox in --no-link mode, which is fine
//
// Users & CI won't see this limitation because they are not using
// yarn portals.
name: 'React Native CLI Latest (Vite | TypeScript)',
script:
'npx @react-native-community/cli@latest init --skip-install --install-pods=false --directory={{beforeDir}} rnapp',
// The CLI downloads `@react-native-community/template` during init even with
// --skip-install; those packages ship in lockstep with each RN release.
minAgeGateExemptions: ['@react-native-community/*', 'react-native', '@react-native/*'],
expected: {
framework: '@storybook/react-native-web-vite',
renderer: '@storybook/react',
Expand Down
9 changes: 9 additions & 0 deletions scripts/sandbox/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
preapproveLocallyPublishedPackages,
refreshBeforeStorybookLockfile,
setupYarn,
writeScaffoldNpmrc,
} from './utils/yarn.ts';

const isCI = process.env.GITHUB_ACTIONS === 'true' || process.env.CI === 'true';
Expand Down Expand Up @@ -297,6 +298,14 @@ const runGenerators = async (
NPM_CONFIG_MIN_RELEASE_AGE: String(BEFORE_SANDBOX_NPM_MIN_RELEASE_AGE_DAYS),
};

const scaffoldCwd = script.includes('{{beforeDir}}') ? createBaseDir : createBeforeDir;
if (minAgeGateExemptions?.length) {
if (scaffoldCwd === createBeforeDir) {
await mkdir(createBeforeDir, { recursive: true });
}
await writeScaffoldNpmrc(scaffoldCwd, minAgeGateExemptions);
}

// Some tools refuse to run inside an existing directory and replace the contents,
// where as others are very picky about what directories can be called. So we need to
// handle different modes of operation.
Expand Down
23 changes: 20 additions & 3 deletions scripts/sandbox/utils/yarn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ensureNpmSupportsMinReleaseAge,
preapproveLocallyPublishedPackages,
refreshBeforeStorybookLockfile,
writeScaffoldNpmrc,
} from './yarn.ts';

vi.mock('node:fs/promises', { spy: true });
Expand Down Expand Up @@ -240,7 +241,7 @@ describe('ensureNpmSupportsMinReleaseAge', () => {
});

it(`accepts npm ${BEFORE_SANDBOX_NPM_MIN_VERSION} and newer`, async () => {
vi.mocked(runCommand).mockResolvedValue({ stdout: '11.10.0\n' } as never);
vi.mocked(runCommand).mockResolvedValue({ stdout: '11.17.0\n' } as never);

await expect(ensureNpmSupportsMinReleaseAge()).resolves.toBeUndefined();

Expand All @@ -249,10 +250,26 @@ describe('ensureNpmSupportsMinReleaseAge', () => {
});

it('fails when npm is older than the min-release-age floor', async () => {
vi.mocked(runCommand).mockResolvedValue({ stdout: '10.9.8\n' } as never);
vi.mocked(runCommand).mockResolvedValue({ stdout: '11.10.0\n' } as never);

await expect(ensureNpmSupportsMinReleaseAge()).rejects.toThrow(
new RegExp(`npm >= ${BEFORE_SANDBOX_NPM_MIN_VERSION}.*found 10\\.9\\.8`)
new RegExp(`npm >= ${BEFORE_SANDBOX_NPM_MIN_VERSION}.*found 11\\.10\\.0`)
);
});
});

describe('writeScaffoldNpmrc', () => {
it('writes min-release-age and exclude patterns for npm scaffolds', async () => {
await writeScaffoldNpmrc(SANDBOX, ['@react-native-community/*', 'multitars']);

expect(vol.readFileSync(`${SANDBOX}/.npmrc`, 'utf-8')).toBe(
'min-release-age=7\nmin-release-age-exclude[]=@react-native-community/*\nmin-release-age-exclude[]=multitars\n'
);
});

it('does nothing when the allowlist is empty', async () => {
await writeScaffoldNpmrc(SANDBOX, []);

expect(vol.existsSync(`${SANDBOX}/.npmrc`)).toBe(false);
});
});
28 changes: 25 additions & 3 deletions scripts/sandbox/utils/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,41 @@ export const BEFORE_SANDBOX_MIN_AGE_GATE = '7d';
export const BEFORE_SANDBOX_MIN_AGE_MINUTES = 7 * 24 * 60;
/** npm `min-release-age` is in days (npm 11.10+). */
export const BEFORE_SANDBOX_NPM_MIN_RELEASE_AGE_DAYS = 7;
/** npm below this version silently ignores `NPM_CONFIG_MIN_RELEASE_AGE`. */
export const BEFORE_SANDBOX_NPM_MIN_VERSION = '11.10.0';
/**
* npm below this version silently ignores `NPM_CONFIG_MIN_RELEASE_AGE`.
* 11.17+ is required for `min-release-age-exclude`, which templates with
* `minAgeGateExemptions` rely on during npx/npm scaffolds.
*/
export const BEFORE_SANDBOX_NPM_MIN_VERSION = '11.17.0';

export async function ensureNpmSupportsMinReleaseAge() {
const { stdout } = await runCommand('npm --version', { cwd: process.cwd() });
const version = String(stdout).trim();
if (!semver.gte(version, BEFORE_SANDBOX_NPM_MIN_VERSION)) {
throw new Error(
`Sandbox generation requires npm >= ${BEFORE_SANDBOX_NPM_MIN_VERSION} so NPM_CONFIG_MIN_RELEASE_AGE is honored (found ${version}). Upgrade with: npm install -g npm@${BEFORE_SANDBOX_NPM_MIN_VERSION}`
`Sandbox generation requires npm >= ${BEFORE_SANDBOX_NPM_MIN_VERSION} so NPM_CONFIG_MIN_RELEASE_AGE and min-release-age-exclude are honored (found ${version}). Upgrade with: npm install -g npm@${BEFORE_SANDBOX_NPM_MIN_VERSION}`
);
}
}

/**
* Templates with a Yarn allowlist also need matching npm exclusions when their
* before-script runs through npx/npm. Array config cannot be expressed reliably
* via environment variables, so write a scratch `.npmrc` in the scaffold cwd.
*/
export async function writeScaffoldNpmrc(cwd: string, minAgeGateExemptions: string[]) {
if (!minAgeGateExemptions.length) {
return;
}

const lines = [
`min-release-age=${BEFORE_SANDBOX_NPM_MIN_RELEASE_AGE_DAYS}`,
...minAgeGateExemptions.map((pattern) => `min-release-age-exclude[]=${pattern}`),
];

await writeFile(join(cwd, '.npmrc'), `${lines.join('\n')}\n`);
}

interface RefreshLockfileOptions {
cwd: string;
debug?: boolean;
Expand Down