Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose cosmos.fixtures.json with renderer URLs #1629

Merged
merged 7 commits into from
Mar 11, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}