@andersundsehr/storybook-typo3 0.1.0 → 0.1.1
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/entry-preview-docs.d.ts +17 -0
- package/dist/entry-preview-docs.js +29 -0
- package/dist/entry-preview.d.ts +10 -0
- package/dist/entry-preview.js +14 -0
- package/dist/functions/convertComponentToSource.d.ts +3 -0
- package/dist/functions/convertComponentToSource.js +118 -0
- package/dist/functions/convertComponentToSource.test.d.ts +1 -0
- package/{src/functions/convertComponentToSource.test.ts → dist/functions/convertComponentToSource.test.js} +44 -50
- package/dist/functions/error.d.ts +1 -0
- package/dist/functions/error.js +5 -0
- package/dist/functions/fetchComponent.d.ts +2 -0
- package/dist/functions/fetchComponent.js +17 -0
- package/dist/functions/fetchPreviewConfig.d.ts +8 -0
- package/dist/functions/fetchPreviewConfig.js +65 -0
- package/dist/functions/fetchRenderAction.d.ts +2 -0
- package/dist/functions/fetchRenderAction.js +20 -0
- package/dist/functions/fetchWithUserRetry.d.ts +1 -0
- package/dist/functions/fetchWithUserRetry.js +23 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/preset.d.ts +10 -0
- package/{src/preset.ts → dist/preset.js} +13 -21
- package/{src/types/types.ts → dist/types/types.d.ts} +19 -51
- package/dist/types/types.js +0 -0
- package/package.json +13 -7
- package/src/entry-preview-docs.ts +0 -31
- package/src/entry-preview.ts +0 -21
- package/src/functions/convertComponentToSource.ts +0 -151
- package/src/functions/fetchComponent.ts +0 -30
- package/src/functions/fetchPreviewConfig.ts +0 -86
- package/src/functions/fetchRenderAction.ts +0 -19
- package/src/functions/fetchWithUserRetry.ts +0 -24
- package/src/index.ts +0 -23
@@ -0,0 +1,17 @@
|
|
1
|
+
import { SourceType } from 'storybook/internal/docs-tools';
|
2
|
+
import { type FluidRenderer } from '@andersundsehr/storybook-typo3';
|
3
|
+
import { DecoratorFunction } from 'storybook/internal/types';
|
4
|
+
export declare const tags: string[];
|
5
|
+
export declare const decorators: DecoratorFunction<FluidRenderer>[];
|
6
|
+
export declare const parameters: {
|
7
|
+
docs: {
|
8
|
+
story: {
|
9
|
+
inline: boolean;
|
10
|
+
};
|
11
|
+
codePanel: boolean;
|
12
|
+
source: {
|
13
|
+
type: SourceType;
|
14
|
+
};
|
15
|
+
toc: boolean;
|
16
|
+
};
|
17
|
+
};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { SNIPPET_RENDERED, SourceType } from 'storybook/internal/docs-tools';
|
2
|
+
import { addons, useEffect } from 'storybook/preview-api';
|
3
|
+
import { convertComponentToSource } from './functions/convertComponentToSource';
|
4
|
+
import { error } from './functions/error';
|
5
|
+
const sourceDecorator = (storyFn, storyContext) => {
|
6
|
+
useEffect(() => {
|
7
|
+
const { id, args, component } = storyContext;
|
8
|
+
if (!component) {
|
9
|
+
error('No component found in story context. This decorator requires a Fluid component to render.', 4123764578913);
|
10
|
+
}
|
11
|
+
const source = convertComponentToSource(component, args);
|
12
|
+
addons.getChannel().emit(SNIPPET_RENDERED, { id, args, source });
|
13
|
+
});
|
14
|
+
return storyFn();
|
15
|
+
};
|
16
|
+
export const tags = ['autodocs'];
|
17
|
+
export const decorators = [sourceDecorator];
|
18
|
+
export const parameters = {
|
19
|
+
docs: {
|
20
|
+
story: {
|
21
|
+
inline: true,
|
22
|
+
},
|
23
|
+
codePanel: true,
|
24
|
+
source: {
|
25
|
+
type: SourceType.DYNAMIC,
|
26
|
+
},
|
27
|
+
toc: true,
|
28
|
+
},
|
29
|
+
};
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { fetchRenderAction } from '@andersundsehr/storybook-typo3';
|
2
|
+
import type { ArgTypesEnhancer, GlobalTypes } from 'storybook/internal/types';
|
3
|
+
export declare const globalTypes: GlobalTypes;
|
4
|
+
export declare const initialGlobals: Record<string, string>;
|
5
|
+
export declare const parameters: {
|
6
|
+
server: {
|
7
|
+
fetchStoryHtml: typeof fetchRenderAction;
|
8
|
+
};
|
9
|
+
};
|
10
|
+
export declare const argTypesEnhancers: ArgTypesEnhancer[];
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { fetchPreviewConfig, fetchRenderAction } from '@andersundsehr/storybook-typo3';
|
2
|
+
import { initGlobalsHandling } from './functions/fetchPreviewConfig';
|
3
|
+
const previewConfig = await fetchPreviewConfig();
|
4
|
+
initGlobalsHandling(previewConfig.globalTypes);
|
5
|
+
export const globalTypes = previewConfig.globalTypes;
|
6
|
+
export const initialGlobals = previewConfig.initialGlobals;
|
7
|
+
export const parameters = {
|
8
|
+
server: {
|
9
|
+
// url: url + '/_storybook/',
|
10
|
+
fetchStoryHtml: fetchRenderAction,
|
11
|
+
},
|
12
|
+
};
|
13
|
+
const enhanceArgTypes = storyContext => storyContext.component?.argTypes || {};
|
14
|
+
export const argTypesEnhancers = [enhanceArgTypes];
|
@@ -0,0 +1,118 @@
|
|
1
|
+
function convertValueToString(key, argType, value, inline) {
|
2
|
+
const createValue = () => {
|
3
|
+
if ('variableName' in value) {
|
4
|
+
if (inline) {
|
5
|
+
return value.variableName; // For inline code, just return the variable name
|
6
|
+
}
|
7
|
+
return '{' + value.variableName + '}'; // For attributes, return the variable name wrapped in curly braces
|
8
|
+
}
|
9
|
+
const valueAsString = String(value.value);
|
10
|
+
if (typeof value.value === 'boolean') {
|
11
|
+
if (inline) {
|
12
|
+
return valueAsString;
|
13
|
+
}
|
14
|
+
return '{' + valueAsString + '}';
|
15
|
+
}
|
16
|
+
if (typeof value.value === 'number') {
|
17
|
+
if (argType?.type?.name === 'int') {
|
18
|
+
return String(Number.parseInt(valueAsString, 10)); // Convert to integer
|
19
|
+
}
|
20
|
+
return String(Number.parseFloat(valueAsString)); // Convert to float
|
21
|
+
}
|
22
|
+
if (inline) {
|
23
|
+
return '\'' + valueAsString.replace(/'/g, '\\\'') + '\''; // Escape single quotes for inline code
|
24
|
+
}
|
25
|
+
return valueAsString.replace(/"/g, '\\"'); // Escape double quotes for attributes
|
26
|
+
};
|
27
|
+
if (inline) {
|
28
|
+
return key + ': ' + createValue();
|
29
|
+
}
|
30
|
+
return key + '="' + createValue() + '"';
|
31
|
+
}
|
32
|
+
function inlineCode(viewHelperArguments, component, defaultSlotContent) {
|
33
|
+
const argsStrings = Object.entries(viewHelperArguments)
|
34
|
+
.map(([argumentName, argumentValue]) => convertValueToString(argumentName, component.argTypes[argumentName], argumentValue, true));
|
35
|
+
const argsString = argsStrings.join(', ');
|
36
|
+
let source = '{';
|
37
|
+
if (defaultSlotContent) {
|
38
|
+
defaultSlotContent = defaultSlotContent.replace(/'/g, '\\\''); // Escape single quotes for inline code
|
39
|
+
source += `'${defaultSlotContent}' -> `;
|
40
|
+
}
|
41
|
+
source += `${component.fullName}(`;
|
42
|
+
if ((source.length + argsString.length) > 80) {
|
43
|
+
source += `\n ${argsStrings.join(',\n ')}\n`;
|
44
|
+
}
|
45
|
+
else {
|
46
|
+
source += argsString;
|
47
|
+
}
|
48
|
+
source += ')}';
|
49
|
+
return source;
|
50
|
+
}
|
51
|
+
function generateOpenTag(viewHelperArguments, component) {
|
52
|
+
const argStrings = Object.entries(viewHelperArguments)
|
53
|
+
.map(([argumentName, argumentValue]) => convertValueToString(argumentName, component.argTypes[argumentName], argumentValue, false));
|
54
|
+
let argsString = argStrings.join(' ');
|
55
|
+
let openTag = `<${component.fullName}`;
|
56
|
+
if (argsString.length > 0) {
|
57
|
+
openTag += ' ' + argsString;
|
58
|
+
}
|
59
|
+
if (openTag.length > 80) {
|
60
|
+
argsString = argStrings.join('\n ');
|
61
|
+
openTag = `<${component.fullName} \n ${argsString}\n`;
|
62
|
+
}
|
63
|
+
return openTag;
|
64
|
+
}
|
65
|
+
const SLOT_PREFIX = 'slot____';
|
66
|
+
function createComponentData(component, args) {
|
67
|
+
const viewHelperArguments = {};
|
68
|
+
const slots = {};
|
69
|
+
for (const [key, value] of Object.entries(args)) {
|
70
|
+
if (key.startsWith(SLOT_PREFIX)) {
|
71
|
+
// This is a slot
|
72
|
+
const slotName = key.replace(SLOT_PREFIX, '');
|
73
|
+
if (value === null || value === undefined || value === '') {
|
74
|
+
continue;
|
75
|
+
}
|
76
|
+
slots[slotName] = value;
|
77
|
+
continue;
|
78
|
+
}
|
79
|
+
if (!key.includes('__')) {
|
80
|
+
// This is a normal argument without virtual subkey
|
81
|
+
viewHelperArguments[key] = { value };
|
82
|
+
continue;
|
83
|
+
}
|
84
|
+
// This is a virtual argument
|
85
|
+
const [variableName] = key.split('__');
|
86
|
+
viewHelperArguments[variableName] = { variableName };
|
87
|
+
}
|
88
|
+
return { viewHelperArguments, slots };
|
89
|
+
}
|
90
|
+
export function convertComponentToSource(component, args) {
|
91
|
+
const { viewHelperArguments, slots } = createComponentData(component, args);
|
92
|
+
let source = '';
|
93
|
+
source += `<html\n xmlns:${component.namespace}="http://typo3.org/ns/${component.collection.replace(/\\/g, '/')}"\n data-namespace-typo3-fluid="true"\n>\n\n`;
|
94
|
+
const usedArgs = [...new Set(Object.keys(args).filter(key => !key.startsWith(SLOT_PREFIX)).map(key => key.split('__')[0]))];
|
95
|
+
source += generateOpenTag(viewHelperArguments, component);
|
96
|
+
if (Object.keys(slots).length <= 0) {
|
97
|
+
source += `/>\n`;
|
98
|
+
source += `\n<!-- or -->\n\n`;
|
99
|
+
source += `{namespace ${component.namespace}=${component.collection}}\n\n`;
|
100
|
+
source += inlineCode(viewHelperArguments, component);
|
101
|
+
return source;
|
102
|
+
}
|
103
|
+
source += '>\n';
|
104
|
+
if (Object.keys(slots).length === 1 && slots.default) {
|
105
|
+
// If there is only a default slot, we can use the inline code
|
106
|
+
source += ` ${slots.default}\n`;
|
107
|
+
source += '</' + component.fullName + '>\n';
|
108
|
+
source += `\n<!-- or -->\n\n`;
|
109
|
+
source += `{namespace ${component.namespace}=${component.collection}}\n\n`;
|
110
|
+
source += inlineCode(viewHelperArguments, component, slots.default);
|
111
|
+
return source;
|
112
|
+
}
|
113
|
+
for (const [slotName, slotValue] of Object.entries(slots)) {
|
114
|
+
source += ` <f:fragment name="${slotName}">${slotValue}</f:fragment>\n`;
|
115
|
+
}
|
116
|
+
source += '</' + component.fullName + '>';
|
117
|
+
return source;
|
118
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -1,17 +1,12 @@
|
|
1
|
-
import test from 'node:test';
|
2
|
-
import { convertComponentToSource } from './convertComponentToSource
|
3
|
-
import type { FluidComponent } from '../types/types.ts';
|
4
|
-
import type { StrictArgs } from 'storybook/internal/csf';
|
5
|
-
|
6
|
-
interface TestCase { component: FluidComponent; args: StrictArgs; expected: string }
|
7
|
-
|
1
|
+
import { test } from 'node:test';
|
2
|
+
import { convertComponentToSource } from './convertComponentToSource';
|
8
3
|
const component = { name: 'test', fullName: 'c:test', argTypes: {}, collection: 'Class', namespace: 'c' };
|
9
|
-
const testCases
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
4
|
+
const testCases = {
|
5
|
+
///////////////////////////////////////////
|
6
|
+
withoutAnyProperties: {
|
7
|
+
component,
|
8
|
+
args: {},
|
9
|
+
expected: `<html
|
15
10
|
xmlns:c="http://typo3.org/ns/Class"
|
16
11
|
data-namespace-typo3-fluid="true"
|
17
12
|
>
|
@@ -23,12 +18,12 @@ const testCases: Record<string, TestCase> = {
|
|
23
18
|
{namespace c=Class}
|
24
19
|
|
25
20
|
{c:test()}`,
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
21
|
+
},
|
22
|
+
///////////////////////////////////////////
|
23
|
+
withSimpleArguments: {
|
24
|
+
component,
|
25
|
+
args: { arg1: 'value1', arg2: 'value2' },
|
26
|
+
expected: `<html
|
32
27
|
xmlns:c="http://typo3.org/ns/Class"
|
33
28
|
data-namespace-typo3-fluid="true"
|
34
29
|
>
|
@@ -40,12 +35,12 @@ const testCases: Record<string, TestCase> = {
|
|
40
35
|
{namespace c=Class}
|
41
36
|
|
42
37
|
{c:test(arg1: 'value1', arg2: 'value2')}`,
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
38
|
+
},
|
39
|
+
///////////////////////////////////////////
|
40
|
+
withSlot: {
|
41
|
+
component,
|
42
|
+
args: { arg1: 'value\'"1', arg2: 'value2', slot____default: 'default \'"slot' },
|
43
|
+
expected: `<html
|
49
44
|
xmlns:c="http://typo3.org/ns/Class"
|
50
45
|
data-namespace-typo3-fluid="true"
|
51
46
|
>
|
@@ -59,12 +54,12 @@ const testCases: Record<string, TestCase> = {
|
|
59
54
|
{namespace c=Class}
|
60
55
|
|
61
56
|
{'default \\'"slot' -> c:test(arg1: 'value\\'"1', arg2: 'value2')}`,
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
57
|
+
},
|
58
|
+
///////////////////////////////////////////
|
59
|
+
withMultipleSlots: {
|
60
|
+
component,
|
61
|
+
args: { slot____a: 'slotA', slot____b: 'slotB', slot____default: 'slotDefault' },
|
62
|
+
expected: `<html
|
68
63
|
xmlns:c="http://typo3.org/ns/Class"
|
69
64
|
data-namespace-typo3-fluid="true"
|
70
65
|
>
|
@@ -74,12 +69,12 @@ const testCases: Record<string, TestCase> = {
|
|
74
69
|
<f:fragment name="b">slotB</f:fragment>
|
75
70
|
<f:fragment name="default">slotDefault</f:fragment>
|
76
71
|
</c:test>`,
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
72
|
+
},
|
73
|
+
///////////////////////////////////////////
|
74
|
+
withTransformedArgument: {
|
75
|
+
component,
|
76
|
+
args: { arg1__argA: 'value1', arg1__argB: 'value2' },
|
77
|
+
expected: `<html
|
83
78
|
xmlns:c="http://typo3.org/ns/Class"
|
84
79
|
data-namespace-typo3-fluid="true"
|
85
80
|
>
|
@@ -91,12 +86,12 @@ const testCases: Record<string, TestCase> = {
|
|
91
86
|
{namespace c=Class}
|
92
87
|
|
93
88
|
{c:test(arg1: arg1)}`,
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
89
|
+
},
|
90
|
+
///////////////////////////////////////////
|
91
|
+
longLines: {
|
92
|
+
component,
|
93
|
+
args: { arg1: 'veryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValue', arg2: 'veryLongValueveryLongValueveryLongValueveryLongValueveryLongValue' },
|
94
|
+
expected: `<html
|
100
95
|
xmlns:c="http://typo3.org/ns/Class"
|
101
96
|
data-namespace-typo3-fluid="true"
|
102
97
|
>
|
@@ -114,13 +109,12 @@ const testCases: Record<string, TestCase> = {
|
|
114
109
|
arg1: 'veryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValueveryLongValue',
|
115
110
|
arg2: 'veryLongValueveryLongValueveryLongValueveryLongValueveryLongValue'
|
116
111
|
)}`,
|
117
|
-
|
118
|
-
|
112
|
+
},
|
113
|
+
///////////////////////////////////////////
|
119
114
|
};
|
120
|
-
|
121
115
|
for (const [name, { component, args, expected }] of Object.entries(testCases)) {
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
116
|
+
test(`convertComponentToSource - ${name}`, (t) => {
|
117
|
+
const result = convertComponentToSource(component, args);
|
118
|
+
t.assert.strictEqual(result, expected);
|
119
|
+
});
|
126
120
|
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare function error(message: string, code: number): never;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { url } from '@andersundsehr/storybook-typo3';
|
2
|
+
import { fetchWithUserRetry } from './fetchWithUserRetry';
|
3
|
+
export async function fetchComponent(component) {
|
4
|
+
if (!component.includes(':')) {
|
5
|
+
const message = 'Component name must be in the format "namespace:name"';
|
6
|
+
alert(message);
|
7
|
+
throw new Error(message);
|
8
|
+
}
|
9
|
+
const data = await fetchWithUserRetry(url + '/_storybook/componentMeta?viewHelper=' + component, {}, 'metadata for component `' + component + '` from TYPO3');
|
10
|
+
return {
|
11
|
+
fullName: component,
|
12
|
+
name: component.split(':')[1],
|
13
|
+
namespace: component.split(':')[0],
|
14
|
+
collection: data.collectionClassName,
|
15
|
+
argTypes: data.argTypes,
|
16
|
+
};
|
17
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import type { GlobalTypes } from 'storybook/internal/types';
|
2
|
+
interface PreviewConfig {
|
3
|
+
globalTypes: GlobalTypes;
|
4
|
+
initialGlobals: Record<string, string>;
|
5
|
+
}
|
6
|
+
export declare function fetchPreviewConfig(currentGlobals?: Record<string, string>): Promise<PreviewConfig>;
|
7
|
+
export declare function initGlobalsHandling(initalGlobalTypes: GlobalTypes): void;
|
8
|
+
export {};
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import { url } from '@andersundsehr/storybook-typo3';
|
2
|
+
import { fetchWithUserRetry } from './fetchWithUserRetry';
|
3
|
+
import { GLOBALS_UPDATED, SET_GLOBALS } from 'storybook/internal/core-events';
|
4
|
+
import { addons } from 'storybook/preview-api';
|
5
|
+
function getLocation() {
|
6
|
+
let location = window.location;
|
7
|
+
if (window.parent?.location && window.parent !== window) {
|
8
|
+
location = window.parent.location;
|
9
|
+
}
|
10
|
+
return location;
|
11
|
+
}
|
12
|
+
function getGlobalsFromUrl() {
|
13
|
+
const url = new URL(getLocation().href);
|
14
|
+
const globalsString = url.searchParams.get('globals') || '';
|
15
|
+
const globalsArray = globalsString.split(';').filter(Boolean);
|
16
|
+
const entries = globalsArray.map(global => global.split(':'));
|
17
|
+
return Object.fromEntries(entries);
|
18
|
+
}
|
19
|
+
function setGlobalsToUrl(globals) {
|
20
|
+
const location = getLocation();
|
21
|
+
const url = new URL(location.href);
|
22
|
+
const globalString = Object.entries(globals).map(([key, value]) => `${key}:${value}`).join(';');
|
23
|
+
url.searchParams.set('globals', globalString);
|
24
|
+
location.href = url.toString();
|
25
|
+
}
|
26
|
+
export async function fetchPreviewConfig(currentGlobals) {
|
27
|
+
const apiEndpoint = url + '/_storybook/preview';
|
28
|
+
const globals = currentGlobals || getGlobalsFromUrl();
|
29
|
+
return await fetchWithUserRetry(apiEndpoint, {
|
30
|
+
method: 'POST',
|
31
|
+
headers: {
|
32
|
+
'Content-Type': 'application/json',
|
33
|
+
},
|
34
|
+
body: JSON.stringify({ globals }),
|
35
|
+
}, 'preview config from TYPO3');
|
36
|
+
}
|
37
|
+
export function initGlobalsHandling(initalGlobalTypes) {
|
38
|
+
const channel = addons.getChannel();
|
39
|
+
let oldGlobalTypes = initalGlobalTypes;
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
41
|
+
channel.on(GLOBALS_UPDATED, async ({ globals }) => {
|
42
|
+
const previewConfig = await fetchPreviewConfig(globals);
|
43
|
+
oldGlobalTypes = previewConfig.globalTypes;
|
44
|
+
const newGlobals = getGlobalsFromUrl();
|
45
|
+
let changed = false;
|
46
|
+
for (const key in previewConfig.globalTypes) {
|
47
|
+
const inputType = previewConfig.globalTypes[key];
|
48
|
+
if (!inputType.toolbar?.items?.some((item) => item.value === globals[key])) {
|
49
|
+
newGlobals[key] = inputType.toolbar?.items?.[0]?.value || '';
|
50
|
+
changed = true;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
if (changed) {
|
54
|
+
// change URL search params and reload page (this is the only way to set the globals, that worked)!!
|
55
|
+
setGlobalsToUrl(newGlobals);
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
if (JSON.stringify(oldGlobalTypes) !== JSON.stringify(previewConfig.globalTypes)) {
|
59
|
+
// this only sets the globalTypes and not the globals :(
|
60
|
+
// see https://github.com/storybookjs/storybook/blob/3ac2fece4c41955e7349f6f825b7d21dd18ff9a9/code/core/src/manager-api/modules/globals.ts#L141-L149
|
61
|
+
channel.emit(SET_GLOBALS, { globals, globalTypes: previewConfig.globalTypes });
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
});
|
65
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { url } from '@andersundsehr/storybook-typo3';
|
2
|
+
import { fetchWithUserRetry } from './fetchWithUserRetry';
|
3
|
+
import { error } from './error';
|
4
|
+
export async function fetchRenderAction(urlA, id, params, storyContext) {
|
5
|
+
if (!storyContext.component) {
|
6
|
+
error('No component found in story context. This function requires a Fluid component to render.', 4123764578913);
|
7
|
+
}
|
8
|
+
const viewHelper = (typeof storyContext.component === 'string') ? storyContext.component : storyContext.component.fullName;
|
9
|
+
const body = {
|
10
|
+
viewHelper: viewHelper,
|
11
|
+
arguments: params,
|
12
|
+
site: storyContext.globals?.site || 'default',
|
13
|
+
siteLanguage: storyContext.globals?.language || 'default',
|
14
|
+
};
|
15
|
+
return await fetchWithUserRetry(url + '/_storybook/render', {
|
16
|
+
method: 'POST',
|
17
|
+
body: JSON.stringify(body),
|
18
|
+
}, 'rendering component in TYPO3', 'text');
|
19
|
+
}
|
20
|
+
;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare function fetchWithUserRetry<T>(url: string, options: RequestInit, message: string, result?: 'json' | 'text'): Promise<T>;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
export async function fetchWithUserRetry(url, options, message, result = 'json') {
|
2
|
+
options = { ...options }; // Clone options to avoid mutating the original
|
3
|
+
options.signal = options.signal || AbortSignal.timeout(5000);
|
4
|
+
try {
|
5
|
+
const response = await fetch(url, options);
|
6
|
+
if (result === 'text') {
|
7
|
+
return await response.text();
|
8
|
+
}
|
9
|
+
return await response.json();
|
10
|
+
}
|
11
|
+
catch (error) {
|
12
|
+
console.error('Fetch failed:', { error });
|
13
|
+
const retry = confirm('Error while fetching ' + message + '. \n\n'
|
14
|
+
+ 'fetch: ' + url + '\n'
|
15
|
+
+ 'ERROR: ' + String(error) + '\n\n'
|
16
|
+
+ 'look at the console for more details.\n\n'
|
17
|
+
+ 'Do you want to retry?');
|
18
|
+
if (retry) {
|
19
|
+
return fetchWithUserRetry(url, options, message);
|
20
|
+
}
|
21
|
+
throw error; // Re-throw the error if the user does not want to retry
|
22
|
+
}
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
export * from './functions/fetchRenderAction';
|
2
|
+
export * from './functions/fetchPreviewConfig';
|
3
|
+
export * from './functions/fetchComponent';
|
4
|
+
export * from './functions/fetchWithUserRetry';
|
5
|
+
export * from './types/types';
|
6
|
+
declare const url: string;
|
7
|
+
export { url };
|
package/dist/index.js
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
/// <reference types="vite/client" />
|
2
|
+
export * from './functions/fetchRenderAction';
|
3
|
+
export * from './functions/fetchPreviewConfig';
|
4
|
+
export * from './functions/fetchComponent';
|
5
|
+
export * from './functions/fetchWithUserRetry';
|
6
|
+
export * from './types/types';
|
7
|
+
const url = (() => {
|
8
|
+
let url = import.meta.env.STORYBOOK_TYPO3_ENDPOINT;
|
9
|
+
if (typeof url !== 'string' || !url) {
|
10
|
+
throw new Error('env STORYBOOK_TYPO3_ENDPOINT is not set or is not a string');
|
11
|
+
}
|
12
|
+
url = url.replace(/_storybook\/?$/, '');
|
13
|
+
url = url.replace(/\/$/, '');
|
14
|
+
if (typeof url !== 'string' || !url) {
|
15
|
+
throw new Error('env STORYBOOK_TYPO3_ENDPOINT is not set or is not a string');
|
16
|
+
}
|
17
|
+
return url;
|
18
|
+
})();
|
19
|
+
export { url };
|
package/dist/preset.d.ts
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
import type { PresetProperty } from 'storybook/internal/types';
|
2
|
+
export declare const addons: string[];
|
3
|
+
export declare const core: PresetProperty<'core'>;
|
4
|
+
export declare const previewAnnotations: PresetProperty<'previewAnnotations'>;
|
5
|
+
export declare const tags: string[];
|
6
|
+
/**
|
7
|
+
* BUGFIX for chromium based browsers and windows
|
8
|
+
* @see https://github.com/talkjs/country-flag-emoji-polyfill?tab=readme-ov-file
|
9
|
+
*/
|
10
|
+
export declare const managerHead = "<style>\n body {\n font-family: \"Twemoji Country Flags\", \"Nunito Sans\", -apple-system, \".SFNSText-Regular\", \"San Francisco\", BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Helvetica, Arial, sans-serif !important;\n }\n @font-face {\n font-family: \"Twemoji Country Flags\";\n unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;\n src: url('https://cdn.jsdelivr.net/npm/country-flag-emoji-polyfill@0.1/dist/TwemojiCountryFlags.woff2') format('woff2');\n font-display: swap;\n }\n</style>";
|
@@ -1,31 +1,23 @@
|
|
1
1
|
import { dirname, join } from 'node:path';
|
2
|
-
|
3
|
-
|
4
|
-
function getAbsolutePath<I extends string>(value: I): I {
|
5
|
-
return dirname(require.resolve(join(value, 'package.json'))) as any;
|
2
|
+
function getAbsolutePath(value) {
|
3
|
+
return dirname(require.resolve(join(value, 'package.json')));
|
6
4
|
}
|
7
|
-
|
8
5
|
export const addons = [
|
9
|
-
|
10
|
-
|
6
|
+
'@storybook/addon-docs',
|
7
|
+
'@storybook/addon-a11y',
|
11
8
|
];
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
disableTelemetry: true,
|
9
|
+
export const core = {
|
10
|
+
builder: getAbsolutePath('@storybook/builder-vite'),
|
11
|
+
renderer: getAbsolutePath('@storybook/server'),
|
12
|
+
disableTelemetry: true,
|
17
13
|
};
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
.concat(require.resolve('./entry-preview'))
|
24
|
-
.concat(docsEnabled ? [require.resolve('./entry-preview-docs')] : []);
|
14
|
+
export const previewAnnotations = async (entry = [], options) => {
|
15
|
+
const docsEnabled = Object.keys(await options.presets.apply('docs', {}, options)).length > 0;
|
16
|
+
return entry
|
17
|
+
.concat(require.resolve('./entry-preview'))
|
18
|
+
.concat(docsEnabled ? [require.resolve('./entry-preview-docs')] : []);
|
25
19
|
};
|
26
|
-
|
27
20
|
export const tags = ['autodocs'];
|
28
|
-
|
29
21
|
/**
|
30
22
|
* BUGFIX for chromium based browsers and windows
|
31
23
|
* @see https://github.com/talkjs/country-flag-emoji-polyfill?tab=readme-ov-file
|
@@ -1,67 +1,37 @@
|
|
1
|
-
// export type { StoryObj } from '@storybook/server';
|
2
1
|
import type { Args, StrictArgs } from '@storybook/server';
|
3
|
-
import {
|
4
|
-
AnnotatedStoryFn,
|
5
|
-
CompatibleString,
|
6
|
-
ComponentAnnotations,
|
7
|
-
DecoratorFunction,
|
8
|
-
LoaderFunction,
|
9
|
-
ProjectAnnotations,
|
10
|
-
StoryAnnotations,
|
11
|
-
StorybookConfig as StorybookConfigBase,
|
12
|
-
StoryContext as StoryContext$1,
|
13
|
-
WebRenderer,
|
14
|
-
} from 'storybook/internal/types';
|
15
|
-
|
2
|
+
import { AnnotatedStoryFn, CompatibleString, ComponentAnnotations, DecoratorFunction, LoaderFunction, ProjectAnnotations, StoryAnnotations, StorybookConfig as StorybookConfigBase, StoryContext as StoryContext$1, WebRenderer } from 'storybook/internal/types';
|
16
3
|
import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite';
|
17
|
-
|
18
4
|
type FrameworkName = CompatibleString<'@andersundsehr/storybook-typo3'>;
|
19
5
|
type BuilderName = CompatibleString<'@storybook/builder-vite'>;
|
20
|
-
|
21
6
|
export interface FrameworkOptions {
|
22
|
-
|
7
|
+
builder?: BuilderOptions;
|
23
8
|
}
|
24
|
-
|
25
9
|
interface StorybookConfigFramework {
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
10
|
+
framework: FrameworkName | {
|
11
|
+
name: FrameworkName;
|
12
|
+
options: FrameworkOptions;
|
13
|
+
};
|
14
|
+
core?: StorybookConfigBase['core'] & {
|
15
|
+
builder?: BuilderName | {
|
16
|
+
name: BuilderName;
|
17
|
+
options: BuilderOptions;
|
18
|
+
};
|
31
19
|
};
|
32
|
-
core?: StorybookConfigBase['core'] & {
|
33
|
-
builder?:
|
34
|
-
| BuilderName
|
35
|
-
| {
|
36
|
-
name: BuilderName;
|
37
|
-
options: BuilderOptions;
|
38
|
-
};
|
39
|
-
};
|
40
20
|
}
|
41
|
-
|
42
21
|
/** The interface for Storybook configuration in `main.ts` files. */
|
43
|
-
export type StorybookConfig = Omit<
|
44
|
-
StorybookConfigBase,
|
45
|
-
keyof StorybookConfigVite | keyof StorybookConfigFramework
|
46
|
-
> &
|
47
|
-
StorybookConfigVite &
|
48
|
-
StorybookConfigFramework;
|
49
|
-
|
22
|
+
export type StorybookConfig = Omit<StorybookConfigBase, keyof StorybookConfigVite | keyof StorybookConfigFramework> & StorybookConfigVite & StorybookConfigFramework;
|
50
23
|
type StoryFnServerReturnType = any;
|
51
|
-
|
52
24
|
export interface FluidComponent {
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
25
|
+
fullName: string;
|
26
|
+
name: string;
|
27
|
+
namespace: string;
|
28
|
+
collection: string;
|
29
|
+
argTypes: Record<string, any>;
|
58
30
|
}
|
59
|
-
|
60
31
|
interface FluidRenderer extends WebRenderer {
|
61
|
-
|
62
|
-
|
32
|
+
component: FluidComponent;
|
33
|
+
storyResult: StoryFnServerReturnType;
|
63
34
|
}
|
64
|
-
|
65
35
|
/**
|
66
36
|
* Metadata to configure the stories for a component.
|
67
37
|
*
|
@@ -80,10 +50,8 @@ type StoryFn<TArgs = Args> = AnnotatedStoryFn<FluidRenderer, TArgs>;
|
|
80
50
|
* @see [Named Story exports](https://storybook.js.org/docs/api/csf#named-story-exports)
|
81
51
|
*/
|
82
52
|
type StoryObj<TArgs = Args> = StoryAnnotations<FluidRenderer, TArgs>;
|
83
|
-
|
84
53
|
type Decorator<TArgs = StrictArgs> = DecoratorFunction<FluidRenderer, TArgs>;
|
85
54
|
type Loader<TArgs = StrictArgs> = LoaderFunction<FluidRenderer, TArgs>;
|
86
55
|
type StoryContext<TArgs = StrictArgs> = StoryContext$1<FluidRenderer, TArgs>;
|
87
56
|
type Preview = ProjectAnnotations<FluidRenderer>;
|
88
|
-
|
89
57
|
export { Decorator, Loader, Meta, Preview, FluidRenderer, StoryContext, StoryFn, StoryObj };
|
File without changes
|
package/package.json
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
{
|
2
2
|
"name": "@andersundsehr/storybook-typo3",
|
3
3
|
"type": "module",
|
4
|
-
"version": "0.1.
|
4
|
+
"version": "0.1.1",
|
5
5
|
"exports": {
|
6
6
|
".": {
|
7
|
-
"import": "./
|
8
|
-
"types": "./
|
7
|
+
"import": "./dist/index.js",
|
8
|
+
"types": "./dist/index.js"
|
9
9
|
},
|
10
10
|
"./preset": {
|
11
|
-
"require": "./
|
12
|
-
"types": "./
|
11
|
+
"require": "./dist/preset.js",
|
12
|
+
"types": "./dist/preset.js"
|
13
13
|
}
|
14
14
|
},
|
15
15
|
"dependencies": {
|
@@ -19,8 +19,14 @@
|
|
19
19
|
"@storybook/addon-a11y": "^9.0.0",
|
20
20
|
"storybook": "^9.0.0"
|
21
21
|
},
|
22
|
+
"devDependencies": {
|
23
|
+
"typescript": "^5.8.3"
|
24
|
+
},
|
22
25
|
"scripts": {
|
23
26
|
"test": "node --test --experimental-strip-types",
|
24
|
-
"test:watch": "node --test --experimental-strip-types --watch"
|
25
|
-
|
27
|
+
"test:watch": "node --test --experimental-strip-types --watch",
|
28
|
+
"build": "tsc",
|
29
|
+
"prepublishOnly": "npm run build"
|
30
|
+
},
|
31
|
+
"files": ["dist"]
|
26
32
|
}
|
@@ -1,31 +0,0 @@
|
|
1
|
-
import { SNIPPET_RENDERED, SourceType } from 'storybook/internal/docs-tools';
|
2
|
-
import { type FluidRenderer } from '@andersundsehr/storybook-typo3';
|
3
|
-
import { DecoratorFunction } from 'storybook/internal/types';
|
4
|
-
import { addons, useEffect } from 'storybook/preview-api';
|
5
|
-
import { convertComponentToSource } from './functions/convertComponentToSource';
|
6
|
-
|
7
|
-
const sourceDecorator: DecoratorFunction<FluidRenderer> = (storyFn, storyContext) => {
|
8
|
-
useEffect(() => {
|
9
|
-
const { id, args, component } = storyContext;
|
10
|
-
const source = convertComponentToSource(component, args);
|
11
|
-
addons.getChannel().emit(SNIPPET_RENDERED, { id, args, source });
|
12
|
-
});
|
13
|
-
|
14
|
-
return storyFn();
|
15
|
-
};
|
16
|
-
|
17
|
-
export const tags = ['autodocs'];
|
18
|
-
|
19
|
-
export const decorators: DecoratorFunction<FluidRenderer>[] = [sourceDecorator];
|
20
|
-
export const parameters = {
|
21
|
-
docs: {
|
22
|
-
story: {
|
23
|
-
inline: true,
|
24
|
-
},
|
25
|
-
codePanel: true,
|
26
|
-
source: {
|
27
|
-
type: SourceType.DYNAMIC,
|
28
|
-
},
|
29
|
-
toc: true,
|
30
|
-
},
|
31
|
-
};
|
package/src/entry-preview.ts
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
import { fetchPreviewConfig, fetchRenderAction, type StoryContext } from '@andersundsehr/storybook-typo3';
|
2
|
-
import type { ArgTypesEnhancer, GlobalTypes } from 'storybook/internal/types';
|
3
|
-
import { initGlobalsHandling } from './functions/fetchPreviewConfig.ts';
|
4
|
-
|
5
|
-
const previewConfig = await fetchPreviewConfig();
|
6
|
-
|
7
|
-
initGlobalsHandling(previewConfig.globalTypes);
|
8
|
-
|
9
|
-
export const globalTypes: GlobalTypes = previewConfig.globalTypes;
|
10
|
-
|
11
|
-
export const initialGlobals: Record<string, string> = previewConfig.initialGlobals;
|
12
|
-
|
13
|
-
export const parameters = {
|
14
|
-
server: {
|
15
|
-
// url: url + '/_storybook/',
|
16
|
-
fetchStoryHtml: fetchRenderAction,
|
17
|
-
},
|
18
|
-
};
|
19
|
-
|
20
|
-
const enhanceArgTypes: ArgTypesEnhancer = (storyContext: StoryContext) => storyContext.component?.argTypes || {};
|
21
|
-
export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes];
|
@@ -1,151 +0,0 @@
|
|
1
|
-
import type { FluidComponent } from '../types/types';
|
2
|
-
import type { StrictArgs } from 'storybook/internal/csf';
|
3
|
-
|
4
|
-
function convertValueToString(key: string, argType: { type?: { name?: 'int' } }, value: ArgumentValue, inline: boolean): string {
|
5
|
-
const createValue = () => {
|
6
|
-
if ('variableName' in value) {
|
7
|
-
if (inline) {
|
8
|
-
return value.variableName; // For inline code, just return the variable name
|
9
|
-
}
|
10
|
-
return '{' + value.variableName + '}'; // For attributes, return the variable name wrapped in curly braces
|
11
|
-
}
|
12
|
-
const valueAsString = String(value.value);
|
13
|
-
if (typeof value.value === 'boolean') {
|
14
|
-
if (inline) {
|
15
|
-
return valueAsString;
|
16
|
-
}
|
17
|
-
return '{' + valueAsString + '}';
|
18
|
-
}
|
19
|
-
if (typeof value.value === 'number') {
|
20
|
-
if (argType?.type?.name === 'int') {
|
21
|
-
return String(Number.parseInt(valueAsString, 10)); // Convert to integer
|
22
|
-
}
|
23
|
-
return String(Number.parseFloat(valueAsString)); // Convert to float
|
24
|
-
}
|
25
|
-
if (inline) {
|
26
|
-
return '\'' + valueAsString.replace(/'/g, '\\\'') + '\''; // Escape single quotes for inline code
|
27
|
-
}
|
28
|
-
return valueAsString.replace(/"/g, '\\"'); // Escape double quotes for attributes
|
29
|
-
};
|
30
|
-
|
31
|
-
if (inline) {
|
32
|
-
return key + ': ' + createValue();
|
33
|
-
}
|
34
|
-
return key + '="' + createValue() + '"';
|
35
|
-
}
|
36
|
-
|
37
|
-
function inlineCode(viewHelperArguments: Arguments, component: FluidComponent, defaultSlotContent?: string) {
|
38
|
-
const argsStrings: string[] = Object.entries(viewHelperArguments)
|
39
|
-
.map(([argumentName, argumentValue]) => convertValueToString(argumentName, component.argTypes[argumentName], argumentValue, true));
|
40
|
-
const argsString = argsStrings.join(', ');
|
41
|
-
|
42
|
-
let source = '{';
|
43
|
-
|
44
|
-
if (defaultSlotContent) {
|
45
|
-
defaultSlotContent = defaultSlotContent.replace(/'/g, '\\\''); // Escape single quotes for inline code
|
46
|
-
source += `'${defaultSlotContent}' -> `;
|
47
|
-
}
|
48
|
-
source += `${component.fullName}(`;
|
49
|
-
if ((source.length + argsString.length) > 80) {
|
50
|
-
source += `\n ${argsStrings.join(',\n ')}\n`;
|
51
|
-
} else {
|
52
|
-
source += argsString;
|
53
|
-
}
|
54
|
-
|
55
|
-
source += ')}';
|
56
|
-
return source;
|
57
|
-
}
|
58
|
-
|
59
|
-
function generateOpenTag(viewHelperArguments: Arguments, component: FluidComponent) {
|
60
|
-
const argStrings: string[] = Object.entries(viewHelperArguments)
|
61
|
-
.map(([argumentName, argumentValue]) => convertValueToString(argumentName, component.argTypes[argumentName], argumentValue, false));
|
62
|
-
let argsString = argStrings.join(' ');
|
63
|
-
let openTag = `<${component.fullName}`;
|
64
|
-
if (argsString.length > 0) {
|
65
|
-
openTag += ' ' + argsString;
|
66
|
-
}
|
67
|
-
if (openTag.length > 80) {
|
68
|
-
argsString = argStrings.join('\n ');
|
69
|
-
openTag = `<${component.fullName} \n ${argsString}\n`;
|
70
|
-
}
|
71
|
-
return openTag;
|
72
|
-
}
|
73
|
-
|
74
|
-
const SLOT_PREFIX = 'slot____';
|
75
|
-
|
76
|
-
type ArgumentValue = {
|
77
|
-
value: unknown;
|
78
|
-
} | {
|
79
|
-
variableName: string;
|
80
|
-
};
|
81
|
-
|
82
|
-
type Arguments = Record<string, ArgumentValue>;
|
83
|
-
type Slots = Record<string, string>;
|
84
|
-
interface ComponentData { viewHelperArguments: Arguments; slots: Slots }
|
85
|
-
|
86
|
-
function createComponentData(component: FluidComponent, args: StrictArgs): ComponentData {
|
87
|
-
const viewHelperArguments: Arguments = {};
|
88
|
-
const slots: Slots = {};
|
89
|
-
|
90
|
-
for (const [key, value] of Object.entries(args)) {
|
91
|
-
if (key.startsWith(SLOT_PREFIX)) {
|
92
|
-
// This is a slot
|
93
|
-
const slotName = key.replace(SLOT_PREFIX, '');
|
94
|
-
if (value === null || value === undefined || value === '') {
|
95
|
-
continue;
|
96
|
-
}
|
97
|
-
|
98
|
-
slots[slotName] = value as string;
|
99
|
-
continue;
|
100
|
-
}
|
101
|
-
|
102
|
-
if (!key.includes('__')) {
|
103
|
-
// This is a normal argument without virtual subkey
|
104
|
-
viewHelperArguments[key] = { value };
|
105
|
-
continue;
|
106
|
-
}
|
107
|
-
|
108
|
-
// This is a virtual argument
|
109
|
-
const [variableName] = key.split('__');
|
110
|
-
viewHelperArguments[variableName] = { variableName };
|
111
|
-
}
|
112
|
-
|
113
|
-
return { viewHelperArguments, slots };
|
114
|
-
}
|
115
|
-
|
116
|
-
export function convertComponentToSource(component: FluidComponent, args: StrictArgs): string {
|
117
|
-
const { viewHelperArguments, slots } = createComponentData(component, args);
|
118
|
-
|
119
|
-
let source = '';
|
120
|
-
source += `<html\n xmlns:${component.namespace}="http://typo3.org/ns/${component.collection.replace(/\\/g, '/')}"\n data-namespace-typo3-fluid="true"\n>\n\n`;
|
121
|
-
|
122
|
-
const usedArgs: string[] = [...new Set(Object.keys(args).filter(key => !key.startsWith(SLOT_PREFIX)).map(key => key.split('__')[0]))];
|
123
|
-
|
124
|
-
source += generateOpenTag(viewHelperArguments, component);
|
125
|
-
|
126
|
-
if (Object.keys(slots).length <= 0) {
|
127
|
-
source += `/>\n`;
|
128
|
-
source += `\n<!-- or -->\n\n`;
|
129
|
-
source += `{namespace ${component.namespace}=${component.collection}}\n\n`;
|
130
|
-
source += inlineCode(viewHelperArguments, component);
|
131
|
-
return source;
|
132
|
-
}
|
133
|
-
source += '>\n';
|
134
|
-
|
135
|
-
if (Object.keys(slots).length === 1 && slots.default) {
|
136
|
-
// If there is only a default slot, we can use the inline code
|
137
|
-
source += ` ${slots.default}\n`;
|
138
|
-
source += '</' + component.fullName + '>\n';
|
139
|
-
source += `\n<!-- or -->\n\n`;
|
140
|
-
source += `{namespace ${component.namespace}=${component.collection}}\n\n`;
|
141
|
-
source += inlineCode(viewHelperArguments, component, slots.default);
|
142
|
-
return source;
|
143
|
-
}
|
144
|
-
|
145
|
-
for (const [slotName, slotValue] of Object.entries(slots)) {
|
146
|
-
source += ` <f:fragment name="${slotName}">${slotValue}</f:fragment>\n`;
|
147
|
-
}
|
148
|
-
source += '</' + component.fullName + '>';
|
149
|
-
|
150
|
-
return source;
|
151
|
-
}
|
@@ -1,30 +0,0 @@
|
|
1
|
-
import { type FluidComponent, url } from '@andersundsehr/storybook-typo3';
|
2
|
-
|
3
|
-
import { fetchWithUserRetry } from './fetchWithUserRetry.ts';
|
4
|
-
|
5
|
-
interface ComponentMetaData {
|
6
|
-
collectionClassName: string;
|
7
|
-
argTypes: Record<string, any>;
|
8
|
-
}
|
9
|
-
|
10
|
-
export async function fetchComponent(component: string): Promise<FluidComponent> {
|
11
|
-
if (!component.includes(':')) {
|
12
|
-
const message = 'Component name must be in the format "namespace:name"';
|
13
|
-
alert(message);
|
14
|
-
throw new Error(message);
|
15
|
-
}
|
16
|
-
|
17
|
-
const data = await fetchWithUserRetry<ComponentMetaData>(
|
18
|
-
url + '/_storybook/componentMeta?viewHelper=' + component,
|
19
|
-
{},
|
20
|
-
'metadata for component `' + component + '` from TYPO3',
|
21
|
-
);
|
22
|
-
|
23
|
-
return {
|
24
|
-
fullName: component,
|
25
|
-
name: component.split(':')[1],
|
26
|
-
namespace: component.split(':')[0],
|
27
|
-
collection: data.collectionClassName,
|
28
|
-
argTypes: data.argTypes,
|
29
|
-
};
|
30
|
-
}
|
@@ -1,86 +0,0 @@
|
|
1
|
-
import { url } from '@andersundsehr/storybook-typo3';
|
2
|
-
import { fetchWithUserRetry } from './fetchWithUserRetry.ts';
|
3
|
-
import type { GlobalTypes, InputType } from 'storybook/internal/types';
|
4
|
-
import { GLOBALS_UPDATED, SET_GLOBALS } from 'storybook/internal/core-events';
|
5
|
-
import { addons } from 'storybook/preview-api';
|
6
|
-
|
7
|
-
function getLocation(): Location {
|
8
|
-
let location = window.location;
|
9
|
-
if (window.parent?.location && window.parent !== window) {
|
10
|
-
location = window.parent.location;
|
11
|
-
}
|
12
|
-
return location;
|
13
|
-
}
|
14
|
-
|
15
|
-
function getGlobalsFromUrl(): Record<string, string> {
|
16
|
-
const url = new URL(getLocation().href);
|
17
|
-
const globalsString = url.searchParams.get('globals') || '';
|
18
|
-
const globalsArray = globalsString.split(';').filter(Boolean);
|
19
|
-
const entries = globalsArray.map(global => global.split(':'));
|
20
|
-
return Object.fromEntries(entries);
|
21
|
-
}
|
22
|
-
|
23
|
-
function setGlobalsToUrl(globals: Record<string, string>) {
|
24
|
-
const location = getLocation();
|
25
|
-
const url = new URL(location.href);
|
26
|
-
const globalString = Object.entries(globals).map(([key, value]) => `${key}:${value}`).join(';');
|
27
|
-
url.searchParams.set('globals', globalString);
|
28
|
-
location.href = url.toString();
|
29
|
-
}
|
30
|
-
|
31
|
-
interface PreviewConfig {
|
32
|
-
globalTypes: GlobalTypes;
|
33
|
-
initialGlobals: Record<string, string>;
|
34
|
-
}
|
35
|
-
|
36
|
-
export async function fetchPreviewConfig(currentGlobals?: Record<string, string>): Promise<PreviewConfig> {
|
37
|
-
const apiEndpoint = url + '/_storybook/preview';
|
38
|
-
|
39
|
-
const globals = currentGlobals || getGlobalsFromUrl();
|
40
|
-
|
41
|
-
return await fetchWithUserRetry<PreviewConfig>(
|
42
|
-
apiEndpoint,
|
43
|
-
{
|
44
|
-
method: 'POST',
|
45
|
-
headers: {
|
46
|
-
'Content-Type': 'application/json',
|
47
|
-
},
|
48
|
-
body: JSON.stringify({ globals }),
|
49
|
-
},
|
50
|
-
'preview config from TYPO3',
|
51
|
-
);
|
52
|
-
}
|
53
|
-
|
54
|
-
export function initGlobalsHandling(initalGlobalTypes: GlobalTypes) {
|
55
|
-
const channel = addons.getChannel();
|
56
|
-
let oldGlobalTypes = initalGlobalTypes;
|
57
|
-
channel.on(GLOBALS_UPDATED, async ({ globals }: { globals: Record<string, string> }) => {
|
58
|
-
const previewConfig = await fetchPreviewConfig(globals);
|
59
|
-
|
60
|
-
oldGlobalTypes = previewConfig.globalTypes;
|
61
|
-
|
62
|
-
const newGlobals: Record<string, string> = getGlobalsFromUrl();
|
63
|
-
|
64
|
-
let changed = false;
|
65
|
-
for (const key in previewConfig.globalTypes) {
|
66
|
-
const inputType: InputType = previewConfig.globalTypes[key];
|
67
|
-
if (!inputType.toolbar?.items?.some(item => item.value === globals[key])) {
|
68
|
-
newGlobals[key] = inputType.toolbar?.items?.[0]?.value || '';
|
69
|
-
changed = true;
|
70
|
-
}
|
71
|
-
}
|
72
|
-
|
73
|
-
if (changed) {
|
74
|
-
// change URL search params and reload page (this is the only way to set the globals, that worked)!!
|
75
|
-
setGlobalsToUrl(newGlobals);
|
76
|
-
return;
|
77
|
-
}
|
78
|
-
|
79
|
-
if (JSON.stringify(oldGlobalTypes) !== JSON.stringify(previewConfig.globalTypes)) {
|
80
|
-
// this only sets the globalTypes and not the globals :(
|
81
|
-
// see https://github.com/storybookjs/storybook/blob/3ac2fece4c41955e7349f6f825b7d21dd18ff9a9/code/core/src/manager-api/modules/globals.ts#L141-L149
|
82
|
-
channel.emit(SET_GLOBALS, { globals, globalTypes: previewConfig.globalTypes });
|
83
|
-
return;
|
84
|
-
}
|
85
|
-
});
|
86
|
-
}
|
@@ -1,19 +0,0 @@
|
|
1
|
-
import { url, type StoryContext } from '@andersundsehr/storybook-typo3';
|
2
|
-
|
3
|
-
import { fetchWithUserRetry } from './fetchWithUserRetry.ts';
|
4
|
-
|
5
|
-
export async function fetchRenderAction(urlA: string, id: string, params: any, storyContext: StoryContext): Promise<string> {
|
6
|
-
const viewHelper = (typeof storyContext.component === 'string') ? storyContext.component : storyContext.component.fullName;
|
7
|
-
|
8
|
-
const body = {
|
9
|
-
viewHelper: viewHelper,
|
10
|
-
arguments: params,
|
11
|
-
site: storyContext.globals?.site || 'default',
|
12
|
-
siteLanguage: storyContext.globals?.language || 'default',
|
13
|
-
};
|
14
|
-
|
15
|
-
return await fetchWithUserRetry<string>(url + '/_storybook/render', {
|
16
|
-
method: 'POST',
|
17
|
-
body: JSON.stringify(body),
|
18
|
-
}, 'rendering component in TYPO3', 'text');
|
19
|
-
};
|
@@ -1,24 +0,0 @@
|
|
1
|
-
export async function fetchWithUserRetry<T>(url: string, options: RequestInit, message: string, result: 'json' | 'text' = 'json'): Promise<T> {
|
2
|
-
options = { ...options }; // Clone options to avoid mutating the original
|
3
|
-
options.signal = options.signal || AbortSignal.timeout(5000);
|
4
|
-
try {
|
5
|
-
const response = await fetch(url, options);
|
6
|
-
if (result === 'text') {
|
7
|
-
return await response.text() as T;
|
8
|
-
}
|
9
|
-
return await response.json() as T;
|
10
|
-
} catch (error) {
|
11
|
-
console.error('Fetch failed:', { error });
|
12
|
-
const retry = confirm(
|
13
|
-
'Error while fetching ' + message + '. \n\n'
|
14
|
-
+ 'fetch: ' + url + '\n'
|
15
|
-
+ 'ERROR: ' + String(error) + '\n\n'
|
16
|
-
+ 'look at the console for more details.\n\n'
|
17
|
-
+ 'Do you want to retry?',
|
18
|
-
);
|
19
|
-
if (retry) {
|
20
|
-
return fetchWithUserRetry(url, options, message);
|
21
|
-
}
|
22
|
-
throw error; // Re-throw the error if the user does not want to retry
|
23
|
-
}
|
24
|
-
}
|
package/src/index.ts
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
export * from './functions/fetchRenderAction.ts';
|
2
|
-
export * from './functions/fetchPreviewConfig.ts';
|
3
|
-
export * from './functions/fetchComponent';
|
4
|
-
export * from './functions/fetchWithUserRetry.ts';
|
5
|
-
export * from './types/types';
|
6
|
-
|
7
|
-
const url = (() => {
|
8
|
-
let url = import.meta.env.STORYBOOK_TYPO3_ENDPOINT;
|
9
|
-
|
10
|
-
if (typeof url !== 'string' || !url) {
|
11
|
-
throw new Error('env STORYBOOK_TYPO3_ENDPOINT is not set or is not a string');
|
12
|
-
}
|
13
|
-
|
14
|
-
url = url.replace(/_storybook\/?$/, '');
|
15
|
-
url = url.replace(/\/$/, '');
|
16
|
-
|
17
|
-
if (typeof url !== 'string' || !url) {
|
18
|
-
throw new Error('env STORYBOOK_TYPO3_ENDPOINT is not set or is not a string');
|
19
|
-
}
|
20
|
-
return url;
|
21
|
-
})();
|
22
|
-
|
23
|
-
export { url };
|