mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
feat(alpinejs): allow customizing the Alpine instance (#9751)
* feat(alpinejs): allows customzing the Alpine instance * chore: add e2e tests * fix: rename script * Update index.ts * fix: lockfile
This commit is contained in:
parent
b3f313138b
commit
1153331cbb
14 changed files with 367 additions and 6 deletions
31
.changeset/heavy-beers-tickle.md
Normal file
31
.changeset/heavy-beers-tickle.md
Normal file
|
@ -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';
|
||||
})
|
||||
}
|
||||
```
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
13
packages/integrations/alpinejs/test/basics.test.js
Normal file
13
packages/integrations/alpinejs/test/basics.test.js
Normal file
|
@ -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')
|
||||
});
|
||||
});
|
13
packages/integrations/alpinejs/test/directive.test.js
Normal file
13
packages/integrations/alpinejs/test/directive.test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
6
packages/integrations/alpinejs/test/fixtures/basics/astro.config.mjs
vendored
Normal file
6
packages/integrations/alpinejs/test/fixtures/basics/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import alpine from '@astrojs/alpinejs';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [alpine()],
|
||||
})
|
11
packages/integrations/alpinejs/test/fixtures/basics/package.json
vendored
Normal file
11
packages/integrations/alpinejs/test/fixtures/basics/package.json
vendored
Normal file
|
@ -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:*"
|
||||
}
|
||||
}
|
8
packages/integrations/alpinejs/test/fixtures/basics/src/pages/index.astro
vendored
Normal file
8
packages/integrations/alpinejs/test/fixtures/basics/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="foo" x-data="{ foo: 'bar' }" x-text="foo"></div>
|
||||
</body>
|
||||
</html>
|
8
packages/integrations/alpinejs/test/fixtures/directive/astro.config.mjs
vendored
Normal file
8
packages/integrations/alpinejs/test/fixtures/directive/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import alpine from '@astrojs/alpinejs';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [alpine({
|
||||
entrypoint: "./src/entrypoint.ts"
|
||||
})],
|
||||
})
|
11
packages/integrations/alpinejs/test/fixtures/directive/package.json
vendored
Normal file
11
packages/integrations/alpinejs/test/fixtures/directive/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@test/alpinejs-directive",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/alpinejs": "workspace:*",
|
||||
"@types/alpinejs": "^3.13.5",
|
||||
"alpinejs": "^3.13.3",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
7
packages/integrations/alpinejs/test/fixtures/directive/src/entrypoint.ts
vendored
Normal file
7
packages/integrations/alpinejs/test/fixtures/directive/src/entrypoint.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { Alpine } from 'alpinejs'
|
||||
|
||||
export default (Alpine: Alpine) => {
|
||||
Alpine.directive('foo', el => {
|
||||
el.textContent = 'bar';
|
||||
})
|
||||
}
|
8
packages/integrations/alpinejs/test/fixtures/directive/src/pages/index.astro
vendored
Normal file
8
packages/integrations/alpinejs/test/fixtures/directive/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="foo" x-data x-foo></div>
|
||||
</body>
|
||||
</html>
|
112
packages/integrations/alpinejs/test/test-utils.js
Normal file
112
packages/integrations/alpinejs/test/test-utils.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { expect, test as testBase } from '@playwright/test';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js';
|
||||
|
||||
export const isWindows = process.platform === 'win32';
|
||||
|
||||
// Get all test files in directory, assign unique port for each of them so they don't conflict
|
||||
const testFiles = await fs.readdir(new URL('.', import.meta.url));
|
||||
const testFileToPort = new Map();
|
||||
for (let i = 0; i < testFiles.length; i++) {
|
||||
const file = testFiles[i];
|
||||
if (file.endsWith('.test.js')) {
|
||||
testFileToPort.set(file.slice(0, -8), 4000 + i);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadFixture(inlineConfig) {
|
||||
if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }");
|
||||
|
||||
// resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath
|
||||
// without this, the main `loadFixture` helper will resolve relative to `packages/astro/test`
|
||||
return baseLoadFixture({
|
||||
...inlineConfig,
|
||||
root: fileURLToPath(new URL(inlineConfig.root, import.meta.url)),
|
||||
server: {
|
||||
port: testFileToPort.get(path.basename(inlineConfig.root)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function testFactory(inlineConfig) {
|
||||
let fixture;
|
||||
|
||||
const test = testBase.extend({
|
||||
astro: async ({}, use) => {
|
||||
fixture = fixture || (await loadFixture(inlineConfig));
|
||||
await use(fixture);
|
||||
},
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
fixture.resetAllFiles();
|
||||
});
|
||||
|
||||
return test;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} page
|
||||
* @returns {Promise<{message: string, hint: string, absoluteFileLocation: string, fileLocation: string}>}
|
||||
*/
|
||||
export async function getErrorOverlayContent(page) {
|
||||
const overlay = await page.waitForSelector('vite-error-overlay', {
|
||||
strict: true,
|
||||
timeout: 10 * 1000,
|
||||
});
|
||||
|
||||
expect(overlay).toBeTruthy();
|
||||
|
||||
const message = await overlay.$$eval('#message-content', (m) => m[0].textContent);
|
||||
const hint = await overlay.$$eval('#hint-content', (m) => m[0].textContent);
|
||||
const [absoluteFileLocation, fileLocation] = await overlay.$$eval('#code header h2', (m) => [
|
||||
m[0].title,
|
||||
m[0].textContent,
|
||||
]);
|
||||
return { message, hint, absoluteFileLocation, fileLocation };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for `astro-island` that contains the `el` to hydrate
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('@playwright/test').Locator} el
|
||||
*/
|
||||
export async function waitForHydrate(page, el) {
|
||||
const astroIsland = page.locator('astro-island', { has: el });
|
||||
const astroIslandId = await astroIsland.last().getAttribute('uid');
|
||||
await page.waitForFunction(
|
||||
(selector) => document.querySelector(selector)?.hasAttribute('ssr') === false,
|
||||
`astro-island[uid="${astroIslandId}"]`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to element manually without making sure the `el` is stable
|
||||
* @param {import('@playwright/test').Locator} el
|
||||
*/
|
||||
export async function scrollToElement(el) {
|
||||
await el.evaluate((node) => {
|
||||
node.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareTestFactory(opts) {
|
||||
const test = testFactory(opts);
|
||||
|
||||
let devServer;
|
||||
|
||||
test.beforeAll(async ({ astro }) => {
|
||||
devServer = await astro.startDevServer();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
return {
|
||||
test,
|
||||
};
|
||||
}
|
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
|
@ -3787,12 +3787,48 @@ importers:
|
|||
|
||||
packages/integrations/alpinejs:
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: 1.40.0
|
||||
version: 1.40.0
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../astro
|
||||
astro-scripts:
|
||||
specifier: workspace:*
|
||||
version: link:../../../scripts
|
||||
vite:
|
||||
specifier: ^5.0.10
|
||||
version: 5.0.12(@types/node@18.19.4)(sass@1.69.6)
|
||||
|
||||
packages/integrations/alpinejs/test/fixtures/basics:
|
||||
dependencies:
|
||||
'@astrojs/alpinejs':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
'@types/alpinejs':
|
||||
specifier: ^3.13.5
|
||||
version: 3.13.5
|
||||
alpinejs:
|
||||
specifier: ^3.13.3
|
||||
version: 3.13.3
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/alpinejs/test/fixtures/directive:
|
||||
dependencies:
|
||||
'@astrojs/alpinejs':
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
'@types/alpinejs':
|
||||
specifier: ^3.13.5
|
||||
version: 3.13.5
|
||||
alpinejs:
|
||||
specifier: ^3.13.3
|
||||
version: 3.13.3
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../../../../astro
|
||||
|
||||
packages/integrations/cloudflare: {}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue