diff --git a/.antd-tools.config.js b/.antd-tools.config.js new file mode 100644 index 000000000..7a5bec367 --- /dev/null +++ b/.antd-tools.config.js @@ -0,0 +1,195 @@ +const fs = require('fs'); +const path = require('path'); +const defaultVars = require('./scripts/default-vars'); +const darkVars = require('./scripts/dark-vars'); +const compactVars = require('./scripts/compact-vars'); + +function generateThemeFileContent(theme) { + return `const { ${theme}ThemeSingle } = require('./theme');\nconst defaultTheme = require('./default-theme');\n +module.exports = { + ...defaultTheme, + ...${theme}ThemeSingle +}`; +} + +// We need compile additional content for antd user +function finalizeCompile() { + if (fs.existsSync(path.join(__dirname, './lib'))) { + // Build a entry less file to dist/antd.less + const componentsPath = path.join(process.cwd(), 'components'); + let componentsLessContent = ''; + // Build components in one file: lib/style/components.less + fs.readdir(componentsPath, (err, files) => { + files.forEach(file => { + if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) { + componentsLessContent += `@import "../${path.posix.join( + file, + 'style', + 'index-pure.less', + )}";\n`; + } + }); + fs.writeFileSync( + path.join(process.cwd(), 'lib', 'style', 'components.less'), + componentsLessContent, + ); + }); + } +} + +function buildThemeFile(theme, vars) { + // Build less entry file: dist/antd.${theme}.less + if (theme !== 'default') { + fs.writeFileSync( + path.join(process.cwd(), 'dist', `antd.${theme}.less`), + `@import "../lib/style/${theme}.less";\n@import "../lib/style/components.less";`, + ); + // eslint-disable-next-line no-console + console.log(`Built a entry less file to dist/antd.${theme}.less`); + } else { + fs.writeFileSync( + path.join(process.cwd(), 'dist', `default-theme.js`), + `module.exports = ${JSON.stringify(vars, null, 2)};\n`, + ); + return; + } + + // Build ${theme}.js: dist/${theme}-theme.js, for less-loader + + fs.writeFileSync( + path.join(process.cwd(), 'dist', `theme.js`), + `const ${theme}ThemeSingle = ${JSON.stringify(vars, null, 2)};\n`, + { + flag: 'a', + }, + ); + + fs.writeFileSync( + path.join(process.cwd(), 'dist', `${theme}-theme.js`), + generateThemeFileContent(theme), + ); + + // eslint-disable-next-line no-console + console.log(`Built a ${theme} theme js file to dist/${theme}-theme.js`); +} + +function finalizeDist() { + if (fs.existsSync(path.join(__dirname, './dist'))) { + // Build less entry file: dist/antd.less + fs.writeFileSync( + path.join(process.cwd(), 'dist', 'antd.less'), + '@import "../lib/style/default.less";\n@import "../lib/style/components.less";', + ); + // eslint-disable-next-line no-console + fs.writeFileSync( + path.join(process.cwd(), 'dist', 'theme.js'), + `const defaultTheme = require('./default-theme.js');\n`, + ); + // eslint-disable-next-line no-console + console.log('Built a entry less file to dist/antd.less'); + buildThemeFile('default', defaultVars); + buildThemeFile('dark', darkVars); + buildThemeFile('compact', compactVars); + buildThemeFile('variable', {}); + fs.writeFileSync( + path.join(process.cwd(), 'dist', `theme.js`), + ` +function getThemeVariables(options = {}) { + let themeVar = { + 'hack': \`true;@import "\${require.resolve('antd/lib/style/color/colorPalette.less')}";\`, + ...defaultTheme + }; + if(options.dark) { + themeVar = { + ...themeVar, + ...darkThemeSingle + } + } + if(options.compact){ + themeVar = { + ...themeVar, + ...compactThemeSingle + } + } + return themeVar; +} + +module.exports = { + darkThemeSingle, + compactThemeSingle, + getThemeVariables +}`, + { + flag: 'a', + }, + ); + } +} + +function isComponentStyleEntry(file) { + return file.path.match(/style(\/|\\)index\.tsx/); +} + +function needTransformStyle(content) { + return content.includes('../../style/index.less') || content.includes('./index.less'); +} + +module.exports = { + compile: { + includeLessFile: [/(\/|\\)components(\/|\\)style(\/|\\)default.less$/], + transformTSFile(file) { + if (isComponentStyleEntry(file)) { + let content = file.contents.toString(); + + if (needTransformStyle(content)) { + const cloneFile = file.clone(); + + // Origin + content = content.replace('../../style/index.less', '../../style/default.less'); + cloneFile.contents = Buffer.from(content); + + return cloneFile; + } + } + }, + transformFile(file) { + if (isComponentStyleEntry(file)) { + const indexLessFilePath = file.path.replace('index.tsx', 'index.less'); + + if (fs.existsSync(indexLessFilePath)) { + // We put origin `index.less` file to `index-pure.less` + const pureFile = file.clone(); + pureFile.contents = Buffer.from(fs.readFileSync(indexLessFilePath, 'utf8')); + pureFile.path = pureFile.path.replace('index.tsx', 'index-pure.less'); + + // Rewrite `index.less` file with `root-entry-name` + const indexLessFile = file.clone(); + indexLessFile.contents = Buffer.from( + [ + // Inject variable + '@root-entry-name: default;', + // Point to origin file + "@import './index-pure.less';", + ].join('\n\n'), + ); + indexLessFile.path = indexLessFile.path.replace('index.tsx', 'index.less'); + + return [indexLessFile, pureFile]; + } + } + + return []; + }, + lessConfig: { + modifyVars: { + 'root-entry-name': 'default', + }, + }, + finalize: finalizeCompile, + }, + dist: { + finalize: finalizeDist, + }, + generateThemeFileContent, + bail: true, +}; diff --git a/.eslintignore b/.eslintignore index 5484d7a9c..1b06d2da6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,4 @@ es/ lib/ _site/ dist/ +components/version/version.tsx diff --git a/.gitignore b/.gitignore index 528ba0b75..c57a39388 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ vetur/ report.html site/src/router/demoRoutes.js + +components/version/version.tsx +~component-api.json diff --git a/antd-tools/apiCollection.js b/antd-tools/apiCollection.js new file mode 100644 index 000000000..99843601c --- /dev/null +++ b/antd-tools/apiCollection.js @@ -0,0 +1,68 @@ +// Read all the api from current documents + +const glob = require('glob'); +const fs = require('fs'); + +const COMPONENT_NAME = /components\/([^/]*)/; +const PROP_NAME = /^\s*\|\s*([^\s|]*)/; + +const components = {}; + +function mappingPropLine(component, line) { + const propMatch = line.match(PROP_NAME); + if (!propMatch) return; + + const propName = propMatch[1]; + if (!/^[a-z]/.test(propName)) return; + + components[component] = Array.from(new Set([...(components[component] || []), propName])); +} + +function apiReport(entities) { + const apis = {}; + Object.keys(entities).forEach(component => { + const apiList = entities[component]; + apiList.forEach(api => { + if (typeof apis[api] === 'function') { + apis[api] = []; + } + apis[api] = [...(apis[api] || []), component]; + }); + }); + + return apis; +} + +function printReport(apis) { + const apiList = Object.keys(apis).map(api => ({ + name: api, + componentList: apis[api], + })); + apiList.sort((a, b) => b.componentList.length - a.componentList.length); + // eslint-disable-next-line no-console + console.log('| name | components | comments |'); + // eslint-disable-next-line no-console + console.log('| ---- | ---------- | -------- |'); + apiList.forEach(({ name, componentList }) => { + // eslint-disable-next-line no-console + console.log('|', name, '|', componentList.join(', '), '| |'); + }); +} + +module.exports = () => { + glob('components/*/*.md', (error, files) => { + files.forEach(filePath => { + // Read md file to parse content + const content = fs.readFileSync(filePath, 'utf8'); + const component = filePath.match(COMPONENT_NAME)[1]; + + // Parse lines to get API + const lines = content.split(/[\r\n]+/); + lines.forEach(line => { + mappingPropLine(component, line); + }); + }); + + printReport(apiReport(components)); + }); +}; diff --git a/antd-tools/cli/run.js b/antd-tools/cli/run.js index b60b54bb0..e1466aa6c 100644 --- a/antd-tools/cli/run.js +++ b/antd-tools/cli/run.js @@ -4,7 +4,6 @@ 'use strict'; require('colorful').colorful(); -require('colorful').isatty = true; const gulp = require('gulp'); const program = require('commander'); diff --git a/antd-tools/getNpm.js b/antd-tools/getNpm.js new file mode 100644 index 000000000..52eee5011 --- /dev/null +++ b/antd-tools/getNpm.js @@ -0,0 +1,17 @@ +'use strict'; + +const runCmd = require('./runCmd'); + +module.exports = function (done) { + if (process.env.NPM_CLI) { + done(process.env.NPM_CLI); + return; + } + runCmd('which', ['tnpm'], code => { + let npm = 'npm'; + if (!code) { + npm = 'tnpm'; + } + done(npm); + }); +}; diff --git a/antd-tools/gulpfile.js b/antd-tools/gulpfile.js index 6a3243cec..9090cfab8 100644 --- a/antd-tools/gulpfile.js +++ b/antd-tools/gulpfile.js @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -const { getProjectPath } = require('./utils/projectHelper'); +const { getProjectPath, getConfig } = require('./utils/projectHelper'); const runCmd = require('./runCmd'); const getBabelCommonConfig = require('./getBabelCommonConfig'); const merge2 = require('merge2'); @@ -26,6 +26,7 @@ const stripCode = require('gulp-strip-code'); const compareVersions = require('compare-versions'); const getTSCommonConfig = require('./getTSCommonConfig'); const replaceLib = require('./replaceLib'); +const sortApiTable = require('./sortApiTable'); const packageJson = require(getProjectPath('package.json')); const tsDefaultReporter = ts.reporter.defaultReporter(); @@ -49,11 +50,17 @@ function dist(done) { } const info = stats.toJson(); + const { dist: { finalize } = {}, bail } = getConfig(); if (stats.hasErrors()) { - console.error(info.errors); + (info.errors || []).forEach(error => { + console.error(error); + }); + // https://github.com/ant-design/ant-design/pull/31662 + if (bail) { + process.exit(1); + } } - if (stats.hasWarnings()) { console.warn(info.warnings); } @@ -68,6 +75,11 @@ function dist(done) { version: false, }); console.log(buildInfo); + // Additional process of dist finalize + if (finalize) { + console.log('[Dist] Finalization...'); + finalize(); + } done(0); }); } @@ -103,7 +115,7 @@ function babelify(js, modules) { if (modules === false) { babelConfig.plugins.push(replaceLib); } - let stream = js.pipe(babel(babelConfig)).pipe( + const stream = js.pipe(babel(babelConfig)).pipe( through2.obj(function z(file, encoding, next) { this.push(file.clone()); if (file.path.match(/\/style\/index\.(js|jsx|ts|tsx)$/)) { @@ -128,33 +140,40 @@ function babelify(js, modules) { next(); }), ); - if (modules === false) { - stream = stream.pipe( - stripCode({ - start_comment: '@remove-on-es-build-begin', - end_comment: '@remove-on-es-build-end', - }), - ); - } return stream.pipe(gulp.dest(modules === false ? esDir : libDir)); } function compile(modules) { + const { compile: { transformTSFile, transformFile, includeLessFile = [] } = {} } = getConfig(); rimraf.sync(modules !== false ? libDir : esDir); + + // =============================== LESS =============================== const less = gulp .src(['components/**/*.less']) .pipe( through2.obj(function (file, encoding, next) { - this.push(file.clone()); + // Replace content + const cloneFile = file.clone(); + const content = file.contents.toString().replace(/^\uFEFF/, ''); + + cloneFile.contents = Buffer.from(content); + + // Clone for css here since `this.push` will modify file.path + const cloneCssFile = cloneFile.clone(); + + this.push(cloneFile); + + // Transform less file if ( - file.path.match(/\/style\/index\.less$/) || - file.path.match(/\/style\/v2-compatible-reset\.less$/) + file.path.match(/(\/|\\)style(\/|\\)index\.less$/) || + file.path.match(/(\/|\\)style(\/|\\)v2-compatible-reset\.less$/) || + includeLessFile.some(regex => file.path.match(regex)) ) { - transformLess(file.path) + transformLess(cloneCssFile.contents.toString(), cloneCssFile.path) .then(css => { - file.contents = Buffer.from(css); - file.path = file.path.replace(/\.less$/, '.css'); - this.push(file); + cloneCssFile.contents = Buffer.from(css); + cloneCssFile.path = cloneCssFile.path.replace(/\.less$/, '.css'); + this.push(cloneCssFile); next(); }) .catch(e => { @@ -170,6 +189,25 @@ function compile(modules) { .src(['components/**/*.@(png|svg)']) .pipe(gulp.dest(modules === false ? esDir : libDir)); let error = 0; + + // =============================== FILE =============================== + let transformFileStream; + + if (transformFile) { + transformFileStream = gulp + .src(['components/**/*.tsx']) + .pipe( + through2.obj(function (file, encoding, next) { + let nextFile = transformFile(file) || file; + nextFile = Array.isArray(nextFile) ? nextFile : [nextFile]; + nextFile.forEach(f => this.push(f)); + next(); + }), + ) + .pipe(gulp.dest(modules === false ? esDir : libDir)); + } + + // ================================ TS ================================ const source = [ 'components/**/*.js', 'components/**/*.jsx', @@ -179,7 +217,29 @@ function compile(modules) { '!components/*/__tests__/*', ]; - const tsResult = gulp.src(source).pipe( + // Strip content if needed + let sourceStream = gulp.src(source); + if (modules === false) { + sourceStream = sourceStream.pipe( + stripCode({ + start_comment: '@remove-on-es-build-begin', + end_comment: '@remove-on-es-build-end', + }), + ); + } + + if (transformTSFile) { + sourceStream = sourceStream.pipe( + through2.obj(function (file, encoding, next) { + let nextFile = transformTSFile(file) || file; + nextFile = Array.isArray(nextFile) ? nextFile : [nextFile]; + nextFile.forEach(f => this.push(f)); + next(); + }), + ); + } + + const tsResult = sourceStream.pipe( ts(tsConfig, { error(e) { tsDefaultReporter.error(e); @@ -199,7 +259,7 @@ function compile(modules) { tsResult.on('end', check); const tsFilesStream = babelify(tsResult.js, modules); const tsd = tsResult.dts.pipe(gulp.dest(modules === false ? esDir : libDir)); - return merge2([less, tsFilesStream, tsd, assets]); + return merge2([less, tsFilesStream, tsd, assets, transformFileStream].filter(s => s)); } function tag() { @@ -420,7 +480,11 @@ gulp.task( const npmArgs = getNpmArgs(); if (npmArgs) { for (let arg = npmArgs.shift(); arg; arg = npmArgs.shift()) { - if (/^pu(b(l(i(sh?)?)?)?)?$/.test(arg) && npmArgs.indexOf('--with-antd-tools') < 0) { + if ( + /^pu(b(l(i(sh?)?)?)?)?$/.test(arg) && + npmArgs.indexOf('--with-antd-tools') < 0 && + !process.env.npm_config_with_antd_tools + ) { reportError(); done(1); return; @@ -430,3 +494,11 @@ gulp.task( done(); }), ); + +gulp.task( + 'sort-api-table', + gulp.series(done => { + sortApiTable(); + done(); + }), +); diff --git a/antd-tools/replaceLib.js b/antd-tools/replaceLib.js index 08d84b4ea..e9da6e86c 100644 --- a/antd-tools/replaceLib.js +++ b/antd-tools/replaceLib.js @@ -12,6 +12,19 @@ function replacePath(path) { path.node.source.value = esModule; } } + + // @ant-design/icons-vue/xxx => @ant-design/icons-vue/es/icons/xxx + const antdIconMatcher = /@ant-design\/icons-vue\/([^/]*)$/; + if (path.node.source && antdIconMatcher.test(path.node.source.value)) { + const esModule = path.node.source.value.replace( + antdIconMatcher, + (_, iconName) => `@ant-design/icons-vue/es/icons/${iconName}`, + ); + const esPath = dirname(getProjectPath('node_modules', esModule)); + if (fs.existsSync(esPath)) { + path.node.source.value = esModule; + } + } } function replaceLib() { diff --git a/antd-tools/runCmd.js b/antd-tools/runCmd.js index f5a822a97..b25ca5711 100644 --- a/antd-tools/runCmd.js +++ b/antd-tools/runCmd.js @@ -1,9 +1,17 @@ 'use strict'; +const isWindows = require('is-windows'); const getRunCmdEnv = require('./utils/getRunCmdEnv'); function runCmd(cmd, _args, fn) { const args = _args || []; + + if (isWindows()) { + args.unshift(cmd); + args.unshift('/c'); + cmd = process.env.ComSpec; + } + const runner = require('child_process').spawn(cmd, args, { // keep color stdio: 'inherit', diff --git a/antd-tools/sortApiTable.js b/antd-tools/sortApiTable.js new file mode 100644 index 000000000..56a2bd21b --- /dev/null +++ b/antd-tools/sortApiTable.js @@ -0,0 +1,165 @@ +const program = require('commander'); +const majo = require('majo'); +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +const unified = require('unified'); +const parse = require('remark-parse'); +const stringify = require('remark-stringify'); + +const yamlConfig = require('remark-yaml-config'); +const frontmatter = require('remark-frontmatter'); + +let fileAPIs = {}; +const remarkWithYaml = unified() + .use(parse) + .use(stringify, { + paddedTable: false, + listItemIndent: 1, + stringLength: () => 3, + }) + .use(frontmatter) + .use(yamlConfig); + +const stream = majo.majo(); + +function getCellValue(node) { + return node.children[0].children[0].value; +} + +// from small to large +const sizeBreakPoints = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl']; + +const whiteMethodList = ['afterChange', 'beforeChange']; + +const groups = { + isDynamic: val => /^on[A-Z]/.test(val) || whiteMethodList.indexOf(val) > -1, + isSize: val => sizeBreakPoints.indexOf(val) > -1, +}; + +function asciiSort(prev, next) { + if (prev > next) { + return 1; + } + + if (prev < next) { + return -1; + } + + return 0; +} + +// follow the alphabet order +function alphabetSort(nodes) { + // use toLowerCase to keep `case insensitive` + return nodes.sort((...comparison) => + asciiSort(...comparison.map(val => getCellValue(val).toLowerCase())), + ); +} + +function sizeSort(nodes) { + return nodes.sort((...comparison) => + asciiSort(...comparison.map(val => sizeBreakPoints.indexOf(getCellValue(val).toLowerCase()))), + ); +} + +function sort(ast, filename) { + const nameMatch = filename.match(/^components\/([^/]*)\//); + const componentName = nameMatch[1]; + fileAPIs[componentName] = fileAPIs[componentName] || { + static: new Set(), + size: new Set(), + dynamic: new Set(), + }; + + ast.children.forEach(child => { + const staticProps = []; + // prefix with `on` + const dynamicProps = []; + // one of ['xs', 'sm', 'md', 'lg', 'xl'] + const sizeProps = []; + + // find table markdown type + if (child.type === 'table') { + // slice will create new array, so sort can affect the original array. + // slice(1) cut down the thead + child.children.slice(1).forEach(node => { + const value = getCellValue(node); + if (groups.isDynamic(value)) { + dynamicProps.push(node); + fileAPIs[componentName].dynamic.add(value); + } else if (groups.isSize(value)) { + sizeProps.push(node); + fileAPIs[componentName].size.add(value); + } else { + staticProps.push(node); + fileAPIs[componentName].static.add(value); + } + }); + + // eslint-disable-next-line + child.children = [ + child.children[0], + ...alphabetSort(staticProps), + ...sizeSort(sizeProps), + ...alphabetSort(dynamicProps), + ]; + } + }); + + return ast; +} + +function sortAPI(md, filename) { + return remarkWithYaml.stringify(sort(remarkWithYaml.parse(md), filename)); +} + +function sortMiddleware(ctx) { + Object.keys(ctx.files).forEach(filename => { + const content = ctx.fileContents(filename); + ctx.writeContents(filename, sortAPI(content, filename)); + }); +} + +module.exports = () => { + fileAPIs = {}; + + program + .version('0.1.0') + .option( + '-f, --file [file]', + 'Specify which file to be transformed', + // default value + 'components/**/index.+(zh-CN|en-US).md', + ) + .option('-o, --output [output]', 'Specify component api output path', '~component-api.json') + .parse(process.argv); + // Get the markdown file all need to be transformed + + /* eslint-disable no-console */ + stream + .source(program.file) + .use(sortMiddleware) + .dest('.') + .then(() => { + if (program.output) { + const data = {}; + Object.keys(fileAPIs).forEach(componentName => { + data[componentName] = { + static: [...fileAPIs[componentName].static], + size: [...fileAPIs[componentName].size], + dynamic: [...fileAPIs[componentName].dynamic], + }; + }); + + const reportPath = path.resolve(program.output); + fs.writeFileSync(reportPath, JSON.stringify(data, null, 2), 'utf8'); + console.log(chalk.cyan(`API list file: ${reportPath}`)); + } + }) + .then(() => { + console.log(chalk.green(`sort ant-design-vue api successfully!`)); + }); + /* eslint-enable no-console */ +}; diff --git a/antd-tools/transformLess.js b/antd-tools/transformLess.js index 370ffc17b..0e949b243 100644 --- a/antd-tools/transformLess.js +++ b/antd-tools/transformLess.js @@ -1,16 +1,14 @@ const less = require('less'); -const { readFileSync } = require('fs'); const path = require('path'); const postcss = require('postcss'); -const NpmImportPlugin = require('less-plugin-npm-import'); const autoprefixer = require('autoprefixer'); +const NpmImportPlugin = require('less-plugin-npm-import'); +const { getConfig } = require('./utils/projectHelper'); -function transformLess(lessFile, config = {}) { +function transformLess(lessContent, lessFilePath, config = {}) { const { cwd = process.cwd() } = config; - const resolvedLessFile = path.resolve(cwd, lessFile); - - let data = readFileSync(resolvedLessFile, 'utf-8'); - data = data.replace(/^\uFEFF/, ''); + const { compile: { lessConfig } = {} } = getConfig(); + const resolvedLessFile = path.resolve(cwd, lessFilePath); // Do less compile const lessOpts = { @@ -18,13 +16,12 @@ function transformLess(lessFile, config = {}) { filename: resolvedLessFile, plugins: [new NpmImportPlugin({ prefix: '~' })], javascriptEnabled: true, + ...lessConfig, }; return less - .render(data, lessOpts) + .render(lessContent, lessOpts) .then(result => postcss([autoprefixer]).process(result.css, { from: undefined })) - .then(r => { - return r.css; - }); + .then(r => r.css); } module.exports = transformLess; diff --git a/antd-tools/utils/CleanUpStatsPlugin.js b/antd-tools/utils/CleanUpStatsPlugin.js index 5029bba39..300168c4f 100644 --- a/antd-tools/utils/CleanUpStatsPlugin.js +++ b/antd-tools/utils/CleanUpStatsPlugin.js @@ -24,13 +24,13 @@ class CleanUpStatsPlugin { apply(compiler) { compiler.hooks.done.tap('CleanUpStatsPlugin', stats => { - const { children } = stats.compilation; + const { children, warnings } = stats.compilation; if (Array.isArray(children)) { stats.compilation.children = children.filter(child => this.shouldPickStatChild(child)); } - // if (Array.isArray(warnings)) { - // stats.compilation.warnings = warnings.filter(message => this.shouldPickWarning(message)); - // } + if (Array.isArray(warnings)) { + stats.compilation.warnings = warnings.filter(message => this.shouldPickWarning(message)); + } }); } } diff --git a/antd-tools/utils/get-npm-args.js b/antd-tools/utils/get-npm-args.js index 9de9013c1..2e11613cc 100644 --- a/antd-tools/utils/get-npm-args.js +++ b/antd-tools/utils/get-npm-args.js @@ -2,6 +2,11 @@ // NOTE: the following code was partially adopted from https://github.com/iarna/in-publish module.exports = function getNpmArgs() { + // https://github.com/iarna/in-publish/pull/14 + if (process.env.npm_command) { + return [process.env.npm_command]; + } + let npmArgv = null; try { diff --git a/antd-tools/utils/getRunCmdEnv.js b/antd-tools/utils/getRunCmdEnv.js index c7b474bb3..12e326050 100644 --- a/antd-tools/utils/getRunCmdEnv.js +++ b/antd-tools/utils/getRunCmdEnv.js @@ -1,6 +1,7 @@ 'use strict'; const path = require('path'); +const isWindows = require('is-windows'); module.exports = function getRunCmdEnv() { const env = {}; @@ -14,7 +15,9 @@ module.exports = function getRunCmdEnv() { .filter(v => v.slice(0, 1).pop().toLowerCase() === 'path') .forEach(v => { const key = v.slice(0, 1).pop(); - env[key] = env[key] ? `${nodeModulesBinDir}:${env[key]}` : nodeModulesBinDir; + env[key] = env[key] + ? `${nodeModulesBinDir}${isWindows() ? ';' : ':'}${env[key]}` + : nodeModulesBinDir; }); return env; }; diff --git a/antd-tools/utils/projectHelper.js b/antd-tools/utils/projectHelper.js index 79cdb57cc..9ade6c777 100644 --- a/antd-tools/utils/projectHelper.js +++ b/antd-tools/utils/projectHelper.js @@ -13,6 +13,7 @@ function resolve(moduleName) { // We need hack the require to ensure use package module first // For example, `typescript` is required by `gulp-typescript` but provided by `antd` +// we do not need for ant-design-vue let injected = false; function injectRequire() { if (injected) return; @@ -45,9 +46,35 @@ function getConfig() { return {}; } +/** + * 是否存在可用的browserslist config + * https://github.com/browserslist/browserslist#queries + * @returns + */ +function isThereHaveBrowserslistConfig() { + try { + const packageJson = require(getProjectPath('package.json')); + if (packageJson.browserslist) { + return true; + } + } catch (e) { + // + } + if (fs.existsSync(getProjectPath('.browserslistrc'))) { + return true; + } + if (fs.existsSync(getProjectPath('browserslist'))) { + return true; + } + // parent项目的配置支持,需要再补充 + // ROWSERSLIST ROWSERSLIST_ENV 变量的形式,需要再补充。 + return false; +} + module.exports = { getProjectPath, resolve, injectRequire, getConfig, + isThereHaveBrowserslistConfig, }; diff --git a/antd-tools/utils/styleUtil.js b/antd-tools/utils/styleUtil.js new file mode 100644 index 000000000..7b05ee315 --- /dev/null +++ b/antd-tools/utils/styleUtil.js @@ -0,0 +1,11 @@ +// We convert less import in es/lib to css file path +function cssInjection(content) { + return content + .replace(/\/style\/?'/g, "/style/css'") + .replace(/\/style\/?"/g, '/style/css"') + .replace(/\.less/g, '.css'); +} + +module.exports = { + cssInjection, +}; diff --git a/components/modal/ActionButton.tsx b/components/_util/ActionButton.tsx similarity index 92% rename from components/modal/ActionButton.tsx rename to components/_util/ActionButton.tsx index cc3766368..fe1eb83de 100644 --- a/components/modal/ActionButton.tsx +++ b/components/_util/ActionButton.tsx @@ -4,6 +4,7 @@ import Button from '../button'; import type { ButtonProps } from '../button'; import type { LegacyButtonType } from '../button/buttonTypes'; import { convertLegacyProps } from '../button/buttonTypes'; +import useDestroyed from './hooks/useDestroyed'; const actionButtonProps = { type: { @@ -32,6 +33,7 @@ export default defineComponent({ const buttonRef = ref(); const loading = ref(false); let timeoutId: any; + const isDestroyed = useDestroyed(); onMounted(() => { if (props.autofocus) { timeoutId = setTimeout(() => buttonRef.value.$el?.focus()); @@ -49,7 +51,9 @@ export default defineComponent({ loading.value = true; returnValueOfOnOk!.then( (...args: any[]) => { - loading.value = false; + if (!isDestroyed.value) { + loading.value = false; + } close(...args); clickedRef.value = false; }, @@ -58,7 +62,9 @@ export default defineComponent({ // eslint-disable-next-line no-console console.error(e); // See: https://github.com/ant-design/ant-design/issues/6183 - loading.value = false; + if (!isDestroyed.value) { + loading.value = false; + } clickedRef.value = false; }, ); diff --git a/components/_util/EventInterface.ts b/components/_util/EventInterface.ts index b9d0bd019..7d6f193c9 100644 --- a/components/_util/EventInterface.ts +++ b/components/_util/EventInterface.ts @@ -6,5 +6,10 @@ export type ChangeEvent = Event & { value?: string | undefined; }; }; +export type CheckboxChangeEvent = Event & { + target: { + checked?: boolean; + }; +}; export type EventHandler = (...args: any[]) => void; diff --git a/components/_util/__tests__/unreachableException.test.js b/components/_util/__tests__/unreachableException.test.js new file mode 100644 index 000000000..4019e4fed --- /dev/null +++ b/components/_util/__tests__/unreachableException.test.js @@ -0,0 +1,8 @@ +import UnreachableException from '../unreachableException'; + +describe('UnreachableException', () => { + it('error thrown matches snapshot', () => { + const exception = new UnreachableException('some value'); + expect(exception.error.message).toMatchInlineSnapshot(`"unreachable case: \\"some value\\""`); + }); +}); diff --git a/components/upload/UploadList/listAnimation.ts b/components/_util/collapseMotion.tsx similarity index 81% rename from components/upload/UploadList/listAnimation.ts rename to components/_util/collapseMotion.tsx index bcc4f6d00..6af29a463 100644 --- a/components/upload/UploadList/listAnimation.ts +++ b/components/_util/collapseMotion.tsx @@ -1,11 +1,11 @@ -import { addClass, removeClass } from '../../vc-util/Dom/class'; import { nextTick } from 'vue'; -import type { CSSMotionProps } from '../../_util/transition'; +import { addClass, removeClass } from '../vc-util/Dom/class'; +import type { CSSMotionProps } from './transition'; -const listAnimation = (name = 'ant-motion-collapse'): CSSMotionProps => { +const collapseMotion = (name = 'ant-motion-collapse', appear = true): CSSMotionProps => { return { name, - appear: true, + appear, css: true, onBeforeEnter: (node: HTMLDivElement) => { node.style.height = '0px'; @@ -47,4 +47,4 @@ const listAnimation = (name = 'ant-motion-collapse'): CSSMotionProps => { }, }; }; -export default listAnimation; +export default collapseMotion; diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts index 56e9d4cfb..079ac95b3 100644 --- a/components/_util/hooks/useConfigInject.ts +++ b/components/_util/hooks/useConfigInject.ts @@ -19,6 +19,7 @@ export default ( pageHeader: ComputedRef<{ ghost: boolean }>; form?: ComputedRef<{ requiredMark?: RequiredMark; + colon?: boolean; }>; autoInsertSpaceInButton: ComputedRef; renderEmpty?: ComputedRef<(componentName?: string) => VueNode>; diff --git a/components/_util/hooks/useDestroyed.ts b/components/_util/hooks/useDestroyed.ts new file mode 100644 index 000000000..d5cd6b0f4 --- /dev/null +++ b/components/_util/hooks/useDestroyed.ts @@ -0,0 +1,12 @@ +import { onBeforeUnmount, ref } from 'vue'; + +const useDestroyed = () => { + const mounted = ref(true); + onBeforeUnmount(() => { + mounted.value = false; + }); + + return mounted; +}; + +export default useDestroyed; diff --git a/components/_util/hooks/usePrefixCls.ts b/components/_util/hooks/usePrefixCls.ts deleted file mode 100644 index 96c62d4b6..000000000 --- a/components/_util/hooks/usePrefixCls.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ComputedRef } from 'vue'; -import { computed, inject } from 'vue'; -import { defaultConfigProvider } from '../../config-provider'; - -export default (name: string, props: Record): ComputedRef => { - const configProvider = inject('configProvider', defaultConfigProvider); - const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); - return prefixCls; -}; diff --git a/components/_util/openAnimation.js b/components/_util/openAnimation.js deleted file mode 100644 index d01df6d74..000000000 --- a/components/_util/openAnimation.js +++ /dev/null @@ -1,67 +0,0 @@ -import cssAnimation from './css-animation'; -import { nextTick } from 'vue'; -import { requestAnimationTimeout, cancelAnimationTimeout } from './requestAnimationTimeout'; - -function animate(node, show, done) { - let height; - let requestAnimationFrameId; - let appearRequestAnimationFrameId; - return cssAnimation(node, 'ant-motion-collapse-legacy', { - start() { - if (appearRequestAnimationFrameId) { - cancelAnimationTimeout(appearRequestAnimationFrameId); - } - if (!show) { - node.style.height = `${node.offsetHeight}px`; - node.style.opacity = '1'; - } else { - height = node.offsetHeight; - // not get offsetHeight when appear - // set it into raf get correct offsetHeight - if (height === 0) { - appearRequestAnimationFrameId = requestAnimationTimeout(() => { - height = node.offsetHeight; - node.style.height = '0px'; - node.style.opacity = '0'; - }); - } else { - node.style.height = '0px'; - node.style.opacity = '0'; - } - } - }, - active() { - if (requestAnimationFrameId) { - cancelAnimationTimeout(requestAnimationFrameId); - } - requestAnimationFrameId = requestAnimationTimeout(() => { - node.style.height = `${show ? height : 0}px`; - node.style.opacity = show ? '1' : '0'; - }); - }, - end() { - if (appearRequestAnimationFrameId) { - cancelAnimationTimeout(appearRequestAnimationFrameId); - } - if (requestAnimationFrameId) { - cancelAnimationTimeout(requestAnimationFrameId); - } - node.style.height = ''; - node.style.opacity = ''; - done && done(); - }, - }); -} - -const animation = { - onEnter(node, done) { - nextTick(() => { - animate(node, true, done); - }); - }, - onLeave(node, done) { - return animate(node, false, done); - }, -}; - -export default animation; diff --git a/components/_util/styleChecker.ts b/components/_util/styleChecker.ts index ae845c67e..59244441a 100644 --- a/components/_util/styleChecker.ts +++ b/components/_util/styleChecker.ts @@ -2,8 +2,8 @@ import canUseDom from './canUseDom'; export const canUseDocElement = () => canUseDom() && window.document.documentElement; -export const isStyleSupport = (styleName: string | Array): boolean => { - if (canUseDocElement()) { +const isStyleNameSupport = (styleName: string | string[]): boolean => { + if (canUseDom() && window.document.documentElement) { const styleNameList = Array.isArray(styleName) ? styleName : [styleName]; const { documentElement } = window.document; @@ -12,6 +12,25 @@ export const isStyleSupport = (styleName: string | Array): boolean => { return false; }; +const isStyleValueSupport = (styleName: string, value: any) => { + if (!isStyleNameSupport(styleName)) { + return false; + } + + const ele = document.createElement('div'); + const origin = ele.style[styleName]; + ele.style[styleName] = value; + return ele.style[styleName] !== origin; +}; + +export function isStyleSupport(styleName: string | string[], styleValue?: any) { + if (!Array.isArray(styleName) && styleValue !== undefined) { + return isStyleValueSupport(styleName, styleValue); + } + + return isStyleNameSupport(styleName); +} + let flexGapSupported: boolean | undefined; export const detectFlexGapSupported = () => { if (!canUseDocElement()) { diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx index cf8177af2..0d9ab7c9f 100644 --- a/components/_util/transition.tsx +++ b/components/_util/transition.tsx @@ -129,13 +129,17 @@ export interface CSSMotionProps extends Partial> { css?: boolean; } -const collapseMotion = (style: Ref, className: Ref): CSSMotionProps => { +const collapseMotion = ( + name = 'ant-motion-collapse', + style: Ref, + className: Ref, +): CSSMotionProps => { return { - name: 'ant-motion-collapse', + name, appear: true, css: true, onBeforeEnter: node => { - className.value = 'ant-motion-collapse'; + className.value = name; style.value = getCollapsedHeight(node); }, onEnter: node => { @@ -148,7 +152,7 @@ const collapseMotion = (style: Ref, className: Ref): CSSM style.value = {}; }, onBeforeLeave: node => { - className.value = 'ant-motion-collapse'; + className.value = name; style.value = getCurrentHeight(node); }, onLeave: node => { diff --git a/components/_util/unreachableException.ts b/components/_util/unreachableException.ts new file mode 100644 index 000000000..9218b14dc --- /dev/null +++ b/components/_util/unreachableException.ts @@ -0,0 +1,7 @@ +export default class UnreachableException { + error: Error; + + constructor(value: any) { + this.error = new Error(`unreachable case: ${JSON.stringify(value)}`); + } +} diff --git a/components/_util/wave.tsx b/components/_util/wave.tsx index 4fd70e193..a733fcbc5 100644 --- a/components/_util/wave.tsx +++ b/components/_util/wave.tsx @@ -24,6 +24,7 @@ export default defineComponent({ name: 'Wave', props: { insertExtraNode: Boolean, + disabled: Boolean, }, setup(props, { slots, expose }) { const instance = getCurrentInstance(); @@ -60,10 +61,11 @@ export default defineComponent({ return insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node'; }; const onClick = (node: HTMLElement, waveColor: string) => { - if (!node || isHidden(node) || node.className.indexOf('-leave') >= 0) { + const { insertExtraNode, disabled } = props; + if (disabled || !node || isHidden(node) || node.className.indexOf('-leave') >= 0) { return; } - const { insertExtraNode } = props; + extraNode = document.createElement('div'); extraNode.className = 'ant-click-animating-node'; const attributeName = getAttributeName(); diff --git a/components/affix/index.en-US.md b/components/affix/index.en-US.md index 75d27a721..910c536c5 100644 --- a/components/affix/index.en-US.md +++ b/components/affix/index.en-US.md @@ -23,9 +23,9 @@ Please note that Affix should not cover other content on the page, especially wh ### events -| Events Name | Description | Arguments | Version | -| ----------- | ---------------------------------------- | ----------------- | ------- | -| change | Callback for when Affix state is changed | Function(affixed) | +| Events Name | Description | Arguments | Version | +| ----------- | ---------------------------------------- | --------------------------- | ------- | +| change | Callback for when Affix state is changed | (affixed?: boolean) => void | | **Note:** Children of `Affix` must not have the property `position: absolute`, but you can set `position: absolute` on `Affix` itself: @@ -35,8 +35,12 @@ Please note that Affix should not cover other content on the page, especially wh ## FAQ -### Affix bind container with `target`, sometime move out of container. +### When binding container with `target` in Affix, elements sometimes move out of the container. -We don't listen window scroll for performance consideration. +We only listen to container scroll events for performance consideration. You can add custom listeners if you still want to, like react demo Related issues:[#3938](https://github.com/ant-design/ant-design/issues/3938) [#5642](https://github.com/ant-design/ant-design/issues/5642) [#16120](https://github.com/ant-design/ant-design/issues/16120) + +### When Affix is ​​used in a horizontal scroll container, the position of the element `left` is incorrect. + +Affix is ​​generally only applicable to areas with one-way scrolling, and only supports usage in vertical scrolling containers. If you want to use it in a horizontal container, you can consider implementing with the native `position: sticky` property. diff --git a/components/affix/index.tsx b/components/affix/index.tsx index e403f96e6..aa451f51f 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -179,10 +179,7 @@ const Affix = defineComponent({ watch( () => props.target, val => { - let newTarget = null; - if (val) { - newTarget = val() || null; - } + const newTarget = val?.() || null; if (state.prevTarget !== newTarget) { removeObserveTarget(currentInstance); if (newTarget) { diff --git a/components/affix/index.zh-CN.md b/components/affix/index.zh-CN.md index 7128d19a0..daddfa00b 100644 --- a/components/affix/index.zh-CN.md +++ b/components/affix/index.zh-CN.md @@ -19,14 +19,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/tX6-md4H6/Affix.svg | 成员 | 说明 | 类型 | 默认值 | 版本 | | --- | --- | --- | --- | --- | | offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | | -| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | | +| offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | 0 | | | target | 设置 `Affix` 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | () => HTMLElement | () => window | | ### 事件 -| 事件名称 | 说明 | 回调参数 | 版本 | -| -------- | ---------------------------- | ----------------- | ---- | --- | -| change | 固定状态改变时触发的回调函数 | Function(affixed) | 无 | | +| 事件名称 | 说明 | 回调参数 | 版本 | | +| -------- | ---------------------------- | --------------------------- | ---- | --- | +| change | 固定状态改变时触发的回调函数 | (affixed?: boolean) => void | - | | **注意:**`Affix` 内的元素不要使用绝对定位,如需要绝对定位的效果,可以直接设置 `Affix` 为绝对定位: @@ -38,6 +38,12 @@ cover: https://gw.alipayobjects.com/zos/alicdn/tX6-md4H6/Affix.svg ### Affix 使用 `target` 绑定容器时,元素会跑到容器外。 -从性能角度考虑,我们只监听容器滚动事件。 +从性能角度考虑,我们只监听容器滚动事件。如果希望任意滚动,你可以在窗体添加滚动监听, 参考 react 版本示例 相关 issue:[#3938](https://github.com/ant-design/ant-design/issues/3938) [#5642](https://github.com/ant-design/ant-design/issues/5642) [#16120](https://github.com/ant-design/ant-design/issues/16120) + +### Affix 在水平滚动容器中使用时, 元素 `left` 位置不正确。 + +Affix 一般只适用于单向滚动的区域,只支持在垂直滚动容器中使用。如果希望在水平容器中使用,你可以考虑使用 原生 `position: sticky` 实现。 + +相关 issue: [#29108](https://github.com/ant-design/ant-design/issues/29108) diff --git a/components/affix/utils.ts b/components/affix/utils.ts index 20543df10..4ce8c11bb 100644 --- a/components/affix/utils.ts +++ b/components/affix/utils.ts @@ -3,19 +3,14 @@ import type { ComponentPublicInstance } from 'vue'; import supportsPassive from '../_util/supportsPassive'; export type BindElement = HTMLElement | Window | null | undefined; -export type Rect = ClientRect | DOMRect; -export function getTargetRect(target: BindElement): ClientRect { +export function getTargetRect(target: BindElement): DOMRect { return target !== window ? (target as HTMLElement).getBoundingClientRect() - : ({ top: 0, bottom: window.innerHeight } as ClientRect); + : ({ top: 0, bottom: window.innerHeight } as DOMRect); } -export function getFixedTop( - placeholderReact: Rect, - targetRect: Rect, - offsetTop: number | undefined, -) { +export function getFixedTop(placeholderReact: DOMRect, targetRect: DOMRect, offsetTop: number) { if (offsetTop !== undefined && targetRect.top > placeholderReact.top - offsetTop) { return `${offsetTop + targetRect.top}px`; } @@ -23,9 +18,9 @@ export function getFixedTop( } export function getFixedBottom( - placeholderReact: Rect, - targetRect: Rect, - offsetBottom: number | undefined, + placeholderReact: DOMRect, + targetRect: DOMRect, + offsetBottom: number, ) { if (offsetBottom !== undefined && targetRect.bottom < placeholderReact.bottom + offsetBottom) { const targetBottomOffset = window.innerHeight - targetRect.bottom; diff --git a/components/alert/__tests__/__snapshots__/demo.test.js.snap b/components/alert/__tests__/__snapshots__/demo.test.js.snap index 7c10e97ce..87c3dee8c 100644 --- a/components/alert/__tests__/__snapshots__/demo.test.js.snap +++ b/components/alert/__tests__/__snapshots__/demo.test.js.snap @@ -1,68 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders ./components/alert/demo/banner.vue correctly 1`] = ` -
+
-
+
-
+
-
+ `; exports[`renders ./components/alert/demo/basic.vue correctly 1`] = ` -
+ `; exports[`renders ./components/alert/demo/closable.vue correctly 1`] = ` -
+ -
+