feat(storybook): migrate storybook builders to devkit (#4758)

This commit is contained in:
Jason Jean 2021-02-10 15:39:49 -05:00 committed by GitHub
parent 57c6bacfb4
commit 651f3b60e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 444 additions and 739 deletions

View File

@ -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",

View File

@ -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"
} }
} }

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -1,5 +0,0 @@
describe('storybook builer', () => {
it('should have a test', () => {
expect(true).toBeTruthy();
});
});

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
import { convertNxExecutor } from '@nrwl/devkit';
import buildStorybookExecutor from './build-storybook.impl';
export default convertNxExecutor(buildStorybookExecutor);

View File

@ -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": {

View File

@ -0,0 +1,4 @@
import { convertNxExecutor } from '@nrwl/devkit';
import storybookExecutor from './storybook.impl';
export default convertNxExecutor(storybookExecutor);

View File

@ -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": {

View File

@ -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);
});
});

View 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;
}

View 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;
}

View File

@ -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

View File

@ -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)) {

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
};
}