1
0
mirror of https://github.com/tabler/tabler.git synced 2026-08-05 19:03:18 +04:00

Fix .min.css source maps by adding the banner before minification (#2776)

This commit is contained in:
Bartosz-Do
2026-08-03 19:20:25 +02:00
committed by GitHub
parent 46c1ce1e6d
commit 52fe729781
4 changed files with 53 additions and 57 deletions
+25 -6
View File
@@ -6,15 +6,23 @@
// live reload) fire once per write burst. Here everything runs in-process and
// each output file is written exactly once, fully processed.
//
// All options mirror the replaced CLI invocations exactly — the output has been
// verified byte-identical (css, rtl, min and their source maps):
// All options mirror the replaced CLI invocations — the output was verified
// byte-identical (css, rtl, min and their source maps), bar the deliberate
// breaks:afterComment deviation documented on minify() below:
// - sass --no-source-map --load-path=node_modules --style expanded
// - postcss (autoprefixer cascade:false, rtlcss for --rtl, external map with
// annotation + sourcesContent — the former core/.build/postcss.config.mjs)
// - cleancss -O1 --format breakWith=lf --with-rebase --source-map
// --source-map-inline-sources --batch --batch-suffix ".min"
//
// Usage: tsx ../.build/build-css.ts <scssDir> <outDir> [--rtl] [--minify]
// --banner replaces the former core/.build/add-banner.ts, which ran as a
// separate step *after* minification and rewrote the finished files in place.
// Prepending the 6-line license comment there shifted every rule down without
// touching the already-written .map, so all mappings pointed 7 lines too high
// (#2766). Here the banner is part of the css before any map is generated, so
// postcss and clean-css both account for it.
//
// Usage: tsx ../.build/build-css.ts <scssDir> <outDir> [--rtl] [--minify] [--banner]
// (cwd = the package)
/// <reference path="./modules.d.ts" />
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
@@ -25,16 +33,18 @@ import postcss, { type Result } from 'postcss'
import autoprefixer from 'autoprefixer'
import rtlcss from 'rtlcss'
import CleanCSS from 'clean-css'
import { addBanner } from '../shared/banner/index.mjs'
const args = process.argv.slice(2)
const flags = args.filter((arg) => arg.startsWith('--'))
const [scssDir, outDir] = args.filter((arg) => !arg.startsWith('--'))
if (!scssDir || !outDir) {
console.error('usage: tsx build-css.ts <scssDir> <outDir> [--rtl] [--minify]')
console.error('usage: tsx build-css.ts <scssDir> <outDir> [--rtl] [--minify] [--banner]')
process.exit(1)
}
const withRtl = flags.includes('--rtl')
const withMinify = flags.includes('--minify')
const withBanner = flags.includes('--banner')
const mapOptions = { inline: false, annotation: true, sourcesContent: true }
const written: string[] = []
@@ -54,7 +64,11 @@ async function compile(entry: string): Promise<{ outFile: string; result: Result
// The sass CLI ends its output files with a newline; the JS API's css string
// does not. Keep the byte-identical CLI behavior (the annotation comment then
// lands after a blank line, exactly like `postcss --replace` produced).
const result = await postcss([autoprefixer({ cascade: false })]).process(`${compiled.css}\n`, {
const css = `${compiled.css}\n`
// Banner goes in before postcss runs, so the map it generates already counts
// the banner's lines — and so does the rtl map chained onto it below.
const input = withBanner ? addBanner(css, outFile) : css
const result = await postcss([autoprefixer({ cascade: false })]).process(input, {
from: outFile,
to: outFile,
map: mapOptions,
@@ -94,10 +108,15 @@ function flushWrites(): void {
// clean-css over every produced file: outDir/x.css → outDir/x.min.css (+ .map).
// Mirrors clean-css-cli's option coercion and batch output naming/annotation
// (see the clean-css-cli package's index.js).
//
// breaks:afterComment is the one deviation: clean-css counts the lines a kept
// `/*! … */` comment spans but not the `*/` it leaves on the last one, so every
// mapping on that line comes out `'*/'.length` columns short. Ending the line
// after the comment puts the css at column 0 — exactly where the map says.
async function minify(files: string[]): Promise<void> {
const minified = await new CleanCSS({
batch: true,
format: 'breakWith=lf',
format: 'breakWith=lf;breaks:afterComment=on',
inline: 'local',
level: { 1: true },
rebase: true,
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs'
import { join, dirname, basename } from 'node:path'
import { fileURLToPath } from 'node:url'
import { sync } from 'glob'
import banner from '../../shared/banner/index.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const styles: string[] = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
interface Plugins {
[key: string]: string
}
const plugins: Plugins = {
'tabler-flags': 'Flags',
'tabler-flags.rtl': 'Flags RTL',
'tabler-marketing': 'Marketing',
'tabler-marketing.rtl': 'Marketing RTL',
'tabler-payments': 'Payments',
'tabler-payments.rtl': 'Payments RTL',
'tabler-socials': 'Socials',
'tabler-socials.rtl': 'Socials RTL',
'tabler-vendors': 'Vendors',
'tabler-vendors.rtl': 'Vendors RTL',
}
styles.forEach((file: string) => {
const content = readFileSync(file, 'utf8')
const filename = basename(file)
const pluginKey = Object.keys(plugins).find((plugin: string) => filename.includes(plugin))
const plugin = pluginKey ? plugins[pluginKey] : undefined
const regex = /^(@charset ['"][a-zA-Z0-9-]+['"];?)\n?/i
let newContent = ''
const bannerText = banner(plugin)
if (content.match(regex)) {
newContent = content.replace(regex, (m: string, m1: string) => {
return `${m1}\n${bannerText}\n`
})
} else {
newContent = `${bannerText}\n${content}`
}
writeFileSync(file, newContent, 'utf8')
})
+1 -2
View File
@@ -9,9 +9,8 @@
"build": "pnpm run clean && pnpm run build-assets && pnpm run copy && pnpm run generate-sri",
"build-assets": "concurrently \"pnpm run css\" \"pnpm run js\"",
"clean": "shx rm -rf dist demo",
"css": "tsx ../.build/build-css.ts scss dist/css --rtl --minify && pnpm run css-banner",
"css": "tsx ../.build/build-css.ts scss dist/css --rtl --minify --banner",
"css-build": "tsx ../.build/build-css.ts scss dist/css",
"css-banner": "tsx .build/add-banner.ts",
"css-lint": "pnpm run css-lint-variables",
"css-lint-variables": "find-unused-sass-variables scss/",
"js": "pnpm run js-build && pnpm run js-build-min",
+27
View File
@@ -18,4 +18,31 @@ function getBanner(pluginFilename) {
*/`
}
// Plugin entrypoint → the name shown in its banner. The `.rtl` and `.min`
// variants of a file share the entrypoint's name; anything without an entry
// here (tabler, tabler-props, tabler-themes) gets the plain "Tabler" banner.
const plugins = {
'tabler-flags': 'Flags',
'tabler-marketing': 'Marketing',
'tabler-payments': 'Payments',
'tabler-socials': 'Socials',
'tabler-vendors': 'Vendors',
}
// e.g. dist/css/tabler-flags.rtl.min.css → the "Tabler Flags" banner.
export function getBannerForFile(file) {
const name = path.basename(file).replace(/(\.rtl)?(\.min)?\.css$/, '')
return getBanner(plugins[name])
}
// Inserts the banner right after the `@charset` rule when there is one — it
// must stay the very first thing in the file — otherwise at the top.
export function addBanner(css, file) {
const charset = /^(@charset ['"][a-zA-Z0-9-]+['"];?)\n?/i
const banner = getBannerForFile(file)
return charset.test(css) ? css.replace(charset, (_match, rule) => `${rule}\n${banner}\n`) : `${banner}\n${css}`
}
export default getBanner