Skip to content
Closed
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: 8 additions & 0 deletions scripts/sandbox/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { dirname, join, relative } from 'path';
import { temporaryDirectory } from '../../code/core/src/common/utils/cli.ts';
import { REPROS_DIRECTORY } from '../utils/constants.ts';
import { commitAllToGit } from './utils/git.ts';
import { sanitizePublishedSandboxes } from './utils/sanitize-published-sandbox.ts';
import { getTemplatesData, renderTemplate } from './utils/template.ts';

export const logger = console;
Expand Down Expand Up @@ -70,6 +71,13 @@ const publish = async (options: PublishOptions & { tmpFolder: string }) => {
logger.log(`🚛 Moving all the repros into the repository`);
await cp(REPROS_DIRECTORY, tmpFolder, { recursive: true });

logger.log(`🧼 Sanitizing published sandboxes (after-storybook .yarnrc.yml + install artifacts)`);
const sanitizeResult = await sanitizePublishedSandboxes(tmpFolder);
logger.log(
`🧼 Sanitize summary: stripped ${sanitizeResult.strippedKeyCount} key(s) from ` +
`${sanitizeResult.filteredYarnrcCount} .yarnrc.yml file(s); removed ${sanitizeResult.removedPaths} excluded path(s)`
);

await commitAllToGit({ cwd: tmpFolder, branch });

logger.info(`
Expand Down
168 changes: 168 additions & 0 deletions scripts/sandbox/utils/sanitize-published-sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
EXCLUDE_GLOBS,
STRIP_KEYS,
sanitizePublishedSandboxes,
} from './sanitize-published-sandbox.ts';

const exists = async (path: string) => {
try {
await stat(path);
return true;
} catch {
return false;
}
};

describe('STRIP_KEYS', () => {
it('contains exactly the documented host-local / verdaccio keys', () => {
// Mutating this list requires deliberate code review — if you are touching it,
// make sure the published sandboxes repository contract is intentional.
expect([...STRIP_KEYS]).toEqual([
'npmRegistryServer',
'unsafeHttpWhitelist',
'enableImmutableInstalls',
'enableMirror',
'logFilters',
'npmMinimalAgeGate',
'pnpFallbackMode',
'enableGlobalCache',
'checksumBehavior',
]);
});
});

describe('EXCLUDE_GLOBS', () => {
it('targets only known install / build artifacts', () => {
expect([...EXCLUDE_GLOBS]).toEqual([
'**/.yarn/cache/**',
'**/.yarn/install-state.gz',
'**/.yarn/build-state.yml',
'**/.yarn/unplugged/**',
'**/.pnp.cjs',
'**/.pnp.loader.mjs',
'**/node_modules/**',
'**/.cache/**',
'**/storybook-static/**',
]);
});
});

describe('sanitizePublishedSandboxes', () => {
let root: string;

beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'sanitize-sandbox-'));
});

afterEach(async () => {
// tmpdir cleanup is best-effort; not strictly required for correctness
});

it('strips every STRIP_KEYS entry from after-storybook/.yarnrc.yml', async () => {
const afterDir = join(root, 'react-vite', 'default-ts', 'after-storybook');
await mkdir(afterDir, { recursive: true });
const yarnrc = [
'nodeLinker: node-modules',
'npmRegistryServer: "http://localhost:6001/"',
'unsafeHttpWhitelist: "localhost"',
'enableImmutableInstalls: false',
'enableMirror: false',
'logFilters: []',
'npmMinimalAgeGate: 0',
'pnpFallbackMode: none',
'enableGlobalCache: true',
'checksumBehavior: ignore',
'',
].join('\n');
await writeFile(join(afterDir, '.yarnrc.yml'), yarnrc);

const result = await sanitizePublishedSandboxes(root);

expect(result.filteredYarnrcCount).toBe(1);
expect(result.strippedKeyCount).toBe(STRIP_KEYS.length);

const sanitized = await readFile(join(afterDir, '.yarnrc.yml'), 'utf-8');
for (const key of STRIP_KEYS) {
expect(sanitized).not.toContain(key);
}
// Non-stripped keys are preserved
expect(sanitized).toContain('nodeLinker');
});

it('leaves before-storybook/.yarnrc.yml untouched', async () => {
const beforeDir = join(root, 'react-vite', 'default-ts', 'before-storybook');
await mkdir(beforeDir, { recursive: true });
const yarnrc = [
'nodeLinker: node-modules',
'enableGlobalCache: true',
'',
].join('\n');
await writeFile(join(beforeDir, '.yarnrc.yml'), yarnrc);

const result = await sanitizePublishedSandboxes(root);

expect(result.filteredYarnrcCount).toBe(0);
const preserved = await readFile(join(beforeDir, '.yarnrc.yml'), 'utf-8');
expect(preserved).toContain('enableGlobalCache');
expect(preserved).toContain('nodeLinker');
});

it('removes EXCLUDE_GLOBS matches from the tree', async () => {
const afterDir = join(root, 'react-vite', 'default-ts', 'after-storybook');
const beforeDir = join(root, 'react-vite', 'default-ts', 'before-storybook');
const afterCache = join(afterDir, '.yarn', 'cache');
const beforeNodeModules = join(beforeDir, 'node_modules', 'some-pkg');

await mkdir(afterCache, { recursive: true });
await writeFile(join(afterCache, 'pkg.zip'), 'binary blob');
await mkdir(beforeNodeModules, { recursive: true });
await writeFile(join(beforeNodeModules, 'index.js'), 'export {}');
await writeFile(join(afterDir, '.pnp.cjs'), '/* zero install */');
await writeFile(join(afterDir, 'README.md'), '# kept');

const result = await sanitizePublishedSandboxes(root);

expect(result.removedPaths).toBeGreaterThan(0);
expect(await exists(afterCache)).toBe(false);
expect(await exists(beforeNodeModules)).toBe(false);
expect(await exists(join(afterDir, '.pnp.cjs'))).toBe(false);
// Non-excluded files survive
expect(await exists(join(afterDir, 'README.md'))).toBe(true);
});

it('writes an empty file when stripping leaves no keys', async () => {
const afterDir = join(root, 'svelte-vite', 'default-ts', 'after-storybook');
await mkdir(afterDir, { recursive: true });
await writeFile(
join(afterDir, '.yarnrc.yml'),
'npmRegistryServer: "http://localhost:6001/"\n'
);

await sanitizePublishedSandboxes(root);

const sanitized = await readFile(join(afterDir, '.yarnrc.yml'), 'utf-8');
expect(sanitized).toBe('');
});

it('is idempotent — a second run is a no-op', async () => {
const afterDir = join(root, 'react-vite', 'default-ts', 'after-storybook');
await mkdir(afterDir, { recursive: true });
await writeFile(
join(afterDir, '.yarnrc.yml'),
'nodeLinker: node-modules\nnpmRegistryServer: "http://localhost:6001/"\n'
);

await sanitizePublishedSandboxes(root);
const second = await sanitizePublishedSandboxes(root);

expect(second.filteredYarnrcCount).toBe(0);
expect(second.strippedKeyCount).toBe(0);
expect(second.removedPaths).toBe(0);
});
});
108 changes: 108 additions & 0 deletions scripts/sandbox/utils/sanitize-published-sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { readFile, rm, writeFile } from 'node:fs/promises';

// eslint-disable-next-line depend/ban-dependencies
import { glob } from 'glob';
import yml from 'yaml';

/**
* Keys stripped from `after-storybook/.yarnrc.yml` before publishing to the public sandboxes
* repository. These are host-local or Verdaccio-bootstrap settings that would either break a
* consumer's install (e.g. `npmRegistryServer: http://localhost:6001/`) or weaken their default
* supply-chain protections (e.g. `npmMinimalAgeGate: 0`).
*
* Mutating this list requires a deliberate code review (the integrity test asserts the exact set).
*/
export const STRIP_KEYS = [
'npmRegistryServer',
'unsafeHttpWhitelist',
'enableImmutableInstalls',
'enableMirror',
'logFilters',
'npmMinimalAgeGate',
'pnpFallbackMode',
'enableGlobalCache',
'checksumBehavior',
] as const;

/**
* Paths excluded from the published sandbox copy. These are install artifacts or build outputs
* that bloat the repo without providing value to consumers (who will re-run `yarn install` against
* the committed lockfile).
*/
export const EXCLUDE_GLOBS = [
'**/.yarn/cache/**',
'**/.yarn/install-state.gz',
'**/.yarn/build-state.yml',
'**/.yarn/unplugged/**',
'**/.pnp.cjs',
'**/.pnp.loader.mjs',
'**/node_modules/**',
'**/.cache/**',
'**/storybook-static/**',
] as const;

export type SanitizeResult = {
filteredYarnrcCount: number;
strippedKeyCount: number;
removedPaths: number;
};

/**
* Sanitize a directory tree that is about to be published to `storybookjs/sandboxes`.
*
* - Removes `STRIP_KEYS` from every `**\/after-storybook/.yarnrc.yml` (verdaccio/host config).
* - Removes paths matching `EXCLUDE_GLOBS` from the tree (install artifacts, build output).
*
* `before-storybook/.yarnrc.yml` is intentionally left untouched: it contains only the
* user-facing Yarn setup we want consumers to reproduce.
*/
export const sanitizePublishedSandboxes = async (rootDir: string): Promise<SanitizeResult> => {
const yarnrcFiles = await glob('**/after-storybook/.yarnrc.yml', {
cwd: rootDir,
absolute: true,
dot: true,
});

let filteredYarnrcCount = 0;
let strippedKeyCount = 0;

for (const file of yarnrcFiles) {
const original = await readFile(file, 'utf-8');
if (!original.trim()) {
continue;
}

const doc = (yml.parse(original) ?? {}) as Record<string, unknown>;
let modified = false;

for (const key of STRIP_KEYS) {
if (key in doc) {
delete doc[key];
modified = true;
strippedKeyCount++;
}
}

if (modified) {
const updated = Object.keys(doc).length === 0 ? '' : yml.stringify(doc);
await writeFile(file, updated);
filteredYarnrcCount++;
}
}

const excluded = await glob([...EXCLUDE_GLOBS], {
cwd: rootDir,
absolute: true,
dot: true,
});

for (const target of excluded) {
await rm(target, { recursive: true, force: true });
}

return {
filteredYarnrcCount,
strippedKeyCount,
removedPaths: excluded.length,
};
};
Loading