react-cosmos 6.0.0-alpha.9 → 6.0.0-canary.0d1dbd5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/config.schema.json +4 -0
  2. package/dist/server/corePlugins/httpProxy.d.ts +2 -0
  3. package/dist/server/corePlugins/httpProxy.js +21 -0
  4. package/dist/server/corePlugins/index.d.ts +2 -0
  5. package/dist/server/corePlugins/index.js +12 -0
  6. package/dist/server/corePlugins/openFile.d.ts +2 -0
  7. package/dist/server/corePlugins/openFile.js +56 -0
  8. package/dist/server/corePlugins/pluginEndpoint.d.ts +2 -0
  9. package/dist/server/corePlugins/pluginEndpoint.js +22 -0
  10. package/dist/server/corePlugins/portRetry.d.ts +2 -0
  11. package/dist/server/corePlugins/portRetry.js +11 -0
  12. package/dist/server/corePlugins/userDepsFile.d.ts +2 -0
  13. package/dist/server/{devServer/corePlugins → corePlugins}/userDepsFile.js +19 -16
  14. package/dist/server/cosmosConfig/createCosmosConfig.js +11 -0
  15. package/dist/server/cosmosConfig/shared.js +2 -2
  16. package/dist/server/cosmosConfig/types.d.ts +6 -0
  17. package/dist/server/cosmosPlugin/readCosmosPluginConfig.js +4 -7
  18. package/dist/server/cosmosPlugin/types.d.ts +11 -0
  19. package/dist/server/devServer/messageHandler.d.ts +1 -1
  20. package/dist/server/devServer/messageHandler.js +2 -2
  21. package/dist/server/devServer/startDevServer.js +23 -23
  22. package/dist/server/export/generateExport.js +23 -12
  23. package/dist/server/shared/findNextAvailablePort.d.ts +1 -0
  24. package/dist/server/shared/findNextAvailablePort.js +29 -0
  25. package/dist/server/shared/importServerPlugins.d.ts +3 -0
  26. package/dist/server/shared/importServerPlugins.js +16 -0
  27. package/dist/server/testHelpers/mockConsole.js +8 -4
  28. package/dist/server/testHelpers/mockFs.d.ts +2 -2
  29. package/dist/server/testHelpers/mockFs.js +26 -8
  30. package/dist/server/utils/fs.d.ts +2 -1
  31. package/dist/server/utils/fs.js +8 -5
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +1 -0
  34. package/package.json +4 -4
  35. package/dist/server/devServer/corePlugins/httpProxy.d.ts +0 -2
  36. package/dist/server/devServer/corePlugins/httpProxy.js +0 -18
  37. package/dist/server/devServer/corePlugins/openFile.d.ts +0 -2
  38. package/dist/server/devServer/corePlugins/openFile.js +0 -53
  39. package/dist/server/devServer/corePlugins/pluginEndpoint.d.ts +0 -2
  40. package/dist/server/devServer/corePlugins/pluginEndpoint.js +0 -17
  41. package/dist/server/devServer/corePlugins/userDepsFile.d.ts +0 -2
  42. package/dist/server/shared/requirePluginModule.d.ts +0 -2
  43. package/dist/server/shared/requirePluginModule.js +0 -10
@@ -78,6 +78,10 @@
78
78
  "description": "Dev server port. [default: 5000]",
79
79
  "type": "number"
80
80
  },
