diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 78c7d707877e..f105227bc405 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -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 @@ -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.') } @@ -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, ]) @@ -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, @@ -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, diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index a19fcf96d979..606b92948069 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -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() diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 07a23a3251d2..1f314020e8fa 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -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 }> loadStylesheet?: ( id: string,