Skip to content

Commit

Permalink
Expose cosmos.fixtures.json with renderer URLs (#1629)
Browse files Browse the repository at this point in the history
  • Loading branch information
ovidiuch committed Mar 11, 2024
1 parent f83799a commit 4cc91ba
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 20 deletions.
Expand Up @@ -4,6 +4,7 @@ import {
FixtureListItem,
} from '../../userModules/fixtureTypes.js';
import { addTreeNodeChild } from '../../utils/tree.js';
import { removeFixtureNameExtension } from '../fixtureUtils.js';
import { FixtureTreeNode } from '../types.js';

export function createRawFixtureTree(fixtures: FixtureList): FixtureTreeNode {
Expand Down Expand Up @@ -55,10 +56,6 @@ function parseFixturePath(fixturePath: string) {
};
}

function removeFixtureNameExtension(fixtureName: string) {
return fixtureName.replace(/\.(js|jsx|ts|tsx|md|mdx)$/, '');
}

function injectNode(
rootNode: FixtureTreeNode,
parents: string[],
Expand Down
@@ -1,3 +1,4 @@
import { removeFixtureNameSuffix } from '../fixtureUtils.js';
import { FixtureTreeNode } from '../types.js';

export function hideFixtureSuffix(
Expand Down Expand Up @@ -25,10 +26,3 @@ export function hideFixtureSuffix(
}, {}),
};
}

function removeFixtureNameSuffix(
fixtureNameWithoutExtension: string,
suffix: string
) {
return fixtureNameWithoutExtension.replace(new RegExp(`\\.${suffix}$`), '');
}
10 changes: 10 additions & 0 deletions packages/react-cosmos-core/src/fixtureTree/fixtureUtils.ts
@@ -0,0 +1,10 @@
export function removeFixtureNameExtension(fixtureName: string) {
return fixtureName.replace(/\.(js|jsx|ts|tsx|md|mdx)$/, '');
}

export function removeFixtureNameSuffix(
fixtureNameWithoutExtension: string,
suffix: string
) {
return fixtureNameWithoutExtension.replace(new RegExp(`\\.${suffix}$`), '');
}
1 change: 1 addition & 0 deletions packages/react-cosmos-core/src/index.ts
Expand Up @@ -9,6 +9,7 @@ export * from './fixtureState/propsTypes.js';
export * from './fixtureState/types.js';
export * from './fixtureState/viewport.js';
export * from './fixtureTree/createFixtureTree/index.js';
export * from './fixtureTree/fixtureUtils.js';
export * from './fixtureTree/flattenFixtureTree.js';
export * from './fixtureTree/types.js';
export * from './message/serverMessage.js';
Expand Down
4 changes: 3 additions & 1 deletion packages/react-cosmos-core/src/renderer/rendererUrl.ts
Expand Up @@ -3,6 +3,8 @@ import { CosmosCommand } from '../server/serverTypes.js';
import { FixtureId } from '../userModules/fixtureTypes.js';
import { buildRendererQueryString } from './rendererQueryString.js';

export type CosmosRendererUrl = null | string | { dev: string; export: string };

