mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 13:17:00 +04:00
Add SRI hashes to the CDN snippets in the docs (#3086)
This commit is contained in:
1 parent
a9efcc241d
commit
bcf5991167
16 files changed
+349
-18
No files matched your search
@@ -107,7 +107,7 @@ pnpm run lint:prettier # formatting
|
|||||||
pnpm run bundlewatch # size budgets (tabler.css 80 kB, tabler.min.css 75 kB)
|
pnpm run bundlewatch # size budgets (tabler.css 80 kB, tabler.min.css 75 kB)
|
||||||
```
|
```
|
||||||
|
|
||||||
- A new entry in `$theme-colors`, `$avatar-sizes` and friends must be regenerated into `shared/lib/tokens.ts` with `pnpm run generate-tokens` — the check gate fails otherwise.
|
- A new entry in `$theme-colors`, `$avatar-sizes` and friends must be regenerated into `shared/lib/tokens.ts` with `pnpm run generate:tokens` — the check gate fails otherwise.
|
||||||
- An unused Sass variable fails `lint:scss`; delete it or use it.
|
- An unused Sass variable fails `lint:scss`; delete it or use it.
|
||||||
- Growth past a bundlewatch limit is a decision, not an accident: raise the number in `core/package.json` deliberately and say so in the PR.
|
- Growth past a bundlewatch limit is a decision, not an accident: raise the number in `core/package.json` deliberately and say so in the PR.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Regenerates shared/data/sri.json, the Subresource Integrity hashes the docs put into the CDN
|
||||||
|
// snippets. The hashes are read from the files jsDelivr already serves for the published
|
||||||
|
// @tabler/core version, never from a local build: a local dist is not the file the browser
|
||||||
|
// downloads, so its hash would make the browser block the stylesheet or the script.
|
||||||
|
//
|
||||||
|
// Run it after a release, before merging `dev` into `main` (docs.tabler.io is built from `main`):
|
||||||
|
//
|
||||||
|
// pnpm run generate:sri (once the new version is on npm)
|
||||||
|
// pnpm run generate:sri --wait (same, but waits for the release to reach npm and the CDN)
|
||||||
|
// pnpm run check:sri (the committed hashes still match the CDN)
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import { join, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { site } from '../shared/lib/site'
|
||||||
|
|
||||||
|
interface SriData {
|
||||||
|
version: string
|
||||||
|
algorithm: string
|
||||||
|
files: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const algorithm = 'sha384'
|
||||||
|
const waitMinutes = 10
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = join(__dirname, '..')
|
||||||
|
const dataFile = join(repoRoot, 'shared/data/sri.json')
|
||||||
|
|
||||||
|
// The files the docs hand out as CDN tags. The plugin bundles in `dist/libs/` are not here: they
|
||||||
|
// are third-party packages the docs only link to, and each one has its own version.
|
||||||
|
const files = ['dist/css/tabler.min.css', 'dist/css/tabler.rtl.min.css', 'dist/js/tabler.min.js', 'dist/js/tabler-theme.min.js', ...site.cssPlugins.flatMap((plugin) => [`dist/css/tabler-${plugin}.min.css`, `dist/css/tabler-${plugin}.rtl.min.css`])]
|
||||||
|
|
||||||
|
/** One GET, retried once: a single dropped connection should not fail a CI job. */
|
||||||
|
async function get(url: string): Promise<Response> {
|
||||||
|
for (let attempt = 1; ; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (response.ok || response.status === 404 || attempt === 2) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
console.warn(`generate:sri: ${url} returned ${response.status}, retrying`)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (attempt === 2) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
console.warn(`generate:sri: ${url} failed (${error instanceof Error ? error.message : String(error)}), retrying`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the version in core/package.json is on npm, and so on the CDN as well. */
|
||||||
|
async function isPublished(): Promise<boolean> {
|
||||||
|
return (await get(`https://registry.npmjs.org/@tabler/core/${site.version}`)).ok
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The integrity value of one published file, downloaded from the CDN the docs point at. */
|
||||||
|
async function hashFile(file: string): Promise<string> {
|
||||||
|
const url = `${site.cdnUrl}/${file}`
|
||||||
|
const response = await get(url)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`generate:sri: ${url} returned ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = Buffer.from(await response.arrayBuffer())
|
||||||
|
|
||||||
|
return `${algorithm}-${createHash(algorithm).update(body).digest('base64')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectHashes(): Promise<SriData> {
|
||||||
|
const hashes = await Promise.all(files.map(async (file) => [file, await hashFile(file)] as const))
|
||||||
|
|
||||||
|
return { version: site.version, algorithm, files: Object.fromEntries(hashes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `pnpm run check:sri` - the committed hashes have to match what the CDN serves today.
|
||||||
|
*
|
||||||
|
* Hashes for an older version are a safe state: the docs render the tags without `integrity`
|
||||||
|
* until `generate:sri` runs. So they only warn.
|
||||||
|
*/
|
||||||
|
async function check(): Promise<void> {
|
||||||
|
if (!existsSync(dataFile)) {
|
||||||
|
throw new Error('check:sri: shared/data/sri.json is missing - run `pnpm run generate:sri` and commit the result')
|
||||||
|
}
|
||||||
|
|
||||||
|
const committed: SriData = JSON.parse(readFileSync(dataFile, 'utf8'))
|
||||||
|
|
||||||
|
// The version pull request changesets opens bumps core/package.json before anything is on npm.
|
||||||
|
// There is nothing to compare against yet, and the hashes get regenerated after the release, so
|
||||||
|
// this is a pass rather than a failure.
|
||||||
|
if (!(await isPublished())) {
|
||||||
|
console.log(`check:sri: @tabler/core v${site.version} is not published yet, nothing to compare`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (committed.version !== site.version) {
|
||||||
|
console.warn(`check:sri: shared/data/sri.json has hashes for v${committed.version}, but @tabler/core is v${site.version} - the docs render the CDN tags without \`integrity\` until you run \`pnpm run generate:sri\``)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = await collectHashes()
|
||||||
|
const problems: string[] = []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (committed.files[file] !== current.files[file]) {
|
||||||
|
problems.push(`${file}: ${committed.files[file] ?? 'missing'} committed, ${current.files[file]} on the CDN`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problems.length > 0) {
|
||||||
|
console.error(`check:sri: shared/data/sri.json is out of date, run \`pnpm run generate:sri\`:\n ${problems.join('\n ')}`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`check:sri: ${files.length} hashes match @tabler/core v${current.version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `--wait`, for running it right after a release: `changeset publish` returns before npm and the
|
||||||
|
* CDN have the new version everywhere, so poll instead of failing on the first 404.
|
||||||
|
*/
|
||||||
|
async function collectWhenPublished(): Promise<SriData> {
|
||||||
|
const deadline = Date.now() + waitMinutes * 60_000
|
||||||
|
|
||||||
|
for (let attempt = 1; ; attempt++) {
|
||||||
|
try {
|
||||||
|
if (await isPublished()) {
|
||||||
|
return await collectHashes()
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (Date.now() >= deadline) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() >= deadline) {
|
||||||
|
throw new Error(`generate:sri: @tabler/core v${site.version} did not show up on the CDN within ${waitMinutes} minutes`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`generate:sri: waiting for @tabler/core v${site.version} on the CDN (attempt ${attempt})`)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 15_000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate(wait: boolean): Promise<void> {
|
||||||
|
if (!wait && !(await isPublished())) {
|
||||||
|
throw new Error(`generate:sri: @tabler/core v${site.version} is not on npm yet - run this after \`changeset publish\``)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = wait ? await collectWhenPublished() : await collectHashes()
|
||||||
|
|
||||||
|
writeFileSync(dataFile, `${JSON.stringify(data, null, 2)}\n`)
|
||||||
|
|
||||||
|
console.log(`generate:sri: wrote ${files.length} ${algorithm} hashes for @tabler/core v${data.version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapped in main() because the root package is CJS (no top-level await).
|
||||||
|
const main = async () => {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
|
await (args.includes('check') ? check() : generate(args.includes('--wait')))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error: unknown) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
// unions used by components can never drift from the stylesheet source of
|
// unions used by components can never drift from the stylesheet source of
|
||||||
// truth. Values are read through the real Sass compiler (after every merge and
|
// truth. Values are read through the real Sass compiler (after every merge and
|
||||||
// !default), not by parsing the source text.
|
// !default), not by parsing the source text.
|
||||||
// Run: pnpm run generate-tokens — (re)write the file
|
// Run: pnpm run generate:tokens — (re)write the file
|
||||||
// pnpm run check:tokens — fail when the committed file is stale
|
// pnpm run check:tokens — fail when the committed file is stale
|
||||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||||
import { join, dirname, relative } from 'node:path'
|
import { join, dirname, relative } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
@@ -64,7 +64,7 @@ compileString(entry, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let output = `// Generated from the core SCSS maps by .build/generate-tokens.ts — DO NOT EDIT.
|
let output = `// Generated from the core SCSS maps by .build/generate-tokens.ts — DO NOT EDIT.
|
||||||
// Regenerate with: pnpm run generate-tokens
|
// Regenerate with: pnpm run generate:tokens
|
||||||
`
|
`
|
||||||
for (const token of TOKENS) {
|
for (const token of TOKENS) {
|
||||||
const keys = keysByName.get(token.name)
|
const keys = keysByName.get(token.name)
|
||||||
@@ -88,7 +88,7 @@ const main = async () => {
|
|||||||
if (mode === 'check') {
|
if (mode === 'check') {
|
||||||
const current = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
const current = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
||||||
if (current !== formatted) {
|
if (current !== formatted) {
|
||||||
console.error(`✖ ${relative(repoRoot, outFile)} is stale — run "pnpm run generate-tokens" and commit the result.`)
|
console.error(`✖ ${relative(repoRoot, outFile)} is stale — run "pnpm run generate:tokens" and commit the result.`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
console.log(`✓ ${relative(repoRoot, outFile)} is up to date.`)
|
console.log(`✓ ${relative(repoRoot, outFile)} is up to date.`)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// and dark SVGs, the $payment-providers list and the demo data.
|
// and dark SVGs, the $payment-providers list and the demo data.
|
||||||
//
|
//
|
||||||
// Run: pnpm run import:payments
|
// Run: pnpm run import:payments
|
||||||
// Then: pnpm run generate-tokens — refreshes PaymentProvider in shared/lib/tokens.ts
|
// Then: pnpm run generate:tokens — refreshes PaymentProvider in shared/lib/tokens.ts
|
||||||
import { execFileSync } from 'node:child_process'
|
import { execFileSync } from 'node:child_process'
|
||||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
@@ -86,7 +86,7 @@ try {
|
|||||||
console.log(`Imported ${providers.length} payment providers from ${relative(repoRoot, sourceDir)} (${added.length} added, ${removed.length} removed)`)
|
console.log(`Imported ${providers.length} payment providers from ${relative(repoRoot, sourceDir)} (${added.length} added, ${removed.length} removed)`)
|
||||||
if (added.length) console.log(` added: ${added.join(', ')}`)
|
if (added.length) console.log(` added: ${added.join(', ')}`)
|
||||||
if (removed.length) console.log(` removed: ${removed.join(', ')}`)
|
if (removed.length) console.log(` removed: ${removed.join(', ')}`)
|
||||||
console.log('Now run `pnpm run generate-tokens` to refresh shared/lib/tokens.ts')
|
console.log('Now run `pnpm run generate:tokens` to refresh shared/lib/tokens.ts')
|
||||||
} finally {
|
} finally {
|
||||||
if (cloneDir) rmSync(cloneDir, { recursive: true, force: true })
|
if (cloneDir) rmSync(cloneDir, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Publishes a release to docs.tabler.io, which is built from `main`. Run it on `dev` once the
|
||||||
|
// release workflow has put the new version on npm:
|
||||||
|
//
|
||||||
|
// 1. refreshes shared/data/sri.json for that version (`generate:sri --wait`),
|
||||||
|
// 2. commits it and pushes `dev`,
|
||||||
|
// 3. merges `dev` into `main` and pushes `main`.
|
||||||
|
//
|
||||||
|
// The merge happens in a temporary worktree, so the current checkout (and a dev server running
|
||||||
|
// from it) never switches branches.
|
||||||
|
//
|
||||||
|
// Run: pnpm run release:main (does all of the above)
|
||||||
|
// pnpm run release:main --dry-run (refreshes and commits locally, pushes nothing)
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = join(__dirname, '..')
|
||||||
|
const sriFile = 'shared/data/sri.json'
|
||||||
|
const dryRun = process.argv.includes('--dry-run')
|
||||||
|
|
||||||
|
const git = (args: string[], cwd = repoRoot): string => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim()
|
||||||
|
const run = (command: string, args: string[]): void => {
|
||||||
|
execFileSync(command, args, { cwd: repoRoot, stdio: 'inherit' })
|
||||||
|
}
|
||||||
|
const step = (message: string): void => {
|
||||||
|
console.log(`\nrelease:main: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { version } = JSON.parse(readFileSync(join(repoRoot, 'core', 'package.json'), 'utf8')) as { version: string }
|
||||||
|
|
||||||
|
const main = (): void => {
|
||||||
|
if (git(['branch', '--show-current']) !== 'dev') {
|
||||||
|
throw new Error('release:main: run this on the `dev` branch')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (git(['status', '--porcelain', '--untracked-files=no']) !== '') {
|
||||||
|
throw new Error('release:main: the working tree has uncommitted changes - commit or stash them first')
|
||||||
|
}
|
||||||
|
|
||||||
|
step('updating dev from origin')
|
||||||
|
git(['fetch', 'origin', 'dev', 'main'])
|
||||||
|
git(['merge', '--ff-only', 'origin/dev'])
|
||||||
|
|
||||||
|
step(`refreshing the SRI hashes for @tabler/core v${version}`)
|
||||||
|
run(join(repoRoot, 'node_modules', '.bin', 'tsx'), [join('.build', 'generate-sri.ts'), '--wait'])
|
||||||
|
|
||||||
|
if (git(['status', '--porcelain', '--', sriFile]) === '') {
|
||||||
|
console.log(`release:main: ${sriFile} is already up to date`)
|
||||||
|
} else {
|
||||||
|
git(['commit', '-m', `Update SRI hashes for v${version}`, '--', sriFile])
|
||||||
|
console.log(`release:main: committed ${sriFile}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
step('--dry-run: stopping before any push')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
step('pushing dev')
|
||||||
|
run('git', ['push', 'origin', 'dev'])
|
||||||
|
|
||||||
|
step('merging dev into main')
|
||||||
|
const worktree = mkdtempSync(join(tmpdir(), 'tabler-main-'))
|
||||||
|
|
||||||
|
try {
|
||||||
|
git(['worktree', 'add', '--detach', worktree, 'origin/main'])
|
||||||
|
git(['merge', '--no-edit', '-m', `Merge dev into main for v${version}`, 'dev'], worktree)
|
||||||
|
execFileSync('git', ['push', 'origin', 'HEAD:main'], { cwd: worktree, stdio: 'inherit' })
|
||||||
|
} finally {
|
||||||
|
git(['worktree', 'remove', '--force', worktree])
|
||||||
|
rmSync(worktree, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
step(`done - docs.tabler.io deploys v${version} from main`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main()
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error(error instanceof Error ? error.message : error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `integrity` and `crossorigin` attributes to the CDN snippets on the installation and plugin pages.
|
||||||
@@ -51,6 +51,25 @@ pick:
|
|||||||
`pnpm run release-notes` prints the body to stdout, so you can read it before
|
`pnpm run release-notes` prints the body to stdout, so you can read it before
|
||||||
the release goes out. Pass a path to write it to a file instead.
|
the release goes out. Pass a path to write it to a file instead.
|
||||||
|
|
||||||
|
## After the release
|
||||||
|
|
||||||
|
docs.tabler.io is built from `main`. Once the release workflow has published
|
||||||
|
the new version, run this on `dev`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm run release:main
|
||||||
|
```
|
||||||
|
|
||||||
|
It refreshes the `integrity` hashes of the CDN snippets in
|
||||||
|
`shared/data/sri.json` (they can only be read once the version is on npm),
|
||||||
|
commits them, pushes `dev`, then merges `dev` into `main` and pushes `main`. The
|
||||||
|
merge runs in a temporary worktree, so your checkout stays on `dev`. Add
|
||||||
|
`--dry-run` to stop after the local commit.
|
||||||
|
|
||||||
|
Until the hashes are refreshed the docs show the CDN tags without `integrity`.
|
||||||
|
That is safe, just less strict. `pnpm run check:sri` compares the committed
|
||||||
|
hashes with the CDN if you want to check them by hand.
|
||||||
|
|
||||||
## Fixing a published release
|
## Fixing a published release
|
||||||
|
|
||||||
Edit the release on GitHub. The workflow only writes the body once, right after
|
Edit the release on GitHub. The workflow only writes the body once, right after
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ dist/
|
|||||||
packages-zip/
|
packages-zip/
|
||||||
.env
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
sri.json
|
|
||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
+2
-2
@@ -63,12 +63,12 @@ Do not edit `dist/` folders — they are generated by the build.
|
|||||||
| `pnpm run check` | Lint plus TypeScript type checks |
|
| `pnpm run check` | Lint plus TypeScript type checks |
|
||||||
| `pnpm run html-diff:baseline` | Snapshot all rendered preview pages (needs the dev server) |
|
| `pnpm run html-diff:baseline` | Snapshot all rendered preview pages (needs the dev server) |
|
||||||
| `pnpm run html-diff` | Byte-compare rendered preview pages against the snapshot |
|
| `pnpm run html-diff` | Byte-compare rendered preview pages against the snapshot |
|
||||||
| `pnpm run generate-tokens` | Regenerate `shared/lib/tokens.ts` from the core SCSS maps |
|
| `pnpm run generate:tokens` | Regenerate `shared/lib/tokens.ts` from the core SCSS maps |
|
||||||
|
|
||||||
Two of these help with refactors:
|
Two of these help with refactors:
|
||||||
|
|
||||||
- **`html-diff`** proves a refactor does not change the rendered demo HTML: run `html-diff:baseline` before your change, refactor, then `html-diff` — it reports the first differing line per page and marks whitespace-only differences.
|
- **`html-diff`** proves a refactor does not change the rendered demo HTML: run `html-diff:baseline` before your change, refactor, then `html-diff` — it reports the first differing line per page and marks whitespace-only differences.
|
||||||
- **`generate-tokens`** keeps the TS unions in `shared/lib/tokens.ts` in sync with the SCSS maps (colors, sizes, breakpoints, payment providers, flags). Edit the SCSS map, regenerate, commit both — a stale file fails `pnpm run lint`.
|
- **`generate:tokens`** keeps the TS unions in `shared/lib/tokens.ts` in sync with the SCSS maps (colors, sizes, breakpoints, payment providers, flags). Edit the SCSS map, regenerate, commit both — a stale file fails `pnpm run lint`.
|
||||||
|
|
||||||
## Before you open a pull request
|
## Before you open a pull request
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ description: Set up Tabler's light, dark, and auto color modes, and learn the th
|
|||||||
related: [/ui/getting-started/customize, /ui/plugins/theme-base-colors]
|
related: [/ui/getting-started/customize, /ui/plugins/theme-base-colors]
|
||||||
---
|
---
|
||||||
|
|
||||||
import { site } from '@shared/lib/site.ts';
|
|
||||||
import { Code } from 'astro:components';
|
import { Code } from 'astro:components';
|
||||||
|
import { cdnThemeJsTag } from '@lib/cdn-snippets.ts';
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ Load the theme script right after the opening `<body>` tag, and do not add `defe
|
|||||||
<Code
|
<Code
|
||||||
lang="html"
|
lang="html"
|
||||||
code={`<body>
|
code={`<body>
|
||||||
<script src="${site.cdnUrl}/dist/js/tabler-theme.min.js"></script>
|
${cdnThemeJsTag()}
|
||||||
...
|
...
|
||||||
</body>`}
|
</body>`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ Put the stylesheet in `<head>` and the script at the end of `<body>`. The file t
|
|||||||
|
|
||||||
That's the whole setup. The CSS covers the layout and components, and the script adds the interactive parts such as dropdowns and modals.
|
That's the whole setup. The CSS covers the layout and components, and the script adds the interactive parts such as dropdowns and modals.
|
||||||
|
|
||||||
|
The `integrity` attribute lets the browser check that the file it downloaded is the one Tabler published, and `crossorigin="anonymous"` is what makes that check possible on a CDN. Each hash belongs to one file of one version, so when you move to a newer Tabler, copy the tags from this page again instead of only changing the version number in the URL.
|
||||||
|
|
||||||
If you'd rather not depend on a CDN, you can download the files and serve them from your own project. The [Download](/ui/getting-started/download) page explains how.
|
If you'd rather not depend on a CDN, you can download the files and serve them from your own project. The [Download](/ui/getting-started/download) page explains how.
|
||||||
|
|
||||||
### Open in your browser
|
### Open in your browser
|
||||||
|
|||||||
@@ -2,15 +2,37 @@
|
|||||||
// re-renders them into the /llms.txt endpoints, and the installation page drops the two tags into
|
// re-renders them into the /llms.txt endpoints, and the installation page drops the two tags into
|
||||||
// a full HTML example.
|
// a full HTML example.
|
||||||
import { site } from '@shared/lib/site.ts'
|
import { site } from '@shared/lib/site.ts'
|
||||||
|
import sri from '@data/sri.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `integrity` and `crossorigin` for a file of the published core package, so the browser can
|
||||||
|
* verify what the CDN sent. The hashes in shared/data/sri.json come from `pnpm run generate:sri`;
|
||||||
|
* until that runs for the current version the tags are rendered without them, rather than with a
|
||||||
|
* hash that would make the browser block the file.
|
||||||
|
*/
|
||||||
|
const sriAttrs = (file: string): string => {
|
||||||
|
const hash = sri.version === site.version ? (sri.files as Record<string, string>)[file] : undefined
|
||||||
|
|
||||||
|
return hash ? ` integrity="${hash}" crossorigin="anonymous"` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `<link>` for one stylesheet of the core package, by its path inside the package. */
|
||||||
|
const cdnLinkTag = (file: string): string => `<link rel="stylesheet" href="${site.cdnUrl}/${file}"${sriAttrs(file)} />`
|
||||||
|
|
||||||
|
/** `<script>` for one script of the core package, by its path inside the package. */
|
||||||
|
const cdnScriptTag = (file: string): string => `<script src="${site.cdnUrl}/${file}"${sriAttrs(file)}></script>`
|
||||||
|
|
||||||
/** `<link>` for the core stylesheet. */
|
/** `<link>` for the core stylesheet. */
|
||||||
export const cdnCssTag = (): string => `<link rel="stylesheet" href="${site.cdnUrl}/dist/css/tabler.min.css" />`
|
export const cdnCssTag = (): string => cdnLinkTag('dist/css/tabler.min.css')
|
||||||
|
|
||||||
/** `<script>` for the core bundle. */
|
/** `<script>` for the core bundle. */
|
||||||
export const cdnJsTag = (): string => `<script src="${site.cdnUrl}/dist/js/tabler.min.js"></script>`
|
export const cdnJsTag = (): string => cdnScriptTag('dist/js/tabler.min.js')
|
||||||
|
|
||||||
|
/** `<script>` for the color mode script, as shown on the Color modes page. */
|
||||||
|
export const cdnThemeJsTag = (): string => cdnScriptTag('dist/js/tabler-theme.min.js')
|
||||||
|
|
||||||
/** Both core tags, as shown by `<CdnImportPackage />`. */
|
/** Both core tags, as shown by `<CdnImportPackage />`. */
|
||||||
export const cdnPackageSnippet = (): string => `${cdnCssTag()}\n${cdnJsTag()}`
|
export const cdnPackageSnippet = (): string => `${cdnCssTag()}\n${cdnJsTag()}`
|
||||||
|
|
||||||
/** `<link>` per plugin stylesheet, as shown by `<CdnImportPlugin />`. */
|
/** `<link>` per plugin stylesheet, as shown by `<CdnImportPlugin />`. */
|
||||||
export const cdnPluginSnippet = (plugins: string[]): string => plugins.map((plugin) => `<link rel="stylesheet" href="${site.cdnUrl}/dist/css/tabler-${plugin}.min.css" />`).join('\n')
|
export const cdnPluginSnippet = (plugins: string[]): string => plugins.map((plugin) => cdnLinkTag(`dist/css/tabler-${plugin}.min.css`)).join('\n')
|
||||||
+2
-1
@@ -24,7 +24,7 @@ import { callouts } from '@components/callouts/index.ts'
|
|||||||
import docs from '@data/docs.json'
|
import docs from '@data/docs.json'
|
||||||
import packageManagers from '@data/package-managers.json'
|
import packageManagers from '@data/package-managers.json'
|
||||||
import payments from '@data/payments.json'
|
import payments from '@data/payments.json'
|
||||||
import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet } from './cdn-snippets.ts'
|
import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet, cdnThemeJsTag } from './cdn-snippets.ts'
|
||||||
|
|
||||||
// Lazy raw imports, same as CodeDocs.astro — node:fs paths break once this is
|
// Lazy raw imports, same as CodeDocs.astro — node:fs paths break once this is
|
||||||
// bundled into dist/.prerender.
|
// bundled into dist/.prerender.
|
||||||
@@ -41,6 +41,7 @@ function resolveCodeTokens(snippet: string): string {
|
|||||||
const tokens: Record<string, () => string> = {
|
const tokens: Record<string, () => string> = {
|
||||||
'${cdnCssTag()}': cdnCssTag,
|
'${cdnCssTag()}': cdnCssTag,
|
||||||
'${cdnJsTag()}': cdnJsTag,
|
'${cdnJsTag()}': cdnJsTag,
|
||||||
|
'${cdnThemeJsTag()}': cdnThemeJsTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(tokens)
|
return Object.entries(tokens)
|
||||||
|
|||||||
+4
-1
@@ -18,12 +18,14 @@
|
|||||||
"format:md": "tsx .build/reformat-mdx.mts",
|
"format:md": "tsx .build/reformat-mdx.mts",
|
||||||
"html-diff": "tsx .build/html-diff.ts check",
|
"html-diff": "tsx .build/html-diff.ts check",
|
||||||
"html-diff:baseline": "tsx .build/html-diff.ts baseline",
|
"html-diff:baseline": "tsx .build/html-diff.ts baseline",
|
||||||
"generate-tokens": "tsx .build/generate-tokens.ts",
|
"generate:tokens": "tsx .build/generate-tokens.ts",
|
||||||
|
"generate:sri": "tsx .build/generate-sri.ts",
|
||||||
"import:icons": "tsx .build/import-icons.ts",
|
"import:icons": "tsx .build/import-icons.ts",
|
||||||
"import:illustrations": "tsx .build/import-illustrations.ts",
|
"import:illustrations": "tsx .build/import-illustrations.ts",
|
||||||
"import:payments": "tsx .build/import-payments.ts",
|
"import:payments": "tsx .build/import-payments.ts",
|
||||||
"update:icons": "pnpm up -w @tabler/icons@latest && pnpm run import:icons",
|
"update:icons": "pnpm up -w @tabler/icons@latest && pnpm run import:icons",
|
||||||
"check:tokens": "tsx .build/generate-tokens.ts check",
|
"check:tokens": "tsx .build/generate-tokens.ts check",
|
||||||
|
"check:sri": "tsx .build/generate-sri.ts check",
|
||||||
"check:docs-links": "tsx .build/check-docs-links.ts",
|
"check:docs-links": "tsx .build/check-docs-links.ts",
|
||||||
"check:css-vars": "tsx .build/check-css-vars.ts",
|
"check:css-vars": "tsx .build/check-css-vars.ts",
|
||||||
"check:open-source": "tsx .build/check-open-source.ts",
|
"check:open-source": "tsx .build/check-open-source.ts",
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
"format:prettier": "prettier --write \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.{ts,mts}\" \"screenshots/.build/**/*.ts\" \"preview/scss/**/*.scss\" \"docs/scss/**/*.scss\" \"{preview,docs,screenshots,shared}/**/*.astro\" \"{preview,docs,screenshots}/**/*.{mjs,mts,ts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
"format:prettier": "prettier --write \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.{ts,mts}\" \"screenshots/.build/**/*.ts\" \"preview/scss/**/*.scss\" \"docs/scss/**/*.scss\" \"{preview,docs,screenshots,shared}/**/*.astro\" \"{preview,docs,screenshots}/**/*.{mjs,mts,ts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
||||||
"check": "pnpm run lint && pnpm run type-check && pnpm run type-check:build",
|
"check": "pnpm run lint && pnpm run type-check && pnpm run type-check:build",
|
||||||
"release-notes": "tsx .build/release-notes.ts",
|
"release-notes": "tsx .build/release-notes.ts",
|
||||||
|
"release:main": "tsx .build/release-main.ts",
|
||||||
"zip-package": "tsx .build/zip-package.ts",
|
"zip-package": "tsx .build/zip-package.ts",
|
||||||
"start": "pnpm dev"
|
"start": "pnpm dev"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"version": "1.5.1",
|
||||||
|
"algorithm": "sha384",
|
||||||
|
"files": {
|
||||||
|
"dist/css/tabler.min.css": "sha384-tLWyEXulonaekaXL6+fCZQJv/MujuGIpVTBzuVEpwSqFDEfLkPA/PAYGTSBBJO2X",
|
||||||
|
"dist/css/tabler.rtl.min.css": "sha384-OnS3EnX7UxAkLI2yYfkqnxg54JzNW/thpEW2NMuobSb0DF7hAWlFoJ2NwyX4Pnzh",
|
||||||
|
"dist/js/tabler.min.js": "sha384-KeeTrPFG36BhSeZepo1c2EzYKGlF6NX5Gtjyn5iEWVj52/Fy0LLbWLin5AqZne99",
|
||||||
|
"dist/js/tabler-theme.min.js": "sha384-r/aXxjzfXhMltNwaj2rBQ4iPtGFgkYA7dyJQlqkqL0cNzMqM/z36OttuWx0B1VWz",
|
||||||
|
"dist/css/tabler-flags.min.css": "sha384-9718xqXlRN3KKvYXJSjptnIWwI0UZDTz/jvavI+LA7Z8JHzfKo9E2CtZWODeVWFG",
|
||||||
|
"dist/css/tabler-flags.rtl.min.css": "sha384-/bGPya5STpgWA72Jm3g2JAsOtXtqTktOiZ9AjnCi9j2uVvXJkxCjkpsFxxebG8+Q",
|
||||||
|
"dist/css/tabler-socials.min.css": "sha384-prVQ+s1ucombzkZVHTwrB+Z68JTcU3PpRuVpr4z+NNr3A3HAkuxCZUh44prqPHAp",
|
||||||
|
"dist/css/tabler-socials.rtl.min.css": "sha384-R8INeoweSrmyzhnlREsmVUq3LkEiWqgwgQgrzDR2BB4QILCBbRSUv0nQVblZoJlg",
|
||||||
|
"dist/css/tabler-payments.min.css": "sha384-GRX8VBRF1x9TRmijkOOuJN5SIVm8iuTcPkYI4sG1YUtryG+pXnby5NCB7J90JT3r",
|
||||||
|
"dist/css/tabler-payments.rtl.min.css": "sha384-GRJTMICo/1buLV+5gulL3zmm+vtFpzdVgoWslOWjv5wpq33JSZ4HO8ZlBF1NuLsF",
|
||||||
|
"dist/css/tabler-vendors.min.css": "sha384-PV1GVwo9+Ko89STPCCqUwPUaP12FVpJA3otCGIqIce0jN0BjQxaQw5i5BRUCcbdx",
|
||||||
|
"dist/css/tabler-vendors.rtl.min.css": "sha384-bwfRGrur1OO42oUbBpU7miL4QLS6DWzkF8nE1ZpuhFWMrPqQSW+y1Mzb5JXlfaYx",
|
||||||
|
"dist/css/tabler-marketing.min.css": "sha384-//fabqiLUxL4Gd5mADLqQsRoeGYpSWjLhYUaUY4Ld1xRUb936UHDf7g66pSNwZVi",
|
||||||
|
"dist/css/tabler-marketing.rtl.min.css": "sha384-jsKlMb77mWkbvW1aLqzhLM5nkbHlYuPAK+kd2cjWM2J6Nrxv8Q9Zu68AWV7MGqhg",
|
||||||
|
"dist/css/tabler-themes.min.css": "sha384-yjfgW/GqjeXpsI6lXUQcwjm72Zs2trYjmpwPV8bzhkDjSTMDPiqXS4oi2liria1a",
|
||||||
|
"dist/css/tabler-themes.rtl.min.css": "sha384-MxUXq26zb5Dt2JgsQ5YsWwJRcWdvFvbgCGIz7wUH0aqYkf/IPAGGaihHdObEGD/o"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Generated from the core SCSS maps by .build/generate-tokens.ts — DO NOT EDIT.
|
// Generated from the core SCSS maps by .build/generate-tokens.ts — DO NOT EDIT.
|
||||||
// Regenerate with: pnpm run generate-tokens
|
// Regenerate with: pnpm run generate:tokens
|
||||||
|
|
||||||
/** Keys of `$theme-colors` (core/scss/_variables.scss). */
|
/** Keys of `$theme-colors` (core/scss/_variables.scss). */
|
||||||
export const themeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark', 'muted', 'blue', 'azure', 'indigo', 'purple', 'pink', 'red', 'orange', 'yellow', 'lime', 'green', 'teal', 'cyan'] as const
|
export const themeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark', 'muted', 'blue', 'azure', 'indigo', 'purple', 'pink', 'red', 'orange', 'yellow', 'lime', 'green', 'teal', 'cyan'] as const
|
||||||
|
|||||||
Reference in new issue
Block a user