Skip to content

Commit

Permalink
Setup Storybook in Angular workspace via builders
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Jan 10, 2023
1 parent 23d7e7e commit 5c7ae17
Show file tree
Hide file tree
Showing 30 changed files with 1,579 additions and 1,641 deletions.
16 changes: 13 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
- [Addon-a11y: Removed deprecated withA11y decorator](#addon-a11y-removed-deprecated-witha11y-decorator)
- [Stories glob matches MDX files](#stories-glob-matches-mdx-files)
- [Add strict mode](#add-strict-mode)
- [Angular: Drop support for Angular \< 14](#angular-drop-support-for-angular--14)
- [Angular: Drop support for calling storybook directly](#angular-drop-support-for-calling-storybook-directly)
- [Docs Changes](#docs-changes)
- [Standalone docs files](#standalone-docs-files)
- [Referencing stories in docs files](#referencing-stories-in-docs-files)
Expand Down Expand Up @@ -306,8 +308,8 @@ To opt-out of the old behavior you can set the `storyStoreV7` feature flag to `f
module.exports = {
features: {
storyStoreV7: false,
}
}
},
};
```

#### Removed global client APIs
Expand Down Expand Up @@ -740,6 +742,15 @@ Starting in 7.0, Storybook's build tools add [`"use strict"`](https://developer.

If user code in `.storybook/preview.js` or stories relies on "sloppy" mode behavior, it will need to be updated. As a workaround, it is sometimes possible to move the sloppy mode code inside a script tag in `.storybook/preview-head.html`.

#### Angular: Drop support for Angular < 14

Starting in 7.0, we drop support for Angular < 14

#### Angular: Drop support for calling storybook directly

In Storybook 6.4 we have deprecated calling Storybook directly (`npm run storybook`) and removed support for it in Storybook 7.0 entirely. Instead you have to set up
the Storybook builder in your `angular.json` and execute `ng run <your-project>:storybook` to start Storybook. Please visit https://github.com/storybookjs/storybook/tree/next/code/frameworks/angular to set up Storybook for Angular correctly.

### Docs Changes

The information hierarchy of docs in Storybook has changed in 7.0. The main difference is that each docs is listed in the sidebar as a separate entry, rather than attached to individual stories.
Expand Down Expand Up @@ -3812,4 +3823,3 @@ If you **are** using these addons, it takes two steps to migrate:
```
<!-- markdown-link-check-enable -->
66 changes: 66 additions & 0 deletions code/frameworks/angular/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Storybook for Angular

- [Storybook for Angular](#storybook-for-angular)
- [Getting Started](#getting-started)
- [Setup Compodoc](#setup-compodoc)
- [Support for multi-project workspace](#support-for-multi-project-workspace)
- [Run Storybook](#run-storybook)

Storybook for Angular is a UI development environment for your Angular components.
With it, you can visualize different states of your UI components and develop them interactively.

Expand All @@ -15,6 +21,66 @@ cd my-angular-app
npx storybook init
```

### Setup Compodoc

When installing, you will be given the option to set up Compodoc, which is a tool for creating documentation for Angular projects.

You can include JSDoc comments above components, directives, and other parts of your Angular code to include documentation for those elements. Compodoc uses these comments to generate documentation for your application. In Storybook, it is useful to add explanatory comments above @Inputs and @Outputs, since these are the main elements that Storybook displays in its user interface. The @Inputs and @Outputs are the elements that you can interact with in Storybook, such as controls.

## Support for multi-project workspace

Storybook supports Angular multi-project workspace. You can setup Storybook for each project in the workspace. When running `npx storybook init` you will be asked for which project Storybook should be set up. Essentially, during initialization, the `angular.json` will be edited to add the Storybook configuration for the selected project. The configuration looks approximately like this:

```json
// angular.json
{
...
"projects": {
...
"your-project": {
...
"architect": {
...
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "your-project:build",
"compodoc": false,
"port": 6006
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "your-project:build",
"compodoc": false,
"outputDir": "dist/storybook/your-project"
}
}
}
}
}
}
```

## Run Storybook

To run Storybook for a particular project, please run:

```sh
ng run your-project:storybook
```

To build Storybook, run:

```sh
ng run your-project:build-storybook
```

You will find the output in `dist/storybook/your-project`.

For more information visit: [storybook.js.org](https://storybook.js.org)

---
Expand Down
8 changes: 4 additions & 4 deletions code/frameworks/angular/src/builders/start-storybook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ function commandBuilder(
return standaloneOptions;
}),
switchMap((standaloneOptions) => runInstance(standaloneOptions)),
map(() => {
return { success: true };
map(({ port }) => {
return { success: true, info: { port } };
})
);
}
Expand All @@ -129,10 +129,10 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext)
};
}
function runInstance(options: StandaloneOptions) {
return new Observable<void>((observer) => {
return new Observable<{ port: number }>((observer) => {
// This Observable intentionally never complete, leaving the process running ;)
buildDevStandalone(options as any).then(
() => observer.next(),
({ port }) => observer.next({ port }),
(error) => observer.error(buildStandaloneErrorHandler(error))
);
});
Expand Down
37 changes: 11 additions & 26 deletions code/frameworks/angular/src/builders/utils/run-compodoc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BuilderContext } from '@angular-devkit/architect';
import { spawn } from 'child_process';
import { Observable } from 'rxjs';
import * as path from 'path';
import { JsPackageManagerFactory } from '@storybook/cli';

const hasTsConfigArg = (args: string[]) => args.indexOf('-p') !== -1;
const hasOutputArg = (args: string[]) =>
Expand All @@ -20,37 +20,22 @@ export const runCompodoc = (
return new Observable<void>((observer) => {
const tsConfigPath = toRelativePath(tsconfig);
const finalCompodocArgs = [
'compodoc',
// Default options
...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]),
...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot}`]),
...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]),
...compodocArgs,
];

try {
context.logger.info(finalCompodocArgs.join(' '));
const child = spawn('npx', finalCompodocArgs, {
cwd: context.workspaceRoot,
shell: true,
});
const packageManager = JsPackageManagerFactory.getPackageManager();

child.stdout.on('data', (data) => {
context.logger.info(data.toString());
});
child.stderr.on('data', (data) => {
context.logger.error(data.toString());
});
try {
const stdout = packageManager.runScript('compodoc', finalCompodocArgs, context.workspaceRoot);

child.on('close', (code) => {
if (code === 0) {
observer.next();
observer.complete();
} else {
observer.error();
}
});
} catch (error) {
observer.error(error);
context.logger.info(stdout);
observer.next();
observer.complete();
} catch (e) {
context.logger.error(e);
observer.error();
}
});
};
2 changes: 1 addition & 1 deletion code/frameworks/angular/src/client/decorateStory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const prepareMain = (
): AngularRenderer['storyResult'] => {
let { template } = story;

const component = story.component ?? context.component;
const { component } = context;
const userDefinedTemplate = !hasNoTemplate(template);

if (!userDefinedTemplate && component) {
Expand Down
15 changes: 14 additions & 1 deletion code/lib/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@
},
"license": "MIT",
"author": "Storybook Team",
"exports": {
".": {
"node": "./dist/index.js",
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"bin": {
"getstorybook": "./bin/index.js",
"sb": "./bin/index.js"
Expand Down Expand Up @@ -97,7 +109,8 @@
},
"bundler": {
"entries": [
"./src/generate.ts"
"./src/generate.ts",
"./src/index.ts"
],
"platform": "node"
},
Expand Down
8 changes: 0 additions & 8 deletions code/lib/cli/src/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,6 @@ describe('Detect', () => {
)
).toBe(false);
});

it('ALREADY_HAS_STORYBOOK if lib is present', () => {
expect(
isStorybookInstalled({
devDependencies: { '@storybook/react': '4.0.0-alpha.21' },
})
).toBe(ProjectType.ALREADY_HAS_STORYBOOK);
});
});

describe('detectFrameworkPreset should return', () => {
Expand Down
11 changes: 7 additions & 4 deletions code/lib/cli/src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function isStorybookInstalled(
false
)
) {
return ProjectType.ALREADY_HAS_STORYBOOK;
return true;
}
}
return false;
Expand Down Expand Up @@ -194,9 +194,8 @@ export function detect(
return ProjectType.UNDETECTED;
}

const storyBookInstalled = isStorybookInstalled(packageJson, options.force);
if (storyBookInstalled) {
return storyBookInstalled;
if (isNxProject(packageJson)) {
return ProjectType.NX;
}

if (options.html) {
Expand All @@ -205,3 +204,7 @@ export function detect(

return detectFrameworkPreset(packageJson || bowerJson);
}

function isNxProject(packageJSON: PackageJson) {
return !!packageJSON.devDependencies?.nx || fs.existsSync('nx.json');
}
116 changes: 116 additions & 0 deletions code/lib/cli/src/generators/ANGULAR/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import fs from 'fs';
import prompts from 'prompts';
import dedent from 'ts-dedent';

import { commandLog } from '../../helpers';

export const ANGULAR_JSON_PATH = 'angular.json';

export const compoDocPreviewPrefix = dedent`
import { setCompodocJson } from "@storybook/addon-docs/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
`.trimStart();

export const promptForCompoDocs = async (): Promise<boolean> => {
const { useCompoDoc } = await prompts({
type: 'confirm',
name: 'useCompoDoc',
message: 'Do you want to use Compodoc for documentation?',
});

return useCompoDoc;
};

export class AngularJSON {
json: {
projects: Record<string, { root: string; architect: Record<string, any> }>;
};

constructor() {
if (!fs.existsSync(ANGULAR_JSON_PATH)) {
commandLog(
'An angular.json file was not found in the current directory. Storybook needs it to work properly.'
);

throw new Error('No angular.json file found');
}

const jsonContent = fs.readFileSync(ANGULAR_JSON_PATH, 'utf8');
this.json = JSON.parse(jsonContent);
}

get projects() {
return this.json.projects;
}

getProjectSettingsByName(projectName: string) {
return this.projects[projectName];
}

async getProjectName() {
const projectKeys = Object.keys(this.projects);

if (projectKeys.length > 1) {
const { projectName } = await prompts({
type: 'select',
name: 'projectName',
message: 'For which project do you want to generate Storybook configuration?',
choices: projectKeys.map((name) => ({
title: name,
value: name,
})),
});

return projectName;
}

return Object.keys(this.projects)[0];
}

addStorybookEntries({
angularProjectName,
storybookFolder,
useCompodoc,
root,
}: {
angularProjectName: string;
storybookFolder: string;
useCompodoc: boolean;
root: string;
}) {
// add an entry to the angular.json file to setup the storybook builders
const { architect } = this.projects[angularProjectName];

const baseOptions = {
configDir: storybookFolder,
browserTarget: `${angularProjectName}:build`,
compodoc: useCompodoc,
...(useCompodoc && { compodocArgs: ['-e', 'json', '-d', root || '.'] }),
};

if (!architect.storybook) {
architect.storybook = {
builder: '@storybook/angular:start-storybook',
options: {
...baseOptions,
port: 6006,
},
};
}

if (!architect['build-storybook']) {
architect['build-storybook'] = {
builder: '@storybook/angular:build-storybook',
options: {
...baseOptions,
outputDir: `dist/storybook/${angularProjectName}`,
},
};
}
}

write() {
fs.writeFileSync(ANGULAR_JSON_PATH, JSON.stringify(this.json, null, 2));
}
}
Loading

0 comments on commit 5c7ae17

Please sign in to comment.