diff --git a/.eleventy.js b/.eleventy.js index ece34f01..83787f0f 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -5,6 +5,7 @@ const markdownIt = require('markdown-it') const { parseHTML } = require('linkedom') const { isProdEnv } = require('./config/env.js') const { mainSections } = require('./config/constants.js') +const { cssTargets } = require('./config/css.js') const initMarkdownLibrary = require('./src/markdown-it.js') const demoLinkTransform = require('./src/transforms/demo-link-transform.js') const answersLinkTransform = require('./src/transforms/answers-link-transform.js') @@ -30,9 +31,6 @@ const { parseChangelog, enrichPost } = require('./src/libs/changelog-parser/chan // имён, а сам плагин лежит в default. const pluginRss = require('@11ty/eleventy-plugin-rss').default const eleventyVitePlugin = require('@11ty/eleventy-plugin-vite').default -const postcssImport = require('postcss-import') -const postcssMediaMinmax = require('postcss-media-minmax') -const autoprefixer = require('autoprefixer') function getAllDocs(collectionAPI) { const dokas = collectionAPI.getFilteredByTag('doka') @@ -420,15 +418,13 @@ module.exports = function (config) { cacheDir: '.vite', + // Тот же движок и те же targets, что в прод-сборке (config/css.js), + // чтобы стили на дев-сервере и на проде не расходились. Минификация + // здесь не включена: на дев-сервере она ни к чему. css: { - postcss: { - // csso здесь не нужен: на дев-сервере минификация не даёт ничего, - // зато ломает сборку. Он тянет css-tree 2.2.1, который не знает - // @container, @scope, вложенные слои и вложенность — а Vite гонит - // через эту цепочку и инлайновые стили демок, поэтому демка с - // @container отдавалась с 500. В прод-сборке csso остаётся: там - // postcss ходит только по src/styles, демки уезжают копией. - plugins: [postcssImport, postcssMediaMinmax, autoprefixer], + transformer: 'lightningcss', + lightningcss: { + targets: cssTargets, }, }, diff --git a/AGENTS.md b/AGENTS.md index 90f0167d..b25b01d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,7 @@ npm run build && npm run lint:html # валидация разметки, н | `.eleventy.js` | Главный конфиг: коллекции, фильтры, трансформации, плагины | | `gulpfile.js` | Продакшн-пайплайн: бандл CSS/JS, кеш-хэши | | `make-links.js` | Симлинки `src/{раздел}` → `content/{раздел}` | -| `config/` | Константы, окружение, цвета разделов | +| `config/` | Константы, окружение, цвета разделов, сборка стилей | | `src/data/` | Глобальные данные 11ty | | `src/layouts/`, `src/includes/` | Базовый шаблон и блоки | | `src/views/` | Страницы: `*.njk` плюс `*.11tydata.js` с данными | diff --git a/config/__tests__/css.js b/config/__tests__/css.js new file mode 100644 index 00000000..2ec2ad4e --- /dev/null +++ b/config/__tests__/css.js @@ -0,0 +1,50 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') + +const { cssTargets, styleEntries, bundleStyle } = require('../css.js') + +const bundleSource = (css) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'doka-css-')) + const filename = path.join(dir, 'index.css') + fs.writeFileSync(filename, css) + + try { + return bundleStyle(filename).toString() + } finally { + fs.rmSync(dir, { recursive: true, force: true }) + } +} + +describe('сборка стилей', () => { + it('собирает @import в один файл', () => { + const result = bundleStyle(path.join('src/styles', styleEntries[0])).toString() + + expect(result).not.toContain('@import') + expect(result.length).toBeGreaterThan(0) + }) + + // Ради этого и уезжали с csso: его css-tree не знал современного синтаксиса + // и ронял всю сборку, а Vite гонит через ту же цепочку стили демок. + it.each([ + ['@container', '@container (width > 400px) { .a { color: red } }'], + ['@scope', '@scope (.a) to (.b) { .c { color: red } }'], + ['вложенные слои', '@layer framework.base { .a { color: red } }'], + ['вложенность', '.a { color: red; &.b { color: blue } }'], + ])('не падает на %s', (_, css) => { + expect(() => bundleSource(css)).not.toThrow() + }) + + it('оставляет @container в выдаче', () => { + expect(bundleSource('@container (width > 400px) { .a { color: red } }')).toContain('@container') + }) + + // Префиксы ставит lightningcss по targets, отдельного autoprefixer больше нет. + it('ставит вендорные префиксы по targets', () => { + expect(bundleSource('.a { user-select: none }')).toContain('-webkit-user-select') + }) + + it('вычисляет targets из browserslist проекта', () => { + expect(Object.keys(cssTargets).length).toBeGreaterThan(0) + }) +}) diff --git a/config/css.js b/config/css.js new file mode 100644 index 00000000..344bfdc8 --- /dev/null +++ b/config/css.js @@ -0,0 +1,22 @@ +const browserslist = require('browserslist') +const { bundle, browserslistToTargets } = require('lightningcss') + +const { browserslist: browsers } = require('../package.json') + +// Единственный источник правды по браузерам — поле browserslist в package.json. +// По нему lightningcss решает и какие вендорные префиксы ставить, и какой +// синтаксис компилировать: раньше первым занимался autoprefixer, вторым — +// postcss-media-minmax, и каждый жил по своим правилам. +const cssTargets = browserslistToTargets(browserslist(browsers)) + +// Точки входа: всё остальное подключается к ним через @import, их же +// lightningcss и собирает в один файл. +const styleEntries = ['index.css', 'index.sc.css', 'dark-theme.css'] + +const bundleStyle = (filename) => bundle({ filename, minify: true, targets: cssTargets }).code + +module.exports = { + cssTargets, + styleEntries, + bundleStyle, +} diff --git a/gulpfile.js b/gulpfile.js index f3d7bc5c..6071428b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,15 +1,10 @@ const path = require('path') const fs = require('fs') -const { rm } = require('node:fs/promises') +const { mkdir, rm, writeFile } = require('node:fs/promises') const gulp = require('gulp') const git = require('gulp-git') const shell = require('gulp-shell') -const postcss = require('gulp-postcss') -const csso = require('postcss-csso') -const pimport = require('postcss-import') -const minmax = require('postcss-media-minmax') -const autoprefixer = require('autoprefixer') // С версии 0.15 gulp-esbuild экспортирует именованные функции вместо самого // себя, а entryPoints и outfile стали обязательными: раньше точка входа // бралась из потока gulp. Без них поток молча повисает, и gulp сообщает @@ -21,6 +16,7 @@ const rev = require('gulp-rev').default const revRewrite = require('gulp-rev-rewrite').default const { contentRepGithub, contentRepFolders } = require(path.join(__dirname, 'config/constants')) +const { styleEntries, bundleStyle } = require(path.join(__dirname, 'config/css')) // Раньше здесь был пакет del. С 7-й версии он ESM-only и больше не callable // (экспортирует deleteAsync/deleteSync), а обе площадки вызова передавали @@ -38,20 +34,16 @@ const makeLinks = shell.task(`node make-links.js --default`, { // Styles -const styles = () => { - return gulp - .src('src/styles/{index.css,index.sc.css,dark-theme.css}') - .pipe( - postcss([ - pimport, - minmax, - autoprefixer, - csso({ - restructure: false, - }), - ]), - ) - .pipe(gulp.dest('dist/styles')) +// lightningcss сам собирает @import, ставит префиксы и минифицирует, поэтому +// gulp-поток здесь не нужен: точки входа известны, файлы он читает с диска. +const styles = async () => { + await mkdir('dist/styles', { recursive: true }) + + await Promise.all( + styleEntries.map((entry) => + writeFile(path.join('dist/styles', entry), bundleStyle(path.join('src/styles', entry))), + ), + ) } // Scripts diff --git a/package-lock.json b/package-lock.json index 55277e85..9312f207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,8 @@ "@babel/core": "^7.29.7", "@babel/preset-env": "^7.29.0", "@eslint/js": "^10.0.1", - "autoprefixer": "^10.4.16", "babel-jest": "^30.4.1", + "browserslist": "^4.28.8", "cross-env": "^10.1.0", "dotenv": "^17.4.2", "editorconfig-checker": "^7.0.0", @@ -30,7 +30,6 @@ "gulp": "^5.0.0", "gulp-esbuild": "^0.15.0", "gulp-git": "^2.4.2", - "gulp-postcss": "^10.0.0", "gulp-rev": "^12.0.0", "gulp-rev-rewrite": "^7.0.0", "gulp-shell": "^0.8.0", @@ -38,14 +37,11 @@ "htmlnano": "^3.4.0", "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", + "lightningcss": "^1.33.0", "linkedom": "^0.18.13", "markdown-it": "^15.0.1", "nano-staged": "^1.0.2", "node-w3c-validator": "^3.0.0", - "postcss": "^8.4.32", - "postcss-csso": "^6.0.1", - "postcss-import": "^17.0.0", - "postcss-media-minmax": "^5.0.0", "prettier": "^3.1.1", "prismjs": "^1.29.0", "simple-git-hooks": "^2.9.0", @@ -6910,43 +6906,6 @@ "node": ">= 10.13.0" } }, - "node_modules/autoprefixer": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", - "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.6", - "caniuse-lite": "^1.0.30001806", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/b4a": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", @@ -8109,42 +8068,6 @@ "node": ">=4" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -9636,20 +9559,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -10188,38 +10097,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/gulp-postcss": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-10.0.0.tgz", - "integrity": "sha512-z1RF2RJEX/BvFsKN11PXai8lRmihZTiHnlJf7Zu8uHaA/Q7Om4IeN8z1NtMAW5OiLwUY02H0DIFl9tHl0CNSgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fancy-log": "^2.0.0", - "plugin-error": "^2.0.1", - "postcss-load-config": "^5.0.0", - "vinyl-sourcemaps-apply": "^0.2.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/gulp-postcss/node_modules/fancy-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", - "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-support": "^1.1.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/gulp-rev": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/gulp-rev/-/gulp-rev-12.0.0.tgz", @@ -12715,19 +12592,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -13929,94 +13793,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-csso": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-csso/-/postcss-csso-6.0.1.tgz", - "integrity": "sha512-ZV4yEziMrx6CEiqabGLrDva0pMD7Fbw7yP+LzJvaynM4OJgTssGN6dHiMsJMJdpmNaLJltXVLsrb/5sxbFa8sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "csso": "^5.0.5" - }, - "engines": { - "node": "^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-import": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-17.0.0.tgz", - "integrity": "sha512-qzVMJhcq39KweT7Fp0HcjE5YxL5yI854Q3CtufxDuZKO9HLQUp2rptB9+U0MIj3pT1ZEVXoqnpeC1mSkfYuz4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-load-config": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", - "integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1", - "yaml": "^2.4.2" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - } - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/postcss-safe-parser": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", @@ -14407,13 +14183,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-cache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", - "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -16610,26 +16379,6 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "source-map": "^0.5.1" - } - }, - "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/vite": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", @@ -16990,22 +16739,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "16.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", diff --git a/package.json b/package.json index e32d102f..d3114d6c 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ } }, "browserslist": [ - "last 2 version", - "not dead", - "not ie <= 11" + "defaults", + "safari >= 16.4", + "ios_saf >= 16.4" ], "nodeW3Cvalidator": { "suppressErrors": [ @@ -59,8 +59,8 @@ "@babel/core": "^7.29.7", "@babel/preset-env": "^7.29.0", "@eslint/js": "^10.0.1", - "autoprefixer": "^10.4.16", "babel-jest": "^30.4.1", + "browserslist": "^4.28.8", "cross-env": "^10.1.0", "dotenv": "^17.4.2", "editorconfig-checker": "^7.0.0", @@ -75,7 +75,6 @@ "gulp": "^5.0.0", "gulp-esbuild": "^0.15.0", "gulp-git": "^2.4.2", - "gulp-postcss": "^10.0.0", "gulp-rev": "^12.0.0", "gulp-rev-rewrite": "^7.0.0", "gulp-shell": "^0.8.0", @@ -83,14 +82,11 @@ "htmlnano": "^3.4.0", "jest": "^30.4.2", "jest-environment-jsdom": "^30.4.1", + "lightningcss": "^1.33.0", "linkedom": "^0.18.13", "markdown-it": "^15.0.1", "nano-staged": "^1.0.2", "node-w3c-validator": "^3.0.0", - "postcss": "^8.4.32", - "postcss-csso": "^6.0.1", - "postcss-import": "^17.0.0", - "postcss-media-minmax": "^5.0.0", "prettier": "^3.1.1", "prismjs": "^1.29.0", "simple-git-hooks": "^2.9.0",