feat(storybook): migrate storybook builders to devkit (#4758)
This commit is contained in:
parent
57c6bacfb4
commit
651f3b60e9
@ -4,10 +4,14 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["**/*.ts"],
|
"files": ["**/*.ts"],
|
||||||
"excludedFiles": ["./src/migrations/**"],
|
"excludedFiles": [
|
||||||
|
"./src/migrations/**",
|
||||||
|
"./src/generators/migrate-defaults-5-to-6/*.spec.ts",
|
||||||
|
"./src/utils/testing.ts"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-restricted-imports": [
|
"no-restricted-imports": [
|
||||||
"warn",
|
"error",
|
||||||
"@nrwl/workspace",
|
"@nrwl/workspace",
|
||||||
"@angular-devkit/core",
|
"@angular-devkit/core",
|
||||||
"@angular-devkit/schematics",
|
"@angular-devkit/schematics",
|
||||||
|
|||||||
@ -1,14 +1,25 @@
|
|||||||
{
|
{
|
||||||
"$schema": "@angular-devkit/architect/src/builders-schema.json",
|
|
||||||
"builders": {
|
"builders": {
|
||||||
"storybook": {
|
"storybook": {
|
||||||
"implementation": "./src/builders/storybook/storybook.impl",
|
"implementation": "./src/executors/storybook/compat",
|
||||||
"schema": "./src/builders/storybook/schema.json",
|
"schema": "./src/executors/storybook/schema.json",
|
||||||
"description": "Serve Storybook"
|
"description": "Serve Storybook"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"implementation": "./src/builders/build-storybook/build-storybook.impl",
|
"implementation": "./src/executors/build-storybook/compat",
|
||||||
"schema": "./src/builders/build-storybook/schema.json",
|
"schema": "./src/executors/build-storybook/schema.json",
|
||||||
|
"description": "Build Storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"executors": {
|
||||||
|
"storybook": {
|
||||||
|
"implementation": "./src/executors/storybook/storybook.impl",
|
||||||
|
"schema": "./src/executors/storybook/schema.json",
|
||||||
|
"description": "Serve Storybook"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"implementation": "./src/executors/build-storybook/build-storybook.impl",
|
||||||
|
"schema": "./src/executors/build-storybook/schema.json",
|
||||||
"description": "Build Storybook"
|
"description": "Build Storybook"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
import * as storybook from '@storybook/core/dist/server/build-static';
|
|
||||||
import { MockBuilderContext } from '@nrwl/workspace/testing';
|
|
||||||
import { getMockContext } from '../../utils/testing';
|
|
||||||
import { run as storybookBuilder } from './build-storybook.impl';
|
|
||||||
|
|
||||||
jest.mock('@nrwl/workspace/src/core/project-graph');
|
|
||||||
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
|
|
||||||
|
|
||||||
describe('Build storybook', () => {
|
|
||||||
let context: MockBuilderContext;
|
|
||||||
let mockCreateProjectGraph: jest.Mock<
|
|
||||||
ReturnType<typeof createProjectGraph>
|
|
||||||
> = createProjectGraph as any;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
context = await getMockContext();
|
|
||||||
context.target = {
|
|
||||||
project: 'testui',
|
|
||||||
target: 'build',
|
|
||||||
};
|
|
||||||
|
|
||||||
mockCreateProjectGraph.mockReturnValue({
|
|
||||||
nodes: {
|
|
||||||
testui: {
|
|
||||||
name: 'testui',
|
|
||||||
type: 'lib',
|
|
||||||
data: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the storybook static standalone build', async () => {
|
|
||||||
const uiFramework = '@storybook/angular';
|
|
||||||
const outputPath = `${context.workspaceRoot}/dist/storybook`;
|
|
||||||
const storybookSpy = spyOn(
|
|
||||||
storybook,
|
|
||||||
'buildStaticStandalone'
|
|
||||||
).and.returnValue(Promise.resolve(true));
|
|
||||||
|
|
||||||
const result = await storybookBuilder(
|
|
||||||
{
|
|
||||||
uiFramework: uiFramework,
|
|
||||||
outputPath: outputPath,
|
|
||||||
config: {
|
|
||||||
pluginPath: join(
|
|
||||||
__dirname,
|
|
||||||
`/../../generators/configuration/root-files/.storybook/main.js`
|
|
||||||
),
|
|
||||||
configPath: join(
|
|
||||||
__dirname,
|
|
||||||
`/../../generators/configuration/root-files/.storybook/webpack.config.js`
|
|
||||||
),
|
|
||||||
srcRoot: join(
|
|
||||||
__dirname,
|
|
||||||
`/../../generators/configuration/root-files/.storybook/tsconfig.json`
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
context
|
|
||||||
).toPromise();
|
|
||||||
|
|
||||||
expect(storybookSpy).toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
context.logger.includes(`Storybook files available in ${outputPath}`)
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
context.logger.includes(`ui framework: ${uiFramework}`)
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import {
|
|
||||||
BuilderContext,
|
|
||||||
createBuilder,
|
|
||||||
BuilderOutput,
|
|
||||||
} from '@angular-devkit/architect';
|
|
||||||
import { JsonObject } from '@angular-devkit/core';
|
|
||||||
import { Observable, from } from 'rxjs';
|
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
|
||||||
import { join, sep, basename } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import { mkdtempSync, statSync, copyFileSync, constants } from 'fs';
|
|
||||||
|
|
||||||
//import { buildStaticStandalone } from '@storybook/core/dist/server/build-static';
|
|
||||||
import * as build from '@storybook/core/standalone';
|
|
||||||
|
|
||||||
import { getRoot } from '../../utils/root';
|
|
||||||
import { setStorybookAppProject } from '../../utils/utils';
|
|
||||||
|
|
||||||
export interface StorybookConfig extends JsonObject {
|
|
||||||
configFolder?: string;
|
|
||||||
configPath?: string;
|
|
||||||
pluginPath?: string;
|
|
||||||
srcRoot?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StorybookBuilderOptions extends JsonObject {
|
|
||||||
uiFramework: string;
|
|
||||||
projectBuildConfig?: string;
|
|
||||||
config: StorybookConfig;
|
|
||||||
quiet?: boolean;
|
|
||||||
outputPath?: string;
|
|
||||||
docsMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
require('dotenv').config();
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
export default createBuilder<StorybookBuilderOptions>(run);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @whatItDoes This is the starting point of the builder.
|
|
||||||
* @param builderConfig
|
|
||||||
*/
|
|
||||||
export function run(
|
|
||||||
options: StorybookBuilderOptions,
|
|
||||||
context: BuilderContext
|
|
||||||
): Observable<BuilderOutput> {
|
|
||||||
context.reportStatus(`Building storybook ...`);
|
|
||||||
context.logger.info(`ui framework: ${options.uiFramework}`);
|
|
||||||
|
|
||||||
const frameworkPath = `${options.uiFramework}/dist/server/options`;
|
|
||||||
return from(import(frameworkPath)).pipe(
|
|
||||||
map((m) => m.default),
|
|
||||||
switchMap((frameworkOptions) =>
|
|
||||||
from(storybookOptionMapper(options, frameworkOptions, context))
|
|
||||||
),
|
|
||||||
switchMap((option) => {
|
|
||||||
context.logger.info(`Storybook builder starting ...`);
|
|
||||||
return runInstance(option);
|
|
||||||
}),
|
|
||||||
map((loaded) => {
|
|
||||||
context.logger.info(`Storybook builder finished ...`);
|
|
||||||
context.logger.info(`Storybook files available in ${options.outputPath}`);
|
|
||||||
const builder: BuilderOutput = { success: true } as BuilderOutput;
|
|
||||||
return builder;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runInstance(options: StorybookBuilderOptions) {
|
|
||||||
return from(build({ ...options, ci: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function storybookOptionMapper(
|
|
||||||
builderOptions: StorybookBuilderOptions,
|
|
||||||
frameworkOptions: any,
|
|
||||||
context: BuilderContext
|
|
||||||
) {
|
|
||||||
setStorybookAppProject(context, builderOptions.projectBuildConfig);
|
|
||||||
|
|
||||||
const storybookConfig = await findOrCreateConfig(
|
|
||||||
builderOptions.config,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
const optionsWithFramework = {
|
|
||||||
...builderOptions,
|
|
||||||
mode: 'static',
|
|
||||||
outputDir: builderOptions.outputPath,
|
|
||||||
configDir: storybookConfig,
|
|
||||||
...frameworkOptions,
|
|
||||||
frameworkPresets: [...(frameworkOptions.frameworkPresets || [])],
|
|
||||||
watch: false,
|
|
||||||
};
|
|
||||||
optionsWithFramework.config;
|
|
||||||
return optionsWithFramework;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findOrCreateConfig(
|
|
||||||
config: StorybookConfig,
|
|
||||||
context: BuilderContext
|
|
||||||
): Promise<string> {
|
|
||||||
if (config.configFolder && statSync(config.configFolder).isDirectory()) {
|
|
||||||
return config.configFolder;
|
|
||||||
} else if (
|
|
||||||
statSync(config.configPath).isFile() &&
|
|
||||||
statSync(config.pluginPath).isFile() &&
|
|
||||||
statSync(config.srcRoot).isFile()
|
|
||||||
) {
|
|
||||||
return createStorybookConfig(
|
|
||||||
config.configPath,
|
|
||||||
config.pluginPath,
|
|
||||||
config.srcRoot
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const sourceRoot = await getRoot(context);
|
|
||||||
if (
|
|
||||||
statSync(
|
|
||||||
join(context.workspaceRoot, sourceRoot, '.storybook')
|
|
||||||
).isDirectory()
|
|
||||||
) {
|
|
||||||
return join(context.workspaceRoot, sourceRoot, '.storybook');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('No configuration settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStorybookConfig(
|
|
||||||
configPath: string,
|
|
||||||
pluginPath: string,
|
|
||||||
srcRoot: string
|
|
||||||
): string {
|
|
||||||
const tmpDir = tmpdir();
|
|
||||||
const tmpFolder = mkdtempSync(`${tmpDir}${sep}`);
|
|
||||||
copyFileSync(
|
|
||||||
configPath,
|
|
||||||
`${tmpFolder}${basename(configPath)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
copyFileSync(
|
|
||||||
pluginPath,
|
|
||||||
`${tmpFolder}${basename(pluginPath)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
copyFileSync(
|
|
||||||
srcRoot,
|
|
||||||
`${tmpFolder}${basename(srcRoot)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
return tmpFolder;
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
describe('storybook builer', () => {
|
|
||||||
it('should have a test', () => {
|
|
||||||
expect(true).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
import {
|
|
||||||
BuilderContext,
|
|
||||||
createBuilder,
|
|
||||||
BuilderOutput,
|
|
||||||
} from '@angular-devkit/architect';
|
|
||||||
import { Observable, from } from 'rxjs';
|
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
|
||||||
import { JsonObject } from '@angular-devkit/core';
|
|
||||||
import { join, sep, basename } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import { mkdtempSync, statSync, copyFileSync, constants } from 'fs';
|
|
||||||
|
|
||||||
import { buildDevStandalone } from '@storybook/core/dist/server/build-dev';
|
|
||||||
|
|
||||||
import { getRoot } from '../../utils/root';
|
|
||||||
import { setStorybookAppProject } from '../../utils/utils';
|
|
||||||
|
|
||||||
export interface StorybookConfig extends JsonObject {
|
|
||||||
configFolder?: string;
|
|
||||||
configPath?: string;
|
|
||||||
pluginPath?: string;
|
|
||||||
srcRoot?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StorybookBuilderOptions extends JsonObject {
|
|
||||||
uiFramework: string;
|
|
||||||
projectBuildConfig?: string;
|
|
||||||
config: StorybookConfig;
|
|
||||||
host?: string;
|
|
||||||
port?: number;
|
|
||||||
quiet?: boolean;
|
|
||||||
ssl?: boolean;
|
|
||||||
sslCert?: string;
|
|
||||||
sslKey?: string;
|
|
||||||
staticDir?: string[];
|
|
||||||
watch?: boolean;
|
|
||||||
docsMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
require('dotenv').config();
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
export default createBuilder<StorybookBuilderOptions>(run);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @whatItDoes This is the starting point of the builder.
|
|
||||||
* @param builderConfig
|
|
||||||
*/
|
|
||||||
function run(
|
|
||||||
options: StorybookBuilderOptions,
|
|
||||||
context: BuilderContext
|
|
||||||
): Observable<BuilderOutput> {
|
|
||||||
const frameworkPath = `${options.uiFramework}/dist/server/options`;
|
|
||||||
return from(import(frameworkPath)).pipe(
|
|
||||||
map((m) => m.default),
|
|
||||||
switchMap((frameworkOptions) =>
|
|
||||||
from(storybookOptionMapper(options, frameworkOptions, context))
|
|
||||||
),
|
|
||||||
switchMap((option) => runInstance(option)),
|
|
||||||
map((loaded) => {
|
|
||||||
const builder: BuilderOutput = { success: true } as BuilderOutput;
|
|
||||||
return builder;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function runInstance(options: StorybookBuilderOptions) {
|
|
||||||
return new Observable<any>((obs) => {
|
|
||||||
buildDevStandalone({ ...options, ci: true })
|
|
||||||
.then((sucess) => obs.next(sucess))
|
|
||||||
.catch((err) => obs.error(err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function storybookOptionMapper(
|
|
||||||
builderOptions: StorybookBuilderOptions,
|
|
||||||
frameworkOptions: any,
|
|
||||||
context: BuilderContext
|
|
||||||
) {
|
|
||||||
setStorybookAppProject(context, builderOptions.projectBuildConfig);
|
|
||||||
|
|
||||||
const storybookConfig = await findOrCreateConfig(
|
|
||||||
builderOptions.config,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
const optionsWithFramework = {
|
|
||||||
...builderOptions,
|
|
||||||
mode: 'dev',
|
|
||||||
configDir: storybookConfig,
|
|
||||||
...frameworkOptions,
|
|
||||||
frameworkPresets: [...(frameworkOptions.frameworkPresets || [])],
|
|
||||||
};
|
|
||||||
optionsWithFramework.config;
|
|
||||||
return optionsWithFramework;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findOrCreateConfig(
|
|
||||||
config: StorybookConfig,
|
|
||||||
context: BuilderContext
|
|
||||||
): Promise<string> {
|
|
||||||
const sourceRoot = await getRoot(context);
|
|
||||||
|
|
||||||
if (config.configFolder && statSync(config.configFolder).isDirectory()) {
|
|
||||||
return config.configFolder;
|
|
||||||
} else if (
|
|
||||||
statSync(config.configPath).isFile() &&
|
|
||||||
statSync(config.pluginPath).isFile() &&
|
|
||||||
statSync(config.srcRoot).isFile()
|
|
||||||
) {
|
|
||||||
return createStorybookConfig(
|
|
||||||
config.configPath,
|
|
||||||
config.pluginPath,
|
|
||||||
config.srcRoot
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
statSync(
|
|
||||||
join(context.workspaceRoot, sourceRoot, '.storybook')
|
|
||||||
).isDirectory()
|
|
||||||
) {
|
|
||||||
return join(context.workspaceRoot, sourceRoot, '.storybook');
|
|
||||||
}
|
|
||||||
throw new Error('No configuration settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
function createStorybookConfig(
|
|
||||||
configPath: string,
|
|
||||||
pluginPath: string,
|
|
||||||
srcRoot: string
|
|
||||||
): string {
|
|
||||||
const tmpDir = tmpdir();
|
|
||||||
const tmpFolder = `${tmpDir}${sep}`;
|
|
||||||
mkdtempSync(tmpFolder);
|
|
||||||
copyFileSync(
|
|
||||||
configPath,
|
|
||||||
`${tmpFolder}/${basename(configPath)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
copyFileSync(
|
|
||||||
pluginPath,
|
|
||||||
`${tmpFolder}/${basename(pluginPath)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
copyFileSync(
|
|
||||||
srcRoot,
|
|
||||||
`${tmpFolder}/${basename(srcRoot)}`,
|
|
||||||
constants.COPYFILE_EXCL
|
|
||||||
);
|
|
||||||
return tmpFolder;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { ExecutorContext, logger } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
import { join } from 'path';
|
||||||
|
jest.mock('@storybook/core/standalone', () =>
|
||||||
|
jest.fn().mockImplementation(() => Promise.resolve())
|
||||||
|
);
|
||||||
|
import * as storybook from '@storybook/core/standalone';
|
||||||
|
import storybookBuilder from './build-storybook.impl';
|
||||||
|
|
||||||
|
import angularStorybookOptions from '@storybook/angular/dist/server/options';
|
||||||
|
|
||||||
|
describe('Build storybook', () => {
|
||||||
|
let context: ExecutorContext;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
context = {
|
||||||
|
root: '/root',
|
||||||
|
cwd: '/root',
|
||||||
|
projectName: 'proj',
|
||||||
|
targetName: 'storybook',
|
||||||
|
workspace: {
|
||||||
|
version: 2,
|
||||||
|
projects: {
|
||||||
|
proj: {
|
||||||
|
root: '',
|
||||||
|
sourceRoot: 'src',
|
||||||
|
targets: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isVerbose: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the storybook static standalone build', async () => {
|
||||||
|
spyOn(logger, 'info');
|
||||||
|
const uiFramework = '@storybook/angular';
|
||||||
|
const outputPath = `${context.root}/dist/storybook`;
|
||||||
|
const config = {
|
||||||
|
pluginPath: join(
|
||||||
|
__dirname,
|
||||||
|
`/../../generators/configuration/root-files/.storybook/main.js`
|
||||||
|
),
|
||||||
|
configPath: join(
|
||||||
|
__dirname,
|
||||||
|
`/../../generators/configuration/root-files/.storybook/webpack.config.js`
|
||||||
|
),
|
||||||
|
srcRoot: join(
|
||||||
|
__dirname,
|
||||||
|
`/../../generators/configuration/root-files/.storybook/tsconfig.json`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await storybookBuilder(
|
||||||
|
{
|
||||||
|
uiFramework: uiFramework,
|
||||||
|
outputPath: outputPath,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(storybook).toHaveBeenCalled();
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
`NX Storybook files available in ${outputPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(`NX ui framework: ${uiFramework}`);
|
||||||
|
expect(result.success).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { basename, join, sep } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { constants, copyFileSync, mkdtempSync, statSync } from 'fs';
|
||||||
|
|
||||||
|
import * as build from '@storybook/core/standalone';
|
||||||
|
|
||||||
|
import { setStorybookAppProject } from '../utils';
|
||||||
|
import { ExecutorContext, logger } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
export interface StorybookConfig {
|
||||||
|
configFolder?: string;
|
||||||
|
configPath?: string;
|
||||||
|
pluginPath?: string;
|
||||||
|
srcRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorybookBuilderOptions {
|
||||||
|
uiFramework: string;
|
||||||
|
projectBuildConfig?: string;
|
||||||
|
config: StorybookConfig;
|
||||||
|
quiet?: boolean;
|
||||||
|
outputPath?: string;
|
||||||
|
docsMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require('dotenv').config();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
export default async function buildStorybookExecutor(
|
||||||
|
options: StorybookBuilderOptions,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
logger.info(`NX ui framework: ${options.uiFramework}`);
|
||||||
|
|
||||||
|
const frameworkPath = `${options.uiFramework}/dist/server/options`;
|
||||||
|
const { default: frameworkOptions } = await import(frameworkPath);
|
||||||
|
const option = storybookOptionMapper(options, frameworkOptions, context);
|
||||||
|
logger.info(`NX Storybook builder starting ...`);
|
||||||
|
await runInstance(option);
|
||||||
|
logger.info(`NX Storybook builder finished ...`);
|
||||||
|
logger.info(`NX Storybook files available in ${options.outputPath}`);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInstance(options: StorybookBuilderOptions): Promise<void> {
|
||||||
|
return build({ ...options, ci: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function storybookOptionMapper(
|
||||||
|
builderOptions: StorybookBuilderOptions,
|
||||||
|
frameworkOptions: any,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
setStorybookAppProject(context, builderOptions.projectBuildConfig);
|
||||||
|
|
||||||
|
const storybookConfig = findOrCreateConfig(builderOptions.config, context);
|
||||||
|
const optionsWithFramework = {
|
||||||
|
...builderOptions,
|
||||||
|
mode: 'static',
|
||||||
|
outputDir: builderOptions.outputPath,
|
||||||
|
configDir: storybookConfig,
|
||||||
|
...frameworkOptions,
|
||||||
|
frameworkPresets: [...(frameworkOptions.frameworkPresets || [])],
|
||||||
|
watch: false,
|
||||||
|
};
|
||||||
|
optionsWithFramework.config;
|
||||||
|
return optionsWithFramework;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOrCreateConfig(
|
||||||
|
config: StorybookConfig,
|
||||||
|
context: ExecutorContext
|
||||||
|
): string {
|
||||||
|
if (config.configFolder && statSync(config.configFolder).isDirectory()) {
|
||||||
|
return config.configFolder;
|
||||||
|
} else if (
|
||||||
|
statSync(config.configPath).isFile() &&
|
||||||
|
statSync(config.pluginPath).isFile() &&
|
||||||
|
statSync(config.srcRoot).isFile()
|
||||||
|
) {
|
||||||
|
return createStorybookConfig(
|
||||||
|
config.configPath,
|
||||||
|
config.pluginPath,
|
||||||
|
config.srcRoot
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const sourceRoot = context.workspace.projects[context.projectName].root;
|
||||||
|
if (statSync(join(context.root, sourceRoot, '.storybook')).isDirectory()) {
|
||||||
|
return join(context.root, sourceRoot, '.storybook');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('No configuration settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorybookConfig(
|
||||||
|
configPath: string,
|
||||||
|
pluginPath: string,
|
||||||
|
srcRoot: string
|
||||||
|
): string {
|
||||||
|
const tmpDir = tmpdir();
|
||||||
|
const tmpFolder = mkdtempSync(`${tmpDir}${sep}`);
|
||||||
|
copyFileSync(
|
||||||
|
configPath,
|
||||||
|
`${tmpFolder}${basename(configPath)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
copyFileSync(
|
||||||
|
pluginPath,
|
||||||
|
`${tmpFolder}${basename(pluginPath)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
copyFileSync(
|
||||||
|
srcRoot,
|
||||||
|
`${tmpFolder}${basename(srcRoot)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
return tmpFolder;
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { convertNxExecutor } from '@nrwl/devkit';
|
||||||
|
import buildStorybookExecutor from './build-storybook.impl';
|
||||||
|
|
||||||
|
export default convertNxExecutor(buildStorybookExecutor);
|
||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Storybook Builder",
|
"title": "Storybook Builder",
|
||||||
|
"cli": "nx",
|
||||||
"description": "Build storybook in production mode",
|
"description": "Build storybook in production mode",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
4
packages/storybook/src/executors/storybook/compat.ts
Normal file
4
packages/storybook/src/executors/storybook/compat.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { convertNxExecutor } from '@nrwl/devkit';
|
||||||
|
import storybookExecutor from './storybook.impl';
|
||||||
|
|
||||||
|
export default convertNxExecutor(storybookExecutor);
|
||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Storybook Dev Builder",
|
"title": "Storybook Dev Builder",
|
||||||
|
"cli": "nx",
|
||||||
"description": "Serve up storybook in development mode",
|
"description": "Serve up storybook in development mode",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { ExecutorContext } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
jest.mock('@storybook/core/server', () => ({
|
||||||
|
buildDevStandalone: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
}));
|
||||||
|
import { buildDevStandalone } from '@storybook/core/server';
|
||||||
|
|
||||||
|
import { vol } from 'memfs';
|
||||||
|
jest.mock('fs', () => require('memfs').fs);
|
||||||
|
|
||||||
|
import storybookExecutor, { StorybookExecutorOptions } from './storybook.impl';
|
||||||
|
|
||||||
|
describe('@nrwl/storybook:storybook', () => {
|
||||||
|
let context: ExecutorContext;
|
||||||
|
let options: StorybookExecutorOptions;
|
||||||
|
beforeEach(() => {
|
||||||
|
options = {
|
||||||
|
uiFramework: '@storybook/angular',
|
||||||
|
port: 4400,
|
||||||
|
config: {
|
||||||
|
configFolder: `/root/.storybook`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vol.fromJSON({});
|
||||||
|
vol.mkdirSync('/root/.storybook', {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
context = {
|
||||||
|
root: '/root',
|
||||||
|
cwd: '/root',
|
||||||
|
projectName: 'proj',
|
||||||
|
targetName: 'storybook',
|
||||||
|
workspace: {
|
||||||
|
version: 2,
|
||||||
|
projects: {
|
||||||
|
proj: {
|
||||||
|
root: '',
|
||||||
|
sourceRoot: 'src',
|
||||||
|
targets: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isVerbose: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide options to storybook', (done) => {
|
||||||
|
storybookExecutor(options, context);
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(buildDevStandalone).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
123
packages/storybook/src/executors/storybook/storybook.impl.ts
Normal file
123
packages/storybook/src/executors/storybook/storybook.impl.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { basename, join, sep } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { constants, copyFileSync, mkdtempSync, statSync } from 'fs';
|
||||||
|
|
||||||
|
import { buildDevStandalone } from '@storybook/core/server';
|
||||||
|
|
||||||
|
import { setStorybookAppProject } from '../utils';
|
||||||
|
import { ExecutorContext } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
export interface StorybookConfig {
|
||||||
|
configFolder?: string;
|
||||||
|
configPath?: string;
|
||||||
|
pluginPath?: string;
|
||||||
|
srcRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorybookExecutorOptions {
|
||||||
|
uiFramework: string;
|
||||||
|
projectBuildConfig?: string;
|
||||||
|
config: StorybookConfig;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
quiet?: boolean;
|
||||||
|
ssl?: boolean;
|
||||||
|
sslCert?: string;
|
||||||
|
sslKey?: string;
|
||||||
|
staticDir?: string[];
|
||||||
|
watch?: boolean;
|
||||||
|
docsMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require('dotenv').config();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
export default async function storybookExecutor(
|
||||||
|
options: StorybookExecutorOptions,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
const frameworkPath = `${options.uiFramework}/dist/server/options`;
|
||||||
|
|
||||||
|
const frameworkOptions = (await import(frameworkPath)).default;
|
||||||
|
const option = storybookOptionMapper(options, frameworkOptions, context);
|
||||||
|
await runInstance(option);
|
||||||
|
|
||||||
|
// This Promise intentionally never resolves, leaving the process running
|
||||||
|
return new Promise<{ success: boolean }>(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInstance(options: StorybookExecutorOptions) {
|
||||||
|
return buildDevStandalone({ ...options, ci: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function storybookOptionMapper(
|
||||||
|
builderOptions: StorybookExecutorOptions,
|
||||||
|
frameworkOptions: any,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
setStorybookAppProject(context, builderOptions.projectBuildConfig);
|
||||||
|
|
||||||
|
const storybookConfig = findOrCreateConfig(builderOptions.config, context);
|
||||||
|
const optionsWithFramework = {
|
||||||
|
...builderOptions,
|
||||||
|
mode: 'dev',
|
||||||
|
configDir: storybookConfig,
|
||||||
|
...frameworkOptions,
|
||||||
|
frameworkPresets: [...(frameworkOptions.frameworkPresets || [])],
|
||||||
|
};
|
||||||
|
optionsWithFramework.config;
|
||||||
|
return optionsWithFramework;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOrCreateConfig(
|
||||||
|
config: StorybookConfig,
|
||||||
|
context: ExecutorContext
|
||||||
|
): string {
|
||||||
|
const sourceRoot = context.workspace.projects[context.projectName].root;
|
||||||
|
|
||||||
|
if (config.configFolder && statSync(config.configFolder).isDirectory()) {
|
||||||
|
return config.configFolder;
|
||||||
|
} else if (
|
||||||
|
statSync(config.configPath).isFile() &&
|
||||||
|
statSync(config.pluginPath).isFile() &&
|
||||||
|
statSync(config.srcRoot).isFile()
|
||||||
|
) {
|
||||||
|
return createStorybookConfig(
|
||||||
|
config.configPath,
|
||||||
|
config.pluginPath,
|
||||||
|
config.srcRoot
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
statSync(join(context.root, sourceRoot, '.storybook')).isDirectory()
|
||||||
|
) {
|
||||||
|
return join(context.root, sourceRoot, '.storybook');
|
||||||
|
}
|
||||||
|
throw new Error('No configuration settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorybookConfig(
|
||||||
|
configPath: string,
|
||||||
|
pluginPath: string,
|
||||||
|
srcRoot: string
|
||||||
|
): string {
|
||||||
|
const tmpDir = tmpdir();
|
||||||
|
const tmpFolder = `${tmpDir}${sep}`;
|
||||||
|
mkdtempSync(tmpFolder);
|
||||||
|
copyFileSync(
|
||||||
|
configPath,
|
||||||
|
`${tmpFolder}/${basename(configPath)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
copyFileSync(
|
||||||
|
pluginPath,
|
||||||
|
`${tmpFolder}/${basename(pluginPath)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
copyFileSync(
|
||||||
|
srcRoot,
|
||||||
|
`${tmpFolder}/${basename(srcRoot)}`,
|
||||||
|
constants.COPYFILE_EXCL
|
||||||
|
);
|
||||||
|
return tmpFolder;
|
||||||
|
}
|
||||||
35
packages/storybook/src/executors/utils.ts
Normal file
35
packages/storybook/src/executors/utils.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ExecutorContext } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
export interface NodePackage {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see: https://github.com/storybookjs/storybook/pull/12565
|
||||||
|
// TODO: this should really be passed as a param to the CLI rather than env
|
||||||
|
export function setStorybookAppProject(
|
||||||
|
context: ExecutorContext,
|
||||||
|
leadStorybookProject: string
|
||||||
|
) {
|
||||||
|
let leadingProject: string;
|
||||||
|
// for libs we check whether the build config should be fetched
|
||||||
|
// from some app
|
||||||
|
|
||||||
|
if (
|
||||||
|
context.workspace.projects[context.projectName].projectType === 'library'
|
||||||
|
) {
|
||||||
|
// we have a lib so let's try to see whether the app has
|
||||||
|
// been set from which we want to get the build config
|
||||||
|
if (leadStorybookProject) {
|
||||||
|
leadingProject = leadStorybookProject;
|
||||||
|
} else {
|
||||||
|
// do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ..for apps we just use the app target itself
|
||||||
|
leadingProject = context.projectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.STORYBOOK_ANGULAR_PROJECT = leadingProject;
|
||||||
|
}
|
||||||
@ -10,9 +10,10 @@ import {
|
|||||||
formatFiles,
|
formatFiles,
|
||||||
updateWorkspaceInTree,
|
updateWorkspaceInTree,
|
||||||
serializeJson,
|
serializeJson,
|
||||||
|
readJsonInTree,
|
||||||
} from '@nrwl/workspace';
|
} from '@nrwl/workspace';
|
||||||
|
|
||||||
import { getTsConfigContent } from '../../utils/utils';
|
import { TsConfig } from '../../utils/utilities';
|
||||||
|
|
||||||
interface ProjectDefinition {
|
interface ProjectDefinition {
|
||||||
root: string;
|
root: string;
|
||||||
@ -73,7 +74,7 @@ function updateStorybookTsConfigPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tsConfig = {
|
const tsConfig = {
|
||||||
storybook: getTsConfigContent(tree, paths.tsConfigStorybook),
|
storybook: readJsonInTree<TsConfig>(tree, paths.tsConfigStorybook),
|
||||||
};
|
};
|
||||||
|
|
||||||
// update extends prop to point to the lib relative tsconfig rather
|
// update extends prop to point to the lib relative tsconfig rather
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
formatFiles,
|
formatFiles,
|
||||||
|
readJsonInTree,
|
||||||
updateWorkspaceInTree,
|
updateWorkspaceInTree,
|
||||||
serializeJson,
|
serializeJson,
|
||||||
} from '@nrwl/workspace';
|
} from '@nrwl/workspace';
|
||||||
|
|
||||||
import { getTsConfigContent, isFramework } from '../../utils/utils';
|
import { isFramework, TsConfig } from '../../utils/utilities';
|
||||||
import { normalize } from '@angular-devkit/core';
|
import { normalize } from '@angular-devkit/core';
|
||||||
|
|
||||||
interface ProjectDefinition {
|
interface ProjectDefinition {
|
||||||
@ -77,14 +78,14 @@ function updateLintTarget(
|
|||||||
>[1]['uiFramework'],
|
>[1]['uiFramework'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainTsConfigContent = getTsConfigContent(tree, paths.tsConfig);
|
const mainTsConfigContent = readJsonInTree<TsConfig>(tree, paths.tsConfig);
|
||||||
|
|
||||||
const tsConfig = {
|
const tsConfig = {
|
||||||
main: mainTsConfigContent,
|
main: mainTsConfigContent,
|
||||||
lib: tree.exists(paths.tsConfigLib)
|
lib: tree.exists(paths.tsConfigLib)
|
||||||
? getTsConfigContent(tree, paths.tsConfigLib)
|
? readJsonInTree<TsConfig>(tree, paths.tsConfigLib)
|
||||||
: mainTsConfigContent,
|
: mainTsConfigContent,
|
||||||
storybook: getTsConfigContent(tree, paths.tsConfigStorybook),
|
storybook: readJsonInTree<TsConfig>(tree, paths.tsConfigStorybook),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isReactProject && Array.isArray(tsConfig.lib.exclude)) {
|
if (isReactProject && Array.isArray(tsConfig.lib.exclude)) {
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
import { BuilderContext } from '@angular-devkit/architect';
|
|
||||||
import { normalize, workspaces } from '@angular-devkit/core';
|
|
||||||
import { NxScopedHost } from '@nrwl/devkit/ngcli-adapter';
|
|
||||||
|
|
||||||
export async function getRoot(context: BuilderContext) {
|
|
||||||
const workspaceHost = workspaces.createWorkspaceHost(
|
|
||||||
new NxScopedHost(normalize(context.workspaceRoot))
|
|
||||||
);
|
|
||||||
const { workspace } = await workspaces.readWorkspace('', workspaceHost);
|
|
||||||
if (workspace.projects.get(context.target.project).root) {
|
|
||||||
return workspace.projects.get(context.target.project).root;
|
|
||||||
} else {
|
|
||||||
context.reportStatus('Error');
|
|
||||||
const message = `${context.target.project} does not have a root. Please define one.`;
|
|
||||||
context.logger.error(message);
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +1,8 @@
|
|||||||
import { join, sep } from 'path';
|
import { join } from 'path';
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import { mkdtempSync } from 'fs';
|
|
||||||
|
|
||||||
import { schema } from '@angular-devkit/core';
|
|
||||||
import { externalSchematic, Rule, Tree } from '@angular-devkit/schematics';
|
import { externalSchematic, Rule, Tree } from '@angular-devkit/schematics';
|
||||||
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
|
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
|
||||||
import { Architect } from '@angular-devkit/architect';
|
|
||||||
import { TestingArchitectHost } from '@angular-devkit/architect/testing';
|
|
||||||
|
|
||||||
import {
|
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
|
||||||
createEmptyWorkspace,
|
|
||||||
MockBuilderContext,
|
|
||||||
} from '@nrwl/workspace/testing';
|
|
||||||
|
|
||||||
const testRunner = new SchematicTestRunner(
|
const testRunner = new SchematicTestRunner(
|
||||||
'@nrwl/storybook',
|
'@nrwl/storybook',
|
||||||
@ -43,14 +34,6 @@ const migrationRunner = new SchematicTestRunner(
|
|||||||
join(__dirname, '../../migrations.json')
|
join(__dirname, '../../migrations.json')
|
||||||
);
|
);
|
||||||
|
|
||||||
export function runSchematic<SchemaOptions = any>(
|
|
||||||
schematicName: string,
|
|
||||||
options: SchemaOptions,
|
|
||||||
tree: Tree
|
|
||||||
) {
|
|
||||||
return testRunner.runSchematicAsync(schematicName, options, tree).toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function callRule(rule: Rule, tree: Tree) {
|
export function callRule(rule: Rule, tree: Tree) {
|
||||||
return testRunner.callRule(rule, tree).toPromise();
|
return testRunner.callRule(rule, tree).toPromise();
|
||||||
}
|
}
|
||||||
@ -135,30 +118,3 @@ export class TestButtonComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTempDir() {
|
|
||||||
const tmpDir = tmpdir();
|
|
||||||
const tmpFolder = `${tmpDir}${sep}`;
|
|
||||||
return mkdtempSync(tmpFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTestArchitect() {
|
|
||||||
const tmpDir = getTempDir();
|
|
||||||
const architectHost = new TestingArchitectHost(tmpDir, tmpDir);
|
|
||||||
const registry = new schema.CoreSchemaRegistry();
|
|
||||||
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
|
|
||||||
|
|
||||||
const architect = new Architect(architectHost, registry);
|
|
||||||
|
|
||||||
await architectHost.addBuilderFromPackage(join(__dirname, '../..'));
|
|
||||||
|
|
||||||
return [architect, architectHost] as [Architect, TestingArchitectHost];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMockContext() {
|
|
||||||
const [architect, architectHost] = await getTestArchitect();
|
|
||||||
|
|
||||||
const context = new MockBuilderContext(architect, architectHost);
|
|
||||||
await context.addBuilderFromPackage(join(__dirname, '../..'));
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
import { Tree } from '@nrwl/devkit';
|
import { Tree } from '@nrwl/devkit';
|
||||||
|
|
||||||
import { get } from 'http';
|
|
||||||
import { CompilerOptions } from 'typescript';
|
import { CompilerOptions } from 'typescript';
|
||||||
|
|
||||||
export interface NodePackage {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
addonDependencies: ['@storybook/addons'],
|
addonDependencies: ['@storybook/addons'],
|
||||||
tsConfigExclusions: ['stories', '**/*.stories.ts'],
|
tsConfigExclusions: ['stories', '**/*.stories.ts'],
|
||||||
@ -54,41 +48,6 @@ export function safeFileDelete(tree: Tree, path: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to retrieve the latest package version from NPM
|
|
||||||
* Return an optional "latest" version in case of error
|
|
||||||
* @param packageName
|
|
||||||
*/
|
|
||||||
export function getLatestNodeVersion(
|
|
||||||
packageName: string
|
|
||||||
): Promise<NodePackage> {
|
|
||||||
const DEFAULT_VERSION = 'latest';
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
return get(`http://registry.npmjs.org/${packageName}`, (res) => {
|
|
||||||
let rawData = '';
|
|
||||||
res.on('data', (chunk) => (rawData += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(rawData);
|
|
||||||
const version = (response && response['dist-tags']) || {};
|
|
||||||
|
|
||||||
resolve(buildPackage(packageName, version.latest));
|
|
||||||
} catch (e) {
|
|
||||||
resolve(buildPackage(packageName));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on('error', () => resolve(buildPackage(packageName)));
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildPackage(
|
|
||||||
name: string,
|
|
||||||
version: string = DEFAULT_VERSION
|
|
||||||
): NodePackage {
|
|
||||||
return { name, version };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TsConfig = {
|
export type TsConfig = {
|
||||||
extends: string;
|
extends: string;
|
||||||
compilerOptions: CompilerOptions;
|
compilerOptions: CompilerOptions;
|
||||||
|
|||||||
@ -1,241 +0,0 @@
|
|||||||
import { BuilderContext } from '@angular-devkit/architect';
|
|
||||||
import {
|
|
||||||
JsonParseMode,
|
|
||||||
join,
|
|
||||||
Path,
|
|
||||||
JsonAstObject,
|
|
||||||
parseJsonAst,
|
|
||||||
JsonValue,
|
|
||||||
} from '@angular-devkit/core';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SchematicsException,
|
|
||||||
Tree,
|
|
||||||
SchematicContext,
|
|
||||||
Source,
|
|
||||||
Rule,
|
|
||||||
mergeWith,
|
|
||||||
apply,
|
|
||||||
forEach,
|
|
||||||
} from '@angular-devkit/schematics';
|
|
||||||
import {
|
|
||||||
createProjectGraph,
|
|
||||||
ProjectType,
|
|
||||||
} from '@nrwl/workspace/src/core/project-graph';
|
|
||||||
|
|
||||||
import { get } from 'http';
|
|
||||||
import {
|
|
||||||
SourceFile,
|
|
||||||
createSourceFile,
|
|
||||||
ScriptTarget,
|
|
||||||
CompilerOptions,
|
|
||||||
} from 'typescript';
|
|
||||||
|
|
||||||
export interface NodePackage {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// see: https://github.com/storybookjs/storybook/pull/12565
|
|
||||||
// TODO: this should really be passed as a param to the CLI rather than env
|
|
||||||
export function setStorybookAppProject(
|
|
||||||
context: BuilderContext,
|
|
||||||
leadStorybookProject: string
|
|
||||||
) {
|
|
||||||
const projGraph = createProjectGraph();
|
|
||||||
|
|
||||||
let leadingProject: string;
|
|
||||||
// for libs we check whether the build config should be fetched
|
|
||||||
// from some app
|
|
||||||
if (projGraph.nodes[context.target.project].type === ProjectType.lib) {
|
|
||||||
// we have a lib so let's try to see whether the app has
|
|
||||||
// been set from which we want to get the build config
|
|
||||||
if (leadStorybookProject) {
|
|
||||||
leadingProject = leadStorybookProject;
|
|
||||||
} else {
|
|
||||||
// do nothing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// ..for apps we just use the app target itself
|
|
||||||
leadingProject = context.target.project;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.STORYBOOK_ANGULAR_PROJECT = leadingProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Constants = {
|
|
||||||
addonDependencies: ['@storybook/addons'],
|
|
||||||
tsConfigExclusions: ['stories', '**/*.stories.ts'],
|
|
||||||
pkgJsonScripts: {
|
|
||||||
storybook: 'start-storybook -p 9001 -c .storybook',
|
|
||||||
},
|
|
||||||
jsonIndentLevel: 2,
|
|
||||||
coreAddonPrefix: '@storybook/addon-',
|
|
||||||
uiFrameworks: {
|
|
||||||
angular: '@storybook/angular',
|
|
||||||
react: '@storybook/react',
|
|
||||||
html: '@storybook/html',
|
|
||||||
} as const,
|
|
||||||
};
|
|
||||||
type Constants = typeof Constants;
|
|
||||||
|
|
||||||
type Framework = {
|
|
||||||
type: keyof Constants['uiFrameworks'];
|
|
||||||
uiFramework: Constants['uiFrameworks'][keyof Constants['uiFrameworks']];
|
|
||||||
};
|
|
||||||
export function isFramework(
|
|
||||||
type: Framework['type'],
|
|
||||||
schema: Pick<Framework, 'uiFramework'>
|
|
||||||
) {
|
|
||||||
if (type === 'angular' && schema.uiFramework === '@storybook/angular') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (type === 'react' && schema.uiFramework === '@storybook/react') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (type === 'html' && schema.uiFramework === '@storybook/html') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function safeFileDelete(tree: Tree, path: string): boolean {
|
|
||||||
if (tree.exists(path)) {
|
|
||||||
tree.delete(path);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to retrieve the latest package version from NPM
|
|
||||||
* Return an optional "latest" version in case of error
|
|
||||||
* @param packageName
|
|
||||||
*/
|
|
||||||
export function getLatestNodeVersion(
|
|
||||||
packageName: string
|
|
||||||
): Promise<NodePackage> {
|
|
||||||
const DEFAULT_VERSION = 'latest';
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
return get(`http://registry.npmjs.org/${packageName}`, (res) => {
|
|
||||||
let rawData = '';
|
|
||||||
res.on('data', (chunk) => (rawData += chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(rawData);
|
|
||||||
const version = (response && response['dist-tags']) || {};
|
|
||||||
|
|
||||||
resolve(buildPackage(packageName, version.latest));
|
|
||||||
} catch (e) {
|
|
||||||
resolve(buildPackage(packageName));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on('error', () => resolve(buildPackage(packageName)));
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildPackage(
|
|
||||||
name: string,
|
|
||||||
version: string = DEFAULT_VERSION
|
|
||||||
): NodePackage {
|
|
||||||
return { name, version };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getJsonFile(tree: Tree, path: string): JsonAstObject {
|
|
||||||
const buffer = tree.read(path);
|
|
||||||
if (buffer === null) {
|
|
||||||
throw new SchematicsException(`Could not read JSON file (${path}).`);
|
|
||||||
}
|
|
||||||
const content = buffer.toString();
|
|
||||||
|
|
||||||
const packageJson = parseJsonAst(content, JsonParseMode.Strict);
|
|
||||||
if (packageJson.kind !== 'object') {
|
|
||||||
throw new SchematicsException('Invalid JSON file. Was expecting an object');
|
|
||||||
}
|
|
||||||
|
|
||||||
return packageJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseJsonAtPath(tree: Tree, path: string): JsonAstObject {
|
|
||||||
const buffer = tree.read(path);
|
|
||||||
|
|
||||||
if (buffer === null) {
|
|
||||||
throw new SchematicsException(`Could not read ${path}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = buffer.toString();
|
|
||||||
|
|
||||||
const json = parseJsonAst(content, JsonParseMode.Strict);
|
|
||||||
if (json.kind !== 'object') {
|
|
||||||
throw new SchematicsException(`Invalid ${path}. Was expecting an object`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TsConfig = {
|
|
||||||
extends: string;
|
|
||||||
compilerOptions: CompilerOptions;
|
|
||||||
include?: string[];
|
|
||||||
exclude?: string[];
|
|
||||||
references?: Array<{ path: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getTsConfigContent(tree: Tree, path: string) {
|
|
||||||
const tsConfig = parseJsonAtPath(tree, path);
|
|
||||||
const content = tsConfig.value as TsConfig;
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTsSourceFile(host: Tree, path: string): SourceFile {
|
|
||||||
const buffer = host.read(path);
|
|
||||||
if (!buffer) {
|
|
||||||
throw new SchematicsException(`Could not read TS file (${path}).`);
|
|
||||||
}
|
|
||||||
const content = buffer.toString();
|
|
||||||
const source = createSourceFile(path, content, ScriptTarget.Latest, true);
|
|
||||||
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyWithOverwrite(source: Source, rules: Rule[]): Rule {
|
|
||||||
return (tree: Tree, _context: SchematicContext) => {
|
|
||||||
const rule = mergeWith(
|
|
||||||
apply(source, [
|
|
||||||
...rules,
|
|
||||||
forEach((fileEntry) => {
|
|
||||||
if (tree.exists(fileEntry.path)) {
|
|
||||||
tree.overwrite(fileEntry.path, fileEntry.content);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fileEntry;
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
return rule(tree, _context);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyWithSkipExisting(source: Source, rules: Rule[]): Rule {
|
|
||||||
return (tree: Tree, _context: SchematicContext) => {
|
|
||||||
const rule = mergeWith(
|
|
||||||
apply(source, [
|
|
||||||
...rules,
|
|
||||||
forEach((fileEntry) => {
|
|
||||||
if (tree.exists(fileEntry.path)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fileEntry;
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
return rule(tree, _context);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user