poveste 0.2.1 → 0.3.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/node/build.js +0 -3
- package/dist/node/builtin-plugins/tailwind-tokens.d.ts +6 -1
- package/dist/node/builtin-plugins/tailwind-tokens.d.ts.map +1 -1
- package/dist/node/builtin-plugins/tailwind-tokens.js +169 -33
- package/dist/node/builtin-plugins/vanilla-support/MountStory.d.ts +3 -3
- package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts +5 -5
- package/dist/node/collect/index.d.ts.map +1 -1
- package/dist/node/collect/index.js +1 -3
- package/dist/node/config.d.ts +2 -18
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/config.js +3 -0
- package/dist/node/dom/env.d.ts.map +1 -1
- package/dist/node/dom/env.js +10 -6
- package/dist/node/index.d.ts +0 -7
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/markdown.d.ts.map +1 -1
- package/dist/node/markdown.js +21 -3
- package/dist/node/stories.js +1 -1
- package/dist/node/style-isolation/entry-css-merger.d.ts.map +1 -1
- package/dist/node/style-isolation/entry-css-merger.js +8 -7
- package/dist/node/style-isolation/transforms.js +32 -8
- package/dist/node/vite.js +1 -1
- package/package.json +10 -12
- package/src/node/__tests__/dom-env.spec.ts +19 -1
- package/src/node/build.ts +2 -6
- package/src/node/builtin-plugins/tailwind-tokens.ts +190 -34
- package/src/node/collect/index.ts +1 -3
- package/src/node/config.ts +3 -0
- package/src/node/dom/env.ts +10 -6
- package/src/node/index.ts +0 -8
- package/src/node/markdown.ts +25 -3
- package/src/node/stories.ts +1 -1
- package/src/node/style-isolation/__tests__/entry-css-merger.spec.ts +37 -18
- package/src/node/style-isolation/__tests__/transforms.spec.ts +46 -8
- package/src/node/style-isolation/entry-css-merger.ts +10 -9
- package/src/node/style-isolation/transforms.ts +36 -7
- package/src/node/vite.ts +4 -4
- package/tsconfig.json +1 -1
package/src/node/markdown.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ServerMarkdownFile } from '@poveste/shared'
|
|
2
|
+
import type { Highlighter } from 'shiki'
|
|
2
3
|
import type { Plugin as VitePlugin } from 'vite'
|
|
3
4
|
import type { Context } from './context.js'
|
|
4
5
|
import { kebabCase } from 'change-case'
|
|
@@ -28,14 +29,35 @@ function notifyMarkdownListChange() {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Shared between the markdown Vite plugin and the markdown file watcher, which
|
|
34
|
+
* both build a renderer — a `poveste build` reaches this three times. The
|
|
35
|
+
* themes and langs are constant, and each instance eagerly loads every bundled
|
|
36
|
+
* grammar, so the duplicates are pure waste.
|
|
37
|
+
*/
|
|
38
|
+
let highlighterPromise: Promise<Highlighter> | undefined
|
|
39
|
+
|
|
40
|
+
function getHighlighter() {
|
|
41
|
+
highlighterPromise ??= createHighlighter({
|
|
33
42
|
themes: ['github-dark'],
|
|
34
43
|
langs: Object.keys(bundledLanguages), // not ideal but markdown-it does not provide async highlight
|
|
35
44
|
})
|
|
36
45
|
|
|
46
|
+
return highlighterPromise
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function createMarkdownRenderer(ctx: Context) {
|
|
50
|
+
const highlighter = await getHighlighter()
|
|
51
|
+
|
|
37
52
|
const md = new MarkdownIt({
|
|
38
|
-
|
|
53
|
+
/*
|
|
54
|
+
* Unprefixed utilities: the `ptw-` prefix went away with Tailwind v4, and
|
|
55
|
+
* `not-prose` in particular is matched by the typography plugin's own
|
|
56
|
+
* `[class~="not-prose"]` selector, so a prefixed spelling silently opts the
|
|
57
|
+
* block back *into* prose. These classes are generated because main.pcss
|
|
58
|
+
* `@source`s this file — v4 auto-detection only scans the app package.
|
|
59
|
+
*/
|
|
60
|
+
highlight: (code, lang) => `<div class="relative not-prose __poveste-code __histoire-code"><div class="absolute top-0 right-0 text-xs text-white/40">${lang}</div>${highlighter.codeToHtml(code, { theme: 'github-dark', lang })}</div>`,
|
|
39
61
|
linkify: true,
|
|
40
62
|
html: true,
|
|
41
63
|
breaks: false,
|
package/src/node/stories.ts
CHANGED
|
@@ -76,7 +76,7 @@ export async function watchStories(newContext: Context) {
|
|
|
76
76
|
watcher
|
|
77
77
|
.on('add', (file) => {
|
|
78
78
|
const storyFile = addStory(file)
|
|
79
|
-
setTimeout(
|
|
79
|
+
setTimeout(notifyStoryChange, 100, storyFile) // Delay in case file renaming fired Add event before Unlink event
|
|
80
80
|
})
|
|
81
81
|
.on('unlink', (file) => {
|
|
82
82
|
removeStory(file)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Rolldown } from 'vite'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
import { entryCssMergerPlugin } from '../entry-css-merger.js'
|
|
4
4
|
import { USER_CSS_MARK_END, USER_CSS_MARK_START } from '../vite-plugin.js'
|
|
@@ -30,25 +30,44 @@ function mark(css: string): string {
|
|
|
30
30
|
return `${USER_CSS_MARK_START}\n${css}\n${USER_CSS_MARK_END}\n`
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Rolldown ignores assignment to `bundle` in generateBundle, so merged CSS is
|
|
34
|
+
// added via this.emitFile. Capture those emits to assert on them.
|
|
35
|
+
function runMerger(plugin: any, bundle: Rolldown.OutputBundle) {
|
|
36
|
+
const emitted = new Map<string, any>()
|
|
37
|
+
const ctx = {
|
|
38
|
+
emitFile: (file: any) => {
|
|
39
|
+
emitted.set(file.fileName, file)
|
|
40
|
+
return file.fileName
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
;(plugin.generateBundle as any).call(ctx, {}, bundle)
|
|
44
|
+
return emitted
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
describe('entry-css-merger', () => {
|
|
34
48
|
it('produces one CSS asset per entry chunk', async () => {
|
|
35
49
|
const plugin = entryCssMergerPlugin()
|
|
36
|
-
const bundle: OutputBundle = {
|
|
50
|
+
const bundle: Rolldown.OutputBundle = {
|
|
37
51
|
'bundle-main.js': fakeChunk('bundle-main', ['route1.css', 'route2.css']),
|
|
38
52
|
'bundle-sandbox.js': fakeChunk('bundle-sandbox', ['sandbox-route.css']),
|
|
39
53
|
'route1.css': fakeAsset('route1.css', 'a {}'),
|
|
40
54
|
'route2.css': fakeAsset('route2.css', 'b {}'),
|
|
41
55
|
'sandbox-route.css': fakeAsset('sandbox-route.css', 'c {}'),
|
|
42
56
|
}
|
|
43
|
-
|
|
57
|
+
const emitted = runMerger(plugin, bundle)
|
|
44
58
|
|
|
45
|
-
const cssAssets =
|
|
59
|
+
const cssAssets = [...emitted.values()].filter(a => a.fileName.endsWith('.css'))
|
|
46
60
|
expect(cssAssets).toHaveLength(2)
|
|
47
|
-
const mainCss =
|
|
48
|
-
const sandboxCss =
|
|
61
|
+
const mainCss = emitted.get('bundle-main.css')
|
|
62
|
+
const sandboxCss = emitted.get('bundle-sandbox.css')
|
|
49
63
|
expect(mainCss?.source).toContain('a {}')
|
|
50
64
|
expect(mainCss?.source).toContain('b {}')
|
|
51
65
|
expect(sandboxCss?.source).toContain('c {}')
|
|
66
|
+
|
|
67
|
+
// The per-chunk CSS assets are dropped from the bundle once merged.
|
|
68
|
+
expect(bundle['route1.css']).toBeUndefined()
|
|
69
|
+
expect(bundle['route2.css']).toBeUndefined()
|
|
70
|
+
expect(bundle['sandbox-route.css']).toBeUndefined()
|
|
52
71
|
})
|
|
53
72
|
|
|
54
73
|
it('wraps marked user CSS in @scope on the main entry when isolation is enabled', async () => {
|
|
@@ -57,14 +76,14 @@ describe('entry-css-merger', () => {
|
|
|
57
76
|
scopeRoot: '.__poveste-render-story',
|
|
58
77
|
mainEntryName: 'bundle-main',
|
|
59
78
|
})
|
|
60
|
-
const bundle: OutputBundle = {
|
|
79
|
+
const bundle: Rolldown.OutputBundle = {
|
|
61
80
|
'bundle-main.js': fakeChunk('bundle-main', ['user.css', 'chrome.css']),
|
|
62
81
|
'user.css': fakeAsset('user.css', mark('body { color: red }')),
|
|
63
82
|
'chrome.css': fakeAsset('chrome.css', '@scope (.poveste-app-root) { .x {} }'),
|
|
64
83
|
}
|
|
65
|
-
|
|
84
|
+
const emitted = runMerger(plugin, bundle)
|
|
66
85
|
|
|
67
|
-
const main =
|
|
86
|
+
const main = emitted.get('bundle-main.css')
|
|
68
87
|
expect(main.source).toContain('@scope (.__poveste-render-story)')
|
|
69
88
|
expect(main.source).toContain('color: red')
|
|
70
89
|
expect(main.source).toContain('@scope (.poveste-app-root)')
|
|
@@ -77,13 +96,13 @@ describe('entry-css-merger', () => {
|
|
|
77
96
|
scopeRoot: '.__poveste-render-story',
|
|
78
97
|
mainEntryName: 'bundle-main',
|
|
79
98
|
})
|
|
80
|
-
const bundle: OutputBundle = {
|
|
99
|
+
const bundle: Rolldown.OutputBundle = {
|
|
81
100
|
'bundle-sandbox.js': fakeChunk('bundle-sandbox', ['user.css']),
|
|
82
101
|
'user.css': fakeAsset('user.css', mark('body { color: red }')),
|
|
83
102
|
}
|
|
84
|
-
|
|
103
|
+
const emitted = runMerger(plugin, bundle)
|
|
85
104
|
|
|
86
|
-
const sandbox =
|
|
105
|
+
const sandbox = emitted.get('bundle-sandbox.css')
|
|
87
106
|
expect(sandbox.source).toContain('body')
|
|
88
107
|
expect(sandbox.source).toContain('color: red')
|
|
89
108
|
expect(sandbox.source).not.toContain('@scope')
|
|
@@ -95,14 +114,14 @@ describe('entry-css-merger', () => {
|
|
|
95
114
|
const main = fakeChunk('bundle-main', ['app.css'], '', { imports: ['vendor.js'], isEntry: true })
|
|
96
115
|
main.fileName = 'bundle-main.js'
|
|
97
116
|
const vendor = { ...fakeChunk('vendor', ['vendor.css']), isEntry: false, fileName: 'vendor.js' }
|
|
98
|
-
const bundle: OutputBundle = {
|
|
117
|
+
const bundle: Rolldown.OutputBundle = {
|
|
99
118
|
'bundle-main.js': main,
|
|
100
119
|
'vendor.js': vendor,
|
|
101
120
|
'app.css': fakeAsset('app.css', '.app {}'),
|
|
102
121
|
'vendor.css': fakeAsset('vendor.css', '.vendor {}'),
|
|
103
122
|
}
|
|
104
|
-
|
|
105
|
-
const mainCss =
|
|
123
|
+
const emitted = runMerger(plugin, bundle)
|
|
124
|
+
const mainCss = emitted.get('bundle-main.css')
|
|
106
125
|
expect(mainCss.source).toContain('.app')
|
|
107
126
|
expect(mainCss.source).toContain('.vendor')
|
|
108
127
|
})
|
|
@@ -113,13 +132,13 @@ describe('entry-css-merger', () => {
|
|
|
113
132
|
scopeRoot: '.__poveste-render-story',
|
|
114
133
|
mainEntryName: 'bundle-main',
|
|
115
134
|
})
|
|
116
|
-
const bundle: OutputBundle = {
|
|
135
|
+
const bundle: Rolldown.OutputBundle = {
|
|
117
136
|
'bundle-main.js': fakeChunk('bundle-main', ['user.css']),
|
|
118
137
|
'user.css': fakeAsset('user.css', mark('body { color: red }')),
|
|
119
138
|
}
|
|
120
|
-
|
|
139
|
+
const emitted = runMerger(plugin, bundle)
|
|
121
140
|
|
|
122
|
-
const main =
|
|
141
|
+
const main = emitted.get('bundle-main.css')
|
|
123
142
|
expect(main.source).not.toContain('@scope')
|
|
124
143
|
expect(main.source).not.toContain(USER_CSS_MARK_START)
|
|
125
144
|
})
|
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { isGlobalImport, wrapChromeCss, wrapUserCss } from '../transforms.js'
|
|
3
3
|
|
|
4
|
+
/** Selectors of every rule inside the wrap, so a test can assert on them whole. */
|
|
5
|
+
function selectorsIn(css: string): string[] {
|
|
6
|
+
return css.split('{')
|
|
7
|
+
.slice(0, -1)
|
|
8
|
+
.map(chunk => chunk.slice(chunk.lastIndexOf('}') + 1).trim())
|
|
9
|
+
.filter(selector => selector.length > 0 && !selector.startsWith('@'))
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
describe('wrapUserCss', () => {
|
|
5
13
|
it('wraps a single rule in @scope (.scope-root)', () => {
|
|
6
|
-
const out = wrapUserCss('
|
|
14
|
+
const out = wrapUserCss('main { color: red }', { scopeRoot: '.__poveste-render-story' })
|
|
7
15
|
expect(out).toContain('@scope (.__poveste-render-story)')
|
|
8
|
-
expect(out).toContain('
|
|
16
|
+
expect(out).toContain('main')
|
|
9
17
|
expect(out).toContain('color: red')
|
|
10
18
|
})
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
// Every root spelling has to land on :scope. A rule left as `html`/`body`
|
|
21
|
+
// targets an ancestor of the scoping root and can never match — silently,
|
|
22
|
+
// which is what made #116 survive this long. Asserting the whole selector
|
|
23
|
+
// rather than "contains :scope": the failure mode is a leftover `html`, and
|
|
24
|
+
// a substring check passes right over it.
|
|
25
|
+
it.each([
|
|
26
|
+
[':root', ':root { --brand: rebeccapurple }', ':scope'],
|
|
27
|
+
['html', 'html { --brand: rebeccapurple }', ':scope'],
|
|
28
|
+
['body', 'body { --brand: rebeccapurple }', ':scope'],
|
|
29
|
+
['BODY', 'BODY { --brand: rebeccapurple }', ':scope'],
|
|
30
|
+
['body .card', 'body .card { --brand: rebeccapurple }', ':scope .card'],
|
|
31
|
+
['body.dark .card', 'body.dark .card { --brand: rebeccapurple }', ':scope.dark .card'],
|
|
32
|
+
['.body-copy', '.body-copy { --brand: rebeccapurple }', '.body-copy'],
|
|
33
|
+
['.page-html', '.page-html { --brand: rebeccapurple }', '.page-html'],
|
|
34
|
+
])('compiles %s to a selector that can still match', (_input, css, expected) => {
|
|
35
|
+
const out = wrapUserCss(css, { scopeRoot: '.__poveste-render-story' })
|
|
36
|
+
|
|
37
|
+
expect(selectorsIn(out)).toEqual([expected])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('rewrites root selectors nested in at-rules', () => {
|
|
41
|
+
const out = wrapUserCss('@media (min-width: 700px) { body { --brand: rebeccapurple } }', {
|
|
14
42
|
scopeRoot: '.__poveste-render-story',
|
|
15
43
|
})
|
|
16
|
-
|
|
17
|
-
expect(out).
|
|
18
|
-
expect(out).toContain('--color-primary: blue')
|
|
44
|
+
|
|
45
|
+
expect(selectorsIn(out)).toEqual([':scope'])
|
|
19
46
|
})
|
|
20
47
|
|
|
21
48
|
it('leaves :scope alone (idempotent)', () => {
|
|
@@ -47,7 +74,18 @@ describe('wrapChromeCss', () => {
|
|
|
47
74
|
scopeLower: '.__poveste-render-story',
|
|
48
75
|
})
|
|
49
76
|
expect(out).toContain('@scope (.poveste-app-root) to (.__poveste-render-story)')
|
|
50
|
-
expect(out).toContain('
|
|
77
|
+
expect(out).toContain('color: red')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Same rule as the user side: inside the wrap, `body` means the app's root
|
|
81
|
+
// container, which is what :scope resolves to (#102).
|
|
82
|
+
it('rewrites html and body to :scope', () => {
|
|
83
|
+
const out = wrapChromeCss('body { font-size: .875rem }\nhtml { color-scheme: dark }', {
|
|
84
|
+
scopeRoot: '.poveste-app-root',
|
|
85
|
+
scopeLower: '.__poveste-render-story',
|
|
86
|
+
})
|
|
87
|
+
expect(out).toContain(':scope')
|
|
88
|
+
expect(out).not.toMatch(/(?:^|[\s,{])(?:html|body)\b/m)
|
|
51
89
|
})
|
|
52
90
|
|
|
53
91
|
it('hoists @import to the top, outside @scope', () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { OutputBundle } from 'rollup'
|
|
2
1
|
import type { Plugin as VitePlugin } from 'vite'
|
|
3
2
|
import { STORY_SCOPE_ROOT } from './selectors.js'
|
|
4
3
|
import { wrapUserCss } from './transforms.js'
|
|
@@ -17,7 +16,7 @@ export function entryCssMergerPlugin(opts: EntryCssMergerOptions = { isolateStyl
|
|
|
17
16
|
name: 'poveste:style-isolation:entry-css-merger',
|
|
18
17
|
apply: 'build',
|
|
19
18
|
enforce: 'post',
|
|
20
|
-
generateBundle(_options, bundle
|
|
19
|
+
generateBundle(_options, bundle) {
|
|
21
20
|
function collectChunkCss(entryFileName: string, entryName: string, acc: Map<string, string[]>) {
|
|
22
21
|
const seen = new Set<string>()
|
|
23
22
|
const queue = [entryFileName]
|
|
@@ -48,6 +47,7 @@ export function entryCssMergerPlugin(opts: EntryCssMergerOptions = { isolateStyl
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
const mergedByEntry = new Map<string, string[]>()
|
|
50
|
+
const mergedCssAssets: string[] = []
|
|
51
51
|
for (const [cssName, entries] of cssAssetUsedBy) {
|
|
52
52
|
const asset = bundle[cssName]
|
|
53
53
|
if (!asset || asset.type !== 'asset') continue
|
|
@@ -59,6 +59,13 @@ export function entryCssMergerPlugin(opts: EntryCssMergerOptions = { isolateStyl
|
|
|
59
59
|
list.push(source)
|
|
60
60
|
mergedByEntry.set(entryName, list)
|
|
61
61
|
}
|
|
62
|
+
mergedCssAssets.push(cssName)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Rolldown ignores assignment to `bundle` in generateBundle, so the merged
|
|
66
|
+
// files are added with this.emitFile. The now-merged per-chunk CSS assets
|
|
67
|
+
// are dropped so they don't ship as unscoped, orphaned duplicates.
|
|
68
|
+
for (const cssName of mergedCssAssets) {
|
|
62
69
|
delete bundle[cssName]
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -68,13 +75,7 @@ export function entryCssMergerPlugin(opts: EntryCssMergerOptions = { isolateStyl
|
|
|
68
75
|
const finalSource = entryName === mainEntryName && opts.isolateStyles
|
|
69
76
|
? wrapMarkedUserCss(merged, scopeRoot)
|
|
70
77
|
: stripMarkers(merged)
|
|
71
|
-
|
|
72
|
-
type: 'asset',
|
|
73
|
-
fileName,
|
|
74
|
-
name: fileName,
|
|
75
|
-
source: finalSource,
|
|
76
|
-
needsCodeReference: false,
|
|
77
|
-
} as any
|
|
78
|
+
this.emitFile({ type: 'asset', fileName, source: finalSource })
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// Re-point CSS metadata: entries claim their own merged file; non-entry
|
|
@@ -16,7 +16,7 @@ export function wrapUserCss(css: string, opts: WrapOptions): string {
|
|
|
16
16
|
return css
|
|
17
17
|
}
|
|
18
18
|
const { hoisted, remainder } = extractHoistableAtRules(css)
|
|
19
|
-
const body = remainder
|
|
19
|
+
const body = hasRootSelector(remainder)
|
|
20
20
|
? rewriteRootToScope(remainder)
|
|
21
21
|
: remainder
|
|
22
22
|
const hoistedBlock = hoisted.length > 0 ? `${hoisted.join('\n')}\n` : ''
|
|
@@ -29,10 +29,10 @@ export function wrapChromeCss(css: string, opts: WrapWithLowerOptions): string {
|
|
|
29
29
|
return css
|
|
30
30
|
}
|
|
31
31
|
const { hoisted, remainder } = extractHoistableAtRules(css)
|
|
32
|
-
// Rewrite
|
|
32
|
+
// Rewrite root selectors to `:scope` so source-level `@scope (:root) to (...)`
|
|
33
33
|
// rules nest correctly under the outer scope; without this, the inner
|
|
34
34
|
// `:root` resolves to the document root which is outside the outer scope.
|
|
35
|
-
const body = remainder
|
|
35
|
+
const body = hasRootSelector(remainder)
|
|
36
36
|
? rewriteRootToScope(remainder)
|
|
37
37
|
: remainder
|
|
38
38
|
const hoistedBlock = hoisted.length > 0 ? `${hoisted.join('\n')}\n` : ''
|
|
@@ -46,19 +46,48 @@ export function isGlobalImport(id: string): boolean {
|
|
|
46
46
|
return parts.includes('global') || parts.some(p => p.startsWith('global='))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// `html` and `body` sit above the scoping root once the sheet is wrapped, so a
|
|
50
|
+
// rule targeting either can never match. Inside a story they mean the same
|
|
51
|
+
// thing `:root` does — the one root there is — which is what `:scope`
|
|
52
|
+
// resolves to. Leaving them out made `body { font-size: 14px }` inert with no
|
|
53
|
+
// error (#116); the chrome wrapper learned the same lesson in #102.
|
|
54
|
+
const ROOT_TYPE_SELECTORS = new Set(['html', 'body'])
|
|
55
|
+
|
|
56
|
+
// Only a gate on the lightningcss round-trip, so a false positive costs a
|
|
57
|
+
// reparse and nothing else. Word-bounded to skip `.sidebar-body` and friends.
|
|
58
|
+
const ROOT_SELECTOR_RE = /:root|(?:^|[\s,{}>+~])(?:html|body)\b/i
|
|
59
|
+
|
|
60
|
+
function hasRootSelector(css: string): boolean {
|
|
61
|
+
return ROOT_SELECTOR_RE.test(css)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Type selectors are ASCII case-insensitive against HTML and lightningcss
|
|
65
|
+
// reports the name as authored, so `BODY { … }` arrives spelled `BODY`.
|
|
66
|
+
function isRootTypeSelector(name: string): boolean {
|
|
67
|
+
return ROOT_TYPE_SELECTORS.has(name.toLowerCase())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Top level of the selector only: lightningcss hands `:is()`/`:where()`/`:not()`
|
|
71
|
+
// over as one part with the inner selectors nested inside, so a root spelled
|
|
72
|
+
// `:is(html, body)` is left alone. Descending needs a decision on `:not()`,
|
|
73
|
+
// where rewriting would change what the rule matches rather than fix it (#124).
|
|
49
74
|
function rewriteRootToScope(css: string): string {
|
|
75
|
+
const scope = { type: 'pseudo-class', kind: 'scope' } as const
|
|
50
76
|
const processed = lightningcssTransform({
|
|
51
77
|
filename: 'user.css',
|
|
52
78
|
code: Buffer.from(css, 'utf8'),
|
|
53
79
|
minify: false,
|
|
54
80
|
visitor: {
|
|
55
81
|
Selector(selector) {
|
|
56
|
-
|
|
82
|
+
return selector.map((part) => {
|
|
57
83
|
if (part.type === 'pseudo-class' && (part as any).kind === 'root') {
|
|
58
|
-
|
|
84
|
+
return scope as unknown as typeof part
|
|
85
|
+
}
|
|
86
|
+
if (part.type === 'type' && isRootTypeSelector(part.name)) {
|
|
87
|
+
return scope as unknown as typeof part
|
|
59
88
|
}
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
return part
|
|
90
|
+
})
|
|
62
91
|
},
|
|
63
92
|
},
|
|
64
93
|
})
|
package/src/node/vite.ts
CHANGED
|
@@ -30,9 +30,9 @@ export async function mergePovesteViteConfig(viteConfig: InlineConfig, ctx: Cont
|
|
|
30
30
|
const command = ctx.mode === 'dev' ? 'serve' : 'build'
|
|
31
31
|
const overrides = typeof ctx.config.vite === 'function'
|
|
32
32
|
? await ctx.config.vite(viteConfig as ViteConfig, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
mode: ctx.mode,
|
|
34
|
+
command,
|
|
35
|
+
})
|
|
36
36
|
: ctx.config.vite
|
|
37
37
|
if (overrides) {
|
|
38
38
|
viteConfig = mergeViteConfig(viteConfig, overrides)
|
|
@@ -324,7 +324,7 @@ export async function getViteConfigWithPlugins(isServer: boolean, ctx: Context):
|
|
|
324
324
|
name: 'poveste:flags',
|
|
325
325
|
enforce: 'pre',
|
|
326
326
|
transform(code, id) {
|
|
327
|
-
if (
|
|
327
|
+
if (/\.(?:vue|js)(?:$|\?)/.test(id)) {
|
|
328
328
|
const original = code
|
|
329
329
|
for (const flag in flags) {
|
|
330
330
|
code = code.replace(new RegExp(flag, 'g'), flags[flag])
|