@storybook-astro/framework 0.1.0-beta.9 → 1.0.0
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/README.md +38 -0
- package/dist/base-IRZo3zgK.d.ts +23 -0
- package/dist/chunk-4SWPVM6R.js +96 -0
- package/dist/chunk-4SWPVM6R.js.map +1 -0
- package/dist/chunk-5EF25G5S.js +69 -0
- package/dist/chunk-5EF25G5S.js.map +1 -0
- package/dist/chunk-7GHEQUPV.js +439 -0
- package/dist/chunk-7GHEQUPV.js.map +1 -0
- package/dist/chunk-C5OH4VBR.js +492 -0
- package/dist/chunk-C5OH4VBR.js.map +1 -0
- package/dist/chunk-DNGQBPT7.js +15 -0
- package/dist/chunk-DNGQBPT7.js.map +1 -0
- package/dist/chunk-E4LB75JN.js +89 -0
- package/dist/chunk-E4LB75JN.js.map +1 -0
- package/dist/chunk-PJEDXZVN.js +240 -0
- package/dist/chunk-PJEDXZVN.js.map +1 -0
- package/dist/chunk-UK43WNEA.js +657 -0
- package/dist/chunk-UK43WNEA.js.map +1 -0
- package/dist/dist-HJOEPVRQ.js +15574 -0
- package/dist/dist-HJOEPVRQ.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +13 -64
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +138 -0
- package/dist/integrations/index.js +8 -196
- package/dist/integrations/index.js.map +1 -1
- package/dist/middleware.d.ts +26 -0
- package/dist/middleware.js +179 -0
- package/dist/middleware.js.map +1 -0
- package/dist/portable-stories-BvdaQigq.d.ts +83 -0
- package/dist/preset.d.ts +14 -0
- package/dist/preset.js +5 -1
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +324 -15539
- package/dist/testing.js.map +1 -1
- package/dist/types-CHTsRtA7.d.ts +42 -0
- package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js +11 -0
- package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map +1 -0
- package/dist/vitest/index.d.ts +19 -0
- package/dist/vitest/index.js +229 -0
- package/dist/vitest/index.js.map +1 -0
- package/package.json +31 -17
- package/src/importAstroConfig.ts +11 -0
- package/src/index.ts +20 -6
- package/src/integrations/alpine.ts +5 -2
- package/src/integrations/base.ts +2 -2
- package/src/integrations/moduleResolver.ts +43 -0
- package/src/integrations/preact.ts +5 -2
- package/src/integrations/react.ts +5 -2
- package/src/integrations/solid.ts +5 -2
- package/src/integrations/svelte.ts +5 -2
- package/src/integrations/vue.ts +5 -2
- package/src/lib/sanitization.test.ts +232 -0
- package/src/lib/sanitization.ts +338 -0
- package/src/lib/ssr-load-module-with-fs-fallback.ts +29 -0
- package/src/middleware.test.ts +48 -0
- package/src/middleware.ts +204 -96
- package/src/module-mocks.ts +16 -0
- package/src/msw-helpers.ts +1 -0
- package/src/msw.ts +58 -0
- package/src/preset.ts +38 -3
- package/src/rules-options.test.ts +71 -0
- package/src/rules-options.ts +87 -0
- package/src/rules.test.ts +183 -0
- package/src/rules.ts +314 -0
- package/src/testing/astro-runtime.ts +219 -0
- package/src/testing/component-utils.ts +32 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/integration-config.ts +121 -0
- package/src/testing/project-root.ts +185 -0
- package/src/testing/renderer-daemon.ts +269 -0
- package/src/testing/story-composition.ts +33 -0
- package/src/testing/types.ts +14 -0
- package/src/testing/working-directory.ts +28 -0
- package/src/testing.ts +1 -254
- package/src/types.ts +16 -4
- package/src/virtual.d.ts +2 -1
- package/src/vite/createVirtualModulePlugin.test.ts +80 -0
- package/src/vite/createVirtualModulePlugin.ts +25 -0
- package/src/viteAstroContainerRenderersPlugin.ts +60 -26
- package/src/vitePluginAstro.ts +12 -5
- package/src/vitePluginAstroBuildPrerender.ts +665 -204
- package/src/vitePluginAstroRoutesFallback.ts +37 -0
- package/src/vitePluginAstroVueFallback.ts +47 -0
- package/src/viteStorybookAstroMiddlewarePlugin.ts +88 -12
- package/src/viteStorybookRendererFallbackPlugin.ts +13 -23
- package/src/vitest/config.ts +95 -0
- package/src/vitest/global-setup.ts +16 -0
- package/src/vitest/index.ts +2 -0
- package/src/vitest/vite-plugins.ts +187 -0
- package/dist/chunk-KTGNRGDJ.js +0 -561
- package/dist/chunk-KTGNRGDJ.js.map +0 -1
package/src/integrations/vue.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Integration } from './base.ts';
|
|
2
2
|
import type { Options as VueOptions } from '@vitejs/plugin-vue';
|
|
3
3
|
import type { Options as VueJsxOptions } from '@vitejs/plugin-vue-jsx';
|
|
4
|
+
import { importModule } from './moduleResolver.ts';
|
|
4
5
|
|
|
5
6
|
export type Options = Pick<VueOptions, 'include' | 'exclude'> & {
|
|
6
7
|
jsx?: boolean | VueJsxOptions;
|
|
@@ -37,8 +38,10 @@ export class VueIntegration implements Integration {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
async loadIntegration() {
|
|
41
|
-
const framework = await
|
|
41
|
+
async loadIntegration(resolveFrom = process.cwd()) {
|
|
42
|
+
const framework = await importModule<{
|
|
43
|
+
default: (options: Options) => Awaited<ReturnType<Integration['loadIntegration']>>;
|
|
44
|
+
}>('@astrojs/vue', resolveFrom);
|
|
42
45
|
|
|
43
46
|
return framework.default(this.options);
|
|
44
47
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { resolveSanitizationOptions, sanitizeRenderPayload } from './sanitization.ts';
|
|
3
|
+
|
|
4
|
+
describe('sanitization', () => {
|
|
5
|
+
test('enables sanitization and sanitizes all slots by default', () => {
|
|
6
|
+
const options = resolveSanitizationOptions();
|
|
7
|
+
|
|
8
|
+
expect(options.enabled).toBe(true);
|
|
9
|
+
expect(options.args).toEqual([]);
|
|
10
|
+
expect(options.slots).toEqual(['**']);
|
|
11
|
+
|
|
12
|
+
const payload = sanitizeRenderPayload(
|
|
13
|
+
{
|
|
14
|
+
args: {
|
|
15
|
+
title: '<b>Leave args as-is</b><script>alert(1)</script>'
|
|
16
|
+
},
|
|
17
|
+
slots: {
|
|
18
|
+
default: '<p>Hello<script>alert(1)</script></p>'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
options
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(payload.args.title).toBe('<b>Leave args as-is</b><script>alert(1)</script>');
|
|
25
|
+
expect(payload.slots.default).toBe('<p>Hello</p>');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('sanitizes only configured arg paths', () => {
|
|
29
|
+
const options = resolveSanitizationOptions({
|
|
30
|
+
args: ['content'],
|
|
31
|
+
slots: []
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const payload = sanitizeRenderPayload(
|
|
35
|
+
{
|
|
36
|
+
args: {
|
|
37
|
+
content: '<p>Hello</p><script>alert(1)</script>',
|
|
38
|
+
title: '<b>Keep me</b><script>alert(1)</script>'
|
|
39
|
+
},
|
|
40
|
+
slots: {}
|
|
41
|
+
},
|
|
42
|
+
options
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(payload.args.content).toBe('<p>Hello</p>');
|
|
46
|
+
expect(payload.args.title).toBe('<b>Keep me</b><script>alert(1)</script>');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('supports wildcard patterns for nested values', () => {
|
|
50
|
+
const options = resolveSanitizationOptions({
|
|
51
|
+
args: ['items.*.html'],
|
|
52
|
+
slots: []
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const payload = sanitizeRenderPayload(
|
|
56
|
+
{
|
|
57
|
+
args: {
|
|
58
|
+
items: [{ html: '<img class="hero" src="x" onerror="alert(1)"><p>Safe</p>' }]
|
|
59
|
+
},
|
|
60
|
+
slots: {}
|
|
61
|
+
},
|
|
62
|
+
options
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(payload.args.items).toEqual([{ html: '<img class="hero" src="x" /><p>Safe</p>' }]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('returns payload untouched when sanitization is disabled', () => {
|
|
69
|
+
const options = resolveSanitizationOptions({
|
|
70
|
+
enabled: false,
|
|
71
|
+
args: ['content']
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const payload = {
|
|
75
|
+
args: {
|
|
76
|
+
content: '<p>Hello</p><script>alert(1)</script>'
|
|
77
|
+
},
|
|
78
|
+
slots: {
|
|
79
|
+
default: '<p>Body<script>alert(1)</script></p>'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const sanitizedPayload = sanitizeRenderPayload(payload, options);
|
|
84
|
+
|
|
85
|
+
expect(sanitizedPayload).toBe(payload);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('merges sanitize-html object options with defaults', () => {
|
|
89
|
+
const options = resolveSanitizationOptions({
|
|
90
|
+
sanitizeHtml: {
|
|
91
|
+
allowedAttributes: {
|
|
92
|
+
section: ['data-foo']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(options.sanitizeHtml.allowedAttributes).toMatchObject({
|
|
98
|
+
a: ['href', 'name', 'target', 'rel'],
|
|
99
|
+
section: ['data-foo']
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('merges sanitize-html classes and styles options', () => {
|
|
104
|
+
const options = resolveSanitizationOptions({
|
|
105
|
+
sanitizeHtml: {
|
|
106
|
+
allowedClasses: {
|
|
107
|
+
p: ['prose']
|
|
108
|
+
},
|
|
109
|
+
allowedStyles: {
|
|
110
|
+
'*': {
|
|
111
|
+
color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(options.sanitizeHtml.allowedClasses).toMatchObject({
|
|
118
|
+
p: ['prose']
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(options.sanitizeHtml.allowedStyles).toMatchObject({
|
|
122
|
+
'*': {
|
|
123
|
+
color: [expect.any(RegExp)]
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('rejects invalid path lists', () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
resolveSanitizationOptions({
|
|
131
|
+
args: ['ok', ' ']
|
|
132
|
+
})
|
|
133
|
+
).toThrow('framework.options.sanitization.args[1] cannot be an empty string.');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('rejects non-array path list values', () => {
|
|
137
|
+
expect(() =>
|
|
138
|
+
resolveSanitizationOptions({
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
args: 'content' as any
|
|
141
|
+
})
|
|
142
|
+
).toThrow('framework.options.sanitization.args must be an array of dot-path patterns.');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('rejects non-string entries in path lists', () => {
|
|
146
|
+
expect(() =>
|
|
147
|
+
resolveSanitizationOptions({
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
args: ['ok', 1 as any]
|
|
150
|
+
})
|
|
151
|
+
).toThrow('framework.options.sanitization.args[1] must be a string.');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('supports ** suffix matching and leaves non-matching paths untouched', () => {
|
|
155
|
+
const options = resolveSanitizationOptions({
|
|
156
|
+
args: ['**.html'],
|
|
157
|
+
slots: []
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const payload = sanitizeRenderPayload(
|
|
161
|
+
{
|
|
162
|
+
args: {
|
|
163
|
+
items: [
|
|
164
|
+
{
|
|
165
|
+
nested: {
|
|
166
|
+
html: '<p>Safe<script>alert(1)</script></p>'
|
|
167
|
+
},
|
|
168
|
+
content: '<p>Leave<script>alert(1)</script></p>'
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
slots: {}
|
|
173
|
+
},
|
|
174
|
+
options
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const items = payload.args.items as Array<{ nested: { html: string }; content: string }>;
|
|
178
|
+
|
|
179
|
+
expect(items[0].nested.html).toBe('<p>Safe</p>');
|
|
180
|
+
expect(items[0].content).toBe('<p>Leave<script>alert(1)</script></p>');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('does not sanitize when path is shorter than pattern and keeps non-string values', () => {
|
|
184
|
+
const options = resolveSanitizationOptions({
|
|
185
|
+
args: ['title.html'],
|
|
186
|
+
slots: []
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const regexValue = /hello/i;
|
|
190
|
+
|
|
191
|
+
const payload = sanitizeRenderPayload(
|
|
192
|
+
{
|
|
193
|
+
args: {
|
|
194
|
+
title: '<p>Keep<script>alert(1)</script></p>',
|
|
195
|
+
count: 1,
|
|
196
|
+
truthy: true,
|
|
197
|
+
pattern: regexValue
|
|
198
|
+
},
|
|
199
|
+
slots: {}
|
|
200
|
+
},
|
|
201
|
+
options
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(payload.args.title).toBe('<p>Keep<script>alert(1)</script></p>');
|
|
205
|
+
expect(payload.args.count).toBe(1);
|
|
206
|
+
expect(payload.args.truthy).toBe(true);
|
|
207
|
+
expect(payload.args.pattern).toBe(regexValue);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('supports null-prototype records during traversal', () => {
|
|
211
|
+
const options = resolveSanitizationOptions({
|
|
212
|
+
args: ['meta.html'],
|
|
213
|
+
slots: []
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const meta = Object.create(null) as Record<string, unknown>;
|
|
217
|
+
|
|
218
|
+
meta.html = '<p>Safe<script>alert(1)</script></p>';
|
|
219
|
+
|
|
220
|
+
const payload = sanitizeRenderPayload(
|
|
221
|
+
{
|
|
222
|
+
args: {
|
|
223
|
+
meta
|
|
224
|
+
},
|
|
225
|
+
slots: {}
|
|
226
|
+
},
|
|
227
|
+
options
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect((payload.args.meta as Record<string, unknown>).html).toBe('<p>Safe</p>');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
import type { IOptions } from 'sanitize-html';
|
|
3
|
+
|
|
4
|
+
type SanitizationPayload = {
|
|
5
|
+
args: Record<string, unknown>;
|
|
6
|
+
slots: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SanitizationOptions = {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
args?: string[];
|
|
12
|
+
slots?: string[];
|
|
13
|
+
sanitizeHtml?: IOptions;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ResolvedSanitizationOptions = {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
args: string[];
|
|
19
|
+
slots: string[];
|
|
20
|
+
sanitizeHtml: IOptions;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_SANITIZE_HTML_OPTIONS: IOptions = {
|
|
24
|
+
allowedTags: [
|
|
25
|
+
'a',
|
|
26
|
+
'abbr',
|
|
27
|
+
'b',
|
|
28
|
+
'blockquote',
|
|
29
|
+
'br',
|
|
30
|
+
'caption',
|
|
31
|
+
'cite',
|
|
32
|
+
'code',
|
|
33
|
+
'col',
|
|
34
|
+
'colgroup',
|
|
35
|
+
'dd',
|
|
36
|
+
'details',
|
|
37
|
+
'dfn',
|
|
38
|
+
'div',
|
|
39
|
+
'dl',
|
|
40
|
+
'dt',
|
|
41
|
+
'em',
|
|
42
|
+
'figcaption',
|
|
43
|
+
'figure',
|
|
44
|
+
'h1',
|
|
45
|
+
'h2',
|
|
46
|
+
'h3',
|
|
47
|
+
'h4',
|
|
48
|
+
'h5',
|
|
49
|
+
'h6',
|
|
50
|
+
'hr',
|
|
51
|
+
'i',
|
|
52
|
+
'img',
|
|
53
|
+
'kbd',
|
|
54
|
+
'li',
|
|
55
|
+
'mark',
|
|
56
|
+
'ol',
|
|
57
|
+
'p',
|
|
58
|
+
'pre',
|
|
59
|
+
'q',
|
|
60
|
+
'rp',
|
|
61
|
+
'rt',
|
|
62
|
+
'ruby',
|
|
63
|
+
's',
|
|
64
|
+
'samp',
|
|
65
|
+
'small',
|
|
66
|
+
'span',
|
|
67
|
+
'strong',
|
|
68
|
+
'sub',
|
|
69
|
+
'summary',
|
|
70
|
+
'sup',
|
|
71
|
+
'table',
|
|
72
|
+
'tbody',
|
|
73
|
+
'td',
|
|
74
|
+
'tfoot',
|
|
75
|
+
'th',
|
|
76
|
+
'thead',
|
|
77
|
+
'time',
|
|
78
|
+
'tr',
|
|
79
|
+
'u',
|
|
80
|
+
'ul',
|
|
81
|
+
'var',
|
|
82
|
+
'wbr'
|
|
83
|
+
],
|
|
84
|
+
allowedAttributes: {
|
|
85
|
+
'*': [
|
|
86
|
+
'aria-describedby',
|
|
87
|
+
'aria-hidden',
|
|
88
|
+
'aria-label',
|
|
89
|
+
'aria-labelledby',
|
|
90
|
+
'class',
|
|
91
|
+
'id',
|
|
92
|
+
'lang',
|
|
93
|
+
'role',
|
|
94
|
+
'title'
|
|
95
|
+
],
|
|
96
|
+
a: ['href', 'name', 'target', 'rel'],
|
|
97
|
+
img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading', 'decoding'],
|
|
98
|
+
td: ['colspan', 'rowspan'],
|
|
99
|
+
th: ['colspan', 'rowspan', 'scope'],
|
|
100
|
+
time: ['datetime']
|
|
101
|
+
},
|
|
102
|
+
allowedSchemes: ['http', 'https', 'mailto', 'tel', 'data'],
|
|
103
|
+
allowedSchemesByTag: {
|
|
104
|
+
a: ['http', 'https', 'mailto', 'tel'],
|
|
105
|
+
img: ['http', 'https', 'data']
|
|
106
|
+
},
|
|
107
|
+
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite', 'srcset'],
|
|
108
|
+
allowProtocolRelative: false,
|
|
109
|
+
disallowedTagsMode: 'discard',
|
|
110
|
+
enforceHtmlBoundary: true,
|
|
111
|
+
parseStyleAttributes: false
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function resolveSanitizationOptions(options?: SanitizationOptions): ResolvedSanitizationOptions {
|
|
115
|
+
if (!options) {
|
|
116
|
+
return {
|
|
117
|
+
enabled: true,
|
|
118
|
+
args: [],
|
|
119
|
+
slots: ['**'],
|
|
120
|
+
sanitizeHtml: mergeSanitizeHtmlOptions()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const enabled = options.enabled ?? true;
|
|
125
|
+
const args = normalizePathList(options.args, 'framework.options.sanitization.args');
|
|
126
|
+
const slots =
|
|
127
|
+
options.slots === undefined
|
|
128
|
+
? ['**']
|
|
129
|
+
: normalizePathList(options.slots, 'framework.options.sanitization.slots');
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
enabled,
|
|
133
|
+
args,
|
|
134
|
+
slots,
|
|
135
|
+
sanitizeHtml: mergeSanitizeHtmlOptions(options.sanitizeHtml)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function sanitizeRenderPayload(
|
|
140
|
+
payload: SanitizationPayload,
|
|
141
|
+
options: ResolvedSanitizationOptions
|
|
142
|
+
): SanitizationPayload {
|
|
143
|
+
if (!options.enabled) {
|
|
144
|
+
return payload;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sanitizedArgs =
|
|
148
|
+
options.args.length > 0
|
|
149
|
+
? sanitizeRecord(payload.args, options.args, options.sanitizeHtml)
|
|
150
|
+
: payload.args;
|
|
151
|
+
|
|
152
|
+
const sanitizedSlots =
|
|
153
|
+
options.slots.length > 0
|
|
154
|
+
? sanitizeRecord(payload.slots, options.slots, options.sanitizeHtml)
|
|
155
|
+
: payload.slots;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
args: sanitizedArgs,
|
|
159
|
+
slots: sanitizedSlots
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mergeSanitizeHtmlOptions(userOptions?: IOptions): IOptions {
|
|
164
|
+
const merged: IOptions = {
|
|
165
|
+
...DEFAULT_SANITIZE_HTML_OPTIONS,
|
|
166
|
+
...userOptions
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
isRecord(DEFAULT_SANITIZE_HTML_OPTIONS.allowedAttributes) &&
|
|
171
|
+
isRecord(userOptions?.allowedAttributes)
|
|
172
|
+
) {
|
|
173
|
+
merged.allowedAttributes = {
|
|
174
|
+
...DEFAULT_SANITIZE_HTML_OPTIONS.allowedAttributes,
|
|
175
|
+
...userOptions.allowedAttributes
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isRecord(userOptions?.allowedClasses)) {
|
|
180
|
+
merged.allowedClasses = {
|
|
181
|
+
...(isRecord(DEFAULT_SANITIZE_HTML_OPTIONS.allowedClasses)
|
|
182
|
+
? DEFAULT_SANITIZE_HTML_OPTIONS.allowedClasses
|
|
183
|
+
: {}),
|
|
184
|
+
...userOptions.allowedClasses
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isRecord(userOptions?.allowedStyles)) {
|
|
189
|
+
merged.allowedStyles = {
|
|
190
|
+
...(isRecord(DEFAULT_SANITIZE_HTML_OPTIONS.allowedStyles)
|
|
191
|
+
? DEFAULT_SANITIZE_HTML_OPTIONS.allowedStyles
|
|
192
|
+
: {}),
|
|
193
|
+
...userOptions.allowedStyles
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return merged;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizePathList(value: unknown, path: string): string[] {
|
|
201
|
+
if (value === undefined) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(value)) {
|
|
206
|
+
throw new Error(`${path} must be an array of dot-path patterns.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const unique = new Set<string>();
|
|
210
|
+
|
|
211
|
+
value.forEach((entry, index) => {
|
|
212
|
+
if (typeof entry !== 'string') {
|
|
213
|
+
throw new Error(`${path}[${index}] must be a string.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const normalized = entry.trim();
|
|
217
|
+
|
|
218
|
+
if (!normalized) {
|
|
219
|
+
throw new Error(`${path}[${index}] cannot be an empty string.`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
unique.add(normalized);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return Array.from(unique);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sanitizeRecord(
|
|
229
|
+
record: Record<string, unknown>,
|
|
230
|
+
patterns: string[],
|
|
231
|
+
options: IOptions
|
|
232
|
+
): Record<string, unknown> {
|
|
233
|
+
const sanitized: Record<string, unknown> = {};
|
|
234
|
+
|
|
235
|
+
Object.entries(record).forEach(([key, value]) => {
|
|
236
|
+
sanitized[key] = sanitizeValue(value, key, patterns, options);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return sanitized;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sanitizeValue(
|
|
243
|
+
value: unknown,
|
|
244
|
+
currentPath: string,
|
|
245
|
+
patterns: string[],
|
|
246
|
+
options: IOptions
|
|
247
|
+
): unknown {
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
if (shouldSanitizePath(currentPath, patterns)) {
|
|
250
|
+
return sanitizeHtml(value, options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (Array.isArray(value)) {
|
|
257
|
+
return value.map((item, index) => {
|
|
258
|
+
const nextPath = `${currentPath}.${index}`;
|
|
259
|
+
|
|
260
|
+
return sanitizeValue(item, nextPath, patterns, options);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (isRecord(value)) {
|
|
265
|
+
const sanitized: Record<string, unknown> = {};
|
|
266
|
+
|
|
267
|
+
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
268
|
+
const nextPath = `${currentPath}.${key}`;
|
|
269
|
+
|
|
270
|
+
sanitized[key] = sanitizeValue(nestedValue, nextPath, patterns, options);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return sanitized;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function shouldSanitizePath(path: string, patterns: string[]): boolean {
|
|
280
|
+
return patterns.some((pattern) => matchesPathPattern(path, pattern));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function matchesPathPattern(path: string, pattern: string): boolean {
|
|
284
|
+
const pathSegments = path.split('.');
|
|
285
|
+
const patternSegments = pattern.split('.');
|
|
286
|
+
|
|
287
|
+
return matchSegments(pathSegments, patternSegments);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function matchSegments(pathSegments: string[], patternSegments: string[]): boolean {
|
|
291
|
+
if (patternSegments.length === 0) {
|
|
292
|
+
return pathSegments.length === 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const [patternHead, ...patternTail] = patternSegments;
|
|
296
|
+
|
|
297
|
+
if (patternHead === '**') {
|
|
298
|
+
if (patternTail.length === 0) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (let index = 0; index <= pathSegments.length; index += 1) {
|
|
303
|
+
const remainingPath = pathSegments.slice(index);
|
|
304
|
+
|
|
305
|
+
if (matchSegments(remainingPath, patternTail)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (pathSegments.length === 0) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const [pathHead, ...pathTail] = pathSegments;
|
|
318
|
+
|
|
319
|
+
if (patternHead === '*' || patternHead === pathHead) {
|
|
320
|
+
return matchSegments(pathTail, patternTail);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
327
|
+
if (typeof value !== 'object' || value === null) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (Array.isArray(value) || value instanceof RegExp) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const prototype = Object.getPrototypeOf(value);
|
|
336
|
+
|
|
337
|
+
return prototype === Object.prototype || prototype === null;
|
|
338
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
|
|
3
|
+
type SsrLoadModuleOptions = {
|
|
4
|
+
fixStacktrace?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export async function ssrLoadModuleWithFsFallback<TModule = unknown>(
|
|
8
|
+
viteServer: Pick<ViteDevServer, 'ssrLoadModule'>,
|
|
9
|
+
id: string,
|
|
10
|
+
options?: SsrLoadModuleOptions
|
|
11
|
+
) {
|
|
12
|
+
const ids = [id];
|
|
13
|
+
|
|
14
|
+
if (id.startsWith('/') && !id.startsWith('/@fs/')) {
|
|
15
|
+
ids.push(`/@fs${id}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let lastError: unknown;
|
|
19
|
+
|
|
20
|
+
for (const candidate of ids) {
|
|
21
|
+
try {
|
|
22
|
+
return await viteServer.ssrLoadModule(candidate, options) as TModule;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
lastError = error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw lastError;
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('middleware', () => {
|
|
4
|
+
describe('Windows absolute path normalization', () => {
|
|
5
|
+
/**
|
|
6
|
+
* Regex pattern used in middleware.ts to detect Windows absolute paths.
|
|
7
|
+
* This pattern matches paths starting with a drive letter (e.g., C:) followed by
|
|
8
|
+
* a forward slash or backslash, indicating a Windows absolute path.
|
|
9
|
+
*/
|
|
10
|
+
const windowsPathRegex = /^[a-zA-Z]:[/\\]/;
|
|
11
|
+
|
|
12
|
+
test('detects Windows absolute paths with forward slashes', () => {
|
|
13
|
+
const pathToTest = 'C:/Users/project/Component.astro';
|
|
14
|
+
|
|
15
|
+
expect(windowsPathRegex.test(pathToTest)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('detects Windows absolute paths with backslashes', () => {
|
|
19
|
+
const pathToTest = 'C:\\Users\\project\\Component.astro';
|
|
20
|
+
|
|
21
|
+
expect(windowsPathRegex.test(pathToTest)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('ignores Unix absolute paths', () => {
|
|
25
|
+
const unixPath = '/Users/project/Component.astro';
|
|
26
|
+
|
|
27
|
+
expect(windowsPathRegex.test(unixPath)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('ignores relative paths', () => {
|
|
31
|
+
const relativePath = './Component.astro';
|
|
32
|
+
|
|
33
|
+
expect(windowsPathRegex.test(relativePath)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('ignores module specifiers', () => {
|
|
37
|
+
const specifier = '@storybook-astro/renderer';
|
|
38
|
+
|
|
39
|
+
expect(windowsPathRegex.test(specifier)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('ignores file URLs', () => {
|
|
43
|
+
const fileUrl = 'file:///C:/Users/project/Component.astro';
|
|
44
|
+
|
|
45
|
+
expect(windowsPathRegex.test(fileUrl)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|