diff --git a/.changeset/heavy-beers-tickle.md b/.changeset/heavy-beers-tickle.md new file mode 100644 index 000000000000..f6b19aa74b76 --- /dev/null +++ b/.changeset/heavy-beers-tickle.md @@ -0,0 +1,31 @@ +--- +"@astrojs/alpinejs": minor +--- + +Allows extending Alpine using the new `entrypoint` configuration + +You can extend Alpine by setting the `entrypoint` option to a root-relative import specifier (for example, `entrypoint: "/src/entrypoint"`). + +The default export of this file should be a function that accepts an Alpine instance prior to starting, allowing the use of custom directives, plugins and other customizations for advanced use cases. + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import alpine from '@astrojs/alpinejs'; + +export default defineConfig({ + // ... + integrations: [alpine({ entrypoint: '/src/entrypoint' })], +}); +``` + +```js +// src/entrypoint.ts +import type { Alpine } from 'alpinejs' + +export default (Alpine: Alpine) => { + Alpine.directive('foo', el => { + el.textContent = 'bar'; + }) +} +``` \ No newline at end of file diff --git a/packages/integrations/alpinejs/package.json b/packages/integrations/alpinejs/package.json index c6abdd6bd5ab..e81e42f0bcba 100644 --- a/packages/integrations/alpinejs/package.json +++ b/packages/integrations/alpinejs/package.json @@ -30,17 +30,20 @@ "scripts": { "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "dev": "astro-scripts dev \"src/**/*.ts\"" + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test:e2e": "playwright test" }, "peerDependencies": { "@types/alpinejs": "^3.0.0", "alpinejs": "^3.0.0" }, "devDependencies": { + "@playwright/test": "1.40.0", "astro": "workspace:*", - "astro-scripts": "workspace:*" + "astro-scripts": "workspace:*", + "vite": "^5.0.10" }, "publishConfig": { "provenance": true } -} +} \ No newline at end of file diff --git a/packages/integrations/alpinejs/src/index.ts b/packages/integrations/alpinejs/src/index.ts index 4a296b8c1f50..a5f20aa67d80 100644 --- a/packages/integrations/alpinejs/src/index.ts +++ b/packages/integrations/alpinejs/src/index.ts @@ -1,16 +1,110 @@ import type { AstroIntegration } from 'astro'; +import type { Plugin } from 'vite'; +import { resolve } from 'node:path'; -export default function createPlugin(): AstroIntegration { +interface Options { + /** + * You can extend Alpine by setting this option to a root-relative import specifier (for example, `entrypoint: "/src/entrypoint"`). + * + * The default export of this file should be a function that accepts an Alpine instance prior to starting, allowing the use of custom directives, plugins and other customizations for advanced use cases. + * + * ```js + * // astro.config.mjs + * import { defineConfig } from 'astro/config'; + * import alpine from '@astrojs/alpinejs'; + * + * export default defineConfig({ + * // ... + * integrations: [alpine({ entrypoint: '/src/entrypoint' })], + * }); + * ``` + * + * ```js + * // src/entrypoint.ts + * import type { Alpine } from 'alpinejs' + * + * export default (Alpine: Alpine) => { + * Alpine.directive('foo', el => { + * el.textContent = 'bar'; + * }) + * } + * ``` + */ + entrypoint?: string; +} + +function virtualEntrypoint(options?: Options): Plugin { + const virtualModuleId = 'virtual:@astrojs/alpinejs/entrypoint'; + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + let isBuild: boolean; + let root: string; + let entrypoint: string | undefined; + + return { + name: '@astrojs/alpinejs/virtual-entrypoint', + config(_, { command }) { + isBuild = command === 'build'; + }, + configResolved(config) { + root = config.root; + if (options?.entrypoint) { + entrypoint = options.entrypoint.startsWith('.') + ? resolve(root, options.entrypoint) + : options.entrypoint; + } + }, + resolveId(id) { + if (id === virtualModuleId) { + return resolvedVirtualModuleId; + } + }, + load(id) { + if (id === resolvedVirtualModuleId) { + if (entrypoint) { + return `\ +import * as mod from ${JSON.stringify(entrypoint)}; + +export const setup = (Alpine) => { + if ('default' in mod) { + mod.default(Alpine); + } else { + ${ + !isBuild + ? `console.warn("[@astrojs/alpinejs] entrypoint \`" + ${JSON.stringify( + entrypoint + )} + "\` does not export a default function. Check out https://docs.astro.build/en/guides/integrations-guide/alpinejs/#entrypoint.");` + : '' + } + } +}`; + } + return `export const setup = () => {};`; + } + }, + }; +} + +export default function createPlugin(options?: Options): AstroIntegration { return { name: '@astrojs/alpinejs', hooks: { - 'astro:config:setup': ({ injectScript }) => { + 'astro:config:setup': ({ injectScript, updateConfig }) => { // This gets injected into the user's page, so the import will pull // from the project's version of Alpine.js in their package.json. injectScript( 'page', - `import Alpine from 'alpinejs'; window.Alpine = Alpine; Alpine.start();` + `import Alpine from 'alpinejs'; +import { setup } from 'virtual:@astrojs/alpinejs/entrypoint'; +setup(Alpine); +window.Alpine = Alpine; +Alpine.start();` ); + updateConfig({ + vite: { + plugins: [virtualEntrypoint(options)], + }, + }); }, }, }; diff --git a/packages/integrations/alpinejs/test/basics.test.js b/packages/integrations/alpinejs/test/basics.test.js new file mode 100644 index 000000000000..ae6a06a2353a --- /dev/null +++ b/packages/integrations/alpinejs/test/basics.test.js @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; +import { prepareTestFactory } from './test-utils.js'; + +const { test } = prepareTestFactory({ root: './fixtures/basics/' }); + +test.describe('Basics', () => { + test('Alpine is working', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const el = page.locator("#foo") + expect(await el.textContent()).toBe('bar') + }); +}); diff --git a/packages/integrations/alpinejs/test/directive.test.js b/packages/integrations/alpinejs/test/directive.test.js new file mode 100644 index 000000000000..440e2a6b5bc2 --- /dev/null +++ b/packages/integrations/alpinejs/test/directive.test.js @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; +import { prepareTestFactory } from './test-utils.js'; + +const { test } = prepareTestFactory({ root: './fixtures/basics/' }); + +test.describe('Basics', () => { + test('Alpine is working', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const el = page.locator('#foo'); + expect(await el.textContent()).toBe('bar'); + }); +}); diff --git a/packages/integrations/alpinejs/test/fixtures/basics/astro.config.mjs b/packages/integrations/alpinejs/test/fixtures/basics/astro.config.mjs new file mode 100644 index 000000000000..9fcc100d959e --- /dev/null +++ b/packages/integrations/alpinejs/test/fixtures/basics/astro.config.mjs @@ -0,0 +1,6 @@ +import { defineConfig } from 'astro/config'; +import alpine from '@astrojs/alpinejs'; + +export default defineConfig({ + integrations: [alpine()], +}) diff --git a/packages/integrations/alpinejs/test/fixtures/basics/package.json b/packages/integrations/alpinejs/test/fixtures/basics/package.json new file mode 100644 index 000000000000..1e210ae67341 --- /dev/null +++ b/packages/integrations/alpinejs/test/fixtures/basics/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/alpinejs-basics", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/alpinejs": "workspace:*", + "@types/alpinejs": "^3.13.5", + "alpinejs": "^3.13.3", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/alpinejs/test/fixtures/basics/src/pages/index.astro b/packages/integrations/alpinejs/test/fixtures/basics/src/pages/index.astro new file mode 100644 index 000000000000..6197e0c64224 --- /dev/null +++ b/packages/integrations/alpinejs/test/fixtures/basics/src/pages/index.astro @@ -0,0 +1,8 @@ + +
+