Skip to content
Draft
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
78 changes: 69 additions & 9 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ export async function applyCompatibilityHooks({
}) {
let features = Features.None
let pluginPaths: [
{ id: string; base: string; reference: boolean; src: SourceLocation | undefined },
{
id: string
exportName: string
base: string
reference: boolean
src: SourceLocation | undefined
},
CssPluginOptions | null,
][] = []
let configPaths: {
id: string
exportName: string
base: string
reference: boolean
src: SourceLocation | undefined
Expand All @@ -59,8 +66,19 @@ export async function applyCompatibilityHooks({
throw new Error('`@plugin` cannot be nested.')
}

let pluginPath = node.params.slice(1, -1)
if (pluginPath.length === 0) {
let parts = segment(node.params, ' ')
if (parts.length === 0) {
throw new Error('`@plugin` must have a path.')
}

if (parts.length > 2) {
throw new Error('`@plugin` must have a path and an optional export name.')
}

let modulePath = parts[0].slice(1, -1)
let exportName = parts[1] ?? 'default'

if (modulePath.length === 0) {
throw new Error('`@plugin` must have a path.')
}

Expand Down Expand Up @@ -109,10 +127,11 @@ export async function applyCompatibilityHooks({

pluginPaths.push([
{
id: pluginPath,
id: modulePath,
base: context.base as string,
reference: !!context.reference,
src: node.src,
exportName,
},
Object.keys(options).length > 0 ? options : null,
])
Expand All @@ -132,8 +151,25 @@ export async function applyCompatibilityHooks({
throw new Error('`@config` cannot be nested.')
}

let parts = segment(node.params, ' ')
if (parts.length === 0) {
throw new Error('`@config` must have a path.')
}

if (parts.length > 2) {
throw new Error('`@config` must have a path and an optional export name.')
}

let modulePath = parts[0].slice(1, -1)
let exportName = parts[1] ?? 'default'

if (modulePath.length === 0) {
throw new Error('`@config` must have a path.')
}

configPaths.push({
id: node.params.slice(1, -1),
id: modulePath,
exportName,
base: context.base as string,
reference: !!context.reference,
src: node.src,
Expand Down Expand Up @@ -176,24 +212,48 @@ export async function applyCompatibilityHooks({

let [configs, pluginDetails] = await Promise.all([
Promise.all(
configPaths.map(async ({ id, base, reference, src }) => {
configPaths.map(async ({ id, base, exportName, reference, src }) => {
let loaded = await loadModule(id, base, 'config')

// TODO: This should be implemented by loadModule
let module = loaded.module

if (exportName !== 'default') {
module = loaded.module[exportName]

if (!module) {
throw new Error(`Config \`${id}\` does not have an export named \`${exportName}\``)
}
}

return {
path: id,
base: loaded.base,
config: loaded.module as UserConfig,
config: module as UserConfig,
reference,
src,
}
}),
),
Promise.all(
pluginPaths.map(async ([{ id, base, reference, src }, pluginOptions]) => {
pluginPaths.map(async ([{ id, base, exportName, reference, src }, pluginOptions]) => {
let loaded = await loadModule(id, base, 'plugin')

// TODO: This should be implemented by loadModule
let module = loaded.module

if (exportName !== 'default') {
module = loaded.module[exportName]

if (!module) {
throw new Error(`Plugin \`${id}\` does not have an export named \`${exportName}\``)
}
}

return {
path: id,
base: loaded.base,
plugin: loaded.module as Plugin,
plugin: module as Plugin,
options: pluginOptions,
reference,
src,
Expand Down
62 changes: 62 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3094,6 +3094,68 @@ describe('plugins', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`)
})

test('@plugin can specify an export name', async () => {
let { build } = await compile(
css`
@tailwind utilities;
@plugin "my-plugin" myPlugin;
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: {
// TODO: The export name should be passed in instead
myPlugin: plugin(({ addUtilities }) => {
addUtilities({
'.text-primary': {
color: 'red',
},
})
}),
},
}),
},
)

let compiled = build(['text-primary'])

expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
".text-primary {
color: red;
}"
`)
})

test('@plugin errors when an export does not exist', async () => {
return expect(
compile(
css`
@tailwind utilities;
@plugin "my-plugin" what;
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: {
// TODO: The export name should be passed in instead
myPlugin: plugin(({ addUtilities }) => {
addUtilities({
'.text-primary': {
color: 'red',
},
})
}),
},
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Plugin \`my-plugin\` does not have an export named \`what\`]`,
)
})

test('@plugin can accept options', async () => {
expect.hasAssertions()

Expand Down
4 changes: 3 additions & 1 deletion packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ type CompileOptions = {
) => Promise<{
path: string
base: string
module: Plugin | Config
// TODO: This is *wrong* the export name should be passed in and this should
// always be `Plugin | Config`
module: Plugin | Config | Record<string, Plugin | Config>
}>
loadStylesheet?: (
id: string,
Expand Down