poveste 0.5.1 → 0.5.2
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/node/collect/worker.d.ts.map +1 -1
- package/dist/node/collect/worker.js +10 -3
- package/dist/node/dom/env.d.ts +3 -0
- package/dist/node/dom/env.d.ts.map +1 -1
- package/dist/node/dom/env.js +31 -0
- package/dist/node/style-isolation/entry-css-merger.js +2 -2
- package/dist/node/style-isolation/selectors.d.ts +1 -0
- package/dist/node/style-isolation/selectors.d.ts.map +1 -1
- package/dist/node/style-isolation/selectors.js +3 -0
- package/dist/node/style-isolation/transforms.d.ts +2 -0
- package/dist/node/style-isolation/transforms.d.ts.map +1 -1
- package/dist/node/style-isolation/transforms.js +20 -9
- package/dist/node/style-isolation/vite-plugin.js +2 -2
- package/package.json +5 -5
- package/src/node/__tests__/dom-env.spec.ts +42 -1
- package/src/node/collect/worker.ts +10 -4
- package/src/node/dom/env.ts +34 -0
- package/src/node/style-isolation/__tests__/transforms.spec.ts +37 -0
- package/src/node/style-isolation/entry-css-merger.ts +2 -2
- package/src/node/style-isolation/selectors.ts +3 -0
- package/src/node/style-isolation/transforms.ts +23 -9
- package/src/node/style-isolation/vite-plugin.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/node/collect/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAatD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,SAAS,EAAE,eAAe,CAAA;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/node/collect/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAatD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,SAAS,EAAE,eAAe,CAAA;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB;kCAmB8B,OAAO,KAAG,OAAO,CAAC,UAAU,CAAC;AAA5D,wBA4DC"}
|
|
@@ -5,10 +5,13 @@ import { createBirpc } from 'birpc';
|
|
|
5
5
|
import { dirname, resolve } from 'pathe';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
7
|
import { ModuleCacheMap, ViteNodeRunner } from 'vite-node/client';
|
|
8
|
-
import { createDomEnv } from '../dom/env.js';
|
|
8
|
+
import { createDomEnv, resetDomEnv } from '../dom/env.js';
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const _moduleCache = new ModuleCacheMap();
|
|
11
11
|
let _runner;
|
|
12
|
+
// Worker-lifetime: externalised runtimes cache the DOM they first saw, so one
|
|
13
|
+
// per story broke re-collection.
|
|
14
|
+
let _domEnv;
|
|
12
15
|
let _rpc;
|
|
13
16
|
// Cleanup module cache
|
|
14
17
|
parentPort.on('message', (message) => {
|
|
@@ -34,7 +37,12 @@ export default async (payload) => {
|
|
|
34
37
|
return _rpc.resolveId(id, importer);
|
|
35
38
|
},
|
|
36
39
|
}));
|
|
37
|
-
|
|
40
|
+
if (_domEnv) {
|
|
41
|
+
resetDomEnv(_domEnv);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
_domEnv = createDomEnv();
|
|
45
|
+
}
|
|
38
46
|
const el = window.document.createElement('div');
|
|
39
47
|
const beforeExecuteTime = performance.now();
|
|
40
48
|
// Mount app to collect stories/variants
|
|
@@ -55,7 +63,6 @@ export default async (payload) => {
|
|
|
55
63
|
s.docsText = text;
|
|
56
64
|
});
|
|
57
65
|
}
|
|
58
|
-
destroyDomEnv();
|
|
59
66
|
const endTime = performance.now();
|
|
60
67
|
console.log(pc.dim(`${payload.storyFile.relativePath} ${Math.round(endTime - startTime)}ms (setup:${Math.round(beforeExecuteTime - startTime)}ms, execute:${Math.round(afterExecuteTime - beforeExecuteTime)}ms, run:${Math.round(afterRunTime - afterExecuteTime)}ms)`));
|
|
61
68
|
return {
|
package/dist/node/dom/env.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare function createDomEnv(): {
|
|
2
2
|
window: Window & typeof globalThis;
|
|
3
3
|
destroy: () => void;
|
|
4
|
+
pristineKeys: Set<string>;
|
|
4
5
|
};
|
|
6
|
+
/** Returns a reused environment to the state a fresh one would have been in. */
|
|
7
|
+
export declare function resetDomEnv(env: Pick<ReturnType<typeof createDomEnv>, 'window' | 'pristineKeys'>): void;
|
|
5
8
|
//# sourceMappingURL=env.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/node/dom/env.ts"],"names":[],"mappings":"AAmCA,wBAAgB,YAAY
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/node/dom/env.ts"],"names":[],"mappings":"AAmCA,wBAAgB,YAAY;;;;EAwD3B;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC,QA4BhG"}
|
package/dist/node/dom/env.js
CHANGED
|
@@ -72,5 +72,36 @@ export function createDomEnv() {
|
|
|
72
72
|
return {
|
|
73
73
|
window,
|
|
74
74
|
destroy,
|
|
75
|
+
// `populateGlobal` does `global.window = global`, so `window` here is
|
|
76
|
+
// `globalThis` — one surface, not two.
|
|
77
|
+
pristineKeys: new Set(Object.keys(globalThis)),
|
|
75
78
|
};
|
|
76
79
|
}
|
|
80
|
+
/** Returns a reused environment to the state a fresh one would have been in. */
|
|
81
|
+
export function resetDomEnv(env) {
|
|
82
|
+
const doc = env.window.document;
|
|
83
|
+
for (const el of [doc.documentElement, doc.head, doc.body]) {
|
|
84
|
+
if (!el)
|
|
85
|
+
continue;
|
|
86
|
+
for (const name of [...el.getAttributeNames()]) {
|
|
87
|
+
el.removeAttribute(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
doc.head?.replaceChildren();
|
|
91
|
+
doc.body?.replaceChildren();
|
|
92
|
+
try {
|
|
93
|
+
// Only present when the key was proxied onto the global, which varies by
|
|
94
|
+
// node version.
|
|
95
|
+
env.window.localStorage?.clear();
|
|
96
|
+
env.window.sessionStorage?.clear();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Unavailable for some jsdom urls.
|
|
100
|
+
}
|
|
101
|
+
// Additions only: the modules that installed the rest are cached, not re-run.
|
|
102
|
+
for (const key of Object.keys(globalThis)) {
|
|
103
|
+
if (!env.pristineKeys.has(key)) {
|
|
104
|
+
delete globalThis[key];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STORY_SCOPE_ROOT } from './selectors.js';
|
|
1
|
+
import { CONTROLS_SLOT, STORY_SCOPE_ROOT } from './selectors.js';
|
|
2
2
|
import { wrapUserCss } from './transforms.js';
|
|
3
3
|
import { USER_CSS_MARK_END, USER_CSS_MARK_START } from './vite-plugin.js';
|
|
4
4
|
export function entryCssMergerPlugin(opts = { isolateStyles: false }) {
|
|
@@ -121,7 +121,7 @@ export function wrapMarkedUserCss(css, scopeRoot) {
|
|
|
121
121
|
}
|
|
122
122
|
const inner = css.slice(start + USER_CSS_MARK_START.length, end).trim();
|
|
123
123
|
if (inner.length > 0) {
|
|
124
|
-
out += `${wrapUserCss(inner, { scopeRoot })}\n`;
|
|
124
|
+
out += `${wrapUserCss(inner, { scopeRoot, excludeRootClass: CONTROLS_SLOT.slice(1) })}\n`;
|
|
125
125
|
}
|
|
126
126
|
cursor = end + USER_CSS_MARK_END.length;
|
|
127
127
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const APP_SCOPE_ROOT = ".poveste-app-root";
|
|
2
2
|
export declare const STORY_SCOPE_ROOT = ".__poveste-render-story";
|
|
3
3
|
export declare const STORY_SCOPE_BOUNDARY = ".__poveste-render-story:not(.__poveste-render-custom-controls)";
|
|
4
|
+
export declare const CONTROLS_SLOT = ".__poveste-controls-slot";
|
|
4
5
|
export declare const DEFAULTS_LAYER = "poveste-defaults";
|
|
5
6
|
export declare const USER_GLOBALS_LAYER = "poveste-user-globals";
|
|
6
7
|
export declare const GLOBAL_LAYER_QUERY = "poveste-global-layer";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../../src/node/style-isolation/selectors.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,cAAc,sBAAsB,CAAA;AACjD,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AAIzD,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;
|
|
1
|
+
{"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../../src/node/style-isolation/selectors.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,cAAc,sBAAsB,CAAA;AACjD,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AAIzD,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;AAGpG,eAAO,MAAM,aAAa,6BAA6B,CAAA;AACvD,eAAO,MAAM,cAAc,qBAAqB,CAAA;AAChD,eAAO,MAAM,kBAAkB,yBAAyB,CAAA;AAGxD,eAAO,MAAM,kBAAkB,yBAAyB,CAAA"}
|
|
@@ -8,6 +8,9 @@ export const STORY_SCOPE_ROOT = '.__poveste-render-story';
|
|
|
8
8
|
// .__poveste-render-custom-controls) but render inside the chrome panel,
|
|
9
9
|
// so the @scope boundary must keep them in chrome scope.
|
|
10
10
|
export const STORY_SCOPE_BOUNDARY = '.__poveste-render-story:not(.__poveste-render-custom-controls)';
|
|
11
|
+
// The panel only — the sandbox body also carries the class above, so excluding
|
|
12
|
+
// that one would stop a `body` rule applying inside the story itself.
|
|
13
|
+
export const CONTROLS_SLOT = '.__poveste-controls-slot';
|
|
11
14
|
export const DEFAULTS_LAYER = 'poveste-defaults';
|
|
12
15
|
export const USER_GLOBALS_LAYER = 'poveste-user-globals';
|
|
13
16
|
// Import query `globalStyles` files carry so the user-CSS plugin leaves them
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../../src/node/style-isolation/transforms.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../../src/node/style-isolation/transforms.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,qGAAqG;IACrG,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAWlE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAc7E;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAKlD"}
|
|
@@ -7,7 +7,7 @@ export function wrapUserCss(css, opts) {
|
|
|
7
7
|
}
|
|
8
8
|
const { hoisted, remainder } = extractHoistableAtRules(css);
|
|
9
9
|
const body = hasRootSelector(remainder)
|
|
10
|
-
? rewriteRootToScope(remainder)
|
|
10
|
+
? rewriteRootToScope(remainder, opts.excludeRootClass)
|
|
11
11
|
: remainder;
|
|
12
12
|
const hoistedBlock = hoisted.length > 0 ? `${hoisted.join('\n')}\n` : '';
|
|
13
13
|
return `${hoistedBlock}@scope (${opts.scopeRoot}) {\n${body}\n}\n`;
|
|
@@ -65,10 +65,21 @@ function isRootTypeSelector(name) {
|
|
|
65
65
|
// under `of` rather than `selectors`, so it is untouched for free.
|
|
66
66
|
const DESCENDABLE_PSEUDO_CLASSES = new Set(['is', 'where']);
|
|
67
67
|
const SCOPE_PART = { type: 'pseudo-class', kind: 'scope' };
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Rewritten roots only, so an author's own classes still apply in the panel.
|
|
69
|
+
function scopeParts(excludeClass) {
|
|
70
|
+
if (!excludeClass) {
|
|
71
|
+
return [SCOPE_PART];
|
|
72
|
+
}
|
|
73
|
+
return [SCOPE_PART, {
|
|
74
|
+
type: 'pseudo-class',
|
|
75
|
+
kind: 'not',
|
|
76
|
+
selectors: [[{ type: 'class', name: excludeClass }]],
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
function rewriteSelector(selector, excludeClass) {
|
|
80
|
+
return selector.flatMap((part, index) => {
|
|
70
81
|
if (part.type === 'pseudo-class' && part.kind === 'root') {
|
|
71
|
-
return
|
|
82
|
+
return scopeParts(excludeClass);
|
|
72
83
|
}
|
|
73
84
|
// A namespace is its own part, immediately before the type selector it
|
|
74
85
|
// qualifies. Rewriting past it emits `*|:scope`, which is invalid — a
|
|
@@ -76,21 +87,21 @@ function rewriteSelector(selector) {
|
|
|
76
87
|
// `svg|body` also means a body in the SVG namespace, which is not the
|
|
77
88
|
// document root, so leaving these alone is right on both counts.
|
|
78
89
|
if (part.type === 'type' && isRootTypeSelector(part.name ?? '')) {
|
|
79
|
-
return selector[index - 1]?.type === 'namespace' ? part :
|
|
90
|
+
return selector[index - 1]?.type === 'namespace' ? [part] : scopeParts(excludeClass);
|
|
80
91
|
}
|
|
81
92
|
if (part.type === 'pseudo-class' && DESCENDABLE_PSEUDO_CLASSES.has(part.kind ?? '') && part.selectors) {
|
|
82
|
-
return { ...part, selectors: part.selectors.map(rewriteSelector) };
|
|
93
|
+
return [{ ...part, selectors: part.selectors.map(s => rewriteSelector(s, excludeClass)) }];
|
|
83
94
|
}
|
|
84
|
-
return part;
|
|
95
|
+
return [part];
|
|
85
96
|
});
|
|
86
97
|
}
|
|
87
|
-
function rewriteRootToScope(css) {
|
|
98
|
+
function rewriteRootToScope(css, excludeClass) {
|
|
88
99
|
const processed = lightningcssTransform({
|
|
89
100
|
filename: 'user.css',
|
|
90
101
|
code: Buffer.from(css, 'utf8'),
|
|
91
102
|
minify: false,
|
|
92
103
|
visitor: {
|
|
93
|
-
Selector: selector => rewriteSelector(selector),
|
|
104
|
+
Selector: selector => rewriteSelector(selector, excludeClass),
|
|
94
105
|
},
|
|
95
106
|
});
|
|
96
107
|
return Buffer.from(processed.code).toString('utf8');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GLOBAL_LAYER_QUERY, STORY_SCOPE_ROOT } from './selectors.js';
|
|
1
|
+
import { CONTROLS_SLOT, GLOBAL_LAYER_QUERY, STORY_SCOPE_ROOT } from './selectors.js';
|
|
2
2
|
import { wrapUserCss } from './transforms.js';
|
|
3
3
|
export const USER_CSS_MARK_START = '/*__PVT_USER_CSS_START__*/';
|
|
4
4
|
export const USER_CSS_MARK_END = '/*__PVT_USER_CSS_END__*/';
|
|
@@ -20,7 +20,7 @@ export function userCssScopePlugin(opts) {
|
|
|
20
20
|
if (hasGlobalQuery(query))
|
|
21
21
|
return null;
|
|
22
22
|
if (mode === 'wrap') {
|
|
23
|
-
return { code: wrapUserCss(code, { scopeRoot: STORY_SCOPE_ROOT }), map: null };
|
|
23
|
+
return { code: wrapUserCss(code, { scopeRoot: STORY_SCOPE_ROOT, excludeRootClass: CONTROLS_SLOT.slice(1) }), map: null };
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
26
26
|
code: `${USER_CSS_MARK_START}\n${code}\n${USER_CSS_MARK_END}\n`,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poveste",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "A fast story book powered by Vite",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan"
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
"sirv": "^3.0.0",
|
|
69
69
|
"unhead": "^3.1.0",
|
|
70
70
|
"vite-node": "^6.0.0",
|
|
71
|
-
"@poveste/app": "^0.5.
|
|
72
|
-
"@poveste/
|
|
73
|
-
"@poveste/
|
|
74
|
-
"@poveste/vendors": "^0.5.
|
|
71
|
+
"@poveste/app": "^0.5.2",
|
|
72
|
+
"@poveste/controls": "^0.5.2",
|
|
73
|
+
"@poveste/shared": "^0.5.2",
|
|
74
|
+
"@poveste/vendors": "^0.5.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { createDomEnv } from '../dom/env.js'
|
|
2
|
+
import { createDomEnv, resetDomEnv } from '../dom/env.js'
|
|
3
3
|
|
|
4
4
|
describe('createDomEnv', () => {
|
|
5
5
|
let env: ReturnType<typeof createDomEnv>
|
|
@@ -54,3 +54,44 @@ describe('createDomEnv', () => {
|
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
describe('resetDomEnv', () => {
|
|
59
|
+
let env: ReturnType<typeof createDomEnv>
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
env?.destroy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns a dirtied environment to its pristine state', () => {
|
|
66
|
+
env = createDomEnv()
|
|
67
|
+
const doc = env.window.document
|
|
68
|
+
|
|
69
|
+
doc.body.classList.add('theme-dark')
|
|
70
|
+
doc.documentElement.setAttribute('lang', 'fr')
|
|
71
|
+
doc.body.append(doc.createElement('div'))
|
|
72
|
+
doc.head.append(doc.createElement('style'))
|
|
73
|
+
// Proxied onto the global only on some node versions.
|
|
74
|
+
env.window.localStorage?.setItem('seen', '1')
|
|
75
|
+
;(env.window as any).__setupInstalled = true
|
|
76
|
+
|
|
77
|
+
resetDomEnv(env)
|
|
78
|
+
|
|
79
|
+
expect(doc.body.getAttributeNames()).toEqual([])
|
|
80
|
+
expect(doc.documentElement.getAttributeNames()).toEqual([])
|
|
81
|
+
expect(doc.body.children).toHaveLength(0)
|
|
82
|
+
expect(doc.head.children).toHaveLength(0)
|
|
83
|
+
expect(env.window.localStorage?.getItem('seen') ?? null).toBeNull()
|
|
84
|
+
// A setup marking itself installed would skip later stories in the worker.
|
|
85
|
+
expect((env.window as any).__setupInstalled).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('leaves the globals the environment itself installed', () => {
|
|
89
|
+
env = createDomEnv()
|
|
90
|
+
|
|
91
|
+
resetDomEnv(env)
|
|
92
|
+
|
|
93
|
+
expect(typeof env.window.ResizeObserver).toBe('function')
|
|
94
|
+
expect(typeof env.window.matchMedia).toBe('function')
|
|
95
|
+
expect(env.window.document.body).toBeTruthy()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -8,7 +8,7 @@ import { createBirpc } from 'birpc'
|
|
|
8
8
|
import { dirname, resolve } from 'pathe'
|
|
9
9
|
import pc from 'picocolors'
|
|
10
10
|
import { ModuleCacheMap, ViteNodeRunner } from 'vite-node/client'
|
|
11
|
-
import { createDomEnv } from '../dom/env.js'
|
|
11
|
+
import { createDomEnv, resetDomEnv } from '../dom/env.js'
|
|
12
12
|
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
14
|
|
|
@@ -25,6 +25,9 @@ export interface ReturnData {
|
|
|
25
25
|
|
|
26
26
|
const _moduleCache = new ModuleCacheMap()
|
|
27
27
|
let _runner: ViteNodeRunner
|
|
28
|
+
// Worker-lifetime: externalised runtimes cache the DOM they first saw, so one
|
|
29
|
+
// per story broke re-collection.
|
|
30
|
+
let _domEnv: ReturnType<typeof createDomEnv> | undefined
|
|
28
31
|
let _rpc: ReturnType<typeof createBirpc<{
|
|
29
32
|
fetchModule: FetchFunction
|
|
30
33
|
resolveId: ResolveIdFunction
|
|
@@ -61,7 +64,12 @@ export default async (payload: Payload): Promise<ReturnData> => {
|
|
|
61
64
|
},
|
|
62
65
|
}))
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
if (_domEnv) {
|
|
68
|
+
resetDomEnv(_domEnv)
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
_domEnv = createDomEnv()
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
const el = window.document.createElement('div')
|
|
67
75
|
|
|
@@ -86,8 +94,6 @@ export default async (payload: Payload): Promise<ReturnData> => {
|
|
|
86
94
|
})
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
destroyDomEnv()
|
|
90
|
-
|
|
91
97
|
const endTime = performance.now()
|
|
92
98
|
console.log(pc.dim(`${payload.storyFile.relativePath} ${Math.round(endTime - startTime)}ms (setup:${Math.round(beforeExecuteTime - startTime)}ms, execute:${Math.round(afterExecuteTime - beforeExecuteTime)}ms, run:${Math.round(afterRunTime - afterExecuteTime)}ms)`))
|
|
93
99
|
|
package/src/node/dom/env.ts
CHANGED
|
@@ -85,5 +85,39 @@ export function createDomEnv() {
|
|
|
85
85
|
return {
|
|
86
86
|
window,
|
|
87
87
|
destroy,
|
|
88
|
+
// `populateGlobal` does `global.window = global`, so `window` here is
|
|
89
|
+
// `globalThis` — one surface, not two.
|
|
90
|
+
pristineKeys: new Set(Object.keys(globalThis)),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Returns a reused environment to the state a fresh one would have been in. */
|
|
95
|
+
export function resetDomEnv(env: Pick<ReturnType<typeof createDomEnv>, 'window' | 'pristineKeys'>) {
|
|
96
|
+
const doc = env.window.document
|
|
97
|
+
|
|
98
|
+
for (const el of [doc.documentElement, doc.head, doc.body]) {
|
|
99
|
+
if (!el) continue
|
|
100
|
+
for (const name of [...el.getAttributeNames()]) {
|
|
101
|
+
el.removeAttribute(name)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
doc.head?.replaceChildren()
|
|
105
|
+
doc.body?.replaceChildren()
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Only present when the key was proxied onto the global, which varies by
|
|
109
|
+
// node version.
|
|
110
|
+
env.window.localStorage?.clear()
|
|
111
|
+
env.window.sessionStorage?.clear()
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Unavailable for some jsdom urls.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Additions only: the modules that installed the rest are cached, not re-run.
|
|
118
|
+
for (const key of Object.keys(globalThis)) {
|
|
119
|
+
if (!env.pristineKeys.has(key)) {
|
|
120
|
+
delete (globalThis as Record<string, unknown>)[key]
|
|
121
|
+
}
|
|
88
122
|
}
|
|
89
123
|
}
|
|
@@ -98,6 +98,43 @@ describe('wrapUserCss', () => {
|
|
|
98
98
|
const occurrences = (out.match(/@scope/g) ?? []).length
|
|
99
99
|
expect(occurrences).toBe(1)
|
|
100
100
|
})
|
|
101
|
+
|
|
102
|
+
// #173. Page-level rules stop at the controls slot, which carries the same
|
|
103
|
+
// scope-root class as the story body; author-written rules still reach it.
|
|
104
|
+
describe('with excludeRootClass', () => {
|
|
105
|
+
const opts = {
|
|
106
|
+
scopeRoot: '.__poveste-render-story',
|
|
107
|
+
excludeRootClass: '__poveste-controls-slot',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
it.each([
|
|
111
|
+
[':root', ':root { background: deeppink }', ':scope:not(.__poveste-controls-slot)'],
|
|
112
|
+
['html', 'html { background: deeppink }', ':scope:not(.__poveste-controls-slot)'],
|
|
113
|
+
['body', 'body { background: deeppink }', ':scope:not(.__poveste-controls-slot)'],
|
|
114
|
+
['body .card', 'body .card { color: red }', ':scope:not(.__poveste-controls-slot) .card'],
|
|
115
|
+
])('keeps a page-level %s rule out of the controls slot', (_name, css, expected) => {
|
|
116
|
+
expect(selectorsIn(wrapUserCss(css, opts))).toEqual([expected])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it.each([
|
|
120
|
+
['.user-card { background: deeppink }', '.user-card'],
|
|
121
|
+
['.body-copy { color: red }', '.body-copy'],
|
|
122
|
+
])('leaves the author-written %s alone', (css, expected) => {
|
|
123
|
+
expect(selectorsIn(wrapUserCss(css, opts))).toEqual([expected])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('carries the exclusion into roots nested in :is()', () => {
|
|
127
|
+
const out = wrapUserCss(':is(html, body) { color: red }', opts)
|
|
128
|
+
|
|
129
|
+
expect(selectorsIn(out)).toEqual([
|
|
130
|
+
':is(:scope:not(.__poveste-controls-slot), :scope:not(.__poveste-controls-slot))',
|
|
131
|
+
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('is opt-in, so the chrome pass is unaffected', () => {
|
|
135
|
+
expect(selectorsIn(wrapUserCss('body { color: red }', { scopeRoot: '.x' }))).toEqual([':scope'])
|
|
136
|
+
})
|
|
137
|
+
})
|
|
101
138
|
})
|
|
102
139
|
|
|
103
140
|
describe('wrapChromeCss', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin as VitePlugin } from 'vite'
|
|
2
|
-
import { STORY_SCOPE_ROOT } from './selectors.js'
|
|
2
|
+
import { CONTROLS_SLOT, STORY_SCOPE_ROOT } from './selectors.js'
|
|
3
3
|
import { wrapUserCss } from './transforms.js'
|
|
4
4
|
import { USER_CSS_MARK_END, USER_CSS_MARK_START } from './vite-plugin.js'
|
|
5
5
|
|
|
@@ -126,7 +126,7 @@ export function wrapMarkedUserCss(css: string, scopeRoot: string): string {
|
|
|
126
126
|
}
|
|
127
127
|
const inner = css.slice(start + USER_CSS_MARK_START.length, end).trim()
|
|
128
128
|
if (inner.length > 0) {
|
|
129
|
-
out += `${wrapUserCss(inner, { scopeRoot })}\n`
|
|
129
|
+
out += `${wrapUserCss(inner, { scopeRoot, excludeRootClass: CONTROLS_SLOT.slice(1) })}\n`
|
|
130
130
|
}
|
|
131
131
|
cursor = end + USER_CSS_MARK_END.length
|
|
132
132
|
}
|
|
@@ -9,6 +9,9 @@ export const STORY_SCOPE_ROOT = '.__poveste-render-story'
|
|
|
9
9
|
// .__poveste-render-custom-controls) but render inside the chrome panel,
|
|
10
10
|
// so the @scope boundary must keep them in chrome scope.
|
|
11
11
|
export const STORY_SCOPE_BOUNDARY = '.__poveste-render-story:not(.__poveste-render-custom-controls)'
|
|
12
|
+
// The panel only — the sandbox body also carries the class above, so excluding
|
|
13
|
+
// that one would stop a `body` rule applying inside the story itself.
|
|
14
|
+
export const CONTROLS_SLOT = '.__poveste-controls-slot'
|
|
12
15
|
export const DEFAULTS_LAYER = 'poveste-defaults'
|
|
13
16
|
export const USER_GLOBALS_LAYER = 'poveste-user-globals'
|
|
14
17
|
// Import query `globalStyles` files carry so the user-CSS plugin leaves them
|
|
@@ -3,6 +3,8 @@ import { transform as lightningcssTransform } from 'lightningcss'
|
|
|
3
3
|
|
|
4
4
|
export interface WrapOptions {
|
|
5
5
|
scopeRoot: string
|
|
6
|
+
/** Bare class name rewritten roots must not match — the panel shares the scope-root class (#173). */
|
|
7
|
+
excludeRootClass?: string
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface WrapWithLowerOptions {
|
|
@@ -17,7 +19,7 @@ export function wrapUserCss(css: string, opts: WrapOptions): string {
|
|
|
17
19
|
}
|
|
18
20
|
const { hoisted, remainder } = extractHoistableAtRules(css)
|
|
19
21
|
const body = hasRootSelector(remainder)
|
|
20
|
-
? rewriteRootToScope(remainder)
|
|
22
|
+
? rewriteRootToScope(remainder, opts.excludeRootClass)
|
|
21
23
|
: remainder
|
|
22
24
|
const hoistedBlock = hoisted.length > 0 ? `${hoisted.join('\n')}\n` : ''
|
|
23
25
|
return `${hoistedBlock}@scope (${opts.scopeRoot}) {\n${body}\n}\n`
|
|
@@ -92,10 +94,22 @@ interface SelectorPart {
|
|
|
92
94
|
selectors?: SelectorPart[][]
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
// Rewritten roots only, so an author's own classes still apply in the panel.
|
|
98
|
+
function scopeParts(excludeClass?: string): SelectorPart[] {
|
|
99
|
+
if (!excludeClass) {
|
|
100
|
+
return [SCOPE_PART]
|
|
101
|
+
}
|
|
102
|
+
return [SCOPE_PART, {
|
|
103
|
+
type: 'pseudo-class',
|
|
104
|
+
kind: 'not',
|
|
105
|
+
selectors: [[{ type: 'class', name: excludeClass }]],
|
|
106
|
+
}]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function rewriteSelector(selector: SelectorPart[], excludeClass?: string): SelectorPart[] {
|
|
110
|
+
return selector.flatMap((part, index) => {
|
|
97
111
|
if (part.type === 'pseudo-class' && part.kind === 'root') {
|
|
98
|
-
return
|
|
112
|
+
return scopeParts(excludeClass)
|
|
99
113
|
}
|
|
100
114
|
// A namespace is its own part, immediately before the type selector it
|
|
101
115
|
// qualifies. Rewriting past it emits `*|:scope`, which is invalid — a
|
|
@@ -103,22 +117,22 @@ function rewriteSelector(selector: SelectorPart[]): SelectorPart[] {
|
|
|
103
117
|
// `svg|body` also means a body in the SVG namespace, which is not the
|
|
104
118
|
// document root, so leaving these alone is right on both counts.
|
|
105
119
|
if (part.type === 'type' && isRootTypeSelector(part.name ?? '')) {
|
|
106
|
-
return selector[index - 1]?.type === 'namespace' ? part :
|
|
120
|
+
return selector[index - 1]?.type === 'namespace' ? [part] : scopeParts(excludeClass)
|
|
107
121
|
}
|
|
108
122
|
if (part.type === 'pseudo-class' && DESCENDABLE_PSEUDO_CLASSES.has(part.kind ?? '') && part.selectors) {
|
|
109
|
-
return { ...part, selectors: part.selectors.map(rewriteSelector) }
|
|
123
|
+
return [{ ...part, selectors: part.selectors.map(s => rewriteSelector(s, excludeClass)) }]
|
|
110
124
|
}
|
|
111
|
-
return part
|
|
125
|
+
return [part]
|
|
112
126
|
})
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
function rewriteRootToScope(css: string): string {
|
|
129
|
+
function rewriteRootToScope(css: string, excludeClass?: string): string {
|
|
116
130
|
const processed = lightningcssTransform({
|
|
117
131
|
filename: 'user.css',
|
|
118
132
|
code: Buffer.from(css, 'utf8'),
|
|
119
133
|
minify: false,
|
|
120
134
|
visitor: {
|
|
121
|
-
Selector: selector => rewriteSelector(selector as SelectorPart[]) as typeof selector,
|
|
135
|
+
Selector: selector => rewriteSelector(selector as SelectorPart[], excludeClass) as typeof selector,
|
|
122
136
|
},
|
|
123
137
|
})
|
|
124
138
|
return Buffer.from(processed.code).toString('utf8')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin as VitePlugin } from 'vite'
|
|
2
|
-
import { GLOBAL_LAYER_QUERY, STORY_SCOPE_ROOT } from './selectors.js'
|
|
2
|
+
import { CONTROLS_SLOT, GLOBAL_LAYER_QUERY, STORY_SCOPE_ROOT } from './selectors.js'
|
|
3
3
|
import { wrapUserCss } from './transforms.js'
|
|
4
4
|
|
|
5
5
|
export const USER_CSS_MARK_START = '/*__PVT_USER_CSS_START__*/'
|
|
@@ -29,7 +29,7 @@ export function userCssScopePlugin(opts: UserCssScopePluginOptions): VitePlugin
|
|
|
29
29
|
if (!isCssPath(path, query)) return null
|
|
30
30
|
if (hasGlobalQuery(query)) return null
|
|
31
31
|
if (mode === 'wrap') {
|
|
32
|
-
return { code: wrapUserCss(code, { scopeRoot: STORY_SCOPE_ROOT }), map: null }
|
|
32
|
+
return { code: wrapUserCss(code, { scopeRoot: STORY_SCOPE_ROOT, excludeRootClass: CONTROLS_SLOT.slice(1) }), map: null }
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
35
35
|
code: `${USER_CSS_MARK_START}\n${code}\n${USER_CSS_MARK_END}\n`,
|