Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async function parseCss(
let customVariants = new Map<string, (designSystem: DesignSystem) => void>()
let customVariantDependencies = new Map<string, Set<string>>()
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let importedUtilityRootsByPrefix = new Map<string, Set<string>>()
let firstThemeRule = null as StyleRule | null
let utilitiesNode = null as AtRule | null
let variantNodes: AtRule[] = []
Expand Down Expand Up @@ -233,6 +234,19 @@ async function parseCss(
}

let utility = createCssUtility(node)
let importPrefix = ctx.context.importPrefix as string | undefined
if (importPrefix) {
let prefixedParams = `${importPrefix}-${node.params}`
utility = createCssUtility({ ...node, params: prefixedParams })

if (utility !== null) {
let roots = importedUtilityRootsByPrefix.get(importPrefix)
if (!roots) importedUtilityRootsByPrefix.set(importPrefix, (roots = new Set()))

let root = unescape(prefixedParams)
roots.add(root.endsWith('-*') ? root.slice(0, -2) : root)
}
}
if (utility === null) {
if (!node.params.endsWith('-*')) {
if (node.params.endsWith('*')) {
Expand Down Expand Up @@ -505,13 +519,36 @@ async function parseCss(
else if (param.startsWith('prefix(')) {
let prefix = param.slice(7, -1)

if (!IS_VALID_PREFIX.test(prefix)) {
throw new Error(
`The prefix "${prefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
)
}

let containsTailwindUtilities = false
let themeNodes: AtRule[] = []

walk(node.nodes, (child) => {
if (child.kind !== 'at-rule') return
if (child.name === '@tailwind' && child.params.startsWith('utilities')) {
containsTailwindUtilities = true
}

if (child.name === '@theme') {
child.params += ` prefix(${prefix})`
themeNodes.push(child)
return WalkAction.Skip
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})

if (containsTailwindUtilities) {
theme.prefix = prefix

for (let themeNode of themeNodes) {
themeNode.params += ` prefix(${prefix})`
}
}

node.nodes = [contextNode({ importPrefix: prefix }, node.nodes)]
}

// Handle important
Expand Down Expand Up @@ -598,6 +635,49 @@ async function parseCss(

let designSystem = buildDesignSystem(theme, utilitiesNode?.src)

if (importedUtilityRootsByPrefix.size > 0) {
let parseCandidate = designSystem.parseCandidate
let importedUtilityRoots = new Set<string>()
for (let roots of importedUtilityRootsByPrefix.values()) {
for (let root of roots) importedUtilityRoots.add(root)
}

function parseCandidateWithoutImportedRoots(candidate: string) {
return parseCandidate(candidate).filter((parsedCandidate) => {
return (
parsedCandidate.kind === 'arbitrary' || !importedUtilityRoots.has(parsedCandidate.root)
)
})
}

designSystem.parseCandidate = (candidate) => {
let [prefix, ...parts] = segment(candidate, ':')
let roots = importedUtilityRootsByPrefix.get(prefix)
if (!roots) return parseCandidateWithoutImportedRoots(candidate)

let base = parts.pop()
if (!base) return []

// Imported utilities use `prefix:name` publicly, but are registered as
// `prefix-name` internally because `@utility` names cannot contain `:`.
let internalCandidate = [...parts, `${prefix}-${base}`].join(':')
if (designSystem.theme.prefix) {
internalCandidate = `${designSystem.theme.prefix}:${internalCandidate}`
}

let results = parseCandidate(internalCandidate)
.filter((parsedCandidate) => {
return parsedCandidate.kind !== 'arbitrary' && roots.has(parsedCandidate.root)
})
.map((parsedCandidate) => ({
...parsedCandidate,
raw: candidate,
}))

return results.length > 0 ? results : parseCandidateWithoutImportedRoots(candidate)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

if (important) {
designSystem.important = important
}
Expand Down
247 changes: 247 additions & 0 deletions packages/tailwindcss/src/prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,253 @@ test('a prefix can be configured via @import prefix(…)', async () => {
expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('')
})

test('custom utilities from imports use the Tailwind import prefix', async () => {
let input = css`
@import 'tailwindcss' prefix(tw);
@import './components.css';
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@tailwind utilities;
`
: css`
@utility card {
color: red;
}
`,
}
}

expect(await run(['underline', 'card'], input, { loadStylesheet })).toEqual('')

expect(await run(['tw:underline', 'tw:card'], input, { loadStylesheet })).toMatchInlineSnapshot(`
"
.tw\\:card {
color: red;
}

.tw\\:underline {
text-decoration-line: underline;
}
"
`)
})

test('custom utilities from prefixed imports use their import prefix', async () => {
let input = css`
@import 'tailwindcss' prefix(tw);
@import './components.css' prefix(ui);

@utility ui-underline {
color: red;
}
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@tailwind utilities;
`
: css`
@utility card {
color: red;
}
`,
}
}

expect(
await run(['underline', 'card', 'tw:card', 'ui:underline', 'tw:ui-card'], input, {
loadStylesheet,
}),
).toEqual('')

expect(await run(['tw:underline', 'ui:card', 'tw:ui-underline'], input, { loadStylesheet }))
.toMatchInlineSnapshot(`
"
.tw\\:ui-underline, .ui\\:card {
color: red;
}

.tw\\:underline {
text-decoration-line: underline;
}
"
`)
})

test('a prefixed custom import does not prefix Tailwind utilities', async () => {
let input = css`
@import 'tailwindcss';
@import './components.css' prefix(ui);
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@tailwind utilities;
`
: css`
@utility card {
color: red;
}
`,
}
}

expect(await run(['card', 'ui-card', 'ui:underline'], input, { loadStylesheet })).toEqual('')

expect(await run(['underline', 'ui:card'], input, { loadStylesheet })).toMatchInlineSnapshot(`
"
.ui\\:card {
color: red;
}

.underline {
text-decoration-line: underline;
}
"
`)
})
Comment thread
greptile-apps[bot] marked this conversation as resolved.

test('custom utilities from prefixed imports support variants', async () => {
let input = css`
@import 'tailwindcss';
@import './components.css' prefix(ui);
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@tailwind utilities;
`
: css`
@utility card {
color: red;
}
`,
}
}

expect(await run(['hover:ui:card'], input, { loadStylesheet })).toEqual('')

expect(await run(['ui:hover:card'], input, { loadStylesheet })).toMatchInlineSnapshot(`
"
@media (hover: hover) {
.ui\\:hover\\:card:hover {
color: red;
}
}
"
`)
})

test('a prefixed custom import with a theme does not prefix Tailwind utilities', async () => {
let input = css`
@import 'tailwindcss';
@import './components.css' prefix(ui);
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@tailwind utilities;
`
: css`
@theme {
--color-red: red;
}

@utility card {
color: var(--color-red);
}
`,
}
}

expect(await run(['ui:underline'], input, { loadStylesheet })).toEqual('')

expect(await run(['underline', 'ui:card'], input, { loadStylesheet })).toMatchInlineSnapshot(`
"
.ui\\:card {
color: var(--color-red);
}

.underline {
text-decoration-line: underline;
}

:root, :host {
--color-red: red;
}
"
`)
})

test('custom import prefixes do not shadow variants with the same name', async () => {
let input = css`
@import 'tailwindcss';
@import './components.css' prefix(sm);
`

async function loadStylesheet(id: string, base: string) {
return {
path: '',
base,
content:
id === 'tailwindcss'
? css`
@theme {
--breakpoint-sm: 40rem;
}

@tailwind utilities;
`
: css`
@utility card {
color: red;
}
`,
}
}

expect(await run(['sm:card', 'sm:flex'], input, { loadStylesheet })).toMatchInlineSnapshot(`
"
.sm\\:card {
color: red;
}

@media (min-width: 40rem) {
.sm\\:flex {
display: flex;
}
}
"
`)
})

test('a prefix must be letters only', async () => {
let input = css`
@theme reference prefix(__);
Expand Down