diff options
author | 2022-03-18 15:35:45 -0700 | |
---|---|---|
committer | 2022-03-18 15:35:45 -0700 | |
commit | 6386c14d00d1d820804f0ee5b1424e73c049fe83 (patch) | |
tree | 3015e834e1d84100fd0871f6a55479bed61c0c14 /packages/integrations | |
parent | 0f376a7c52d3a22ff32b33e0afc34dd306ed70c4 (diff) | |
download | astro-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')
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" + } +} |