@storybook/angular 9.0.0-beta.1 → 9.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builders/build-storybook/index.d.ts +2 -16
- package/dist/builders/build-storybook/index.mjs +78 -0
- package/dist/builders/build-storybook/index.spec.mjs +187 -0
- package/dist/builders/build-storybook/schema.json +2 -4
- package/dist/builders/start-storybook/index.d.ts +2 -14
- package/dist/builders/start-storybook/index.mjs +99 -0
- package/dist/builders/start-storybook/index.spec.mjs +186 -0
- package/dist/builders/start-storybook/schema.json +3 -6
- package/dist/builders/utils/error-handler.mjs +33 -0
- package/dist/builders/utils/run-compodoc.mjs +31 -0
- package/dist/builders/utils/run-compodoc.spec.mjs +74 -0
- package/dist/builders/utils/standalone-options.mjs +1 -0
- package/dist/client/angular-beta/AbstractRenderer.mjs +164 -0
- package/dist/client/angular-beta/CanvasRenderer.mjs +9 -0
- package/dist/client/angular-beta/ComputesTemplateFromComponent.mjs +154 -0
- package/dist/client/angular-beta/ComputesTemplateFromComponent.test.mjs +728 -0
- package/dist/client/angular-beta/DocsRenderer.mjs +35 -0
- package/dist/client/angular-beta/RendererFactory.mjs +50 -0
- package/dist/client/angular-beta/RendererFactory.test.mjs +233 -0
- package/dist/client/angular-beta/StorybookModule.mjs +23 -0
- package/dist/client/angular-beta/StorybookModule.test.mjs +319 -0
- package/dist/client/angular-beta/StorybookProvider.mjs +22 -0
- package/dist/client/angular-beta/StorybookWrapperComponent.mjs +123 -0
- package/dist/client/angular-beta/__testfixtures__/input.component.mjs +73 -0
- package/dist/client/angular-beta/__testfixtures__/test.module.mjs +17 -0
- package/dist/client/angular-beta/utils/BootstrapQueue.mjs +49 -0
- package/dist/client/angular-beta/utils/BootstrapQueue.test.mjs +162 -0
- package/dist/client/angular-beta/utils/NgComponentAnalyzer.mjs +84 -0
- package/dist/client/angular-beta/utils/NgComponentAnalyzer.test.mjs +386 -0
- package/dist/client/angular-beta/utils/NgModulesAnalyzer.mjs +37 -0
- package/dist/client/angular-beta/utils/NgModulesAnalyzer.test.mjs +22 -0
- package/dist/client/angular-beta/utils/PropertyExtractor.mjs +158 -0
- package/dist/client/angular-beta/utils/PropertyExtractor.test.mjs +175 -0
- package/dist/client/angular-beta/utils/StoryUID.mjs +38 -0
- package/dist/client/argsToTemplate.mjs +55 -0
- package/dist/client/argsToTemplate.test.mjs +100 -0
- package/dist/client/config.mjs +4 -0
- package/dist/client/decorateStory.mjs +45 -0
- package/dist/client/decorateStory.test.mjs +301 -0
- package/dist/client/decorators.mjs +63 -0
- package/dist/client/decorators.test.mjs +157 -0
- package/dist/client/docs/__testfixtures__/doc-button/input.mjs +201 -0
- package/dist/client/docs/angular-properties.test.mjs +34 -0
- package/dist/client/docs/compodoc.js +5 -1
- package/dist/client/docs/compodoc.mjs +248 -0
- package/dist/client/docs/compodoc.test.mjs +130 -0
- package/dist/client/docs/config.mjs +16 -0
- package/dist/client/docs/index.mjs +1 -0
- package/dist/client/docs/sourceDecorator.mjs +48 -0
- package/dist/client/docs/types.mjs +1 -0
- package/dist/client/globals.mjs +31 -0
- package/dist/client/index.mjs +9 -0
- package/dist/client/portable-stories.mjs +26 -0
- package/dist/client/preview-prod.mjs +2 -0
- package/dist/client/public-types.mjs +1 -0
- package/dist/client/render.mjs +14 -0
- package/dist/client/types.mjs +1 -0
- package/dist/node/index.mjs +3 -0
- package/dist/server/__mocks-ng-workspace__/minimal-config/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/some-config/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/with-angularBrowserTarget/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/with-lib/projects/pattern-lib/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/with-nx/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/with-nx-workspace/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/with-options-styles/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/without-projects-entry/projects/pattern-lib/src/main.mjs +2 -0
- package/dist/server/__mocks-ng-workspace__/without-tsConfig/src/main.mjs +2 -0
- package/dist/server/angular-cli-webpack.mjs +80 -0
- package/dist/server/framework-preset-angular-cli.mjs +81 -0
- package/dist/server/framework-preset-angular-docs.mjs +6 -0
- package/dist/server/framework-preset-angular-ivy.mjs +56 -0
- package/dist/server/plugins/storybook-normalize-angular-entry-plugin.mjs +52 -0
- package/dist/server/preset-options.mjs +1 -0
- package/dist/server/utils/filter-out-styling-rules.mjs +13 -0
- package/dist/server/utils/module-is-available.mjs +9 -0
- package/package.json +5 -15
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { logger, instance as npmLog } from 'storybook/internal/node-logger';
|
|
2
|
+
import { dedent } from 'ts-dedent';
|
|
3
|
+
export const printErrorDetails = (error) => {
|
|
4
|
+
// Duplicate code for Standalone error handling
|
|
5
|
+
// Source: https://github.com/storybookjs/storybook/blob/39c7ba09ad84fbd466f9c25d5b92791a5450b9f6/lib/core-server/src/build-dev.ts#L136
|
|
6
|
+
npmLog.heading = '';
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
if (error.error) {
|
|
9
|
+
logger.error(error.error);
|
|
10
|
+
}
|
|
11
|
+
else if (error.stats && error.stats.compilation.errors) {
|
|
12
|
+
error.stats.compilation.errors.forEach((e) => logger.plain(e));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
logger.error(error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else if (error.compilation?.errors) {
|
|
19
|
+
error.compilation.errors.forEach((e) => logger.plain(e));
|
|
20
|
+
}
|
|
21
|
+
logger.line();
|
|
22
|
+
};
|
|
23
|
+
export const errorSummary = (error) => {
|
|
24
|
+
return error.close
|
|
25
|
+
? dedent `
|
|
26
|
+
FATAL broken build!, will close the process,
|
|
27
|
+
Fix the error below and restart storybook.
|
|
28
|
+
`
|
|
29
|
+
: dedent `
|
|
30
|
+
Broken build, fix the error above.
|
|
31
|
+
You may need to refresh the browser.
|
|
32
|
+
`;
|
|
33
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isAbsolute, relative } from 'node:path';
|
|
2
|
+
import { JsPackageManagerFactory } from 'storybook/internal/common';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
const hasTsConfigArg = (args) => args.indexOf('-p') !== -1;
|
|
5
|
+
const hasOutputArg = (args) => args.indexOf('-d') !== -1 || args.indexOf('--output') !== -1;
|
|
6
|
+
// relative is necessary to workaround a compodoc issue with
|
|
7
|
+
// absolute paths on windows machines
|
|
8
|
+
const toRelativePath = (pathToTsConfig) => {
|
|
9
|
+
return isAbsolute(pathToTsConfig) ? relative('.', pathToTsConfig) : pathToTsConfig;
|
|
10
|
+
};
|
|
11
|
+
export const runCompodoc = ({ compodocArgs, tsconfig }, context) => {
|
|
12
|
+
return new Observable((observer) => {
|
|
13
|
+
const tsConfigPath = toRelativePath(tsconfig);
|
|
14
|
+
const finalCompodocArgs = [
|
|
15
|
+
...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]),
|
|
16
|
+
...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]),
|
|
17
|
+
...compodocArgs,
|
|
18
|
+
];
|
|
19
|
+
const packageManager = JsPackageManagerFactory.getPackageManager();
|
|
20
|
+
try {
|
|
21
|
+
const stdout = packageManager.runPackageCommandSync('compodoc', finalCompodocArgs, context.workspaceRoot, 'inherit');
|
|
22
|
+
context.logger.info(stdout);
|
|
23
|
+
observer.next();
|
|
24
|
+
observer.complete();
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
context.logger.error(e);
|
|
28
|
+
observer.error();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { take } from 'rxjs/operators';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { runCompodoc } from './run-compodoc';
|
|
4
|
+
const mockRunScript = vi.fn();
|
|
5
|
+
vi.mock('storybook/internal/common', () => ({
|
|
6
|
+
JsPackageManagerFactory: {
|
|
7
|
+
getPackageManager: () => ({
|
|
8
|
+
runPackageCommandSync: mockRunScript,
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
const builderContextLoggerMock = {
|
|
13
|
+
createChild: vi.fn(),
|
|
14
|
+
log: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
info: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
fatal: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
describe('runCompodoc', () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
mockRunScript.mockClear();
|
|
24
|
+
});
|
|
25
|
+
const builderContextMock = {
|
|
26
|
+
workspaceRoot: 'path/to/project',
|
|
27
|
+
logger: builderContextLoggerMock,
|
|
28
|
+
};
|
|
29
|
+
it('should run compodoc with tsconfig from context', async () => {
|
|
30
|
+
runCompodoc({
|
|
31
|
+
compodocArgs: [],
|
|
32
|
+
tsconfig: 'path/to/tsconfig.json',
|
|
33
|
+
}, builderContextMock)
|
|
34
|
+
.pipe(take(1))
|
|
35
|
+
.subscribe();
|
|
36
|
+
expect(mockRunScript).toHaveBeenCalledWith('compodoc', ['-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], 'path/to/project', 'inherit');
|
|
37
|
+
});
|
|
38
|
+
it('should run compodoc with tsconfig from compodocArgs', async () => {
|
|
39
|
+
runCompodoc({
|
|
40
|
+
compodocArgs: ['-p', 'path/to/tsconfig.stories.json'],
|
|
41
|
+
tsconfig: 'path/to/tsconfig.json',
|
|
42
|
+
}, builderContextMock)
|
|
43
|
+
.pipe(take(1))
|
|
44
|
+
.subscribe();
|
|
45
|
+
expect(mockRunScript).toHaveBeenCalledWith('compodoc', ['-d', 'path/to/project', '-p', 'path/to/tsconfig.stories.json'], 'path/to/project', 'inherit');
|
|
46
|
+
});
|
|
47
|
+
it('should run compodoc with default output folder.', async () => {
|
|
48
|
+
runCompodoc({
|
|
49
|
+
compodocArgs: [],
|
|
50
|
+
tsconfig: 'path/to/tsconfig.json',
|
|
51
|
+
}, builderContextMock)
|
|
52
|
+
.pipe(take(1))
|
|
53
|
+
.subscribe();
|
|
54
|
+
expect(mockRunScript).toHaveBeenCalledWith('compodoc', ['-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], 'path/to/project', 'inherit');
|
|
55
|
+
});
|
|
56
|
+
it('should run with custom output folder specified with --output compodocArgs', async () => {
|
|
57
|
+
runCompodoc({
|
|
58
|
+
compodocArgs: ['--output', 'path/to/customFolder'],
|
|
59
|
+
tsconfig: 'path/to/tsconfig.json',
|
|
60
|
+
}, builderContextMock)
|
|
61
|
+
.pipe(take(1))
|
|
62
|
+
.subscribe();
|
|
63
|
+
expect(mockRunScript).toHaveBeenCalledWith('compodoc', ['-p', 'path/to/tsconfig.json', '--output', 'path/to/customFolder'], 'path/to/project', 'inherit');
|
|
64
|
+
});
|
|
65
|
+
it('should run with custom output folder specified with -d compodocArgs', async () => {
|
|
66
|
+
runCompodoc({
|
|
67
|
+
compodocArgs: ['-d', 'path/to/customFolder'],
|
|
68
|
+
tsconfig: 'path/to/tsconfig.json',
|
|
69
|
+
}, builderContextMock)
|
|
70
|
+
.pipe(take(1))
|
|
71
|
+
.subscribe();
|
|
72
|
+
expect(mockRunScript).toHaveBeenCalledWith('compodoc', ['-p', 'path/to/tsconfig.json', '-d', 'path/to/customFolder'], 'path/to/project', 'inherit');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
|
+
import { stringify } from 'telejson';
|
|
4
|
+
import { getApplication } from './StorybookModule';
|
|
5
|
+
import { storyPropsProvider } from './StorybookProvider';
|
|
6
|
+
import { queueBootstrapping } from './utils/BootstrapQueue';
|
|
7
|
+
import { PropertyExtractor } from './utils/PropertyExtractor';
|
|
8
|
+
const applicationRefs = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Attribute name for the story UID that may be written to the targetDOMNode.
|
|
11
|
+
*
|
|
12
|
+
* If a target DOM node has a story UID attribute, it will be used as part of the selector for the
|
|
13
|
+
* Angular component.
|
|
14
|
+
*/
|
|
15
|
+
export const STORY_UID_ATTRIBUTE = 'data-sb-story-uid';
|
|
16
|
+
export class AbstractRenderer {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.previousStoryRenderInfo = new Map();
|
|
19
|
+
}
|
|
20
|
+
/** Wait and destroy the platform */
|
|
21
|
+
static resetApplications(domNode) {
|
|
22
|
+
applicationRefs.forEach((appRef, appDOMNode) => {
|
|
23
|
+
if (!appRef.destroyed && (!domNode || appDOMNode === domNode)) {
|
|
24
|
+
appRef.destroy();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Bootstrap main angular module with main component or send only new `props` with storyProps$
|
|
30
|
+
*
|
|
31
|
+
* @param storyFnAngular {StoryFnAngularReturnType}
|
|
32
|
+
* @param forced {boolean} If :
|
|
33
|
+
*
|
|
34
|
+
* - True render will only use the StoryFn `props' in storyProps observable that will update sotry's
|
|
35
|
+
* component/template properties. Improves performance without reloading the whole
|
|
36
|
+
* module&component if props changes
|
|
37
|
+
* - False fully recharges or initializes angular module & component
|
|
38
|
+
*
|
|
39
|
+
* @param component {Component}
|
|
40
|
+
*/
|
|
41
|
+
async render({ storyFnAngular, forced, component, targetDOMNode, }) {
|
|
42
|
+
const targetSelector = this.generateTargetSelectorFromStoryId(targetDOMNode.id);
|
|
43
|
+
const newStoryProps$ = new BehaviorSubject(storyFnAngular.props);
|
|
44
|
+
if (!this.fullRendererRequired({
|
|
45
|
+
targetDOMNode,
|
|
46
|
+
storyFnAngular,
|
|
47
|
+
moduleMetadata: {
|
|
48
|
+
...storyFnAngular.moduleMetadata,
|
|
49
|
+
},
|
|
50
|
+
forced,
|
|
51
|
+
})) {
|
|
52
|
+
this.storyProps$.next(storyFnAngular.props);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await this.beforeFullRender(targetDOMNode);
|
|
56
|
+
// Complete last BehaviorSubject and set a new one for the current module
|
|
57
|
+
if (this.storyProps$) {
|
|
58
|
+
this.storyProps$.complete();
|
|
59
|
+
}
|
|
60
|
+
this.storyProps$ = newStoryProps$;
|
|
61
|
+
this.initAngularRootElement(targetDOMNode, targetSelector);
|
|
62
|
+
const analyzedMetadata = new PropertyExtractor(storyFnAngular.moduleMetadata, component);
|
|
63
|
+
await analyzedMetadata.init();
|
|
64
|
+
const storyUid = this.generateStoryUIdFromRawStoryUid(targetDOMNode.getAttribute(STORY_UID_ATTRIBUTE));
|
|
65
|
+
const componentSelector = storyUid !== null ? `${targetSelector}[${storyUid}]` : targetSelector;
|
|
66
|
+
if (storyUid !== null) {
|
|
67
|
+
const element = targetDOMNode.querySelector(targetSelector);
|
|
68
|
+
element.toggleAttribute(storyUid, true);
|
|
69
|
+
}
|
|
70
|
+
const application = getApplication({
|
|
71
|
+
storyFnAngular,
|
|
72
|
+
component,
|
|
73
|
+
targetSelector: componentSelector,
|
|
74
|
+
analyzedMetadata,
|
|
75
|
+
});
|
|
76
|
+
const providers = [
|
|
77
|
+
storyPropsProvider(newStoryProps$),
|
|
78
|
+
...analyzedMetadata.applicationProviders,
|
|
79
|
+
...(storyFnAngular.applicationConfig?.providers ?? []),
|
|
80
|
+
];
|
|
81
|
+
if (STORYBOOK_ANGULAR_OPTIONS?.experimentalZoneless) {
|
|
82
|
+
const { provideExperimentalZonelessChangeDetection } = await import('@angular/core');
|
|
83
|
+
if (!provideExperimentalZonelessChangeDetection) {
|
|
84
|
+
throw new Error('Experimental zoneless change detection requires Angular 18 or higher');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
providers.unshift(provideExperimentalZonelessChangeDetection());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const applicationRef = await queueBootstrapping(() => {
|
|
91
|
+
return bootstrapApplication(application, {
|
|
92
|
+
...storyFnAngular.applicationConfig,
|
|
93
|
+
providers,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
applicationRefs.set(targetDOMNode, applicationRef);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Only ASCII alphanumerics can be used as HTML tag name. https://html.spec.whatwg.org/#elements-2
|
|
100
|
+
*
|
|
101
|
+
* Therefore, stories break when non-ASCII alphanumerics are included in target selector.
|
|
102
|
+
* https://github.com/storybookjs/storybook/issues/15147
|
|
103
|
+
*
|
|
104
|
+
* This method returns storyId when it doesn't contain any non-ASCII alphanumerics. Otherwise, it
|
|
105
|
+
* generates a valid HTML tag name from storyId by removing non-ASCII alphanumerics from storyId,
|
|
106
|
+
* prefixing "sb-", and suffixing "-component"
|
|
107
|
+
*
|
|
108
|
+
* @memberof AbstractRenderer
|
|
109
|
+
* @protected
|
|
110
|
+
*/
|
|
111
|
+
generateTargetSelectorFromStoryId(id) {
|
|
112
|
+
const invalidHtmlTag = /[^A-Za-z0-9-]/g;
|
|
113
|
+
const storyIdIsInvalidHtmlTagName = invalidHtmlTag.test(id);
|
|
114
|
+
return storyIdIsInvalidHtmlTagName ? `sb-${id.replace(invalidHtmlTag, '')}-component` : id;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Angular is unable to handle components that have selectors with accented attributes.
|
|
118
|
+
*
|
|
119
|
+
* Therefore, stories break when meta's title contains accents.
|
|
120
|
+
* https://github.com/storybookjs/storybook/issues/29132
|
|
121
|
+
*
|
|
122
|
+
* This method filters accents from a given raw id. For example, this method converts
|
|
123
|
+
* 'Example/Button with an "é" accent' into 'Example/Button with an "e" accent'.
|
|
124
|
+
*
|
|
125
|
+
* @memberof AbstractRenderer
|
|
126
|
+
* @protected
|
|
127
|
+
*/
|
|
128
|
+
generateStoryUIdFromRawStoryUid(rawStoryUid) {
|
|
129
|
+
if (rawStoryUid === null) {
|
|
130
|
+
return rawStoryUid;
|
|
131
|
+
}
|
|
132
|
+
const accentCharacters = /[\u0300-\u036f]/g;
|
|
133
|
+
return rawStoryUid.normalize('NFD').replace(accentCharacters, '');
|
|
134
|
+
}
|
|
135
|
+
/** Adds DOM element that angular will use as bootstrap component. */
|
|
136
|
+
initAngularRootElement(targetDOMNode, targetSelector) {
|
|
137
|
+
targetDOMNode.innerHTML = '';
|
|
138
|
+
targetDOMNode.appendChild(document.createElement(targetSelector));
|
|
139
|
+
}
|
|
140
|
+
fullRendererRequired({ targetDOMNode, storyFnAngular, moduleMetadata, forced, }) {
|
|
141
|
+
const previousStoryRenderInfo = this.previousStoryRenderInfo.get(targetDOMNode);
|
|
142
|
+
const currentStoryRender = {
|
|
143
|
+
storyFnAngular,
|
|
144
|
+
moduleMetadataSnapshot: stringify(moduleMetadata, { maxDepth: 50 }),
|
|
145
|
+
};
|
|
146
|
+
this.previousStoryRenderInfo.set(targetDOMNode, currentStoryRender);
|
|
147
|
+
if (
|
|
148
|
+
// check `forceRender` of story RenderContext
|
|
149
|
+
!forced ||
|
|
150
|
+
// if it's the first rendering and storyProps$ is not init
|
|
151
|
+
!this.storyProps$) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
// force the rendering if the template has changed
|
|
155
|
+
const hasChangedTemplate = !!storyFnAngular?.template &&
|
|
156
|
+
previousStoryRenderInfo?.storyFnAngular?.template !== storyFnAngular.template;
|
|
157
|
+
if (hasChangedTemplate) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// force the rendering if the metadata structure has changed
|
|
161
|
+
const hasChangedModuleMetadata = currentStoryRender.moduleMetadataSnapshot !== previousStoryRenderInfo?.moduleMetadataSnapshot;
|
|
162
|
+
return hasChangedModuleMetadata;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { getComponentDecoratorMetadata, getComponentInputsOutputs, } from './utils/NgComponentAnalyzer';
|
|
2
|
+
/**
|
|
3
|
+
* Check if the name matches the criteria for a valid identifier. A valid identifier can only
|
|
4
|
+
* contain letters, digits, underscores, or dollar signs. It cannot start with a digit.
|
|
5
|
+
*/
|
|
6
|
+
const isValidIdentifier = (name) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
7
|
+
/**
|
|
8
|
+
* Returns the property name, if it can be accessed with dot notation. If not, it returns
|
|
9
|
+
* `this['propertyName']`.
|
|
10
|
+
*/
|
|
11
|
+
export const formatPropInTemplate = (propertyName) => isValidIdentifier(propertyName) ? propertyName : `this['${propertyName}']`;
|
|
12
|
+
const separateInputsOutputsAttributes = (ngComponentInputsOutputs, props = {}) => {
|
|
13
|
+
const inputs = ngComponentInputsOutputs.inputs
|
|
14
|
+
.filter((i) => i.templateName in props)
|
|
15
|
+
.map((i) => i.templateName);
|
|
16
|
+
const outputs = ngComponentInputsOutputs.outputs
|
|
17
|
+
.filter((o) => o.templateName in props)
|
|
18
|
+
.map((o) => o.templateName);
|
|
19
|
+
return {
|
|
20
|
+
inputs,
|
|
21
|
+
outputs,
|
|
22
|
+
otherProps: Object.keys(props).filter((k) => ![...inputs, ...outputs].includes(k)),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Converts a component into a template with inputs/outputs present in initial props
|
|
27
|
+
*
|
|
28
|
+
* @param component
|
|
29
|
+
* @param initialProps
|
|
30
|
+
* @param innerTemplate
|
|
31
|
+
*/
|
|
32
|
+
export const computesTemplateFromComponent = (component, initialProps, innerTemplate = '') => {
|
|
33
|
+
const ngComponentMetadata = getComponentDecoratorMetadata(component);
|
|
34
|
+
const ngComponentInputsOutputs = getComponentInputsOutputs(component);
|
|
35
|
+
if (!ngComponentMetadata.selector) {
|
|
36
|
+
// Allow to add renderer component when NgComponent selector is undefined
|
|
37
|
+
return `<ng-container *ngComponentOutlet="storyComponent"></ng-container>`;
|
|
38
|
+
}
|
|
39
|
+
const { inputs: initialInputs, outputs: initialOutputs } = separateInputsOutputsAttributes(ngComponentInputsOutputs, initialProps);
|
|
40
|
+
const templateInputs = initialInputs.length > 0
|
|
41
|
+
? ` ${initialInputs.map((i) => `[${i}]="${formatPropInTemplate(i)}"`).join(' ')}`
|
|
42
|
+
: '';
|
|
43
|
+
const templateOutputs = initialOutputs.length > 0
|
|
44
|
+
? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}`
|
|
45
|
+
: '';
|
|
46
|
+
return buildTemplate(ngComponentMetadata.selector, innerTemplate, templateInputs, templateOutputs);
|
|
47
|
+
};
|
|
48
|
+
/** Stringify an object with a placholder in the circular references. */
|
|
49
|
+
function stringifyCircular(obj) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
return JSON.stringify(obj, (key, value) => {
|
|
52
|
+
if (typeof value === 'object' && value !== null) {
|
|
53
|
+
if (seen.has(value)) {
|
|
54
|
+
return '[Circular]';
|
|
55
|
+
}
|
|
56
|
+
seen.add(value);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const createAngularInputProperty = ({ propertyName, value, argType, }) => {
|
|
62
|
+
let templateValue;
|
|
63
|
+
switch (typeof value) {
|
|
64
|
+
case 'string':
|
|
65
|
+
templateValue = `'${value}'`;
|
|
66
|
+
break;
|
|
67
|
+
case 'object':
|
|
68
|
+
templateValue = stringifyCircular(value)
|
|
69
|
+
.replace(/'/g, '\u2019')
|
|
70
|
+
.replace(/\\"/g, '\u201D')
|
|
71
|
+
.replace(/"([^-"]+)":/g, '$1: ')
|
|
72
|
+
.replace(/"/g, "'")
|
|
73
|
+
.replace(/\u2019/g, "\\'")
|
|
74
|
+
.replace(/\u201D/g, "\\'")
|
|
75
|
+
.split(',')
|
|
76
|
+
.join(', ');
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
templateValue = value;
|
|
80
|
+
}
|
|
81
|
+
return `[${propertyName}]="${templateValue}"`;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Converts a component into a template with inputs/outputs present in initial props
|
|
85
|
+
*
|
|
86
|
+
* @param component
|
|
87
|
+
* @param initialProps
|
|
88
|
+
* @param innerTemplate
|
|
89
|
+
*/
|
|
90
|
+
export const computesTemplateSourceFromComponent = (component, initialProps, argTypes) => {
|
|
91
|
+
const ngComponentMetadata = getComponentDecoratorMetadata(component);
|
|
92
|
+
if (!ngComponentMetadata) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!ngComponentMetadata.selector) {
|
|
96
|
+
// Allow to add renderer component when NgComponent selector is undefined
|
|
97
|
+
return `<ng-container *ngComponentOutlet="${component.name}"></ng-container>`;
|
|
98
|
+
}
|
|
99
|
+
const ngComponentInputsOutputs = getComponentInputsOutputs(component);
|
|
100
|
+
const { inputs: initialInputs, outputs: initialOutputs } = separateInputsOutputsAttributes(ngComponentInputsOutputs, initialProps);
|
|
101
|
+
const templateInputs = initialInputs.length > 0
|
|
102
|
+
? ` ${initialInputs
|
|
103
|
+
.map((propertyName) => createAngularInputProperty({
|
|
104
|
+
propertyName,
|
|
105
|
+
value: initialProps[propertyName],
|
|
106
|
+
argType: argTypes?.[propertyName],
|
|
107
|
+
}))
|
|
108
|
+
.join(' ')}`
|
|
109
|
+
: '';
|
|
110
|
+
const templateOutputs = initialOutputs.length > 0
|
|
111
|
+
? ` ${initialOutputs.map((i) => `(${i})="${formatPropInTemplate(i)}($event)"`).join(' ')}`
|
|
112
|
+
: '';
|
|
113
|
+
return buildTemplate(ngComponentMetadata.selector, '', templateInputs, templateOutputs);
|
|
114
|
+
};
|
|
115
|
+
const buildTemplate = (selector, innerTemplate, inputs, outputs) => {
|
|
116
|
+
// https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements
|
|
117
|
+
const voidElements = [
|
|
118
|
+
'area',
|
|
119
|
+
'base',
|
|
120
|
+
'br',
|
|
121
|
+
'col',
|
|
122
|
+
'command',
|
|
123
|
+
'embed',
|
|
124
|
+
'hr',
|
|
125
|
+
'img',
|
|
126
|
+
'input',
|
|
127
|
+
'keygen',
|
|
128
|
+
'link',
|
|
129
|
+
'meta',
|
|
130
|
+
'param',
|
|
131
|
+
'source',
|
|
132
|
+
'track',
|
|
133
|
+
'wbr',
|
|
134
|
+
];
|
|
135
|
+
const firstSelector = selector.split(',')[0];
|
|
136
|
+
const templateReplacers = [
|
|
137
|
+
[/(^.*?)(?=[,])/, '$1'],
|
|
138
|
+
[/(^\..+)/, 'div$1'],
|
|
139
|
+
[/(^\[.+?])/, 'div$1'],
|
|
140
|
+
[/([\w[\]]+)(\s*,[\w\s-[\],]+)+/, `$1`],
|
|
141
|
+
[/#([\w-]+)/, ` id="$1"`],
|
|
142
|
+
[/((\.[\w-]+)+)/, (_, c) => ` class="${c.split `.`.join ` `.trim()}"`],
|
|
143
|
+
[/(\[.+?])/g, (_, a) => ` ${a.slice(1, -1)}`],
|
|
144
|
+
[
|
|
145
|
+
/([\S]+)(.*)/,
|
|
146
|
+
(template, elementSelector) => {
|
|
147
|
+
return voidElements.some((element) => elementSelector === element)
|
|
148
|
+
? template.replace(/([\S]+)(.*)/, `<$1$2${inputs}${outputs} />`)
|
|
149
|
+
: template.replace(/([\S]+)(.*)/, `<$1$2${inputs}${outputs}>${innerTemplate}</$1>`);
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
];
|
|
153
|
+
return templateReplacers.reduce((prevSelector, [searchValue, replacer]) => prevSelector.replace(searchValue, replacer), firstSelector);
|
|
154
|
+
};
|