@quasar/vite-plugin
Version:
Vite plugin for Quasar Framework
249 lines (209 loc) • 6.97 kB
JavaScript
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { quasarPath } from './quasar-path.js'
const declRegex = /^\$([\w-]+)\s*:\s*(.*)$/
const useSassBuiltinRegex = /^@use\s+(['"])sass:([\w-]+)\1\s*;?$/
const varRefRegex = /\$([\w-]+)/g
const blockCommentRegex = /\/\*[\s\S]*?\*\//g
function stripLineComment(line) {
let quote = null
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (quote !== null) {
if (char === '\\') i++
else if (char === quote) quote = null
} else if (char === '"' || char === "'") {
quote = char
} else if (char === '/' && line[i + 1] === '/') {
return line.slice(0, i)
}
}
return line
}
/**
* Bracket balance of a declaration value, ignoring quoted strings.
* Returns NaN for an unterminated string, which makes every
* comparison fail and so bails out the parser.
*/
function bracketBalance(str) {
let balance = 0
let quote = null
for (let i = 0; i < str.length; i++) {
const char = str[i]
if (quote !== null) {
if (char === '\\') i++
else if (char === quote) quote = null
} else if (char === '"' || char === "'") {
quote = char
} else if (char === '(' || char === '[') {
balance++
} else if (char === ')' || char === ']') {
balance--
}
}
return quote !== null ? Number.NaN : balance
}
/**
* Parses a variables file into "@use sass:*" namespaces and a flat list
* of variable declarations. Returns null when anything else is
* encountered, so that the caller can fall back to full injection.
* Handles both syntaxes: indented (single-line declarations) and scss
* (multi-line values are folded into one line, which stays valid in
* both syntaxes since values are whitespace-insensitive).
*
* Also used by the Quasar UI build to precompute
* dist/transforms/sass-variables.json.
*/
export function parseVariablesFile(text) {
const uses = []
const decls = []
const lines = text.replace(blockCommentRegex, ' ').split('\n')
for (let i = 0; i < lines.length; i++) {
const line = stripLineComment(lines[i]).trim()
if (line === '') continue
const useMatch = useSassBuiltinRegex.exec(line)
if (useMatch !== null) {
uses.push(useMatch[2])
continue
}
const declMatch = declRegex.exec(line)
if (declMatch === null) return null
let value = declMatch[2]
while (bracketBalance(value) > 0) {
if (++i >= lines.length) return null
value += ' ' + stripLineComment(lines[i]).trim()
}
if (bracketBalance(value) !== 0) return null
value = value.trim()
if (value.endsWith(';')) {
value = value.slice(0, -1).trim()
}
if (value === '') return null
decls.push({ name: declMatch[1], value })
}
return { uses, decls }
}
let precomputedQuasarVariables
/**
* The Quasar UI build precomputes the parse of its own variables file
* into dist/transforms/sass-variables.json. Returns null when the file
* is absent (older Quasar version) or does not match the expected
* format, in which case the variables file gets parsed at runtime.
*/
export function loadPrecomputedQuasarVariables() {
if (precomputedQuasarVariables !== void 0) {
return precomputedQuasarVariables
}
precomputedQuasarVariables = null
try {
const data = JSON.parse(
readFileSync(
join(quasarPath, 'dist/transforms/sass-variables.json'),
'utf8'
)
)
if (
Array.isArray(data.uses) &&
data.uses.every(ns => typeof ns === 'string') &&
Array.isArray(data.declarations) &&
data.declarations.every(
decl => typeof decl.name === 'string' && typeof decl.value === 'string'
)
) {
precomputedQuasarVariables = {
uses: data.uses,
decls: data.declarations
}
}
} catch {
// keep the runtime parsing fallback
}
return precomputedQuasarVariables
}
/**
* Builds a dependency graph of all variable declarations from the
* injected files (custom variables file first, then Quasar's, mirroring
* the injection order). Quasar's own declarations come exclusively from
* the precomputed parse shipped in its dist (guaranteed by the peer
* dependency floor). Returns null when unavailable or when the custom
* file cannot be confidently parsed as declarations-only, in which
* case full injection is used.
*/
export function buildVariablesGraph(sassVariables) {
const quasarParsed = loadPrecomputedQuasarVariables()
if (quasarParsed === null) return null
const namespaces = new Set()
const entries = []
if (typeof sassVariables === 'string') {
let parsed
try {
parsed = parseVariablesFile(readFileSync(sassVariables, 'utf8'))
} catch {
return null
}
if (parsed === null) return null
for (const ns of parsed.uses) namespaces.add(ns)
entries.push(...parsed.decls)
}
for (const ns of quasarParsed.uses) namespaces.add(ns)
// copies, since the entries get augmented below and the
// precomputed declarations are a shared, cached structure
entries.push(
...quasarParsed.decls.map(decl => ({
name: decl.name,
value: decl.value
}))
)
const byName = new Map()
const nsRefRegex =
namespaces.size !== 0
? new RegExp(`\\b(?:${[...namespaces].join('|')})\\.`)
: null
entries.forEach((entry, index) => {
entry.deps = [...entry.value.matchAll(varRefRegex)].map(m => m[1])
entry.usesNamespace = nsRefRegex !== null && nsRefRegex.test(entry.value)
entry.text = `$${entry.name}: ${entry.value}`
const indexList = byName.get(entry.name)
if (indexList !== void 0) indexList.push(index)
else byName.set(entry.name, [index])
})
return { entries, byName, namespaces: [...namespaces] }
}
/**
* Resolves the transitive closure of the given variable names.
* Unknown names are ignored (they are either defined by the style
* content itself or produce the same sass error full injection would).
* The returned declarations keep their original file order, which is
* exactly the order full injection would evaluate them in.
*/
export function resolveVariablesClosure(graph, refNames) {
const included = new Set()
const visited = new Set()
const pending = [...refNames]
let usesNamespace = false
while (pending.length !== 0) {
const name = pending.pop()
if (visited.has(name)) continue
visited.add(name)
const indexList = graph.byName.get(name)
if (indexList === void 0) continue
for (const index of indexList) {
included.add(index)
const entry = graph.entries[index]
if (entry.usesNamespace === true) {
usesNamespace = true
}
for (const dep of entry.deps) {
if (visited.has(dep) === false) {
pending.push(dep)
}
}
}
}
return {
declarations: [...included]
.sort((a, b) => a - b)
.map(i => graph.entries[i].text),
usesNamespace
}
}