-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: generate overrides.css on generate #11735
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
@import "foo/foo.less"; | ||
|
||
@red: green; | ||
.a { | ||
aaa: @red; | ||
bbb: @primary-color; | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.hoo { | ||
color: red; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import assert from 'assert'; | ||
import fs from 'fs'; | ||
import { join } from 'path'; | ||
import { compileLess } from './compileLess'; | ||
|
||
const fixturesDir = join(__dirname, '../../../fixtures'); | ||
|
||
// 在 jest 下跑会出错,所以只能手动跑来验证了 | ||
// test('normal', async () => { | ||
(async () => { | ||
const filePath = join(fixturesDir, 'overrides/less/index.less'); | ||
const modifyVars = { | ||
'primary-color': '#1DA57A', | ||
}; | ||
const alias = { | ||
barbar: join(filePath, '../node_modules/bar'), | ||
}; | ||
const result = await compileLess( | ||
fs.readFileSync(filePath, 'utf-8'), | ||
filePath, | ||
modifyVars, | ||
alias, | ||
); | ||
assert( | ||
result.includes( | ||
` | ||
.bar { | ||
color: red; | ||
} | ||
.foo { | ||
color: red; | ||
} | ||
.a { | ||
aaa: green; | ||
bbb: #1DA57A; | ||
} | ||
`.trim(), | ||
), | ||
); | ||
})().catch((e) => { | ||
console.error(e); | ||
}); | ||
// }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import less from '@umijs/bundler-utils/compiled/less'; | ||
|
||
export async function compileLess( | ||
lessContent: string, | ||
filePath: string, | ||
modifyVars: Record<string, string> = {}, | ||
alias: Record<string, string> = {}, | ||
) { | ||
const result = await less.render(lessContent, { | ||
filename: filePath, | ||
plugins: [ | ||
new (require('less-plugin-resolve') as any)({ | ||
aliases: alias, | ||
}), | ||
], | ||
javascriptEnabled: true, | ||
modifyVars, | ||
}); | ||
return result.css; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,48 @@ | ||
import { winPath } from '@umijs/utils'; | ||
import { existsSync } from 'fs'; | ||
import { existsSync, readFileSync } from 'fs'; | ||
import { join } from 'path'; | ||
import { expandCSSPaths } from '../../commands/dev/watch'; | ||
import type { IApi } from '../../types'; | ||
import { compileLess } from './compileLess'; | ||
import { transform } from './transform'; | ||
|
||
export function getOverridesCSS(absSrcPath: string) { | ||
return expandCSSPaths(join(absSrcPath, 'overrides')).find(existsSync); | ||
} | ||
|
||
export default (api: IApi) => { | ||
api.modifyConfig((memo) => { | ||
if (getOverridesCSS(api.paths.absSrcPath)) { | ||
memo.extraPostCSSPlugins ??= []; | ||
memo.extraPostCSSPlugins.push( | ||
// prefix #root for overrides.{ext} style file, to make sure selector priority is higher than async chunk style | ||
require('postcss-prefix-selector')({ | ||
// why not #root? | ||
// antd will insert dom into body, prefix #root will not works for that | ||
prefix: 'body', | ||
transform( | ||
_p: string, | ||
selector: string, | ||
prefixedSelector: string, | ||
filePath: string, | ||
) { | ||
const isOverridesFile = | ||
winPath(api.appData.overridesCSS[0]) === winPath(filePath); | ||
|
||
if (isOverridesFile) { | ||
if (selector === 'html') { | ||
// special :first-child to promote html selector priority | ||
return `html:first-child`; | ||
} else if (/^body([\s+~>[:]|$)/.test(selector)) { | ||
// use html parent to promote body selector priority | ||
return `html ${selector}`; | ||
} | ||
|
||
return prefixedSelector; | ||
} | ||
|
||
return selector; | ||
let cachedContent: string | null = null; | ||
api.onGenerateFiles(async () => { | ||
if (api.appData.overridesCSS.length) { | ||
const filePath = api.appData.overridesCSS[0]; | ||
let content = readFileSync(filePath, 'utf-8'); | ||
if (content === cachedContent) return; | ||
const isLess = filePath.endsWith('.less'); | ||
if (isLess) { | ||
content = await compileLess( | ||
content, | ||
filePath, | ||
{ | ||
...api.config.theme, | ||
...api.config.lessLoader?.modifyVars, | ||
}, | ||
}), | ||
); | ||
api.config.alias, | ||
); | ||
} | ||
content = await transform(content, filePath); | ||
api.writeTmpFile({ | ||
path: 'core/overrides.css', | ||
content, | ||
noPluginDir: true, | ||
}); | ||
cachedContent = content; | ||
} | ||
}); | ||
|
||
return memo; | ||
api.addEntryImports(() => { | ||
return [ | ||
api.appData.overridesCSS.length && { | ||
source: '@@/core/overrides.css', | ||
}, | ||
].filter(Boolean); | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { winPath } from '@umijs/utils'; | ||
import { join } from 'path'; | ||
import { transform } from './transform'; | ||
|
||
const fixturesDir = join(__dirname, '../../../fixtures'); | ||
|
||
test('transform selector', async () => { | ||
const result = await transform( | ||
` | ||
html {} | ||
.a {} | ||
#b {} | ||
div {} | ||
@media (max-width: 100px) { | ||
.b {} | ||
} | ||
`, | ||
'/foo/bar/hoo.css', | ||
); | ||
expect(result).toEqual(` | ||
html:first-child {} | ||
body .a {} | ||
body #b {} | ||
body div {} | ||
@media (max-width: 100px) { | ||
body .b {} | ||
} | ||
`); | ||
}); | ||
|
||
test('transform import', async () => { | ||
const filePath = join(fixturesDir, 'overrides/normal/foo/bar/hoo.css'); | ||
const result = await transform( | ||
` | ||
@import "a"; | ||
@import "~b"; | ||
@import "./a.css"; | ||
@import './a.css'; | ||
@import "../a.css"; | ||
@import "../not-exists.css"; | ||
@import "a.css"; | ||
@import "child/a.css"; | ||
@import "child/not-exists.css"; | ||
`, | ||
filePath, | ||
); | ||
expect(result.replace(new RegExp(`${winPath(fixturesDir)}`, 'g'), '')) | ||
.toEqual(` | ||
@import "a"; | ||
@import "~b"; | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import "/overrides/normal/foo/a.css"; | ||
@import "../not-exists.css"; | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import "/overrides/normal/foo/bar/child/a.css"; | ||
@import "child/not-exists.css"; | ||
`); | ||
}); | ||
|
||
test('transform import with url', async () => { | ||
const filePath = join(fixturesDir, 'overrides/normal/foo/bar/hoo.css'); | ||
const result = await transform( | ||
` | ||
@import url("a.css"); | ||
@import url('a.css'); | ||
@import url(a.css); | ||
@import url(not-exists.css); | ||
`, | ||
filePath, | ||
); | ||
expect(result.replace(new RegExp(`${winPath(fixturesDir)}`, 'g'), '')) | ||
.toEqual(` | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import "/overrides/normal/foo/bar/a.css"; | ||
@import url(not-exists.css); | ||
`); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { winPath } from '@umijs/utils'; | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import type { AtRule } from 'postcss'; | ||
|
||
export async function transform(cssContent: string, filePath: string) { | ||
const importPlugin = { | ||
postcssPlugin: 'importPlugin', | ||
AtRule: { | ||
import(atRule: AtRule) { | ||
let origin = atRule.params; | ||
// remove url() wrapper | ||
origin = origin.replace(/^url\((.+)\)$/g, '$1'); | ||
// remove start ' or " and end ' or " | ||
origin = origin.replace(/^'(.+)'$/g, '$1').replace(/^"(.+)"$/g, '$1'); | ||
|
||
// ~ 开头的肯定是从 node_modules 下查找 | ||
if (origin.startsWith('~')) return; | ||
if (origin.startsWith('/')) return; | ||
|
||
function getResolvedPath(origin: string) { | ||
return winPath(path.resolve(path.dirname(filePath), origin)); | ||
} | ||
|
||
// 已经包含 ./ 和 ../ 的场景,不需要额外处理 | ||
// CSS 会优先找相对路径,如果找不到,会再找 node_modules 下的 | ||
const resolvedPath = getResolvedPath(origin); | ||
if (fs.existsSync(resolvedPath)) { | ||
atRule.params = `"${resolvedPath}"`; | ||
} else { | ||
// Warn user if file not exists, but it should be existed | ||
if (origin.startsWith('./') || origin.startsWith('../')) { | ||
console.warn(`File does not exist: ${resolvedPath}`); | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
const selectorPlugin = require('postcss-prefix-selector')({ | ||
// why not #root? | ||
// antd will insert dom into body, prefix #root will not work for that | ||
prefix: 'body', | ||
transform(_p: string, selector: string, prefixedSelector: string) { | ||
if (selector === 'html') { | ||
// special :first-child to promote html selector priority | ||
return `html:first-child`; | ||
} else if (/^body([\s+~>[:]|$)/.test(selector)) { | ||
// use html parent to promote body selector priority | ||
return `html ${selector}`; | ||
} | ||
return prefixedSelector; | ||
}, | ||
}); | ||
const result = await require('postcss')([ | ||
selectorPlugin, | ||
importPlugin, | ||
]).process(cssContent, {}); | ||
return result.css; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个地方放弃了
scss
/sass
是一个 Breaking change 。