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

CLI: Use ink in CLI #30202

Draft
wants to merge 24 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
11 changes: 10 additions & 1 deletion code/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ module.exports = {
},
},
{
files: ['*.js', '*.jsx', '*.json', '*.html', '**/.storybook/*.ts', '**/.storybook/*.tsx'],
files: [
'*.js',
'*.jsx',
'*.json',
'*.html',
'**/.storybook/*.ts',
'**/.storybook/*.tsx',
'**/.storybook/**/*.ts',
'**/.storybook/**/*.tsx',
],
parserOptions: {
project: null,
},
Expand Down
42 changes: 41 additions & 1 deletion code/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { join } from 'node:path';

import { nodePolyfills } from 'vite-plugin-node-polyfills';
import topLevelAwait from 'vite-plugin-top-level-await';

import type { StorybookConfig } from '../frameworks/react-vite';

const componentsPath = join(__dirname, '../core/src/components');
Expand Down Expand Up @@ -93,6 +96,10 @@ const config: StorybookConfig = {
directory: '../addons/test/template/stories',
titlePrefix: 'addons/test',
},
{
directory: '../lib/create-storybook/src/ink',
titlePrefix: 'CLI/create-storybook',
},
],
addons: [
'@storybook/addon-themes',
Expand Down Expand Up @@ -139,8 +146,35 @@ const config: StorybookConfig = {
const { mergeConfig } = await import('vite');

return mergeConfig(viteConfig, {
plugins: [
nodePolyfills({
globals: {
process: false,
global: false,
Buffer: false,
},
overrides: {
process: require.resolve(join(__dirname, './mocks/node-process.ts')),
module: require.resolve(join(__dirname, './mocks/node-module.ts')),
util: require.resolve(join(__dirname, './mocks/node-util.ts')),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer/'),
assert: require.resolve('assert/'),
os: require.resolve('os-browserify/browser'),
tty: require.resolve('tty-browserify'),
},
}),
topLevelAwait(),
],
resolve: {
alias: {
// ink related
'cli-cursor': require.resolve(join(__dirname, './mocks/cli-cursor.ts')),
'is-in-ci': require.resolve(join(__dirname, './mocks/is-in-ci.ts')),
'yoga-wasm-web/auto': 'https://cdn.jsdelivr.net/npm/[email protected]/dist/browser.js',
'yoga-wasm-web': 'https://cdn.jsdelivr.net/npm/[email protected]/+esm',

// storybook related
...(configType === 'DEVELOPMENT'
? {
'@storybook/components': componentsPath,
Expand All @@ -154,7 +188,13 @@ const config: StorybookConfig = {
},
optimizeDeps: {
force: true,
include: ['@storybook/blocks'],
include: [
'@storybook/blocks',
'vite-plugin-node-polyfills/shims/buffer',
'vite-plugin-node-polyfills/shims/global',
'@xterm/xterm',
'ink',
],
},
build: {
// disable sourcemaps in CI to not run out of memory
Expand Down
10 changes: 10 additions & 0 deletions code/.storybook/mocks/cli-cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* This module takes the stdin, but ink doesn't pass it to the methods; that causes the methods to
* assume node:process-stdin, but that breaks, causing the code to fail. I mocked the 2 methods, so
* they do nothing. There is no cursor in storybook.
*/

export default {
show: () => {},
hide: () => {},
};
6 changes: 6 additions & 0 deletions code/.storybook/mocks/is-in-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* This module is mocked, because it uses something node-internal that's not in our node-polyfills
* In storybook, we don't need to know if we are in a CI environment, because we will never be.
*/

export default false;
7 changes: 7 additions & 0 deletions code/.storybook/mocks/node-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* This module is mocked, because ink, and a few ink related modules use the named exports, which
* the node-polyfills to not supply
*/

export const builtinModules = [];
export const env = {};
12 changes: 12 additions & 0 deletions code/.storybook/mocks/node-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* This module is mocked, because ink, and a few ink related modules use the named exports, which
* the node-polyfills to not supply
*/
import process from 'vite-plugin-node-polyfills/shims/process';

export * from 'vite-plugin-node-polyfills/shims/process';

export default process;

export const cwd = () => '/';
export const env = {};
10 changes: 10 additions & 0 deletions code/.storybook/mocks/node-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* This module is mocked, because ink, and a few ink related modules use the named exports, which
* the node-polyfills to not supply
*/

export * from 'util';

export const isDeepStrictEqual = (a: any, b: any) => {
return a === b;
};
5 changes: 5 additions & 0 deletions code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ import { DocsContext } from '@storybook/blocks';
import { global } from '@storybook/global';
import type { Decorator, Loader, ReactRenderer } from '@storybook/react';

// import { Terminal } from '@xterm/xterm';
// import '@xterm/xterm/css/xterm.css';
// import { render } from 'ink';
import { DocsPageWrapper } from '../lib/blocks/src/components';
import { isChromatic } from './isChromatic';

const { document } = global;
globalThis.CONFIG_TYPE = 'DEVELOPMENT';

// console.log({ Terminal, render });

const ThemeBlock = styled.div<{ side: 'left' | 'right'; layout: string }>(
{
position: 'absolute',
Expand Down
3 changes: 3 additions & 0 deletions code/.storybook/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defaultExclude, defineProject, mergeConfig } from 'vitest/config';

import Inspect from 'vite-plugin-inspect';
import topLevelAwait from 'vite-plugin-top-level-await';

import { vitestCommonConfig } from '../vitest.workspace';

Expand Down Expand Up @@ -31,6 +32,7 @@ export default mergeConfig(
})
),
...extraPlugins,
topLevelAwait(),
],
test: {
name: 'storybook-ui',
Expand All @@ -39,6 +41,7 @@ export default mergeConfig(
'../node_modules/**',
'**/__mockdata__/**',
'../**/__mockdata__/**',
'**/ink/**', // these stories need a lot of node dependencies aliased, which breaks portable stories
'**/Zoom.stories.tsx', // expected to fail in Vitest because of fetching /iframe.html to cause ECONNREFUSED
'**/lib/blocks/src/**', // won't work because of https://github.com/storybookjs/storybook/issues/29783
],
Expand Down
9 changes: 8 additions & 1 deletion code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@
"recast": "^0.23.5",
"semver": "^7.6.2",
"util": "^0.12.5",
"ws": "^8.2.3"
"ws": "^8.2.3",
"yoga-wasm-web": "^0.3.3"
},
"devDependencies": {
"@aw-web-design/x-default-browser": "1.4.126",
Expand All @@ -301,6 +302,7 @@
"@emotion/styled": "^11.11.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@inkjs/ui": "^2.0.0",
"@ndelangen/get-tarball": "^3.0.7",
"@polka/compression": "^1.0.0-next.28",
"@popperjs/core": "^2.6.0",
Expand Down Expand Up @@ -328,6 +330,7 @@
"@types/react-transition-group": "^4",
"@types/semver": "^7.5.8",
"@types/ws": "^8",
"@xterm/xterm": "^5.5.0",
"@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10",
"@yarnpkg/fslib": "2.10.3",
"@yarnpkg/libzip": "2.3.0",
Expand All @@ -354,16 +357,19 @@
"es-toolkit": "^1.22.0",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0",
"esbuild-plugin-alias": "^0.2.1",
"esbuild-plugin-text-replace": "^1.3.0",
"execa": "^8.0.1",
"fd-package-json": "^1.2.0",
"fetch-retry": "^6.0.0",
"figures": "^6.1.0",
"find-cache-dir": "^5.0.0",
"find-up": "^7.0.0",
"flush-promises": "^1.0.2",
"fuse.js": "^3.6.1",
"get-npm-tarball-url": "^2.0.3",
"glob": "^10.0.0",
"globby": "^14.0.1",
"ink": "^5.1.0",
"jiti": "^1.21.6",
"js-yaml": "^4.1.0",
"lazy-universal-dotenv": "^4.0.0",
Expand All @@ -383,6 +389,7 @@
"pretty-hrtime": "^1.0.3",
"prompts": "^2.4.0",
"react": "^18.2.0",
"react-devtools-core": "^6.0.1",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-helmet-async": "^1.3.0",
Expand Down
16 changes: 14 additions & 2 deletions code/core/scripts/prep.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import { existsSync, mkdirSync, watch } from 'node:fs';
import { existsSync, watch } from 'node:fs';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';

import type { Metafile } from 'esbuild';
import type { Plugin as EsbuildPlugin, Metafile } from 'esbuild';
// eslint-disable-next-line import/no-extraneous-dependencies
import replacePlugin from 'esbuild-plugin-text-replace';

import {
dedent,
Expand Down Expand Up @@ -124,6 +126,16 @@ async function run() {
platform: 'neutral',
mainFields: ['main', 'module', 'node'],
conditions: ['node', 'module', 'import', 'require'],
plugins: [
replacePlugin({
include: /node_modules\/ink/,
pattern: [[`process.env['DEV']`, `'false'`]],
}) as any as EsbuildPlugin,
],
define: {
'process.env.NODE_ENV': '"production"',
'process.env.DEV': '"false"',
},
} satisfies EsbuildContextOptions;

const browserAliases = {
Expand Down
11 changes: 10 additions & 1 deletion code/core/src/cli/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ command('dev')
await dev({ ...options, packageJson: pkg }).catch(() => process.exit(1));
});

command('ink')
.option('-n, --name <name>', 'The name (this is a dummy parameter)')
.action(async (options) => {
const { run } = await import('../ink/app');
await run(options);
});

command('build')
.option('-o, --output-dir <dir-name>', 'Directory where to store built files')
.option('-c, --config-dir <dir-name>', 'Directory where to load Storybook configurations from')
Expand All @@ -108,7 +115,9 @@ command('build')
.option('--docs', 'Build a documentation-only site using addon-docs')
.option('--test', 'Build stories optimized for testing purposes.')
.action(async (options) => {
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
const { env } = process;
env.NODE_ENV = env.NODE_ENV || 'production';

logger.setLevel(options.loglevel);
consoleLogger.log(picocolors.bold(`${pkg.name} v${pkg.version}\n`));

Expand Down
3 changes: 2 additions & 1 deletion code/core/src/cli/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ function printError(error: any) {
}

export const dev = async (cliOptions: CLIOptions) => {
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const { env } = process;
env.NODE_ENV = env.NODE_ENV || 'development';

const packageJson = await findPackage(__dirname);
invariant(packageJson, 'Failed to find the closest package.json file.');
Expand Down
61 changes: 61 additions & 0 deletions code/core/src/cli/ink/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';

import { debounce } from 'es-toolkit';
import { Box, Text, render } from 'ink';

declare global {
// eslint-disable-next-line no-var
var CLI_APP_INSTANCE: ReturnType<typeof render> | undefined;
}

if (globalThis.CLI_APP_INSTANCE) {
globalThis.CLI_APP_INSTANCE.unmount();
}

interface Options {
name?: string[];
}

export async function run(options: Options) {
const state = {
name: options.name || 'stranger',
width: process.stdout.columns || 120,
height: process.stdout.rows || 40,
};

process.stdout.write('\x1Bc');
globalThis.CLI_APP_INSTANCE = render(
<Box>
<Text>
{state.name} - {state.width} x {state.height}
</Text>
</Box>
);

const { rerender, waitUntilExit } = globalThis.CLI_APP_INSTANCE;

const update = debounce(
() => {
state.width = process.stdout.columns || 120;
state.height = process.stdout.rows || 40;

process.stdout.write('\x1Bc');
rerender(
<Box>
<Text>
{state.name} - {state.width} x {state.height}
</Text>
</Box>
);
},
8,
{ edges: ['trailing'] }
);

process.stdout.on('resize', () => {
process.stdout.write('\x1Bc');
update();
});

await waitUntilExit();
}
4 changes: 2 additions & 2 deletions code/core/src/core-server/utils/doTelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getPrecedingUpgrade, telemetry } from '@storybook/core/telemetry';
import type { CoreConfig, Options } from '@storybook/core/types';

import type Polka from 'polka';
import type { Polka } from 'polka';
import invariant from 'tiny-invariant';

import { sendTelemetryError } from '../withTelemetry';
Expand All @@ -11,7 +11,7 @@ import { summarizeIndex } from './summarizeIndex';
import { versionStatus } from './versionStatus';

export async function doTelemetry(
app: Polka.Polka,
app: Polka,
core: CoreConfig,
initializedStoryIndexGenerator: Promise<StoryIndexGenerator | undefined>,
options: Options
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/core-server/utils/getStoryIndexGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { normalizeStories } from '@storybook/core/common';
import type { DocsOptions, Options } from '@storybook/core/types';

import type Polka from 'polka';
import type { Polka } from 'polka';

import { StoryIndexGenerator } from './StoryIndexGenerator';
import type { ServerChannel } from './get-server-channel';
import { useStoriesJson } from './stories-json';

export async function getStoryIndexGenerator(
app: Polka.Polka,
app: Polka,
options: Options,
serverChannel: ServerChannel
): Promise<StoryIndexGenerator | undefined> {
Expand Down
Loading
Loading