summaryrefslogtreecommitdiff
path: root/packages/integrations
diff options
context:
space:
mode:
authorGravatar Fred K. Schott <fkschott@gmail.com> 2022-03-18 15:35:45 -0700
committerGravatar GitHub <noreply@github.com> 2022-03-18 15:35:45 -0700
commit6386c14d00d1d820804f0ee5b1424e73c049fe83 (patch)
tree3015e834e1d84100fd0871f6a55479bed61c0c14 /packages/integrations
parent0f376a7c52d3a22ff32b33e0afc34dd306ed70c4 (diff)
downloadastro-6386c14d00d1d820804f0ee5b1424e73c049fe83.tar.gz
astro-6386c14d00d1d820804f0ee5b1424e73c049fe83.tar.zst
astro-6386c14d00d1d820804f0ee5b1424e73c049fe83.zip
Astro Integration System (#2820)
* update examples * add initial integrations * update tests * update astro * update ci * get final tests working * update injectelement todo * update ben code review * respond to final code review feedback
Diffstat (limited to 'packages/integrations')
-rw-r--r--packages/integrations/lit/.gitignore1
-rw-r--r--packages/integrations/lit/client-shim.js10
-rw-r--r--packages/integrations/lit/client-shim.min.js79
-rw-r--r--packages/integrations/lit/hydration-support.js1
-rw-r--r--packages/integrations/lit/package.json38
-rw-r--r--packages/integrations/lit/server-shim.js5
-rw-r--r--packages/integrations/lit/server.js72
-rw-r--r--packages/integrations/lit/src/index.ts42
-rw-r--r--packages/integrations/lit/tsconfig.json10
-rw-r--r--packages/integrations/partytown/package.json32
-rw-r--r--packages/integrations/partytown/src/index.ts33
-rw-r--r--packages/integrations/partytown/src/sirv.ts241
-rw-r--r--packages/integrations/partytown/tsconfig.json10
-rw-r--r--packages/integrations/preact/client.js4
-rw-r--r--packages/integrations/preact/package.json43
-rw-r--r--packages/integrations/preact/server.js35
-rw-r--r--packages/integrations/preact/src/index.ts45
-rw-r--r--packages/integrations/preact/static-html.js24
-rw-r--r--packages/integrations/preact/tsconfig.json10
-rw-r--r--packages/integrations/react/client.js13
-rw-r--r--packages/integrations/react/jsx-runtime.js8
-rw-r--r--packages/integrations/react/package.json43
-rw-r--r--packages/integrations/react/server.js67
-rw-r--r--packages/integrations/react/src/index.ts54
-rw-r--r--packages/integrations/react/static-html.js24
-rw-r--r--packages/integrations/react/tsconfig.json10
-rw-r--r--packages/integrations/sitemap/package.json31
-rw-r--r--packages/integrations/sitemap/src/index.ts38
-rw-r--r--packages/integrations/sitemap/tsconfig.json10
-rw-r--r--packages/integrations/solid/client.js14
-rw-r--r--packages/integrations/solid/package.json41
-rw-r--r--packages/integrations/solid/server.js28
-rw-r--r--packages/integrations/solid/src/index.ts57
-rw-r--r--packages/integrations/solid/static-html.js12
-rw-r--r--packages/integrations/solid/tsconfig.json10
-rw-r--r--packages/integrations/svelte/Wrapper.svelte21
-rw-r--r--packages/integrations/svelte/Wrapper.svelte.ssr.js14
-rw-r--r--packages/integrations/svelte/client.js14
-rw-r--r--packages/integrations/svelte/package.json43
-rw-r--r--packages/integrations/svelte/server.js15
-rw-r--r--packages/integrations/svelte/src/index.ts48
-rw-r--r--packages/integrations/svelte/tsconfig.json10
-rw-r--r--packages/integrations/tailwind/base.css3
-rw-r--r--packages/integrations/tailwind/package.json42
-rw-r--r--packages/integrations/tailwind/src/index.ts31
-rw-r--r--packages/integrations/tailwind/tsconfig.json10
-rw-r--r--packages/integrations/turbolinks/client.js2
-rw-r--r--packages/integrations/turbolinks/package.json32
-rw-r--r--packages/integrations/turbolinks/src/index.ts15
-rw-r--r--packages/integrations/turbolinks/tsconfig.json10
-rw-r--r--packages/integrations/vue/client.js14
-rw-r--r--packages/integrations/vue/package.json41
-rw-r--r--packages/integrations/vue/server.js22
-rw-r--r--packages/integrations/vue/src/index.ts35
-rw-r--r--packages/integrations/vue/static-html.js27
-rw-r--r--packages/integrations/vue/tsconfig.json10
56 files changed, 1654 insertions, 0 deletions
diff --git a/packages/integrations/lit/.gitignore b/packages/integrations/lit/.gitignore
new file mode 100644
index 000000000..40b878db5
--- /dev/null
+++ b/packages/integrations/lit/.gitignore
@@ -0,0 +1 @@
+node_modules/ \ No newline at end of file
diff --git a/packages/integrations/lit/client-shim.js b/packages/integrations/lit/client-shim.js
new file mode 100644
index 000000000..cab3fe4d9
--- /dev/null
+++ b/packages/integrations/lit/client-shim.js
@@ -0,0 +1,10 @@
+async function polyfill() {
+ const { hydrateShadowRoots } = await import('@webcomponents/template-shadowroot/template-shadowroot.js');
+ hydrateShadowRoots(document.body);
+}
+
+const polyfillCheckEl = new DOMParser().parseFromString(`<p><template shadowroot="open"></template></p>`, 'text/html', { includeShadowRoots: true }).querySelector('p');
+
+if (!polyfillCheckEl || !polyfillCheckEl.shadowRoot) {
+ polyfill();
+}
diff --git a/packages/integrations/lit/client-shim.min.js b/packages/integrations/lit/client-shim.min.js
new file mode 100644
index 000000000..0c6a452d8
--- /dev/null
+++ b/packages/integrations/lit/client-shim.min.js
@@ -0,0 +1,79 @@
+/** @license Copyright 2020 Google LLC (BSD-3-Clause) */
+/** Bundled JS generated from "@astrojs/lit/client-shim.js" */
+var N = Object.defineProperty;
+var i = (t, n) => () => (t && (n = t((t = 0))), n);
+var b = (t, n) => {
+ for (var a in n) N(t, a, { get: n[a], enumerable: !0 });
+};
+function s() {
+ if (d === void 0) {
+ let t = document.createElement('div');
+ (t.innerHTML = '<div><template shadowroot="open"></template></div>'), (d = !!t.firstElementChild.shadowRoot);
+ }
+ return d;
+}
+var d,
+ m = i(() => {});
+var p,
+ c,
+ f,
+ u = i(() => {
+ (p = (t) => t.parentElement === null), (c = (t) => t.tagName === 'TEMPLATE'), (f = (t) => t.nodeType === Node.ELEMENT_NODE);
+ });
+var h,
+ E = i(() => {
+ m();
+ u();
+ h = (t) => {
+ var n;
+ if (s()) return;
+ let a = [],
+ e = t.firstElementChild;
+ for (; e !== t && e !== null; )
+ if (c(e)) a.push(e), (e = e.content);
+ else if (e.firstElementChild !== null) e = e.firstElementChild;
+ else if (f(e) && e.nextElementSibling !== null) e = e.nextElementSibling;
+ else {
+ let o;
+ for (; e !== t && e !== null; )
+ if (p(e)) {
+ o = a.pop();
+ let r = o.parentElement,
+ l = o.getAttribute('shadowroot');
+ if (((e = o), l === 'open' || l === 'closed')) {
+ let y = o.hasAttribute('shadowrootdelegatesfocus');
+ try {
+ r.attachShadow({ mode: l, delegatesFocus: y }).append(o.content);
+ } catch {}
+ } else o = void 0;
+ } else {
+ let r = e.nextElementSibling;
+ if (r != null) {
+ (e = r), o !== void 0 && o.parentElement.removeChild(o);
+ break;
+ }
+ let l = (n = e.parentElement) === null || n === void 0 ? void 0 : n.nextElementSibling;
+ if (l != null) {
+ (e = l), o !== void 0 && o.parentElement.removeChild(o);
+ break;
+ }
+ (e = e.parentElement), o !== void 0 && (o.parentElement.removeChild(o), (o = void 0));
+ }
+ }
+ };
+ });
+var w = i(() => {
+ E();
+});
+var v = {};
+b(v, { hasNativeDeclarativeShadowRoots: () => s, hydrateShadowRoots: () => h });
+var S = i(() => {
+ m();
+ w();
+});
+async function g() {
+ let { hydrateShadowRoots: t } = await Promise.resolve().then(() => (S(), v));
+ t(document.body);
+}
+var x = new DOMParser().parseFromString('<p><template shadowroot="open"></template></p>', 'text/html', { includeShadowRoots: !0 }).querySelector('p');
+(!x || !x.shadowRoot) && g();
diff --git a/packages/integrations/lit/hydration-support.js b/packages/integrations/lit/hydration-support.js
new file mode 100644
index 000000000..0c21646fb
--- /dev/null
+++ b/packages/integrations/lit/hydration-support.js
@@ -0,0 +1 @@
+import 'lit/experimental-hydrate-support.js';
diff --git a/packages/integrations/lit/package.json b/packages/integrations/lit/package.json
new file mode 100644
index 000000000..2e76308f1
--- /dev/null
+++ b/packages/integrations/lit/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@astrojs/lit",
+ "version": "0.0.1",
+ "description": "Use Lit components within Astro",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/lit"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./server.js": "./server.js",
+ "./client-shim.js": "./client-shim.js",
+ "./hydration-support.js": "./hydration-support.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@lit-labs/ssr": "^2.0.2"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*"
+ },
+ "peerDependencies": {
+ "@webcomponents/template-shadowroot": "^0.1.0",
+ "lit": "^2.1.3"
+ }
+}
diff --git a/packages/integrations/lit/server-shim.js b/packages/integrations/lit/server-shim.js
new file mode 100644
index 000000000..054679592
--- /dev/null
+++ b/packages/integrations/lit/server-shim.js
@@ -0,0 +1,5 @@
+import { installWindowOnGlobal } from '@lit-labs/ssr/lib/dom-shim.js';
+installWindowOnGlobal();
+
+window.global = window;
+document.getElementsByTagName = () => [];
diff --git a/packages/integrations/lit/server.js b/packages/integrations/lit/server.js
new file mode 100644
index 000000000..1622ef619
--- /dev/null
+++ b/packages/integrations/lit/server.js
@@ -0,0 +1,72 @@
+import './server-shim.js';
+import '@lit-labs/ssr/lib/render-lit-html.js';
+import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
+
+function isCustomElementTag(name) {
+ return typeof name === 'string' && /-/.test(name);
+}
+
+function getCustomElementConstructor(name) {
+ if (typeof customElements !== 'undefined' && isCustomElementTag(name)) {
+ return customElements.get(name) || null;
+ }
+ return null;
+}
+
+async function isLitElement(Component) {
+ const Ctr = getCustomElementConstructor(Component);
+ return !!(Ctr && Ctr._$litElement$);
+}
+
+async function check(Component, _props, _children) {
+ // Lit doesn't support getting a tagName from a Constructor at this time.
+ // So this must be a string at the moment.
+ return !!(await isLitElement(Component));
+}
+
+function* render(tagName, attrs, children) {
+ const instance = new LitElementRenderer(tagName);
+
+ // LitElementRenderer creates a new element instance, so copy over.
+ const Ctr = getCustomElementConstructor(tagName);
+ for (let [name, value] of Object.entries(attrs)) {
+ // check if this is a reactive property
+ if (name in Ctr.prototype) {
+ instance.setProperty(name, value);
+ } else {
+ instance.setAttribute(name, value);
+ }
+ }
+
+ instance.connectedCallback();
+
+ yield `<${tagName}`;
+ yield* instance.renderAttributes();
+ yield `>`;
+ const shadowContents = instance.renderShadow({});
+ if (shadowContents !== undefined) {
+ yield '<template shadowroot="open">';
+ yield* shadowContents;
+ yield '</template>';
+ }
+ yield children || ''; // don’t print “undefined” as string
+ yield `</${tagName}>`;
+}
+
+async function renderToStaticMarkup(Component, props, children) {
+ let tagName = Component;
+
+ let out = '';
+ for (let chunk of render(tagName, props, children)) {
+ out += chunk;
+ }
+
+ return {
+ html: out,
+ };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/lit/src/index.ts b/packages/integrations/lit/src/index.ts
new file mode 100644
index 000000000..bf256eb84
--- /dev/null
+++ b/packages/integrations/lit/src/index.ts
@@ -0,0 +1,42 @@
+import { readFileSync } from 'node:fs';
+import type { AstroIntegration } from 'astro';
+
+function getViteConfiguration() {
+ return {
+ optimizeDeps: {
+ include: [
+ '@astrojs/lit/client-shim.js',
+ '@astrojs/lit/hydration-support.js',
+ '@webcomponents/template-shadowroot/template-shadowroot.js',
+ 'lit/experimental-hydrate-support.js',
+ ],
+ exclude: ['@astrojs/lit/server.js'],
+ },
+ ssr: {
+ external: ['lit-element/lit-element.js', '@lit-labs/ssr/lib/install-global-dom-shim.js', '@lit-labs/ssr/lib/render-lit-html.js', '@lit-labs/ssr/lib/lit-element-renderer.js'],
+ },
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/lit',
+ hooks: {
+ 'astro:config:setup': ({ updateConfig, addRenderer, injectScript }) => {
+ // Inject the necessary polyfills on every page (inlined for speed).
+ injectScript('head-inline', readFileSync(new URL('../client-shim.min.js', import.meta.url), { encoding: 'utf-8' }));
+ // Inject the hydration code, before a component is hydrated.
+ injectScript('before-hydration', `import '@astrojs/lit/hydration-support.js';`);
+ // Add the lit renderer so that Astro can understand lit components.
+ addRenderer({
+ name: '@astrojs/lit',
+ serverEntrypoint: '@astrojs/lit/server.js',
+ });
+ // Update the vite configuration.
+ updateConfig({
+ vite: getViteConfiguration(),
+ });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/lit/tsconfig.json b/packages/integrations/lit/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/lit/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/partytown/package.json b/packages/integrations/partytown/package.json
new file mode 100644
index 000000000..38d9653d1
--- /dev/null
+++ b/packages/integrations/partytown/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@astrojs/partytown",
+ "description": "Astro + Partytown integration",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/partytown"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@builder.io/partytown": "^0.4.0",
+ "mrmime": "^1.0.0"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*"
+ }
+}
diff --git a/packages/integrations/partytown/src/index.ts b/packages/integrations/partytown/src/index.ts
new file mode 100644
index 000000000..479f86b07
--- /dev/null
+++ b/packages/integrations/partytown/src/index.ts
@@ -0,0 +1,33 @@
+import type { AstroConfig, AstroIntegration } from 'astro';
+import sirv from './sirv.js';
+import { partytownSnippet } from '@builder.io/partytown/integration';
+import { copyLibFiles } from '@builder.io/partytown/utils';
+import { fileURLToPath } from 'url';
+import { createRequire } from 'module';
+import path from 'path';
+const resolve = createRequire(import.meta.url).resolve;
+
+export default function createPlugin(): AstroIntegration {
+ let config: AstroConfig;
+ let partytownSnippetHtml: string;
+ const partytownEntrypoint = resolve('@builder.io/partytown/package.json');
+ const partytownLibDirectory = path.resolve(partytownEntrypoint, '../lib');
+ return {
+ name: '@astrojs/partytown',
+ hooks: {
+ 'astro:config:setup': ({ config: _config, command, injectScript }) => {
+ partytownSnippetHtml = partytownSnippet({ debug: command === 'dev' });
+ injectScript('head-inline', partytownSnippetHtml);
+ },
+ 'astro:config:done': ({ config: _config }) => {
+ config = _config;
+ },
+ 'astro:server:setup': ({ server }) => {
+ server.middlewares.use(sirv(partytownLibDirectory, { mount: '/~partytown', dev: true, etag: true, extensions: [] }));
+ },
+ 'astro:build:done': async () => {
+ await copyLibFiles(fileURLToPath(new URL('~partytown', config.dist)), { debugDir: false });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/partytown/src/sirv.ts b/packages/integrations/partytown/src/sirv.ts
new file mode 100644
index 000000000..860a715bf
--- /dev/null
+++ b/packages/integrations/partytown/src/sirv.ts
@@ -0,0 +1,241 @@
+// TODO: The below has been modified from the original sirv package to support
+// the feature of mounting the served files from a certain path (in this case, `/~partytown/`)
+// It would be good to bring this into Astro for all integrations to take advantage of,
+// and potentially also to respect your config automatically for things like `base` path.
+// @ts-nocheck
+
+/**
+ * @license
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (https://lukeed.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import * as fs from 'fs';
+import { join, normalize, resolve } from 'path';
+// import { totalist } from 'totalist/sync';
+// import { parse } from '@polka/url';
+import { lookup } from 'mrmime';
+import { URL } from 'url';
+
+const noop = () => {};
+
+function isMatch(uri, arr) {
+ for (let i = 0; i < arr.length; i++) {
+ if (arr[i].test(uri)) return true;
+ }
+}
+
+function toAssume(uri, extns) {
+ let i = 0,
+ x,
+ len = uri.length - 1;
+ if (uri.charCodeAt(len) === 47) {
+ uri = uri.substring(0, len);
+ }
+
+ let arr = [],
+ tmp = `${uri}/index`;
+ for (; i < extns.length; i++) {
+ x = extns[i] ? `.${extns[i]}` : '';
+ if (uri) arr.push(uri + x);
+ arr.push(tmp + x);
+ }
+
+ return arr;
+}
+
+function viaCache(cache, uri, extns) {
+ let i = 0,
+ data,
+ arr = toAssume(uri, extns);
+ for (; i < arr.length; i++) {
+ if ((data = cache[arr[i]])) return data;
+ }
+}
+
+function viaLocal(dir, isEtag, uri, extns) {
+ let i = 0,
+ arr = toAssume(uri, extns);
+ let abs, stats, name, headers;
+ for (; i < arr.length; i++) {
+ abs = normalize(join(dir, (name = arr[i])));
+ if (abs.startsWith(dir) && fs.existsSync(abs)) {
+ stats = fs.statSync(abs);
+ if (stats.isDirectory()) continue;
+ headers = toHeaders(name, stats, isEtag);
+ headers['Cache-Control'] = isEtag ? 'no-cache' : 'no-store';
+ return { abs, stats, headers };
+ }
+ }
+}
+
+function is404(req, res) {
+ return (res.statusCode = 404), res.end();
+}
+
+function send(req, res, file, stats, headers) {
+ let code = 200,
+ tmp,
+ opts = {};
+ headers = { ...headers };
+
+ for (let key in headers) {
+ tmp = res.getHeader(key);
+ if (tmp) headers[key] = tmp;
+ }
+
+ if ((tmp = res.getHeader('content-type'))) {
+ headers['Content-Type'] = tmp;
+ }
+
+ if (req.headers.range) {
+ code = 206;
+ let [x, y] = req.headers.range.replace('bytes=', '').split('-');
+ let end = (opts.end = parseInt(y, 10) || stats.size - 1);
+ let start = (opts.start = parseInt(x, 10) || 0);
+
+ if (start >= stats.size || end >= stats.size) {
+ res.setHeader('Content-Range', `bytes */${stats.size}`);
+ res.statusCode = 416;
+ return res.end();
+ }
+
+ headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
+ headers['Content-Length'] = end - start + 1;
+ headers['Accept-Ranges'] = 'bytes';
+ }
+
+ res.writeHead(code, headers);
+ fs.createReadStream(file, opts).pipe(res);
+}
+
+const ENCODING = {
+ '.br': 'br',
+ '.gz': 'gzip',
+};
+
+function toHeaders(name, stats, isEtag) {
+ let enc = ENCODING[name.slice(-3)];
+
+ let ctype = lookup(name.slice(0, enc && -3)) || '';
+ if (ctype === 'text/html') ctype += ';charset=utf-8';
+
+ let headers = {
+ 'Content-Length': stats.size,
+ 'Content-Type': ctype,
+ 'Last-Modified': stats.mtime.toUTCString(),
+ };
+
+ if (enc) headers['Content-Encoding'] = enc;
+ if (isEtag) headers['ETag'] = `W/"${stats.size}-${stats.mtime.getTime()}"`;
+
+ return headers;
+}
+
+export default function (dir, opts = {}) {
+ dir = resolve(dir || '.');
+
+ let mountTo = opts.mount || '';
+ let isNotFound = opts.onNoMatch || is404;
+ let setHeaders = opts.setHeaders || noop;
+
+ let extensions = opts.extensions || ['html', 'htm'];
+ let gzips = opts.gzip && extensions.map((x) => `${x}.gz`).concat('gz');
+ let brots = opts.brotli && extensions.map((x) => `${x}.br`).concat('br');
+
+ const FILES = {};
+
+ let fallback = '/';
+ let isEtag = !!opts.etag;
+ let isSPA = !!opts.single;
+ if (typeof opts.single === 'string') {
+ let idx = opts.single.lastIndexOf('.');
+ fallback += !!~idx ? opts.single.substring(0, idx) : opts.single;
+ }
+
+ let ignores = [];
+ if (opts.ignores !== false) {
+ ignores.push(/[/]([A-Za-z\s\d~$._-]+\.\w+){1,}$/); // any extn
+ if (opts.dotfiles) ignores.push(/\/\.\w/);
+ else ignores.push(/\/\.well-known/);
+ [].concat(opts.ignores || []).forEach((x) => {
+ ignores.push(new RegExp(x, 'i'));
+ });
+ }
+
+ let cc = opts.maxAge != null && `public,max-age=${opts.maxAge}`;
+ if (cc && opts.immutable) cc += ',immutable';
+ else if (cc && opts.maxAge === 0) cc += ',must-revalidate';
+
+ if (!opts.dev) {
+ totalist(dir, (name, abs, stats) => {
+ if (/\.well-known[\\+\/]/.test(name)) {
+ } // keep
+ else if (!opts.dotfiles && /(^\.|[\\+|\/+]\.)/.test(name)) return;
+
+ let headers = toHeaders(name, stats, isEtag);
+ if (cc) headers['Cache-Control'] = cc;
+
+ FILES['/' + name.normalize().replace(/\\+/g, '/')] = { abs, stats, headers };
+ });
+ }
+
+ let lookup = opts.dev ? viaLocal.bind(0, dir, isEtag) : viaCache.bind(0, FILES);
+
+ return function (req, res, next) {
+ let extns = [''];
+ let pathname = new URL(req.url, 'https://example.dev').pathname;
+ // NEW
+ if (mountTo && pathname.startsWith(mountTo)) {
+ pathname = pathname.substring(mountTo.length);
+ }
+ // NEW END
+ let val = req.headers['accept-encoding'] || '';
+ if (gzips && val.includes('gzip')) extns.unshift(...gzips);
+ if (brots && /(br|brotli)/i.test(val)) extns.unshift(...brots);
+ extns.push(...extensions); // [...br, ...gz, orig, ...exts]
+
+ if (pathname.indexOf('%') !== -1) {
+ try {
+ pathname = decodeURIComponent(pathname);
+ } catch (err) {
+ /* malform uri */
+ }
+ }
+
+ let data = lookup(pathname, extns) || (isSPA && !isMatch(pathname, ignores) && lookup(fallback, extns));
+ if (!data) return next ? next() : isNotFound(req, res);
+
+ if (isEtag && req.headers['if-none-match'] === data.headers['ETag']) {
+ res.writeHead(304);
+ return res.end();
+ }
+
+ if (gzips || brots) {
+ res.setHeader('Vary', 'Accept-Encoding');
+ }
+
+ setHeaders(res, pathname, data.stats);
+ send(req, res, data.abs, data.stats, data.headers);
+ };
+}
diff --git a/packages/integrations/partytown/tsconfig.json b/packages/integrations/partytown/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/partytown/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/preact/client.js b/packages/integrations/preact/client.js
new file mode 100644
index 000000000..85c18c76c
--- /dev/null
+++ b/packages/integrations/preact/client.js
@@ -0,0 +1,4 @@
+import { h, render } from 'preact';
+import StaticHtml from './static-html.js';
+
+export default (element) => (Component, props, children) => render(h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), element);
diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json
new file mode 100644
index 000000000..f360e91e6
--- /dev/null
+++ b/packages/integrations/preact/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@astrojs/preact",
+ "description": "Use Preact components within Astro",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/preact"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./client": "./client",
+ "./client.js": "./client.js",
+ "./server": "./server",
+ "./server.js": "./server.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^7.16.7",
+ "preact-render-to-string": "^5.1.19"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "preact": "^10.6.5"
+ },
+ "peerDependencies": {
+ "preact": "^10.6.5"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.0.0"
+ }
+}
diff --git a/packages/integrations/preact/server.js b/packages/integrations/preact/server.js
new file mode 100644
index 000000000..25b1a1530
--- /dev/null
+++ b/packages/integrations/preact/server.js
@@ -0,0 +1,35 @@
+import { h, Component as BaseComponent } from 'preact';
+import render from 'preact-render-to-string';
+import StaticHtml from './static-html.js';
+
+function check(Component, props, children) {
+ if (typeof Component !== 'function') return false;
+
+ if (Component.prototype != null && typeof Component.prototype.render === 'function') {
+ return BaseComponent.isPrototypeOf(Component);
+ }
+
+ try {
+ const { html } = renderToStaticMarkup(Component, props, children);
+ if (typeof html !== 'string') {
+ return false;
+ }
+
+ // There are edge cases (SolidJS) where Preact *might* render a string,
+ // but components would be <undefined></undefined>
+
+ return !/\<undefined\>/.test(html);
+ } catch (err) {
+ return false;
+ }
+}
+
+function renderToStaticMarkup(Component, props, children) {
+ const html = render(h(Component, props, children != null ? h(StaticHtml, { value: children }) : children));
+ return { html };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/preact/src/index.ts b/packages/integrations/preact/src/index.ts
new file mode 100644
index 000000000..113284c31
--- /dev/null
+++ b/packages/integrations/preact/src/index.ts
@@ -0,0 +1,45 @@
+import { AstroIntegration } from 'astro';
+
+function getRenderer() {
+ return {
+ name: '@astrojs/preact',
+ clientEntrypoint: '@astrojs/preact/client',
+ serverEntrypoint: '@astrojs/preact/server',
+ jsxImportSource: 'preact',
+ jsxTransformOptions: async () => {
+ const {
+ default: { default: jsx },
+ // @ts-expect-error types not found
+ } = await import('@babel/plugin-transform-react-jsx');
+ return {
+ plugins: [jsx({}, { runtime: 'automatic', importSource: 'preact' })],
+ };
+ },
+ };
+}
+
+function getViteConfiguration() {
+ return {
+ optimizeDeps: {
+ include: ['@astrojs/preact/client', 'preact', 'preact/jsx-runtime', 'preact-render-to-string'],
+ exclude: ['@astrojs/preact/server'],
+ },
+ ssr: {
+ external: ['preact-render-to-string'],
+ },
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/preact',
+ hooks: {
+ 'astro:config:setup': ({ addRenderer }) => {
+ addRenderer(getRenderer());
+ return {
+ vite: getViteConfiguration(),
+ };
+ },
+ },
+ };
+}
diff --git a/packages/integrations/preact/static-html.js b/packages/integrations/preact/static-html.js
new file mode 100644
index 000000000..9af8002a7
--- /dev/null
+++ b/packages/integrations/preact/static-html.js
@@ -0,0 +1,24 @@
+import { h } from 'preact';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `div` to render that content as VNodes.
+ *
+ * As a bonus, we can signal to Preact that this subtree is
+ * entirely static and will never change via `shouldComponentUpdate`.
+ */
+const StaticHtml = ({ value }) => {
+ if (!value) return null;
+ return h('astro-fragment', { dangerouslySetInnerHTML: { __html: value } });
+};
+
+/**
+ * This tells Preact to opt-out of re-rendering this subtree,
+ * In addition to being a performance optimization,
+ * this also allows other frameworks to attach to `children`.
+ *
+ * See https://preactjs.com/guide/v8/external-dom-mutations
+ */
+StaticHtml.shouldComponentUpdate = () => false;
+
+export default StaticHtml;
diff --git a/packages/integrations/preact/tsconfig.json b/packages/integrations/preact/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/preact/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
new file mode 100644
index 000000000..a6bc7d3bc
--- /dev/null
+++ b/packages/integrations/react/client.js
@@ -0,0 +1,13 @@
+import { createElement } from 'react';
+import { hydrate } from 'react-dom';
+import StaticHtml from './static-html.js';
+
+export default (element) => (Component, props, children) =>
+ hydrate(
+ createElement(
+ Component,
+ { ...props, suppressHydrationWarning: true },
+ children != null ? createElement(StaticHtml, { value: children, suppressHydrationWarning: true }) : children
+ ),
+ element
+ );
diff --git a/packages/integrations/react/jsx-runtime.js b/packages/integrations/react/jsx-runtime.js
new file mode 100644
index 000000000..d86f698b9
--- /dev/null
+++ b/packages/integrations/react/jsx-runtime.js
@@ -0,0 +1,8 @@
+// This module is a simple wrapper around react/jsx-runtime so that
+// it can run in Node ESM. 'react' doesn't declare this module as an export map
+// So we have to use the .js. The .js is not added via the babel automatic JSX transform
+// hence this module as a workaround.
+import jsxr from 'react/jsx-runtime.js';
+const { jsx, jsxs, Fragment } = jsxr;
+
+export { jsx, jsxs, Fragment };
diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json
new file mode 100644
index 000000000..022b1d710
--- /dev/null
+++ b/packages/integrations/react/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@astrojs/react",
+ "description": "Use React components within Astro",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/react"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./client.js": "./client.js",
+ "./server.js": "./server.js",
+ "./package.json": "./package.json",
+ "./jsx-runtime": "./jsx-runtime.js"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^7.16.7"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "peerDependencies": {
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.0.0"
+ }
+}
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js
new file mode 100644
index 000000000..1c0c41286
--- /dev/null
+++ b/packages/integrations/react/server.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import ReactDOM from 'react-dom/server.js';
+import StaticHtml from './static-html.js';
+
+const reactTypeof = Symbol.for('react.element');
+
+function errorIsComingFromPreactComponent(err) {
+ return err.message && (err.message.startsWith("Cannot read property '__H'") || err.message.includes("(reading '__H')"));
+}
+
+function check(Component, props, children) {
+ // Note: there are packages that do some unholy things to create "components".
+ // Checking the $$typeof property catches most of these patterns.
+ if (typeof Component === 'object') {
+ const $$typeof = Component['$$typeof'];
+ return $$typeof && $$typeof.toString().slice('Symbol('.length).startsWith('react');
+ }
+ if (typeof Component !== 'function') return false;
+
+ if (Component.prototype != null && typeof Component.prototype.render === 'function') {
+ return React.Component.isPrototypeOf(Component) || React.PureComponent.isPrototypeOf(Component);
+ }
+
+ let error = null;
+ let isReactComponent = false;
+ function Tester(...args) {
+ try {
+ const vnode = Component(...args);
+ if (vnode && vnode['$$typeof'] === reactTypeof) {
+ isReactComponent = true;
+ }
+ } catch (err) {
+ if (!errorIsComingFromPreactComponent(err)) {
+ error = err;
+ }
+ }
+
+ return React.createElement('div');
+ }
+
+ renderToStaticMarkup(Tester, props, children, {});
+
+ if (error) {
+ throw error;
+ }
+ return isReactComponent;
+}
+
+function renderToStaticMarkup(Component, props, children, metadata) {
+ delete props['class'];
+ const vnode = React.createElement(Component, {
+ ...props,
+ children: children != null ? React.createElement(StaticHtml, { value: children }) : undefined,
+ });
+ let html;
+ if (metadata && metadata.hydrate) {
+ html = ReactDOM.renderToString(vnode);
+ } else {
+ html = ReactDOM.renderToStaticMarkup(vnode);
+ }
+ return { html };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/react/src/index.ts b/packages/integrations/react/src/index.ts
new file mode 100644
index 000000000..128c6406d
--- /dev/null
+++ b/packages/integrations/react/src/index.ts
@@ -0,0 +1,54 @@
+import { AstroIntegration } from 'astro';
+
+function getRenderer() {
+ return {
+ name: '@astrojs/react',
+ clientEntrypoint: '@astrojs/react/client.js',
+ serverEntrypoint: '@astrojs/react/server.js',
+ jsxImportSource: 'react',
+ jsxTransformOptions: async () => {
+ const {
+ default: { default: jsx },
+ // @ts-expect-error types not found
+ } = await import('@babel/plugin-transform-react-jsx');
+ return {
+ plugins: [
+ jsx(
+ {},
+ {
+ runtime: 'automatic',
+ importSource: '@astrojs/react',
+ }
+ ),
+ ],
+ };
+ },
+ };
+}
+
+function getViteConfiguration() {
+ return {
+ optimizeDeps: {
+ include: ['@astrojs/react/client.js', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'],
+ exclude: ['@astrojs/react/server.js'],
+ },
+ resolve: {
+ dedupe: ['react', 'react-dom'],
+ },
+ ssr: {
+ external: ['react-dom/server.js'],
+ },
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/react',
+ hooks: {
+ 'astro:config:setup': ({ addRenderer, updateConfig }) => {
+ addRenderer(getRenderer());
+ updateConfig({ vite: getViteConfiguration() });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/react/static-html.js b/packages/integrations/react/static-html.js
new file mode 100644
index 000000000..47130d786
--- /dev/null
+++ b/packages/integrations/react/static-html.js
@@ -0,0 +1,24 @@
+import { createElement as h } from 'react';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `div` to render that content as VNodes.
+ *
+ * As a bonus, we can signal to React that this subtree is
+ * entirely static and will never change via `shouldComponentUpdate`.
+ */
+const StaticHtml = ({ value }) => {
+ if (!value) return null;
+ return h('astro-fragment', { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: value } });
+};
+
+/**
+ * This tells React to opt-out of re-rendering this subtree,
+ * In addition to being a performance optimization,
+ * this also allows other frameworks to attach to `children`.
+ *
+ * See https://preactjs.com/guide/v8/external-dom-mutations
+ */
+StaticHtml.shouldComponentUpdate = () => false;
+
+export default StaticHtml;
diff --git a/packages/integrations/react/tsconfig.json b/packages/integrations/react/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/react/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json
new file mode 100644
index 000000000..e91f9a098
--- /dev/null
+++ b/packages/integrations/sitemap/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@astrojs/sitemap",
+ "description": "Generate a sitemap for Astro",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/sitemap"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "sitemap": "^7.1.1"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*"
+ }
+}
diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts
new file mode 100644
index 000000000..28bc61396
--- /dev/null
+++ b/packages/integrations/sitemap/src/index.ts
@@ -0,0 +1,38 @@
+import fs from 'node:fs';
+import type { AstroConfig, AstroIntegration } from 'astro';
+const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/;
+
+/** Construct sitemap.xml given a set of URLs */
+function generateSitemap(pages: string[]) {
+ // TODO: find way to respect <link rel="canonical"> URLs here
+ // TODO: find way to exclude pages from sitemap
+ const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url));
+ urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
+ let sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
+ for (const url of urls) {
+ sitemap += `<url><loc>${url}</loc></url>`;
+ }
+ sitemap += `</urlset>\n`;
+ return sitemap;
+}
+
+export default function createPlugin(): AstroIntegration {
+ let config: AstroConfig;
+ return {
+ name: '@astrojs/sitemap',
+ hooks: {
+ 'astro:config:done': async ({ config: _config }) => {
+ config = _config;
+ },
+ 'astro:build:done': async ({ pages, dir }) => {
+ const finalSiteUrl = config.buildOptions.site;
+ if (!finalSiteUrl) {
+ return;
+ }
+ const pageUrls = pages.map((p) => new URL(p.pathname, finalSiteUrl).href);
+ const sitemapContent = generateSitemap(pageUrls);
+ fs.writeFileSync(new URL('sitemap.xml', dir), sitemapContent);
+ },
+ },
+ };
+}
diff --git a/packages/integrations/sitemap/tsconfig.json b/packages/integrations/sitemap/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/sitemap/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/solid/client.js b/packages/integrations/solid/client.js
new file mode 100644
index 000000000..b67b3acdb
--- /dev/null
+++ b/packages/integrations/solid/client.js
@@ -0,0 +1,14 @@
+import { hydrate, createComponent } from 'solid-js/web';
+
+export default (element) => (Component, props, childHTML) => {
+ let children;
+ if (childHTML != null) {
+ children = document.createElement('astro-fragment');
+ children.innerHTML = childHTML;
+ }
+
+ // Using Solid's `hydrate` method ensures that a `root` is created
+ // in order to properly handle reactivity. It also handles
+ // components that are not native HTML elements.
+ hydrate(() => createComponent(Component, { ...props, children }), element);
+};
diff --git a/packages/integrations/solid/package.json b/packages/integrations/solid/package.json
new file mode 100644
index 000000000..5f332631d
--- /dev/null
+++ b/packages/integrations/solid/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@astrojs/solid-js",
+ "version": "0.0.1",
+ "description": "Use Solid components within Astro",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/solid"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./*": "./*",
+ "./client.js": "./client.js",
+ "./server.js": "./server.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "babel-preset-solid": "^1.3.6"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "solid-js": "^1.3.6"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.3.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.0.0"
+ }
+}
diff --git a/packages/integrations/solid/server.js b/packages/integrations/solid/server.js
new file mode 100644
index 000000000..d32d60a64
--- /dev/null
+++ b/packages/integrations/solid/server.js
@@ -0,0 +1,28 @@
+import { renderToString, ssr, createComponent } from 'solid-js/web';
+
+function check(Component, props, children) {
+ if (typeof Component !== 'function') return false;
+ try {
+ const { html } = renderToStaticMarkup(Component, props, children);
+ return typeof html === 'string';
+ } catch (err) {
+ return false;
+ }
+}
+
+function renderToStaticMarkup(Component, props, children) {
+ const html = renderToString(() =>
+ createComponent(Component, {
+ ...props,
+ // In Solid SSR mode, `ssr` creates the expected structure for `children`.
+ // In Solid client mode, `ssr` is just a stub.
+ children: children != null ? ssr(`<astro-fragment>${children}</astro-fragment>`) : children,
+ })
+ );
+ return { html: html + `<script>window._$HY||(_$HY={events:[],completed:new WeakSet,r:{}})</script>` };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/solid/src/index.ts b/packages/integrations/solid/src/index.ts
new file mode 100644
index 000000000..1205c6d09
--- /dev/null
+++ b/packages/integrations/solid/src/index.ts
@@ -0,0 +1,57 @@
+import type { AstroIntegration, AstroRenderer } from 'astro';
+
+function getRenderer(): AstroRenderer {
+ return {
+ name: '@astrojs/solid-js',
+ clientEntrypoint: '@astrojs/solid-js/client.js',
+ serverEntrypoint: '@astrojs/solid-js/server.js',
+ jsxImportSource: 'solid-js',
+ jsxTransformOptions: async ({ ssr }) => {
+ // @ts-expect-error types not found
+ const [{ default: solid }] = await Promise.all([import('babel-preset-solid')]);
+ const options = {
+ presets: [solid({}, { generate: ssr ? 'ssr' : 'dom', hydratable: true })],
+ plugins: [],
+ };
+
+ return options;
+ },
+ };
+}
+
+function getViteConfiguration(isDev: boolean) {
+ // https://github.com/solidjs/vite-plugin-solid
+ // We inject the dev mode only if the user explicitely wants it or if we are in dev (serve) mode
+ const nestedDeps = ['solid-js', 'solid-js/web', 'solid-js/store', 'solid-js/html', 'solid-js/h'];
+ return {
+ /**
+ * We only need esbuild on .ts or .js files.
+ * .tsx & .jsx files are handled by us
+ */
+ esbuild: { include: /\.ts$/ },
+ resolve: {
+ conditions: ['solid', ...(isDev ? ['development'] : [])],
+ dedupe: nestedDeps,
+ alias: [{ find: /^solid-refresh$/, replacement: '/@solid-refresh' }],
+ },
+ optimizeDeps: {
+ include: nestedDeps,
+ exclude: ['@astrojs/solid-js/server.js'],
+ },
+ ssr: {
+ external: ['babel-preset-solid'],
+ },
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/solid-js',
+ hooks: {
+ 'astro:config:setup': ({ command, addRenderer, updateConfig }) => {
+ addRenderer(getRenderer());
+ updateConfig({ vite: getViteConfiguration(command === 'dev') });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/solid/static-html.js b/packages/integrations/solid/static-html.js
new file mode 100644
index 000000000..9f969eac9
--- /dev/null
+++ b/packages/integrations/solid/static-html.js
@@ -0,0 +1,12 @@
+import { ssr } from 'solid-js/web';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `astro-fragment` to render that content as VNodes.
+ */
+const StaticHtml = ({ innerHTML }) => {
+ if (!innerHTML) return null;
+ return ssr(`<astro-fragment>${innerHTML}</astro-fragment>`);
+};
+
+export default StaticHtml;
diff --git a/packages/integrations/solid/tsconfig.json b/packages/integrations/solid/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/solid/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/svelte/Wrapper.svelte b/packages/integrations/svelte/Wrapper.svelte
new file mode 100644
index 000000000..c1ee77d91
--- /dev/null
+++ b/packages/integrations/svelte/Wrapper.svelte
@@ -0,0 +1,21 @@
+<script>
+/**
+ * Why do we need a wrapper component?
+ *
+ * Astro passes `children` as a string of HTML, so we need
+ * a way to render that content.
+ *
+ * Rather than passing a magical prop which needs special
+ * handling, using this wrapper allows Svelte users to just
+ * use `<slot />` like they would for any other component.
+ */
+const { __astro_component: Component, __astro_children, ...props } = $$props;
+</script>
+
+<svelte:component this={Component} {...props}>
+ {#if __astro_children != null}
+ <astro-fragment>
+ {@html __astro_children}
+ </astro-fragment>
+ {/if}
+</svelte:component>
diff --git a/packages/integrations/svelte/Wrapper.svelte.ssr.js b/packages/integrations/svelte/Wrapper.svelte.ssr.js
new file mode 100644
index 000000000..9bca437b5
--- /dev/null
+++ b/packages/integrations/svelte/Wrapper.svelte.ssr.js
@@ -0,0 +1,14 @@
+/* App.svelte generated by Svelte v3.38.2 */
+import { create_ssr_component, missing_component, validate_component } from 'svelte/internal';
+
+const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
+ const { __astro_component: Component, __astro_children, ...props } = $$props;
+ const children = {};
+ if (__astro_children != null) {
+ children.default = () => `<astro-fragment>${__astro_children}</astro-fragment>`;
+ }
+
+ return `${validate_component(Component || missing_component, 'svelte:component').$$render($$result, Object.assign(props), {}, children)}`;
+});
+
+export default App;
diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js
new file mode 100644
index 000000000..c10c7afa0
--- /dev/null
+++ b/packages/integrations/svelte/client.js
@@ -0,0 +1,14 @@
+import SvelteWrapper from './Wrapper.svelte';
+
+export default (target) => {
+ return (component, props, children) => {
+ delete props['class'];
+ try {
+ new SvelteWrapper({
+ target,
+ props: { __astro_component: component, __astro_children: children, ...props },
+ hydrate: true,
+ });
+ } catch (e) {}
+ };
+};
diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json
new file mode 100644
index 000000000..a6fec87a4
--- /dev/null
+++ b/packages/integrations/svelte/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@astrojs/svelte",
+ "version": "0.0.1",
+ "description": "Use Svelte components within Astro",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/svelte"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./*": "./*",
+ "./client.js": "./client.js",
+ "./server.js": "./server.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.37",
+ "postcss-load-config": "^3.1.1",
+ "svelte-preprocess": "^4.10.2"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "svelte": "^3.46.4"
+ },
+ "peerDependencies": {
+ "svelte": "^3.46.4"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.0.0"
+ }
+}
diff --git a/packages/integrations/svelte/server.js b/packages/integrations/svelte/server.js
new file mode 100644
index 000000000..c51b2f4b4
--- /dev/null
+++ b/packages/integrations/svelte/server.js
@@ -0,0 +1,15 @@
+import SvelteWrapper from './Wrapper.svelte.ssr.js';
+
+function check(Component) {
+ return Component['render'] && Component['$$render'];
+}
+
+async function renderToStaticMarkup(Component, props, children) {
+ const { html } = SvelteWrapper.render({ __astro_component: Component, __astro_children: children, ...props });
+ return { html };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts
new file mode 100644
index 000000000..f5a3dd945
--- /dev/null
+++ b/packages/integrations/svelte/src/index.ts
@@ -0,0 +1,48 @@
+import type { AstroIntegration, AstroRenderer } from 'astro';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import preprocess from 'svelte-preprocess';
+
+function getRenderer(): AstroRenderer {
+ return {
+ name: '@astrojs/svelte',
+ clientEntrypoint: '@astrojs/svelte/client.js',
+ serverEntrypoint: '@astrojs/svelte/server.js',
+ };
+}
+
+function getViteConfiguration(isDev: boolean) {
+ return {
+ optimizeDeps: {
+ include: ['@astrojs/svelte/client.js', 'svelte', 'svelte/internal'],
+ exclude: ['@astrojs/svelte/server.js'],
+ },
+ plugins: [
+ svelte({
+ emitCss: true,
+ compilerOptions: { dev: isDev, hydratable: true },
+ preprocess: [
+ preprocess({
+ less: true,
+ sass: { renderSync: true },
+ scss: { renderSync: true },
+ stylus: true,
+ typescript: true,
+ }),
+ ],
+ }),
+ ],
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/svelte',
+ hooks: {
+ // Anything that gets returned here is merged into Astro Config
+ 'astro:config:setup': ({ command, updateConfig, addRenderer }) => {
+ addRenderer(getRenderer());
+ updateConfig({ vite: getViteConfiguration(command === 'dev') });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/svelte/tsconfig.json b/packages/integrations/svelte/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/svelte/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/tailwind/base.css b/packages/integrations/tailwind/base.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/integrations/tailwind/base.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/integrations/tailwind/package.json b/packages/integrations/tailwind/package.json
new file mode 100644
index 000000000..99116f5c6
--- /dev/null
+++ b/packages/integrations/tailwind/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@astrojs/tailwind",
+ "description": "Tailwind + Astro Integrations",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/tailwind"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./base.css": "./base.css",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "tailwindcss": "^3.0.23",
+ "autoprefixer": "^10.4.4",
+ "postcss": "^8.4.12"
+ },
+ "devDependencies": {
+ "@types/tailwindcss": "^3.0.9",
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*"
+ },
+ "pnpm": {
+ "peerDependencyRules": {
+ "ignoreMissing": [
+ "postcss"
+ ]
+ }
+ }
+}
diff --git a/packages/integrations/tailwind/src/index.ts b/packages/integrations/tailwind/src/index.ts
new file mode 100644
index 000000000..30905f9d1
--- /dev/null
+++ b/packages/integrations/tailwind/src/index.ts
@@ -0,0 +1,31 @@
+import type { AstroIntegration } from 'astro';
+import { fileURLToPath } from 'url';
+import path from 'path';
+import tailwindPlugin from 'tailwindcss';
+import autoprefixerPlugin from 'autoprefixer';
+
+function getDefaultTailwindConfig(srcUrl: URL) {
+ return {
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+ content: [path.join(fileURLToPath(srcUrl), `**`, `*.{astro,html,js,jsx,svelte,ts,tsx,vue}`)],
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/tailwind',
+ hooks: {
+ 'astro:config:setup': ({ config, injectScript }) => {
+ // Inject the Tailwind postcss plugin
+ config.styleOptions.postcss.plugins.push(tailwindPlugin(getDefaultTailwindConfig(config.src)));
+ config.styleOptions.postcss.plugins.push(autoprefixerPlugin);
+
+ // Inject the Tailwind base import
+ injectScript('page-ssr', `import '@astrojs/tailwind/base.css';`);
+ },
+ },
+ };
+}
diff --git a/packages/integrations/tailwind/tsconfig.json b/packages/integrations/tailwind/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/tailwind/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/turbolinks/client.js b/packages/integrations/turbolinks/client.js
new file mode 100644
index 000000000..6dde8c193
--- /dev/null
+++ b/packages/integrations/turbolinks/client.js
@@ -0,0 +1,2 @@
+import Turbolinks from 'turbolinks';
+export { Turbolinks };
diff --git a/packages/integrations/turbolinks/package.json b/packages/integrations/turbolinks/package.json
new file mode 100644
index 000000000..99834fe77
--- /dev/null
+++ b/packages/integrations/turbolinks/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@astrojs/turbolinks",
+ "description": "Turbolinks + Astro Integrations",
+ "version": "0.0.1",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/turbolinks"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./client.js": "./client.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "turbolinks": "^5.2.0"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*"
+ }
+}
diff --git a/packages/integrations/turbolinks/src/index.ts b/packages/integrations/turbolinks/src/index.ts
new file mode 100644
index 000000000..3299736ba
--- /dev/null
+++ b/packages/integrations/turbolinks/src/index.ts
@@ -0,0 +1,15 @@
+import type { AstroIntegration } from 'astro';
+
+export default function createPlugin(): AstroIntegration {
+ return {
+ name: '@astrojs/turbolinks',
+ hooks: {
+ 'astro:config:setup': ({ injectScript }) => {
+ // This gets injected into the user's page, so we need to re-export Turbolinks
+ // from our own package so that package managers like pnpm don't get mad and
+ // can follow the import correctly.
+ injectScript('page', `import {Turbolinks} from "@astrojs/turbolinks/client.js"; Turbolinks.start();`);
+ },
+ },
+ };
+}
diff --git a/packages/integrations/turbolinks/tsconfig.json b/packages/integrations/turbolinks/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/turbolinks/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}
diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/client.js
new file mode 100644
index 000000000..0ba4e8106
--- /dev/null
+++ b/packages/integrations/vue/client.js
@@ -0,0 +1,14 @@
+import { h, createSSRApp } from 'vue';
+import StaticHtml from './static-html.js';
+
+export default (element) => (Component, props, children) => {
+ delete props['class'];
+ // Expose name on host component for Vue devtools
+ const name = Component.name ? `${Component.name} Host` : undefined;
+ const slots = {};
+ if (children != null) {
+ slots.default = () => h(StaticHtml, { value: children });
+ }
+ const app = createSSRApp({ name, render: () => h(Component, props, slots) });
+ app.mount(element, true);
+};
diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json
new file mode 100644
index 000000000..8fce8e77a
--- /dev/null
+++ b/packages/integrations/vue/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@astrojs/vue",
+ "version": "0.0.1",
+ "description": "Use Vue components within Astro",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/withastro/astro.git",
+ "directory": "packages/integrations/vue"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": "./dist/index.js",
+ "./*": "./*",
+ "./client.js": "./client.js",
+ "./server.js": "./server.js",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "dependencies": {
+ "@vitejs/plugin-vue": "^2.2.0"
+ },
+ "devDependencies": {
+ "astro": "workspace:*",
+ "astro-scripts": "workspace:*",
+ "vue": "^3.2.30"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.30"
+ },
+ "engines": {
+ "node": "^14.15.0 || >=16.0.0"
+ }
+}
diff --git a/packages/integrations/vue/server.js b/packages/integrations/vue/server.js
new file mode 100644
index 000000000..1ae2b757b
--- /dev/null
+++ b/packages/integrations/vue/server.js
@@ -0,0 +1,22 @@
+import { h, createSSRApp } from 'vue';
+import { renderToString } from 'vue/server-renderer';
+import StaticHtml from './static-html.js';
+
+function check(Component) {
+ return !!Component['ssrRender'];
+}
+
+async function renderToStaticMarkup(Component, props, children) {
+ const slots = {};
+ if (children != null) {
+ slots.default = () => h(StaticHtml, { value: children });
+ }
+ const app = createSSRApp({ render: () => h(Component, props, slots) });
+ const html = await renderToString(app);
+ return { html };
+}
+
+export default {
+ check,
+ renderToStaticMarkup,
+};
diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts
new file mode 100644
index 000000000..20adf0f66
--- /dev/null
+++ b/packages/integrations/vue/src/index.ts
@@ -0,0 +1,35 @@
+import type { AstroIntegration, AstroRenderer } from 'astro';
+import vue from '@vitejs/plugin-vue';
+
+function getRenderer(): AstroRenderer {
+ return {
+ name: '@astrojs/vue',
+ clientEntrypoint: '@astrojs/vue/client.js',
+ serverEntrypoint: '@astrojs/vue/server.js',
+ };
+}
+
+function getViteConfiguration() {
+ return {
+ optimizeDeps: {
+ include: ['@astrojs/vue/client.js', 'vue'],
+ exclude: ['@astrojs/vue/server.js'],
+ },
+ plugins: [vue()],
+ ssr: {
+ external: ['@vue/server-renderer'],
+ },
+ };
+}
+
+export default function (): AstroIntegration {
+ return {
+ name: '@astrojs/vue',
+ hooks: {
+ 'astro:config:setup': ({ addRenderer, updateConfig }) => {
+ addRenderer(getRenderer());
+ updateConfig({ vite: getViteConfiguration() });
+ },
+ },
+ };
+}
diff --git a/packages/integrations/vue/static-html.js b/packages/integrations/vue/static-html.js
new file mode 100644
index 000000000..ff1459b6f
--- /dev/null
+++ b/packages/integrations/vue/static-html.js
@@ -0,0 +1,27 @@
+import { h, defineComponent } from 'vue';
+
+/**
+ * Astro passes `children` as a string of HTML, so we need
+ * a wrapper `div` to render that content as VNodes.
+ *
+ * This is the Vue + JSX equivalent of using `<div v-html="value" />`
+ */
+const StaticHtml = defineComponent({
+ props: {
+ value: String,
+ },
+ setup({ value }) {
+ if (!value) return () => null;
+ return () => h('astro-fragment', { innerHTML: value });
+ },
+});
+
+/**
+ * Other frameworks have `shouldComponentUpdate` in order to signal
+ * that this subtree is entirely static and will not be updated
+ *
+ * Fortunately, Vue is smart enough to figure that out without any
+ * help from us, so this just works out of the box!
+ */
+
+export default StaticHtml;
diff --git a/packages/integrations/vue/tsconfig.json b/packages/integrations/vue/tsconfig.json
new file mode 100644
index 000000000..44baf375c
--- /dev/null
+++ b/packages/integrations/vue/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "module": "ES2020",
+ "outDir": "./dist",
+ "target": "ES2020"
+ }
+}