export function createRendererUrl(
rendererUrl: string,
fixtureId?: FixtureId,
Expand All @@ -24,7 +26,7 @@ export function createRendererUrl(
}

export function pickRendererUrl(
rendererUrl: undefined | null | string | { dev: string; export: string },
rendererUrl: undefined | CosmosRendererUrl,
command: CosmosCommand
): null | string {
return rendererUrl && typeof rendererUrl === 'object'
Expand Down
91 changes: 91 additions & 0 deletions packages/react-cosmos/src/corePlugins/fixturesJsonPlugin.ts
@@ -0,0 +1,91 @@
import express from 'express';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
CosmosCommand,
createRendererUrl,
pickRendererUrl,
removeFixtureNameExtension,
removeFixtureNameSuffix,
} from 'react-cosmos-core';
import { CosmosConfig } from '../cosmosConfig/types.js';
import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
import { findUserModulePaths } from '../userModules/findUserModulePaths.js';
import { importKeyPath } from '../userModules/shared.js';

export type CosmosFixtureJson = {
filePath: string;
cleanPath: string[];
rendererUrl: string;
};

export type CosmosFixturesJson = {
rendererUrl: string | null;
fixtures: CosmosFixtureJson[];
};

export const fixturesJsonPlugin: CosmosServerPlugin = {
name: 'fixturesJson',

devServer({ cosmosConfig, expressApp }) {
expressApp.get(
'/cosmos.fixtures.json',
(req: express.Request, res: express.Response) => {
res.json(createFixtureItems(cosmosConfig, 'dev'));
}
);
},

async export({ cosmosConfig }) {
const { exportPath } = cosmosConfig;
const json = createFixtureItems(cosmosConfig, 'export');
await fs.writeFile(
path.join(exportPath, 'cosmos.fixtures.json'),
JSON.stringify(json, null, 2)
);
},
};

function createFixtureItems(
cosmosConfig: CosmosConfig,
command: CosmosCommand
): CosmosFixturesJson {
const rendererUrl = pickRendererUrl(cosmosConfig.rendererUrl, command);
if (!rendererUrl) {
return {
rendererUrl: null,
fixtures: [],
};
}

const { fixturesDir, fixtureFileSuffix } = cosmosConfig;
const { fixturePaths } = findUserModulePaths(cosmosConfig);

return {
rendererUrl,
fixtures: fixturePaths.map(filePath => {
const relPath = importKeyPath(filePath, cosmosConfig.rootDir);
const fixtureId = { path: relPath };
return {
filePath: relPath,
cleanPath: cleanFixturePath(relPath, fixturesDir, fixtureFileSuffix),
rendererUrl: createRendererUrl(rendererUrl, fixtureId, true),
};
}),
};
}

function cleanFixturePath(
filePath: string,
fixturesDir: string,
fixtureSuffix: string
) {
const paths = filePath.split('/').filter(p => p !== fixturesDir);
return [
...paths.slice(0, -1),
removeFixtureNameSuffix(
removeFixtureNameExtension(paths[paths.length - 1]),
fixtureSuffix
),
];
}
2 changes: 2 additions & 0 deletions packages/react-cosmos/src/corePlugins/index.ts
@@ -1,12 +1,14 @@
import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
import { fixtureWatcherPlugin } from './fixtureWatcherPlugin.js';
import { fixturesJsonPlugin } from './fixturesJsonPlugin.js';
import { httpProxyPlugin } from './httpProxyPlugin.js';
import { openFilePlugin } from './openFilePlugin.js';
import { pluginEndpointPlugin } from './pluginEndpointPlugin.js';
import { portRetryPlugin } from './portRetryPlugin.js';

export const coreServerPlugins: CosmosServerPlugin[] = [
portRetryPlugin,
fixturesJsonPlugin,
httpProxyPlugin,
openFilePlugin,
pluginEndpointPlugin,
Expand Down
4 changes: 3 additions & 1 deletion packages/react-cosmos/src/cosmosConfig/types.ts
@@ -1,3 +1,5 @@
import { CosmosRendererUrl } from 'react-cosmos-core';

interface HttpsOptions {
keyPath: string;
certPath: string;
Expand Down Expand Up @@ -34,7 +36,7 @@ export type CosmosConfig = {
portRetries: number;
plugins: string[];
publicUrl: string;
rendererUrl: null | string | { dev: string; export: string };
rendererUrl: CosmosRendererUrl;
rootDir: string;
staticPath: null | string;
watchDirs: string[];
Expand Down
8 changes: 2 additions & 6 deletions packages/react-cosmos/src/getFixtures/importUserModules.ts
@@ -1,12 +1,11 @@
import path from 'path';
import {
ByPath,
ReactDecoratorModule,
ReactFixtureModule,
} from 'react-cosmos-core';
import { CosmosConfig } from '../cosmosConfig/types.js';
import { findUserModulePaths } from '../userModules/findUserModulePaths.js';
import { slash } from '../utils/slash.js';
import { importKeyPath } from '../userModules/shared.js';

type UserModules = {
fixtures: ByPath<ReactFixtureModule>;
Expand All @@ -33,10 +32,7 @@ export function importUserModules({

function importModules<T>(paths: string[], rootDir: string) {
const modules = paths.map(p => {
// Converting to forward slashes on Windows is important because the
// slashes are used for generating a sorted list of fixtures and
// decorators.
const relPath = slash(path.relative(rootDir, p));
const relPath = importKeyPath(p, rootDir);
return { relPath, module: require(p) };
});

Expand Down
4 changes: 4 additions & 0 deletions packages/react-cosmos/src/index.ts
@@ -1,3 +1,7 @@
export {
CosmosFixtureJson,
CosmosFixturesJson,
} from './corePlugins/fixturesJsonPlugin.js';
export * from './cosmosConfig/createCosmosConfig.js';
export * from './cosmosConfig/detectCosmosConfig.js';
export * from './cosmosConfig/getCosmosConfigAtPath.js';
Expand Down
3 changes: 3 additions & 0 deletions packages/react-cosmos/src/userModules/shared.ts
Expand Up @@ -54,6 +54,9 @@ export function createImportMap(
}

export function importKeyPath(filePath: string, rootDir: string) {
// Converting to forward slashes on Windows is important because the
// slashes are used for generating a sorted list of fixtures and
// decorators.
return slash(path.relative(rootDir, filePath));
}

Expand Down
47 changes: 46 additions & 1 deletion tests/helpers/webTests.ts
@@ -1,4 +1,5 @@
import { Page, expect, test } from '@playwright/test';
import { APIRequestContext, Page, expect, test } from '@playwright/test';
import { CosmosFixtureJson, CosmosFixturesJson } from 'react-cosmos';
import { exampleName } from './envVars.js';

export function webTests(url: string) {
Expand Down Expand Up @@ -85,8 +86,52 @@ export function webTests(url: string) {
expect(await response.text()).toContain('nom nom nom');
});
});

test.describe('cosmos.fixture.json', () => {
test('contains renderer URL', async ({ request }) => {
const { rendererUrl } = await getFixturesJson(request, url);
expect(typeof rendererUrl).toBe('string');
});

test('contains fixture data', async ({ request }) => {
const { fixtures } = await getFixturesJson(request, url);
expect(fixtures).toContainEqual({
filePath: 'src/WelcomeMessage/WelcomeMessage.fixture.tsx',
cleanPath: ['src', 'WelcomeMessage', 'WelcomeMessage'],
rendererUrl: expect.stringContaining(
'?fixtureId=%7B%22path%22%3A%22src%2FWelcomeMessage%2FWelcomeMessage.fixture.tsx%22%7D&locked=true'
),
});
});

test('contains fixture renderer URL', async ({ request, page }) => {
const { fixtures } = await getFixturesJson(request, url);
const fixture = expectFixture(fixtures, 'HelloWorld.mdx');
await page.goto(resolveRendererUrl(url, fixture.rendererUrl));
await expect(page.getByText('Hello World!')).toBeVisible();
});
});
}

function rendererRoot(page: Page) {
return page.frameLocator('iframe').locator('#root');
}

async function getFixturesJson(request: APIRequestContext, url: string) {
const response = await request.get(url + '/cosmos.fixtures.json');
return (await response.json()) as CosmosFixturesJson;
}

function expectFixture(fixtures: CosmosFixtureJson[], fileName: string) {
const fixture = fixtures.find(f => f.filePath.endsWith(fileName));
expect(fixture).toBeTruthy();
return fixture!;
}

function resolveRendererUrl(url: string, rendererUrl: string) {
try {
return new URL(rendererUrl).href;
} catch (err) {
return new URL(rendererUrl, url).href;
}
}

0 comments on commit 4cc91ba

Please sign in to comment.