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:
+25
-6
@@ -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,
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user