81
+ "portRetries": {
82
+ "description": "Max number of port retries. [default: 10]",
83
+ "type": "number"
84
+ },
81
85
  "https": {
82
86
  "description": "Server will be served over HTTPS",
83
87
  "type": "boolean"
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const httpProxyServerPlugin: CosmosServerPlugin;
@@ -0,0 +1,21 @@
1
+ import { createProxyMiddleware } from 'http-proxy-middleware';
2
+ export const httpProxyServerPlugin = {
3
+ name: 'httpProxy',
4
+ devServer({ platformType, cosmosConfig, expressApp }) {
5
+ if (platformType !== 'web')
6
+ return;
7
+ const httpProxyConfig = getHttpProxyCosmosConfig(cosmosConfig);
8
+ Object.keys(httpProxyConfig).forEach(context => {
9
+ const config = httpProxyConfig[context];
10
+ if (typeof config === 'string') {
11
+ expressApp.use(context, createProxyMiddleware(context, { target: config }));
12
+ }
13
+ else if (typeof config === 'object') {
14
+ expressApp.use(context, createProxyMiddleware(context, config));
15
+ }
16
+ });
17
+ },
18
+ };
19
+ function getHttpProxyCosmosConfig(cosmosConfig) {
20
+ return (cosmosConfig.httpProxy || {});
21
+ }
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const coreServerPlugins: CosmosServerPlugin[];
@@ -0,0 +1,12 @@
1
+ import { httpProxyServerPlugin } from './httpProxy.js';
2
+ import { openFileServerPlugin } from './openFile.js';
3
+ import { pluginEndpointServerPlugin } from './pluginEndpoint.js';
4
+ import { portRetryServerPlugin } from './portRetry.js';
5
+ import { userDepsFileServerPlugin } from './userDepsFile.js';
6
+ export const coreServerPlugins = [
7
+ portRetryServerPlugin,
8
+ userDepsFileServerPlugin,
9
+ httpProxyServerPlugin,
10
+ openFileServerPlugin,
11
+ pluginEndpointServerPlugin,
12
+ ];
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const openFileServerPlugin: CosmosServerPlugin;
@@ -0,0 +1,56 @@
1
+ import launchEditor from '@skidding/launch-editor';
2
+ import fs from 'fs';
3
+ import open from 'open';
4
+ import path from 'path';
5
+ export const openFileServerPlugin = {
6
+ name: 'openFile',
7
+ devServer({ cosmosConfig, expressApp }) {
8
+ expressApp.get('/_open', (req, res) => {
9
+ const { filePath, line, column } = getReqQuery(req);
10
+ if (!filePath) {
11
+ res.status(400).send(`File path missing`);
12
+ return;
13
+ }
14
+ const absFilePath = resolveFilePath(cosmosConfig.rootDir, filePath);
15
+ if (!fs.existsSync(absFilePath)) {
16
+ res.status(404).send(`File not found at path: ${absFilePath}`);
17
+ return;
18
+ }
19
+ new Promise((resolve, reject) => {
20
+ const file = `${absFilePath}:${line}:${column}`;
21
+ launchEditor(file, (fileName, errorMsg) => reject(errorMsg));
22
+ // If launchEditor doesn't report error within 500ms we assume it worked
23
+ setTimeout(resolve, 500);
24
+ })
25
+ // Fall back to open in case launchEditor fails. launchEditor only works
26
+ // when the editor app is already open, but is favorable because it can
27
+ // open a code file on a specific line & column.
28
+ .catch(err => open(absFilePath))
29
+ .catch(err => res.status(500).send('Failed to open file'))
30
+ .then(() => res.send());
31
+ });
32
+ },
33
+ };
34
+ function getReqQuery(req) {
35
+ const { filePath, line, column } = req.query;
36
+ if (typeof filePath !== 'string')
37
+ throw new Error('filePath missing');
38
+ return {
39
+ filePath,
40
+ line: typeof line === 'string' ? parseInt(line, 10) : 1,
41
+ column: typeof column === 'string' ? parseInt(column, 10) : 1,
42
+ };
43
+ }
44
+ function resolveFilePath(rootDir, filePath) {
45
+ // This heuristic is needed because the open file endpoint is used for
46
+ // multiple applications, which provide different file path types:
47
+ // 1. Edit fixture button: Sends path relative to Cosmos rootDir
48
+ // 2. react-error-overlay runtime error: Sends absolute path
49
+ // 3. react-error-overlay compile error: Sends path relative to CWD
50
+ if (path.isAbsolute(filePath)) {
51
+ return filePath;
52
+ }
53
+ const cosmosRelPath = path.join(rootDir, filePath);
54
+ const cwdRelPath = path.join(process.cwd(), filePath);
55
+ return fs.existsSync(cosmosRelPath) ? cosmosRelPath : cwdRelPath;
56
+ }
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const pluginEndpointServerPlugin: CosmosServerPlugin;
@@ -0,0 +1,22 @@
1
+ import path from 'node:path';
2
+ import { resolveSilent } from '../utils/resolveSilent.js';
3
+ export const pluginEndpointServerPlugin = {
4
+ name: 'pluginEndpoint',
5
+ devServer({ cosmosConfig, expressApp }) {
6
+ expressApp.get('/_plugin/*.js', (req, res) => {
7
+ const modulePath = req.params['0'];
8
+ if (!modulePath) {
9
+ res.sendStatus(404);
10
+ return;
11
+ }
12
+ // The module path is always absolute, but Windows paths don't start
13
+ // with a slash (e.g. C:\foo\bar.js)
14
+ const resolvedPath = resolveSilent(path.isAbsolute(modulePath) ? modulePath : `/${modulePath}`);
15
+ if (!resolvedPath) {
16
+ res.sendStatus(404);
17
+ return;
18
+ }
19
+ res.sendFile(resolvedPath);
20
+ });
21
+ },
22
+ };
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const portRetryServerPlugin: CosmosServerPlugin;
@@ -0,0 +1,11 @@
1
+ import { findNextAvailablePort } from '../shared/findNextAvailablePort.js';
2
+ export const portRetryServerPlugin = {
3
+ name: 'portRetry',
4
+ async config({ cosmosConfig }) {
5
+ const { port, portRetries } = cosmosConfig;
6
+ return {
7
+ ...cosmosConfig,
8
+ port: portRetries ? await findNextAvailablePort(port, portRetries) : port,
9
+ };
10
+ },
11
+ };
@@ -0,0 +1,2 @@
1
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
2
+ export declare const userDepsFileServerPlugin: CosmosServerPlugin;
@@ -1,21 +1,24 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { startFixtureWatcher } from '../../userDeps/fixtureWatcher.js';
4
- import { getPlaygroundUrl } from '../../shared/playgroundUrl.js';
5
- import { generateUserDepsModule } from '../../userDeps/generateUserDepsModule.js';
6
- import { getCliArgs } from '../../utils/cli.js';
7
- export async function userDepsFileDevServerPlugin(args) {
8
- if (!shouldGenerateUserDepsFile(args.platformType))
9
- return;
10
- const { cosmosConfig } = args;
11
- await generateUserDepsFile(cosmosConfig);
12
- const watcher = await startFixtureWatcher(cosmosConfig, 'all', () => {
13
- generateUserDepsFile(cosmosConfig);
14
- });
15
- return () => {
16
- watcher.close();
17
- };
18
- }
3
+ import { getPlaygroundUrl } from '../shared/playgroundUrl.js';
4
+ import { startFixtureWatcher } from '../userDeps/fixtureWatcher.js';
5
+ import { generateUserDepsModule } from '../userDeps/generateUserDepsModule.js';
6
+ import { getCliArgs } from '../utils/cli.js';
7
+ export const userDepsFileServerPlugin = {
8
+ name: 'userDepsFile',
9
+ async devServer(args) {
10
+ if (!shouldGenerateUserDepsFile(args.platformType))
11
+ return;
12
+ const { cosmosConfig } = args;
13
+ await generateUserDepsFile(cosmosConfig);
14
+ const watcher = await startFixtureWatcher(cosmosConfig, 'all', () => {
15
+ generateUserDepsFile(cosmosConfig);
16
+ });
17
+ return () => {
18
+ watcher.close();
19
+ };
20
+ },
21
+ };
19
22
  function shouldGenerateUserDepsFile(platformType) {
20
23
  return (platformType === 'native' ||
21
24
  // CLI support for --external-userdeps flag (useful with react-native-web)
@@ -16,12 +16,14 @@ export function createCosmosConfig(rootDir, cosmosConfigInput = {}) {
16
16
  httpsOptions: getHttpsOptions(cosmosConfigInput, rootDir),
17
17
  ignore: getIgnore(cosmosConfigInput),
18
18
  port: getPort(cosmosConfigInput),
19
+ portRetries: getPortRetries(cosmosConfigInput),
19
20
  plugins: getPlugins(cosmosConfigInput, rootDir),
20
21
  publicUrl: getPublicUrl(cosmosConfigInput),
21
22
  staticPath: getStaticPath(cosmosConfigInput, rootDir),
22
23
  userDepsFilePath: getUserDepsFilePath(cosmosConfigInput, rootDir),
23
24
  watchDirs: getWatchDirs(cosmosConfigInput, rootDir),
24
25
  rendererUrl: cosmosConfigInput.rendererUrl ?? null,
26
+ dom: getDomConfig(cosmosConfigInput.dom || {}),
25
27
  ui: cosmosConfigInput.ui || {},
26
28
  };
27
29
  }
@@ -78,6 +80,9 @@ function getPort(cosmosConfigInput) {
78
80
  const { port = 5000 } = cosmosConfigInput;
79
81
  return port;
80
82
  }
83
+ function getPortRetries({ portRetries = 10 }) {
84
+ return portRetries;
85
+ }
81
86
  function getGlobalImports(cosmosConfigInput, rootDir) {
82
87
  const { globalImports = [] } = cosmosConfigInput;
83
88
  return globalImports.map(globalImport => resolveLoose(rootDir, globalImport));
@@ -86,3 +91,9 @@ function getPlugins(cosmosConfigInput, rootDir) {
86
91
  const { plugins = [] } = cosmosConfigInput;
87
92
  return plugins.map(plugin => resolveLoose(rootDir, plugin));
88
93
  }
94
+ function getDomConfig(cosmosConfigInput) {
95
+ const { containerQuerySelector = null } = cosmosConfigInput;
96
+ return {
97
+ containerQuerySelector,
98
+ };
99
+ }
@@ -1,7 +1,7 @@
1
- import { importModule } from '../utils/fs.js';
1
+ import { importJson } from '../utils/fs.js';
2
2
  export function getCurrentDir() {
3
3
  return process.cwd();
4
4
  }
5
5
  export async function importConfigFile(cosmosConfigPath) {
6
- return importModule(cosmosConfigPath);
6
+ return importJson(cosmosConfigPath);
7
7
  }
@@ -2,10 +2,14 @@ interface HttpsOptions {
2
2
  keyPath: string;
3
3
  certPath: string;
4
4
  }
5
+ export type CosmosDomConfig = {
6
+ containerQuerySelector: null | string;
7
+ };
5
8
  export type CosmosConfig = {
6
9
  exportPath: string;
7
10
  detectLocalPlugins: boolean;
8
11
  disablePlugins: boolean;
12
+ dom: CosmosDomConfig;
9
13
  fixtureFileSuffix: string;
10
14
  fixturesDir: string;
11
15
  globalImports: string[];
@@ -14,6 +18,7 @@ export type CosmosConfig = {
14
18
  httpsOptions: null | HttpsOptions;
15
19
  ignore: string[];
16
20
  port: number;
21
+ portRetries: number;
17
22
  plugins: string[];
18
23
  publicUrl: string;
19
24
  rendererUrl: string | null;
@@ -26,5 +31,6 @@ export type CosmosConfig = {
26
31
  [pluginName: string]: {};
27
32
  };
28
33
  };
34
+ export type CosmosDomConfigInput = Partial<CosmosDomConfig>;
29
35
  export type CosmosConfigInput = Partial<CosmosConfig>;
30
36
  export {};
@@ -1,8 +1,8 @@
1
1
  import path from 'path';
2
- import { importModule } from '../utils/fs.js';
2
+ import { importJson } from '../utils/fs.js';
3
3
  import { resolveSilent } from '../utils/resolveSilent.js';
4
4
  export async function readCosmosPluginConfig({ rootDir, configPath, relativePaths, }) {
5
- const rawConfig = await importModule(configPath);
5
+ const rawConfig = await importJson(configPath);
6
6
  const pluginRootDir = path.dirname(configPath);
7
7
  const config = {
8
8
  name: rawConfig.name,
@@ -13,11 +13,8 @@ export async function readCosmosPluginConfig({ rootDir, configPath, relativePath
13
13
  if (rawConfig.ui) {
14
14
  config.ui = resolvePluginPath(config.name, rootDir, pluginRootDir, rawConfig.ui, relativePaths);
15
15
  }
16
- if (rawConfig.devServer) {
17
- config.devServer = resolvePluginPath(config.name, rootDir, pluginRootDir, rawConfig.devServer, relativePaths);
18
- }
19
- if (rawConfig.export) {
20
- config.export = resolvePluginPath(config.name, rootDir, pluginRootDir, rawConfig.export, relativePaths);
16
+ if (rawConfig.server) {
17
+ config.server = resolvePluginPath(config.name, rootDir, pluginRootDir, rawConfig.server, relativePaths);
21
18
  }
22
19
  return config;
23
20
  }
@@ -4,6 +4,11 @@ import http from 'http';
4
4
  import { MessageType } from 'react-cosmos-core';
5
5
  import { CosmosConfig } from '../cosmosConfig/types.js';
6
6
  export type PlatformType = 'web' | 'native';
7
+ export type CosmosConfigPluginArgs = {
8
+ cosmosConfig: CosmosConfig;
9
+ platformType: PlatformType;
10
+ };
11
+ export type CosmosConfigPlugin = (args: CosmosConfigPluginArgs) => Promise<CosmosConfig> | CosmosConfig;
7
12
  export type DevServerPluginArgs = {
8
13
  cosmosConfig: CosmosConfig;
9
14
  platformType: PlatformType;
@@ -19,3 +24,9 @@ export type ExportPluginArgs = {
19
24
  cosmosConfig: CosmosConfig;
20
25
  };
21
26
  export type ExportPlugin = (args: ExportPluginArgs) => unknown;
27
+ export type CosmosServerPlugin = {
28
+ name: string;
29
+ config?: CosmosConfigPlugin;
30
+ devServer?: DevServerPlugin;
31
+ export?: ExportPlugin;
32
+ };
@@ -3,5 +3,5 @@ import http from 'http';
3
3
  import { ServerMessage } from 'react-cosmos-core';
4
4
  export declare function createMessageHandler(httpServer: http.Server): {
5
5
  sendMessage: (msg: ServerMessage) => void;
6
- cleanUp: () => void;
6
+ close: () => void;
7
7
  };
@@ -21,9 +21,9 @@ export function createMessageHandler(httpServer) {
21
21
  }
22
22
  });
23
23
  }
24
- function cleanUp() {
24
+ function close() {
25
25
  wss.clients.forEach(client => client.close());
26
26
  wss.close();
27
27
  }
28
- return { sendMessage, cleanUp };
28
+ return { sendMessage, close };
29
29
  }
@@ -1,24 +1,15 @@
1
1
  import path from 'path';
2
+ import { coreServerPlugins } from '../corePlugins/index.js';
2
3
  import { detectCosmosConfig, detectCosmosConfigPath, } from '../cosmosConfig/detectCosmosConfig.js';
3
4
  import { getPluginConfigs } from '../cosmosPlugin/pluginConfigs.js';
5
+ import { importServerPlugins } from '../shared/importServerPlugins.js';
4
6
  import { logPluginInfo } from '../shared/logPluginInfo.js';
5
- import { requirePluginModule } from '../shared/requirePluginModule.js';
6
7
  import { serveStaticDir } from '../shared/staticServer.js';
7
- import { httpProxyDevServerPlugin } from './corePlugins/httpProxy.js';
8
- import openFileDevServerPlugin from './corePlugins/openFile.js';
9
- import pluginEndpointDevServerPlugin from './corePlugins/pluginEndpoint.js';
10
- import { userDepsFileDevServerPlugin } from './corePlugins/userDepsFile.js';
11
8
  import { createExpressApp } from './expressApp.js';
12
9
  import { createHttpServer } from './httpServer.js';
13
10
  import { createMessageHandler } from './messageHandler.js';
14
- const builtInPlugins = [
15
- userDepsFileDevServerPlugin,
16
- httpProxyDevServerPlugin,
17
- openFileDevServerPlugin,
18
- pluginEndpointDevServerPlugin,
19
- ];
20
11
  export async function startDevServer(platformType) {
21
- const cosmosConfig = await detectCosmosConfig();
12
+ let cosmosConfig = await detectCosmosConfig();
22
13
  logCosmosConfigInfo();
23
14
  const pluginConfigs = await getPluginConfigs({
24
15
  cosmosConfig,
@@ -28,6 +19,19 @@ export async function startDevServer(platformType) {
28
19
  relativePaths: false,
29
20
  });
30
21
  logPluginInfo(pluginConfigs);
22
+ const userPlugins = await importServerPlugins(pluginConfigs, cosmosConfig.rootDir);
23
+ const plugins = [...coreServerPlugins, ...userPlugins];
24
+ for (const plugin of plugins) {
25
+ if (plugin.config) {
26
+ try {
27
+ cosmosConfig = await plugin.config({ cosmosConfig, platformType });
28
+ }
29
+ catch (err) {
30
+ console.log(`[Cosmos][plugin:${plugin.name}] Config hook failed`);
31
+ throw err;
32
+ }
33
+ }
34
+ }
31
35
  const app = await createExpressApp(platformType, cosmosConfig, pluginConfigs);
32
36
  const httpServer = await createHttpServer(cosmosConfig, app);
33
37
  const msgHandler = createMessageHandler(httpServer.server);
@@ -37,13 +41,14 @@ export async function startDevServer(platformType) {
37
41
  const pluginCleanupCallbacks = [];
38
42
  async function cleanUp() {
39
43
  await Promise.all(pluginCleanupCallbacks.map(cleanup => cleanup()));
44
+ msgHandler.close();
40
45
  await httpServer.stop();
41
- msgHandler.cleanUp();
42
46
  }
43
- const userPlugins = await getDevServerPlugins(pluginConfigs, cosmosConfig.rootDir);
44
- for (const plugin of [...builtInPlugins, ...userPlugins]) {
47
+ for (const plugin of plugins) {
45
48
  try {
46
- const pluginReturn = await plugin({
49
+ if (!plugin.devServer)
50
+ continue;
51
+ const pluginReturn = await plugin.devServer({
47
52
  cosmosConfig,
48
53
  platformType,
49
54
  httpServer: httpServer.server,
@@ -58,7 +63,7 @@ export async function startDevServer(platformType) {
58
63
  catch (err) {
59
64
  // Log when a plugin fails to clean up, but continue to attempt
60
65
  // to clean up the remaining plugins
61
- console.log(`[Cosmos][${plugin.name}] Dev server plugin cleanup failed`);
66
+ console.log(`[Cosmos][plugin:${plugin.name}] Dev server cleanup failed`);
62
67
  console.log(err);
63
68
  }
64
69
  });
@@ -67,7 +72,7 @@ export async function startDevServer(platformType) {
67
72
  catch (err) {
68
73
  // Abort starting server if a plugin init fails and attempt cleanup of all
69
74
  // plugins that have already initialized
70
- console.log(`[Cosmos][${plugin.name}] Dev server plugin init failed`);
75
+ console.log(`[Cosmos][plugin:${plugin.name}] Dev server init failed`);
71
76
  cleanUp();
72
77
  throw err;
73
78
  }
@@ -86,8 +91,3 @@ function logCosmosConfigInfo() {
86
91
  const relConfigPath = path.relative(process.cwd(), cosmosConfigPath);
87
92
  console.log(`[Cosmos] Using cosmos config found at ${relConfigPath}`);
88
93
  }
89
- async function getDevServerPlugins(pluginConfigs, rootDir) {
90
- return Promise.all(pluginConfigs
91
- .filter(pluginConfig => pluginConfig.devServer)
92
- .map(pluginConfig => requirePluginModule(rootDir, pluginConfig, 'devServer')));
93
- }
@@ -1,16 +1,17 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { removeLeadingSlash, } from 'react-cosmos-core';
4
+ import { coreServerPlugins } from '../corePlugins/index.js';
4
5
  import { detectCosmosConfig } from '../cosmosConfig/detectCosmosConfig.js';
5
6
  import { getPluginConfigs } from '../cosmosPlugin/pluginConfigs.js';
7
+ import { importServerPlugins } from '../shared/importServerPlugins.js';
6
8
  import { logPluginInfo } from '../shared/logPluginInfo.js';
7
9
  import { getExportPlaygroundHtml } from '../shared/playgroundHtml.js';
8
- import { requirePluginModule } from '../shared/requirePluginModule.js';
9
10
  import { getStaticPath } from '../shared/staticPath.js';
10
11
  import { resolve } from '../utils/resolve.js';
11
- const builtInPlugins = [];
12
12
  export async function generateExport() {
13
- const cosmosConfig = await detectCosmosConfig();
13
+ const platformType = 'web';
14
+ let cosmosConfig = await detectCosmosConfig();
14
15
  const pluginConfigs = await getPluginConfigs({
15
16
  cosmosConfig,
16
17
  // Relative paths work in exports because all plugin modules are copied
@@ -18,19 +19,34 @@ export async function generateExport() {
18
19
  relativePaths: true,
19
20
  });
20
21
  logPluginInfo(pluginConfigs);
22
+ const userPlugins = await importServerPlugins(pluginConfigs, cosmosConfig.rootDir);
23
+ const plugins = [...coreServerPlugins, ...userPlugins];
24
+ for (const plugin of plugins) {
25
+ if (plugin.config) {
26
+ try {
27
+ cosmosConfig = await plugin.config({ cosmosConfig, platformType });
28
+ }
29
+ catch (err) {
30
+ console.log(`[Cosmos][plugin:${plugin.name}] Config hook failed`);
31
+ throw err;
32
+ }
33
+ }
34
+ }
21
35
  // Clear previous export (or other files at export path)
22
36
  const { exportPath } = cosmosConfig;
23
37
  await fs.rm(exportPath, { recursive: true, force: true });
38
+ await fs.mkdir(exportPath, { recursive: true });
24
39
  // Copy static assets first, so that the built index.html overrides the its
25
40
  // template file (in case the static assets are served from the root path)
26
41
  await copyStaticAssets(cosmosConfig);
27
- const userPlugins = await getExportPlugins(pluginConfigs, cosmosConfig.rootDir);
28
- for (const plugin of [...builtInPlugins, ...userPlugins]) {
42
+ for (const plugin of plugins) {
43
+ if (!plugin.export)
44
+ continue;
29
45
  try {
30
- await plugin({ cosmosConfig });
46
+ await plugin.export({ cosmosConfig });
31
47
  }
32
48
  catch (err) {
33
- console.log(`[Cosmos][${plugin.name}] Export plugin failed`);
49
+ console.log(`[Cosmos][plugin:${plugin.name}] Export failed`);
34
50
  throw err;
35
51
  }
36
52
  }
@@ -59,11 +75,6 @@ async function copyStaticAssets(cosmosConfig) {
59
75
  const exportStaticPath = path.resolve(exportPath, removeLeadingSlash(publicUrl));
60
76
  fs.cp(staticPath, exportStaticPath, { recursive: true });
61
77
  }
62
- async function getExportPlugins(pluginConfigs, rootDir) {
63
- return Promise.all(pluginConfigs
64
- .filter(pluginConfig => pluginConfig.export)
65
- .map(pluginConfig => requirePluginModule(rootDir, pluginConfig, 'export')));
66
- }
67
78
  async function exportPlaygroundFiles(cosmosConfig, pluginConfigs) {
68
79
  const { exportPath } = cosmosConfig;
69
80
  // Copy UI plugins to export path and embed them in the playground HTML
@@ -0,0 +1 @@
1
+ export declare function findNextAvailablePort(port: number, retries: number, retriesLeft?: number): Promise<number>;
@@ -0,0 +1,29 @@
1
+ import net from 'net';
2
+ // Inspired by https://stackoverflow.com/q/19129570
3
+ export async function findNextAvailablePort(port, retries, retriesLeft = retries) {
4
+ return new Promise((resolve, reject) => {
5
+ const socket = new net.Socket();
6
+ socket.on('connect', () => {
7
+ console.log(`[Cosmos] Port ${port} already in use, trying next...`);
8
+ if (retriesLeft > 0) {
9
+ socket.destroy();
10
+ resolve(findNextAvailablePort(port + 1, retries, retriesLeft - 1));
11
+ }
12
+ else {
13
+ reject(`No available port found after ${retries} retries.`);
14
+ }
15
+ });
16
+ socket.on('error', (error) => {
17
+ if (error.code === 'ECONNREFUSED')
18
+ resolve(port);
19
+ else
20
+ reject(error);
21
+ });
22
+ socket.setTimeout(400);
23
+ socket.on('timeout', () => {
24
+ socket.destroy();
25
+ resolve(port);
26
+ });
27
+ socket.connect(port, '0.0.0.0');
28
+ });
29
+ }
@@ -0,0 +1,3 @@
1
+ import { CosmosPluginConfig } from 'react-cosmos-core';
2
+ import { CosmosServerPlugin } from '../cosmosPlugin/types.js';
3
+ export declare function importServerPlugins(pluginConfigs: CosmosPluginConfig[], rootDir: string): Promise<CosmosServerPlugin[]>;
@@ -0,0 +1,16 @@
1
+ import path from 'node:path';
2
+ import { importModule } from '../utils/fs.js';
3
+ export async function importServerPlugins(pluginConfigs, rootDir) {
4
+ return Promise.all(pluginConfigs
5
+ .filter(pluginConfig => pluginConfig.server)
6
+ .map(pluginConfig => importServerModule(pluginConfig, rootDir)));
7
+ }
8
+ async function importServerModule(pluginConfig, rootDir) {
9
+ const modulePath = pluginConfig.server;
10
+ if (!modulePath) {
11
+ throw new Error(`Server module missing in plugin "${pluginConfig.name}"`);
12
+ }
13
+ const absPath = path.resolve(rootDir, modulePath);
14
+ const module = await importModule(absPath);
15
+ return module.default;
16
+ }
@@ -1,11 +1,15 @@
1
1
  export async function mockConsole(cb) {
2
- const origConsoleLog = console.log;
3
- console.log = jest.fn();
4
2
  const expectedLogs = [];
5
- const ret = await cb({
3
+ const origConsoleLog = console.log;
4
+ console.log = jest.fn((...args) => {
5
+ if (typeof args[0] !== 'string' || !expectedLogs.includes(args[0])) {
6
+ origConsoleLog(...args);
7
+ }
8
+ });
9
+ const cbReturn = await cb({
6
10
  expectLog: (msg) => expectedLogs.push(msg),
7
11
  });
8
12
  expectedLogs.forEach(msg => expect(console.log).toBeCalledWith(msg));
9
13
  console.log = origConsoleLog;
10
- return ret;
14
+ return cbReturn;
11
15
  }
@@ -1,5 +1,5 @@
1
1
  import { CosmosConfig } from '../cosmosConfig/types.js';
2
+ export declare function mockFile(filePath: string, fileMock: {}): void;
2
3
  export declare function mockCosmosConfig(cosmosConfigPath: string, cosmosConfig: Partial<CosmosConfig>): void;
3
- export declare function mockFile(filePath: string, fileContent: {}): void;
4
- export declare function mockDir(dirPath: string): void;
4
+ export declare function mockCwdModuleDefault(filePath: string, fileMock: {}): void;
5
5
  export declare function resetFsMock(): void;
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import { getCwdPath } from './cwd.js';
3
3
  jest.mock('../utils/fs', () => {
4
4
  const actual = jest.requireActual('../utils/fs');
@@ -8,8 +8,20 @@ jest.mock('../utils/fs', () => {
8
8
  async function importModule(moduleId) {
9
9
  if (!mocked)
10
10
  return actual.importModule(moduleId);
11
+ if (!fileMocks.hasOwnProperty(moduleId) &&
12
+ !fileMocks.hasOwnProperty(`${moduleId}.js`)) {
13
+ throw new Error(`Cannot find module '${moduleId}'`);
14
+ }
11
15
  return fileMocks[moduleId] || fileMocks[`${moduleId}.js`];
12
16
  }
17
+ async function importJson(filePath) {
18
+ if (!mocked)
19
+ return actual.importJson(filePath);
20
+ if (!fileMocks.hasOwnProperty(filePath)) {
21
+ throw new Error(`Cannot find JSON '${filePath}'`);
22
+ }
23
+ return fileMocks[filePath];
24
+ }
13
25
  function moduleExists(moduleId) {
14
26
  if (!mocked)
15
27
  return actual.moduleExists(moduleId);
@@ -28,6 +40,7 @@ jest.mock('../utils/fs', () => {
28
40
  }
29
41
  return {
30
42
  importModule,
43
+ importJson,
31
44
  moduleExists,
32
45
  fileExists,
33
46
  dirExists,
@@ -35,6 +48,10 @@ jest.mock('../utils/fs', () => {
35
48
  mocked = true;
36
49
  fileMocks = { ...fileMocks, [filePath]: fileMock };
37
50
  },
51
+ __mockJson(filePath, jsonMock) {
52
+ mocked = true;
53
+ fileMocks = { ...fileMocks, [filePath]: jsonMock };
54
+ },
38
55
  __mockDir(dirPath) {
39
56
  mocked = true;
40
57
  dirMocks = [...dirMocks, dirPath];
@@ -45,16 +62,17 @@ jest.mock('../utils/fs', () => {
45
62
  },
46
63
  };
47
64
  });
65
+ export function mockFile(filePath, fileMock) {
66
+ requireMocked().__mockFile(filePath, fileMock);
67
+ requireMocked().__mockDir(path.dirname(filePath));
68
+ }
48
69
  export function mockCosmosConfig(cosmosConfigPath, cosmosConfig) {
49
- mockFile(cosmosConfigPath, cosmosConfig);
70
+ const absPath = getCwdPath(cosmosConfigPath);
71
+ mockFile(absPath, cosmosConfig);
50
72
  }
51
- export function mockFile(filePath, fileContent) {
73
+ export function mockCwdModuleDefault(filePath, fileMock) {
52
74
  const absPath = getCwdPath(filePath);
53
- requireMocked().__mockFile(absPath, fileContent);
54
- requireMocked().__mockDir(path.dirname(absPath));
55
- }
56
- export function mockDir(dirPath) {
57
- requireMocked().__mockDir(getCwdPath(dirPath));
75
+ mockFile(absPath, { default: fileMock });
58
76
  }
59
77
  export function resetFsMock() {
60
78
  requireMocked().__resetMock();
@@ -1,4 +1,5 @@
1
- export declare function importModule<T>(moduleId: string): Promise<T>;
1
+ export declare function importModule<T>(filePath: string): Promise<T>;
2
+ export declare function importJson<T>(filePath: string): Promise<T>;
2
3
  export declare function moduleExists(moduleId: string): boolean | "";
3
4
  export declare function fileExists(filePath: string): boolean;
4
5
  export declare function dirExists(dirPath: string): boolean;
@@ -1,11 +1,14 @@
1
1
  // NOTE: This API has been extracted to easily mock the file system in tests
2
- import fs from 'fs';
2
+ import fs from 'node:fs';
3
+ import url from 'node:url';
3
4
  import { requireModule } from './requireModule.js';
4
5
  import { resolve } from './resolve.js';
5
- export async function importModule(moduleId) {
6
- return moduleId.endsWith('.json')
7
- ? requireModule(moduleId)
8
- : import(moduleId);
6
+ export async function importModule(filePath) {
7
+ const fileUrl = url.pathToFileURL(filePath).href;
8
+ return import(fileUrl);
9
+ }
10
+ export async function importJson(filePath) {
11
+ return requireModule(filePath);
9
12
  }
10
13
  // Better than fs.exists because it works for module paths without an extension
11
14
  export function moduleExists(moduleId) {
package/dist/server.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from './server/cosmosPlugin/findCosmosPluginConfigs.js';
6
6
  export * from './server/cosmosPlugin/pluginConfigs.js';
7
7
  export * from './server/cosmosPlugin/types.js';
8
8
  export * from './server/getFixtures/getFixtures.js';
9
+ export * from './server/shared/findNextAvailablePort.js';
9
10
  export { RENDERER_FILENAME } from './server/shared/playgroundHtml.js';
10
11
  export * from './server/shared/playgroundUrl.js';
11
12
  export * from './server/shared/staticServer.js';
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@ export * from './server/cosmosPlugin/findCosmosPluginConfigs.js';
6
6
  export * from './server/cosmosPlugin/pluginConfigs.js';
7
7
  export * from './server/cosmosPlugin/types.js';
8
8
  export * from './server/getFixtures/getFixtures.js';
9
+ export * from './server/shared/findNextAvailablePort.js';
9
10
  export { RENDERER_FILENAME } from './server/shared/playgroundHtml.js';
10
11
  export * from './server/shared/playgroundUrl.js';
11
12
  export * from './server/shared/staticServer.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cosmos",
3
- "version": "6.0.0-alpha.9",
3
+ "version": "6.0.0-canary.0d1dbd5.0+0d1dbd5",
4
4
  "description": "Sandbox for developing and testing UI components in isolation",
5
5
  "repository": "https://github.com/react-cosmos/react-cosmos/tree/main/packages/react-cosmos",
6
6
  "license": "MIT",
@@ -17,8 +17,8 @@
17
17
  "open": "^8.4.2",
18
18
  "pem": "^1.14.6",
19
19
  "pkg-up": "^4.0.0",
20
- "react-cosmos-core": "^6.0.0-alpha.9",
21
- "react-cosmos-ui": "^6.0.0-alpha.9",
20
+ "react-cosmos-core": "^6.0.0-canary.0d1dbd5.0+0d1dbd5",
21
+ "react-cosmos-ui": "^6.0.0-canary.0d1dbd5.0+0d1dbd5",
22
22
  "ws": "^8.12.1",
23
23
  "yargs": "^17.7.1"
24
24
  },
@@ -27,5 +27,5 @@
27
27
  "cosmos-export": "./bin/cosmos-export.js",
28
28
  "cosmos-native": "./bin/cosmos-native.js"
29
29
  },
30
- "gitHead": "b155b223f904f5cc29e2f13ac79ab7c4b302ed2b"
30
+ "gitHead": "0d1dbd57a7e2e495a9a60741b3908c353482fcf7"
31
31
  }
@@ -1,2 +0,0 @@
1
- import { DevServerPluginArgs } from '../../cosmosPlugin/types.js';
2
- export declare function httpProxyDevServerPlugin({ platformType, cosmosConfig, expressApp, }: DevServerPluginArgs): void;
@@ -1,18 +0,0 @@
1
- import { createProxyMiddleware } from 'http-proxy-middleware';
2
- export function httpProxyDevServerPlugin({ platformType, cosmosConfig, expressApp, }) {
3
- if (platformType !== 'web')
4
- return;
5
- const httpProxyConfig = getHttpProxyCosmosConfig(cosmosConfig);
6
- Object.keys(httpProxyConfig).forEach(context => {
7
- const config = httpProxyConfig[context];
8
- if (typeof config === 'string') {
9
- expressApp.use(context, createProxyMiddleware(context, { target: config }));
10
- }
11
- else if (typeof config === 'object') {
12
- expressApp.use(context, createProxyMiddleware(context, config));
13
- }
14
- });
15
- }
16
- function getHttpProxyCosmosConfig(cosmosConfig) {
17
- return (cosmosConfig.httpProxy || {});
18
- }
@@ -1,2 +0,0 @@
1
- import { DevServerPluginArgs } from '../../cosmosPlugin/types.js';
2
- export default function openFileDevServerPlugin({ cosmosConfig, expressApp, }: DevServerPluginArgs): void;
@@ -1,53 +0,0 @@
1
- import launchEditor from '@skidding/launch-editor';
2
- import fs from 'fs';
3
- import open from 'open';
4
- import path from 'path';
5
- export default function openFileDevServerPlugin({ cosmosConfig, expressApp, }) {
6
- expressApp.get('/_open', (req, res) => {
7
- const { filePath, line, column } = getReqQuery(req);
8
- if (!filePath) {
9
- res.status(400).send(`File path missing`);
10
- return;
11
- }
12
- const absFilePath = resolveFilePath(cosmosConfig.rootDir, filePath);
13
- if (!fs.existsSync(absFilePath)) {
14
- res.status(404).send(`File not found at path: ${absFilePath}`);
15
- return;
16
- }
17
- new Promise((resolve, reject) => {
18
- const file = `${absFilePath}:${line}:${column}`;
19
- launchEditor(file, (fileName, errorMsg) => reject(errorMsg));
20
- // If launchEditor doesn't report error within 500ms we assume it worked
21
- setTimeout(resolve, 500);
22
- })
23
- // Fall back to open in case launchEditor fails. launchEditor only works
24
- // when the editor app is already open, but is favorable because it can
25
- // open a code file on a specific line & column.
26
- .catch(err => open(absFilePath))
27
- .catch(err => res.status(500).send('Failed to open file'))
28
- .then(() => res.send());
29
- });
30
- }
31
- function getReqQuery(req) {
32
- const { filePath, line, column } = req.query;
33
- if (typeof filePath !== 'string')
34
- throw new Error('filePath missing');
35
- return {
36
- filePath,
37
- line: typeof line === 'string' ? parseInt(line, 10) : 1,
38
- column: typeof column === 'string' ? parseInt(column, 10) : 1,
39
- };
40
- }
41
- function resolveFilePath(rootDir, filePath) {
42
- // This heuristic is needed because the open file endpoint is used for
43
- // multiple applications, which provide different file path types:
44
- // 1. Edit fixture button: Sends path relative to Cosmos rootDir
45
- // 2. react-error-overlay runtime error: Sends absolute path
46
- // 3. react-error-overlay compile error: Sends path relative to CWD
47
- if (path.isAbsolute(filePath)) {
48
- return filePath;
49
- }
50
- const cosmosRelPath = path.join(rootDir, filePath);
51
- const cwdRelPath = path.join(process.cwd(), filePath);
52
- return fs.existsSync(cosmosRelPath) ? cosmosRelPath : cwdRelPath;
53
- }
@@ -1,2 +0,0 @@
1
- import { DevServerPluginArgs } from '../../cosmosPlugin/types.js';
2
- export default function pluginEndpointDevServerPlugin({ cosmosConfig, expressApp, }: DevServerPluginArgs): void;
@@ -1,17 +0,0 @@
1
- import { resolveFromSilent, resolveSilent } from '../../utils/resolveSilent.js';
2
- export default function pluginEndpointDevServerPlugin({ cosmosConfig, expressApp, }) {
3
- expressApp.get('/_plugin/*.js', (req, res) => {
4
- const moduleId = req.params['0'];
5
- if (!moduleId) {
6
- res.sendStatus(404);
7
- return;
8
- }
9
- const resolvedPath = resolveSilent(`/${moduleId}`) ||
10
- resolveFromSilent(cosmosConfig.rootDir, moduleId);
11
- if (!resolvedPath) {
12
- res.sendStatus(404);
13
- return;
14
- }
15
- res.sendFile(resolvedPath);
16
- });
17
- }
@@ -1,2 +0,0 @@
1
- import { DevServerPluginArgs } from '../../cosmosPlugin/types.js';
2
- export declare function userDepsFileDevServerPlugin(args: DevServerPluginArgs): Promise<(() => void) | undefined>;
@@ -1,2 +0,0 @@
1
- import { CosmosPluginConfig } from 'react-cosmos-core';
2
- export declare function requirePluginModule<T>(rootDir: string, pluginConfig: CosmosPluginConfig, configKey: 'ui' | 'devServer' | 'export'): Promise<T>;
@@ -1,10 +0,0 @@
1
- import path from 'path';
2
- import { pathToFileURL } from 'url';
3
- export async function requirePluginModule(rootDir, pluginConfig, configKey) {
4
- const moduleId = pluginConfig[configKey];
5
- if (!moduleId) {
6
- throw new Error(`Module missing in plugin "${pluginConfig.name}": ${configKey}`);
7
- }
8
- const esModule = await import(pathToFileURL(path.resolve(rootDir, moduleId)).href);
9
- return esModule.default;
10
- }