feat(storybook): migrate storybook builders to devkit (#4758)
This commit is contained in:
parent
57c6bacfb4
commit
651f3b60e9
@ -4,10 +4,14 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"excludedFiles": ["./src/migrations/**"],
|
||||
"excludedFiles": [
|
||||
"./src/migrations/**",
|
||||
"./src/generators/migrate-defaults-5-to-6/*.spec.ts",
|
||||
"./src/utils/testing.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
"error",
|
||||
"@nrwl/workspace",
|
||||
"@angular-devkit/core",
|
||||
"@angular-devkit/schematics",
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
{
|
||||
"$schema": "@angular-devkit/architect/src/builders-schema.json",
|
||||
"builders": {
|
||||
"storybook": {
|
||||
"implementation": "./src/builders/storybook/storybook.impl",
|
||||
"schema": "./src/builders/storybook/schema.json",
|
||||
"implementation": "./src/executors/storybook/compat",
|
||||
"schema": "./src/executors/storybook/schema.json",
|
||||
"description": "Serve Storybook"
|
||||
},
|
||||
"build": {
|
||||
"implementation": "./src/builders/build-storybook/build-storybook.impl",
|
||||
"schema": "./src/builders/build-storybook/schema.json",
|
||||
"implementation": "./src/executors/build-storybook/compat",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
"cli": "nx",
|
||||
"description": "Build storybook in production mode",
|
||||
"type": "object",
|
||||
"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",
|
||||
"cli": "nx",
|
||||
"description": "Serve up storybook in development mode",
|
||||
"type": "object",
|
||||
"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,
|
||||
updateWorkspaceInTree,
|
||||
serializeJson,
|
||||
readJsonInTree,
|
||||
} from '@nrwl/workspace';
|
||||
|
||||
import { getTsConfigContent } from '../../utils/utils';
|
||||
import { TsConfig } from '../../utils/utilities';
|
||||
|
||||
interface ProjectDefinition {
|
||||
root: string;
|
||||
@ -73,7 +74,7 @@ function updateStorybookTsConfigPath(
|
||||
}
|
||||
|
||||
const tsConfig = {
|
||||
storybook: getTsConfigContent(tree, paths.tsConfigStorybook),
|
||||
storybook: readJsonInTree<TsConfig>(tree, paths.tsConfigStorybook),
|
||||
};
|
||||
|
||||
// update extends prop to point to the lib relative tsconfig rather
|
||||
|
||||
@ -8,11 +8,12 @@ import {
|
||||
|
||||
import {
|
||||
formatFiles,
|
||||
readJsonInTree,
|
||||
updateWorkspaceInTree,
|
||||
serializeJson,
|
||||
} from '@nrwl/workspace';
|
||||
|
||||
import { getTsConfigContent, isFramework } from '../../utils/utils';
|
||||
import { isFramework, TsConfig } from '../../utils/utilities';
|
||||
import { normalize } from '@angular-devkit/core';
|
||||
|
||||
interface ProjectDefinition {
|
||||
@ -77,14 +78,14 @@ function updateLintTarget(
|
||||
>[1]['uiFramework'],
|
||||
});
|
||||
|
||||
const mainTsConfigContent = getTsConfigContent(tree, paths.tsConfig);
|
||||
const mainTsConfigContent = readJsonInTree<TsConfig>(tree, paths.tsConfig);
|
||||
|
||||
const tsConfig = {
|
||||
main: mainTsConfigContent,
|
||||
lib: tree.exists(paths.tsConfigLib)
|
||||
? getTsConfigContent(tree, paths.tsConfigLib)
|
||||
? readJsonInTree<TsConfig>(tree, paths.tsConfigLib)
|
||||
: mainTsConfigContent,
|
||||
storybook: getTsConfigContent(tree, paths.tsConfigStorybook),
|
||||
storybook: readJsonInTree<TsConfig>(tree, paths.tsConfigStorybook),
|
||||
};
|
||||
|
||||
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 { tmpdir } from 'os';
|
||||
import { mkdtempSync } from 'fs';
|
||||
|
||||
import { schema } from '@angular-devkit/core';
|
||||
import { join } from 'path';
|
||||
import { externalSchematic, Rule, Tree } from '@angular-devkit/schematics';
|
||||
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
|
||||
import { Architect } from '@angular-devkit/architect';
|
||||
import { TestingArchitectHost } from '@angular-devkit/architect/testing';
|
||||
|
||||
import {
|
||||
createEmptyWorkspace,
|
||||
MockBuilderContext,
|
||||
} from '@nrwl/workspace/testing';
|
||||
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
|
||||
|
||||
const testRunner = new SchematicTestRunner(
|
||||
'@nrwl/storybook',
|
||||
@ -43,14 +34,6 @@ const migrationRunner = new SchematicTestRunner(
|
||||
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) {
|
||||
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 { get } from 'http';
|
||||
import { CompilerOptions } from 'typescript';
|
||||
|
||||
export interface NodePackage {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const Constants = {
|
||||
addonDependencies: ['@storybook/addons'],
|
||||
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 = {
|
||||
extends: string;
|
||||
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