summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Drew Powers <1369770+drwpow@users.noreply.github.com> 2021-05-06 10:38:53 -0600
committerGravatar GitHub <noreply@github.com> 2021-05-06 10:38:53 -0600
commitb81abd5b2c0e68f05c99eaad54eb5b9a6bc092db (patch)
treee21c6c1318b5bf2cf7c6cb74290a6262fdab6e22
parent64f4f74fb641194c12edf3888a55a3359c9f594f (diff)
downloadastro-b81abd5b2c0e68f05c99eaad54eb5b9a6bc092db.tar.gz
astro-b81abd5b2c0e68f05c99eaad54eb5b9a6bc092db.tar.zst
astro-b81abd5b2c0e68f05c99eaad54eb5b9a6bc092db.zip
Add CSS bundling (#172)
* Add CSS bundling * Add Changeset * Update build script * Count better * Fix stats * Cleanup * Add test * Show profile ms under 750ms
-rw-r--r--.changeset/great-cats-train.md5
-rw-r--r--examples/blog/package.json2
-rw-r--r--examples/kitchen-sink/package.json2
-rw-r--r--examples/portfolio/package.json2
-rw-r--r--examples/snowpack/package.json4
-rw-r--r--examples/tailwindcss/package.json2
-rw-r--r--package.json6
-rw-r--r--packages/astro/package.json7
-rw-r--r--packages/astro/src/@types/astro.ts30
-rw-r--r--packages/astro/src/@types/shorthash.d.ts5
-rw-r--r--packages/astro/src/build.ts457
-rw-r--r--packages/astro/src/build/bundle/css.ts139
-rw-r--r--packages/astro/src/build/bundle/js.ts95
-rw-r--r--packages/astro/src/build/page.ts (renamed from packages/astro/src/build/bundle.ts)276
-rw-r--r--packages/astro/src/build/sitemap.ts26
-rw-r--r--packages/astro/src/build/static.ts28
-rw-r--r--packages/astro/src/build/stats.ts48
-rw-r--r--packages/astro/src/build/util.ts39
-rw-r--r--packages/astro/test/astro-css-bundling.test.js50
-rw-r--r--packages/astro/test/astro-rss.test.js10
-rw-r--r--packages/astro/test/astro-sitemap.test.js14
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro9
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/css/colors.css4
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/css/page-index.css3
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/css/page-one.css3
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/css/page-two.css3
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/css/typography.css6
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro15
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro14
-rw-r--r--packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro15
-rw-r--r--packages/astro/test/helpers.js2
-rw-r--r--packages/create-astro/src/templates/blank/package.json2
-rw-r--r--packages/create-astro/src/templates/starter/package.json2
-rw-r--r--tools/astro-languageserver/package.json2
-rw-r--r--www/package.json2
-rw-r--r--yarn.lock132
36 files changed, 891 insertions, 570 deletions
diff --git a/.changeset/great-cats-train.md b/.changeset/great-cats-train.md
new file mode 100644
index 000000000..35ce4a4df
--- /dev/null
+++ b/.changeset/great-cats-train.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Add CSS bundling
diff --git a/examples/blog/package.json b/examples/blog/package.json
index 15b0209b1..20f97487c 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -8,7 +8,7 @@
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'"
},
"devDependencies": {
- "astro": "0.0.11",
+ "astro": "^0.0.12",
"nodemon": "^2.0.7"
},
"snowpack": {
diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json
index ba35948fb..c2a99adb7 100644
--- a/examples/kitchen-sink/package.json
+++ b/examples/kitchen-sink/package.json
@@ -8,7 +8,7 @@
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'"
},
"devDependencies": {
- "astro": "0.0.11",
+ "astro": "^0.0.12",
"nodemon": "^2.0.7"
},
"snowpack": {
diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json
index b3567282d..ffe44b97a 100644
--- a/examples/portfolio/package.json
+++ b/examples/portfolio/package.json
@@ -8,7 +8,7 @@
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'"
},
"devDependencies": {
- "astro": "0.0.11"
+ "astro": "^0.0.12"
},
"snowpack": {
"workspaceRoot": "../.."
diff --git a/examples/snowpack/package.json b/examples/snowpack/package.json
index 0b7dbfd90..8a095b13f 100644
--- a/examples/snowpack/package.json
+++ b/examples/snowpack/package.json
@@ -12,7 +12,7 @@
"lint": "prettier --check \"src/**/*.js\""
},
"dependencies": {
- "astro": "0.0.11",
+ "astro": "^0.0.12",
"date-fns": "^2.19.0",
"deepmerge": "^4.2.2",
"docsearch.js": "^2.6.3",
@@ -26,7 +26,7 @@
"@11ty/eleventy-plugin-syntaxhighlight": "^3.0.4",
"@contentful/rich-text-html-renderer": "^14.1.2",
"@contentful/rich-text-types": "^14.1.2",
- "astro": "0.0.11",
+ "astro": "^0.0.12",
"eleventy-plugin-nesting-toc": "^1.2.0",
"luxon": "^1.25.0",
"markdown-it": "^12.0.2",
diff --git a/examples/tailwindcss/package.json b/examples/tailwindcss/package.json
index 7889890db..c00350964 100644
--- a/examples/tailwindcss/package.json
+++ b/examples/tailwindcss/package.json
@@ -8,7 +8,7 @@
"astro-dev": "nodemon --delay 0.5 -w ../../packages/astro/dist -x '../../packages/astro/astro.mjs dev'"
},
"devDependencies": {
- "astro": "0.0.11",
+ "astro": "^0.0.12",
"tailwindcss": "^2.1.1"
},
"snowpack": {
diff --git a/package.json b/package.json
index 314e216fd..9ad0ee961 100644
--- a/package.json
+++ b/package.json
@@ -32,18 +32,18 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.18.0",
- "cheerio": "^1.0.0-rc.5",
+ "cheerio": "^1.0.0-rc.6",
"cheerio-select-tmp": "^0.1.1",
"del": "^6.0.0",
+ "esbuild": "^0.11.17",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"execa": "^5.0.0",
"lerna": "^4.0.0",
"prettier": "^2.2.1",
- "tiny-glob": "^0.2.8",
- "esbuild": "^0.11.17",
"svelte": "^3.38.0",
+ "tiny-glob": "^0.2.8",
"typescript": "^4.2.4",
"uvu": "^0.5.1"
},
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 81c5e0645..59505a3f9 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -43,8 +43,8 @@
"astro-parser": "0.0.9",
"astro-prism": "0.0.2",
"autoprefixer": "^10.2.5",
- "cheerio": "^1.0.0-rc.5",
- "domhandler": "^4.1.0",
+ "cheerio": "^1.0.0-rc.6",
+ "del": "^6.0.0",
"es-module-lexer": "^0.4.1",
"esbuild": "^0.10.1",
"estree-walker": "^3.0.0",
@@ -62,6 +62,7 @@
"micromark-extension-gfm": "^0.3.3",
"micromark-extension-mdx-expression": "^0.3.2",
"micromark-extension-mdx-jsx": "^0.3.3",
+ "mime": "^2.5.2",
"moize": "^6.0.1",
"node-fetch": "^2.6.1",
"picomatch": "^2.2.3",
@@ -76,6 +77,7 @@
"rollup": "^2.43.1",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.32.8",
+ "shorthash": "^0.0.2",
"snowpack": "^3.3.7",
"source-map-support": "^0.5.19",
"string-width": "^5.0.0",
@@ -92,6 +94,7 @@
"@types/babel__traverse": "^7.11.1",
"@types/estree": "0.0.46",
"@types/github-slugger": "^1.3.0",
+ "@types/mime": "^2.0.3",
"@types/node": "^14.14.31",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.2",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 049105970..df7dbc4d6 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -63,6 +63,36 @@ export type RuntimeMode = 'development' | 'production';
export type Params = Record<string, string | number>;
+/** Entire output of `astro build`, stored in memory */
+export interface BuildOutput {
+ [dist: string]: BuildFile;
+}
+
+export interface BuildFile {
+ /** The original location. Needed for code frame errors. */
+ srcPath: URL;
+ /** File contents */
+ contents: string | Buffer;
+ /** File content type (to determine encoding, etc) */
+ contentType: string;
+ /** Encoding */
+ encoding?: 'utf8';
+}
+
+/** Mapping of every URL and its required assets. All URLs are absolute relative to the project. */
+export type BundleMap = {
+ [pageUrl: string]: PageDependencies;
+};
+
+export interface PageDependencies {
+ /** JavaScript files needed for page. No distinction between blocking/non-blocking or sync/async. */
+ js: Set<string>;
+ /** CSS needed for page, whether imported via <link>, JS, or Astro component. */
+ css: Set<string>;
+ /** Images needed for page. Can be loaded via CSS, <link>, or otherwise. */
+ images: Set<string>;
+}
+
export interface CreateCollection<T = any> {
data: ({ params }: { params: Params }) => T[];
routes?: Params[];
diff --git a/packages/astro/src/@types/shorthash.d.ts b/packages/astro/src/@types/shorthash.d.ts
new file mode 100644
index 000000000..02eb5ba51
--- /dev/null
+++ b/packages/astro/src/@types/shorthash.d.ts
@@ -0,0 +1,5 @@
+declare module 'shorthash' {
+ function unique(string: string): string;
+
+ export default { unique };
+}
diff --git a/packages/astro/src/build.ts b/packages/astro/src/build.ts
index a09043db7..9661b5cc4 100644
--- a/packages/astro/src/build.ts
+++ b/packages/astro/src/build.ts
@@ -1,40 +1,24 @@
import 'source-map-support/register.js';
-import type { AstroConfig, RuntimeMode } from './@types/astro';
+import type { AstroConfig, BundleMap, BuildOutput, RuntimeMode, PageDependencies } from './@types/astro';
import type { LogOptions } from './logger';
-import type { AstroRuntime, LoadResult } from './runtime';
-import { existsSync, promises as fsPromises } from 'fs';
-import { bold, green, yellow } from 'kleur/colors';
+import fs from 'fs';
import path from 'path';
-import cheerio from 'cheerio';
import { fileURLToPath } from 'url';
+import { performance } from 'perf_hooks';
+import cheerio from 'cheerio';
+import del from 'del';
+import { bold, green, yellow } from 'kleur/colors';
+import mime from 'mime';
import { fdir } from 'fdir';
-import { defaultLogDestination, error, info, trapWarn } from './logger.js';
-import { createRuntime } from './runtime.js';
-import { bundle, collectDynamicImports } from './build/bundle.js';
-import { generateRSS } from './build/rss.js';
+import { bundleCSS } from './build/bundle/css.js';
+import { bundleJS, collectJSImports } from './build/bundle/js';
+import { buildCollectionPage, buildStaticPage, getPageType } from './build/page.js';
import { generateSitemap } from './build/sitemap.js';
-import { collectStatics } from './build/static.js';
-import { canonicalURL } from './build/util.js';
-import { createURLStats, mapBundleStatsToURLStats, logURLStats } from './build/stats.js';
-
-const { mkdir, readFile, writeFile } = fsPromises;
-
-interface PageBuildOptions {
- astroRoot: URL;
- dist: URL;
- filepath: URL;
- runtime: AstroRuntime;
- site?: string;
- sitemap: boolean;
- statics: Set<string>;
-}
-
-interface PageResult {
- canonicalURLs: string[];
- rss?: string;
- statusCode: number;
-}
+import { logURLStats, collectBundleStats, mapBundleStatsToURLStats } from './build/stats.js';
+import { getDistPath, sortSet, stopTimer } from './build/util.js';
+import { debug, defaultLogDestination, error, info, trapWarn } from './logger.js';
+import { createRuntime } from './runtime.js';
const logging: LogOptions = {
level: 'debug',
@@ -51,145 +35,20 @@ async function allPages(root: URL) {
return files as string[];
}
-/** Utility for merging two Set()s */
-function mergeSet(a: Set<string>, b: Set<string>) {
- for (let str of b) {
- a.add(str);
- }
- return a;
-}
-
-/** Utility for writing to file (async) */
-async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf8' | null) {
- const outFolder = new URL('./', outPath);
- await mkdir(outFolder, { recursive: true });
- await writeFile(outPath, bytes, encoding || 'binary');
-}
-
-interface WriteResultOptions {
- srcPath: string;
- result: LoadResult;
- outPath: URL;
- encoding: null | 'utf8';
-}
-
-/** Utility for writing a build result to disk */
-async function writeResult({ srcPath, result, outPath, encoding }: WriteResultOptions) {
- if (result.statusCode === 500 || result.statusCode === 404) {
- error(logging, 'build', ` Failed to build ${srcPath}\n${' '.repeat(9)}`, result.error?.message ?? `Unexpected load result (${result.statusCode})`);
- } else if (result.statusCode !== 200) {
- error(logging, 'build', ` Failed to build ${srcPath}\n${' '.repeat(9)}`, `Unexpected load result (${result.statusCode}) for ${fileURLToPath(outPath)}`);
- } else {
- const bytes = result.contents;
- await writeFilep(outPath, bytes, encoding);
- }
-}
-
-/** Collection utility */
-function getPageType(filepath: URL): 'collection' | 'static' {
- if (/\$[^.]+.astro$/.test(filepath.pathname)) return 'collection';
- return 'static';
-}
-
-/** Build collection */
-async function buildCollectionPage({ astroRoot, dist, filepath, runtime, site, statics }: PageBuildOptions): Promise<PageResult> {
- const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro
- const pagePath = `/${rel.replace(/\$([^.]+)\.astro$/, '$1')}`;
- const srcPath = fileURLToPath(new URL('pages/' + rel, astroRoot));
- const builtURLs = new Set<string>(); // !important: internal cache that prevents building the same URLs
-
- /** Recursively build collection URLs */
- async function loadCollection(url: string): Promise<LoadResult | undefined> {
- if (builtURLs.has(url)) return; // this stops us from recursively building the same pages over and over
- const result = await runtime.load(url);
- builtURLs.add(url);
- if (result.statusCode === 200) {
- const outPath = new URL('./' + url + '/index.html', dist);
- await writeResult({ srcPath, result, outPath, encoding: 'utf8' });
- mergeSet(statics, collectStatics(result.contents.toString('utf8')));
- }
- return result;
- }
-
- const result = (await loadCollection(pagePath)) as LoadResult;
-
- if (result.statusCode >= 500) {
- throw new Error((result as any).error);
- }
- if (result.statusCode === 200 && !result.collectionInfo) {
- throw new Error(`[${rel}]: Collection page must export createCollection() function`);
- }
-
- let rss: string | undefined;
-
- // note: for pages that require params (/tag/:tag), we will get a 404 but will still get back collectionInfo that tell us what the URLs should be
- if (result.collectionInfo) {
- // build subsequent pages
- await Promise.all(
- [...result.collectionInfo.additionalURLs].map(async (url) => {
- // for the top set of additional URLs, we render every new URL generated
- const addlResult = await loadCollection(url);
- builtURLs.add(url);
- if (addlResult && addlResult.collectionInfo) {
- // believe it or not, we may still have a few unbuilt pages left. this is our last crawl:
- await Promise.all([...addlResult.collectionInfo.additionalURLs].map(async (url2) => loadCollection(url2)));
- }
- })
- );
-
- if (result.collectionInfo.rss) {
- if (!site) throw new Error(`[${rel}] createCollection() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
- rss = generateRSS({ ...(result.collectionInfo.rss as any), site }, rel.replace(/\$([^.]+)\.astro$/, '$1'));
- }
- }
-
- return {
- canonicalURLs: [...builtURLs].filter((url) => !url.endsWith('/1')), // note: canonical URLs are controlled by the collection, so these are canonical (but exclude "/1" pages as those are duplicates of the index)
- statusCode: result.statusCode,
- rss,
- };
-}
-
-/** Build static page */
-async function buildStaticPage({ astroRoot, dist, filepath, runtime, sitemap, statics }: PageBuildOptions): Promise<PageResult> {
- const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro
- const pagePath = `/${rel.replace(/\.(astro|md)$/, '')}`;
- let canonicalURLs: string[] = [];
-
- let relPath = './' + rel.replace(/\.(astro|md)$/, '.html');
- if (!relPath.endsWith('index.html')) {
- relPath = relPath.replace(/\.html$/, '/index.html');
- }
-
- const srcPath = fileURLToPath(new URL('pages/' + rel, astroRoot));
- const outPath = new URL(relPath, dist);
- const result = await runtime.load(pagePath);
-
- await writeResult({ srcPath, result, outPath, encoding: 'utf8' });
-
- if (result.statusCode === 200) {
- mergeSet(statics, collectStatics(result.contents.toString('utf8')));
-
- // get Canonical URL (if user has specified one manually, use that)
- if (sitemap) {
- const $ = cheerio.load(result.contents);
- const canonicalTag = $('link[rel="canonical"]');
- canonicalURLs.push(canonicalTag.attr('href') || pagePath.replace(/index$/, ''));
- }
- }
-
- return {
- canonicalURLs,
- statusCode: result.statusCode,
- };
+/** Is this URL remote? */
+function isRemote(url: string) {
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) return true;
+ return false;
}
/** The primary build action */
export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const { projectRoot, astroRoot } = astroConfig;
- const pageRoot = new URL('./pages/', astroRoot);
- const componentRoot = new URL('./components/', astroRoot);
const dist = new URL(astroConfig.dist + '/', projectRoot);
+ const pageRoot = new URL('./pages/', astroRoot);
+ const buildState: BuildOutput = {};
+ const depTree: BundleMap = {};
+ const timer: Record<string, number> = {};
const runtimeLogging: LogOptions = {
level: 'error',
@@ -200,63 +59,34 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const runtime = await createRuntime(astroConfig, { mode, logging: runtimeLogging });
const { runtimeConfig } = runtime;
const { backendSnowpack: snowpack } = runtimeConfig;
- const resolvePackageUrl = (pkgName: string) => snowpack.getUrlForPackage(pkgName);
-
- const imports = new Set<string>();
- const statics = new Set<string>();
- const collectImportsOptions = { astroConfig, logging, resolvePackageUrl, mode };
-
- let builtURLs: string[] = [];
- let urlStats = createURLStats();
- let importsToUrl = new Map<string, Set<string>>();
const pages = await allPages(pageRoot);
+ // 0. erase build directory
+ await del(fileURLToPath(dist));
+
+ /**
+ * 1. Build Pages
+ * Source files are built in parallel and stored in memory. Most assets are also gathered here, too.
+ */
+ timer.build = performance.now();
try {
info(logging, 'build', yellow('! building pages...'));
- // Vue also console.warns, this silences it.
- const release = trapWarn();
+ const release = trapWarn(); // Vue also console.warns, this silences it.
await Promise.all(
pages.map(async (pathname) => {
const filepath = new URL(`file://${pathname}`);
-
- const pageType = getPageType(filepath);
- const pageOptions: PageBuildOptions = { astroRoot, dist, filepath, runtime, site: astroConfig.buildOptions.site, sitemap: astroConfig.buildOptions.sitemap, statics };
- let urls: string[];
- if (pageType === 'collection') {
- const { canonicalURLs, rss } = await buildCollectionPage(pageOptions);
- urls = canonicalURLs;
- builtURLs.push(...canonicalURLs);
- if (rss) {
- const basename = path
- .relative(fileURLToPath(astroRoot) + '/pages', pathname)
- .replace(/^\$/, '')
- .replace(/\.astro$/, '');
- await writeFilep(new URL(`file://${path.join(fileURLToPath(dist), 'feed', basename + '.xml')}`), rss, 'utf8');
- }
- } else {
- const { canonicalURLs } = await buildStaticPage(pageOptions);
- urls = canonicalURLs;
- builtURLs.push(...canonicalURLs);
- }
-
- const dynamicImports = await collectDynamicImports(filepath, collectImportsOptions);
- mergeSet(imports, dynamicImports);
-
- // Keep track of urls and dynamic imports for stats.
- for(const url of urls) {
- urlStats.set(url, {
- dynamicImports,
- stats: []
- });
- }
-
- for(let imp of dynamicImports) {
- if(!importsToUrl.has(imp)) {
- importsToUrl.set(imp, new Set<string>());
- }
- mergeSet(importsToUrl.get(imp)!, new Set(urls));
- }
+ const buildPage = getPageType(filepath) === 'collection' ? buildCollectionPage : buildStaticPage;
+ await buildPage({
+ astroConfig,
+ buildState,
+ filepath,
+ logging,
+ mode,
+ resolvePackageUrl: (pkgName: string) => snowpack.getUrlForPackage(pkgName),
+ runtime,
+ site: astroConfig.buildOptions.site,
+ });
})
);
info(logging, 'build', green('✔'), 'pages built.');
@@ -266,65 +96,182 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
await runtime.shutdown();
return 1;
}
-
- info(logging, 'build', yellow('! scanning pages...'));
- for (const pathname of await allPages(componentRoot)) {
- mergeSet(imports, await collectDynamicImports(new URL(`file://${pathname}`), collectImportsOptions));
- }
- info(logging, 'build', green('✔'), 'pages scanned.');
-
- if (imports.size > 0) {
- try {
- info(logging, 'build', yellow('! bundling client-side code.'));
- const bundleStats = await bundle(imports, { dist, runtime, astroConfig });
- mapBundleStatsToURLStats(urlStats, importsToUrl, bundleStats);
- info(logging, 'build', green('✔'), 'bundling complete.');
- } catch (err) {
- error(logging, 'build', err);
- await runtime.shutdown();
- return 1;
+ debug(logging, 'build', `built pages [${stopTimer(timer.build)}]`);
+
+ // after pages are built, build depTree
+ timer.deps = performance.now();
+ const scanPromises: Promise<void>[] = [];
+ for (const id of Object.keys(buildState)) {
+ if (buildState[id].contentType !== 'text/html') continue; // only scan HTML files
+ const pageDeps = findDeps(buildState[id].contents as string, {
+ astroConfig,
+ srcPath: buildState[id].srcPath,
+ });
+ depTree[id] = pageDeps;
+
+ // while scanning we will find some unbuilt files; make sure those are all built while scanning
+ for (const url of [...pageDeps.js, ...pageDeps.css, ...pageDeps.images]) {
+ if (!buildState[url])
+ scanPromises.push(
+ runtime.load(url).then((result) => {
+ if (result.statusCode !== 200) {
+ throw new Error((result as any).error); // there shouldn’t be a build error here
+ }
+ buildState[url] = {
+ srcPath: new URL(url, projectRoot),
+ contents: result.contents,
+ contentType: result.contentType || mime.getType(url) || '',
+ };
+ })
+ );
}
}
-
- for (let url of statics) {
- const outPath = new URL('.' + url, dist);
- const result = await runtime.load(url);
-
- await writeResult({ srcPath: url, result, outPath, encoding: null });
+ await Promise.all(scanPromises);
+ debug(logging, 'build', `scanned deps [${stopTimer(timer.deps)}]`);
+
+ /**
+ * 2. Bundling 1st Pass: In-memory
+ * Bundle CSS, and anything else that can happen in memory (for now, JS bundling happens after writing to disk)
+ */
+ info(logging, 'build', yellow('! optimizing css...'));
+ timer.prebundle = performance.now();
+ await Promise.all([
+ bundleCSS({ buildState, astroConfig, logging, depTree }).then(() => {
+ debug(logging, 'build', `bundled CSS [${stopTimer(timer.prebundle)}]`);
+ }),
+ // TODO: optimize images?
+ ]);
+ // TODO: minify HTML?
+ info(logging, 'build', green('✔'), 'css optimized.');
+
+ /**
+ * 3. Write to disk
+ * Also clear in-memory bundle
+ */
+ // collect stats output
+ const urlStats = await collectBundleStats(buildState, depTree);
+
+ // collect JS imports for bundling
+ const jsImports = await collectJSImports(buildState);
+
+ // write sitemap
+ if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) {
+ timer.sitemap = performance.now();
+ info(logging, 'build', yellow('! creating sitemap...'));
+ const sitemap = generateSitemap(buildState, astroConfig.buildOptions.site);
+ const sitemapPath = new URL('sitemap.xml', dist);
+ await fs.promises.mkdir(path.dirname(fileURLToPath(sitemapPath)), { recursive: true });
+ await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
+ info(logging, 'build', green('✔'), 'sitemap built.');
+ debug(logging, 'build', `built sitemap [${stopTimer(timer.sitemap)}]`);
+ } else if (astroConfig.buildOptions.sitemap) {
+ info(logging, 'tip', `Set "buildOptions.site" in astro.config.mjs to generate a sitemap.xml, or set "buildOptions.sitemap: false" to disable this message.`);
}
- if (existsSync(astroConfig.public)) {
+ timer.write = performance.now();
+
+ // write to disk and free up memory
+ await Promise.all(
+ Object.keys(buildState).map(async (id) => {
+ const outPath = new URL(`.${id}`, dist);
+ const parentDir = path.posix.dirname(fileURLToPath(outPath));
+ await fs.promises.mkdir(parentDir, { recursive: true });
+ await fs.promises.writeFile(outPath, buildState[id].contents, buildState[id].encoding);
+ delete buildState[id];
+ delete depTree[id];
+ })
+ );
+ debug(logging, 'build', `wrote files to disk [${stopTimer(timer.write)}]`);
+
+ /**
+ * 4. Copy Public Assets
+ */
+ if (fs.existsSync(astroConfig.public)) {
info(logging, 'build', yellow(`! copying public folder...`));
+ timer.public = performance.now();
const pub = astroConfig.public;
const publicFiles = (await new fdir().withFullPaths().crawl(fileURLToPath(pub)).withPromise()) as string[];
- for (const filepath of publicFiles) {
- const fileUrl = new URL(`file://${filepath}`);
- const rel = path.relative(pub.pathname, fileUrl.pathname);
- const outUrl = new URL('./' + rel, dist);
-
- const bytes = await readFile(fileUrl);
- await writeFilep(outUrl, bytes, null);
- }
+ await Promise.all(
+ publicFiles.map(async (filepath) => {
+ const fileUrl = new URL(`file://${filepath}`);
+ const rel = path.relative(fileURLToPath(pub), fileURLToPath(fileUrl));
+ const outPath = new URL(path.join('.', rel), dist);
+ await fs.promises.mkdir(path.dirname(fileURLToPath(outPath)), { recursive: true });
+ await fs.promises.copyFile(fileUrl, outPath);
+ })
+ );
+ debug(logging, 'build', `copied public folder [${stopTimer(timer.public)}]`);
info(logging, 'build', green('✔'), 'public folder copied.');
} else {
if (path.basename(astroConfig.public.toString()) !== 'public') {
info(logging, 'tip', yellow(`! no public folder ${astroConfig.public} found...`));
}
}
- // build sitemap
- if (astroConfig.buildOptions.sitemap && astroConfig.buildOptions.site) {
- info(logging, 'build', yellow('! creating a sitemap...'));
- const sitemap = generateSitemap(builtURLs.map((url) => ({ canonicalURL: canonicalURL(url, astroConfig.buildOptions.site) })));
- await writeFile(new URL('./sitemap.xml', dist), sitemap, 'utf8');
- info(logging, 'build', green('✔'), 'sitemap built.');
- } else if (astroConfig.buildOptions.sitemap) {
- info(logging, 'tip', `Set "buildOptions.site" in astro.config.mjs to generate a sitemap.xml`);
- }
- // Log in a table-like view.
- logURLStats(logging, urlStats, builtURLs);
+ /**
+ * 5. Bundling 2nd pass: On disk
+ * Bundle JS, which requires hard files to optimize
+ */
+ info(logging, 'build', yellow(`! bundling...`));
+ if (jsImports.size > 0) {
+ try {
+ timer.bundleJS = performance.now();
+ const jsStats = await bundleJS(jsImports, { dist: new URL(dist + '/', projectRoot), runtime });
+ mapBundleStatsToURLStats({ urlStats, depTree, bundleStats: jsStats });
+ debug(logging, 'build', `bundled JS [${stopTimer(timer.bundleJS)}]`);
+ info(logging, 'build', green(`✔`), 'bundling complete.');
+ } catch (err) {
+ error(logging, 'build', err);
+ await runtime.shutdown();
+ return 1;
+ }
+ }
+ /**
+ * 6. Print stats
+ */
+ logURLStats(logging, urlStats);
await runtime.shutdown();
info(logging, 'build', bold(green('▶ Build Complete!')));
return 0;
}
+
+/** Given an HTML string, collect <link> and <img> tags */
+export function findDeps(html: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL }): PageDependencies {
+ const pageDeps: PageDependencies = {
+ js: new Set<string>(),
+ css: new Set<string>(),
+ images: new Set<string>(),
+ };
+
+ const $ = cheerio.load(html);
+
+ $('script').each((i, el) => {
+ const src = $(el).attr('src');
+ if (src && !isRemote(src)) {
+ pageDeps.js.add(getDistPath(src, { astroConfig, srcPath }));
+ }
+ });
+
+ $('link[href]').each((i, el) => {
+ const href = $(el).attr('href');
+ if (href && !isRemote(href) && ($(el).attr('rel') === 'stylesheet' || $(el).attr('type') === 'text/css' || href.endsWith('.css'))) {
+ const dist = getDistPath(href, { astroConfig, srcPath });
+ pageDeps.css.add(dist);
+ }
+ });
+
+ $('img[src]').each((i, el) => {
+ const src = $(el).attr('src');
+ if (src && !isRemote(src)) {
+ pageDeps.images.add(getDistPath(src, { astroConfig, srcPath }));
+ }
+ });
+
+ // sort (makes things a bit more predictable)
+ pageDeps.js = sortSet(pageDeps.js);
+ pageDeps.css = sortSet(pageDeps.css);
+ pageDeps.images = sortSet(pageDeps.images);
+
+ return pageDeps;
+}
diff --git a/packages/astro/src/build/bundle/css.ts b/packages/astro/src/build/bundle/css.ts
new file mode 100644
index 000000000..11f978140
--- /dev/null
+++ b/packages/astro/src/build/bundle/css.ts
@@ -0,0 +1,139 @@
+import type { AstroConfig, BuildOutput, BundleMap } from '../../@types/astro';
+import type { LogOptions } from '../../logger.js';
+
+import { performance } from 'perf_hooks';
+import shorthash from 'shorthash';
+import cheerio from 'cheerio';
+import esbuild from 'esbuild';
+import { getDistPath, getSrcPath, stopTimer } from '../util.js';
+import { debug } from '../../logger.js';
+
+// config
+const COMMON_URL = `/_astro/common-[HASH].css`; // [HASH] will be replaced
+
+/**
+ * Bundle CSS
+ * For files within dep tree, find ways to combine them.
+ * Current logic:
+ * - If CSS appears across multiple pages, combine into `/_astro/common.css` bundle
+ * - Otherwise, combine page CSS into one request as `/_astro/[page].css` bundle
+ *
+ * This operation _should_ be relatively-safe to do in parallel with other bundling,
+ * assuming other bundling steps don’t touch CSS. While this step does modify HTML,
+ * it doesn’t keep anything in local memory so other processes may modify HTML too.
+ *
+ * This operation mutates the original references of the buildOutput not only for
+ * safety (prevents possible conflicts), but for efficiency.
+ */
+export async function bundleCSS({
+ astroConfig,
+ buildState,
+ logging,
+ depTree,
+}: {
+ astroConfig: AstroConfig;
+ buildState: BuildOutput;
+ logging: LogOptions;
+ depTree: BundleMap;
+}): Promise<void> {
+ const timer: Record<string, number> = {};
+ const cssMap = new Map<string, string>();
+
+ // 1. organize CSS into common or page-specific CSS
+ timer.bundle = performance.now();
+ for (const [pageUrl, { css }] of Object.entries(depTree)) {
+ for (const cssUrl of css.keys()) {
+ if (cssMap.has(cssUrl)) {
+ // scenario 1: if multiple URLs require this CSS, upgrade to common chunk
+ cssMap.set(cssUrl, COMMON_URL);
+ } else {
+ // scenario 2: otherwise, assume this CSS is page-specific
+ cssMap.set(cssUrl, '/_astro' + pageUrl.replace(/.html$/, '').replace(/^\./, '') + '-[HASH].css');
+ }
+ }
+ }
+
+ // 2. bundle
+ timer.bundle = performance.now();
+ await Promise.all(
+ Object.keys(buildState).map(async (id) => {
+ if (buildState[id].contentType !== 'text/css') return;
+
+ const newUrl = cssMap.get(id);
+ if (!newUrl) return;
+
+ // if new bundle, create
+ if (!buildState[newUrl]) {
+ buildState[newUrl] = {
+ srcPath: getSrcPath(id, { astroConfig }), // this isn’t accurate, but we can at least reference a file in the bundle
+ contents: '',
+ contentType: 'text/css',
+ encoding: 'utf8',
+ };
+ }
+
+ // append to bundle, delete old file
+ (buildState[newUrl] as any).contents += Buffer.isBuffer(buildState[id].contents) ? buildState[id].contents.toString('utf8') : buildState[id].contents;
+ delete buildState[id];
+ })
+ );
+ debug(logging, 'css', `bundled [${stopTimer(timer.bundle)}]`);
+
+ // 3. minify
+ timer.minify = performance.now();
+ await Promise.all(
+ Object.keys(buildState).map(async (id) => {
+ if (buildState[id].contentType !== 'text/css') return;
+ const { code } = await esbuild.transform(buildState[id].contents as string, {
+ loader: 'css',
+ minify: true,
+ });
+ buildState[id].contents = code;
+ })
+ );
+ debug(logging, 'css', `minified [${stopTimer(timer.minify)}]`);
+
+ // 4. determine hashes based on CSS content (deterministic), and update HTML <link> tags with final hashed URLs
+ timer.hashes = performance.now();
+ const cssHashes = new Map<string, string>();
+ for (const id of Object.keys(buildState)) {
+ if (!id.includes('[HASH].css')) continue; // iterate through buildState, looking to replace [HASH]
+
+ const hash = shorthash.unique(buildState[id].contents as string);
+ const newID = id.replace(/\[HASH\]/, hash);
+ cssHashes.set(id, newID);
+ buildState[newID] = buildState[id]; // copy ref without cloning to save memory
+ delete buildState[id]; // delete old ref
+ }
+ debug(logging, 'css', `built hashes [${stopTimer(timer.hashes)}]`);
+
+ // 5. update HTML <link> tags with final hashed URLs
+ timer.html = performance.now();
+ await Promise.all(
+ Object.keys(buildState).map(async (id) => {
+ if (buildState[id].contentType !== 'text/html') return;
+
+ const $ = cheerio.load(buildState[id].contents);
+ const pageCSS = new Set<string>(); // keep track of page-specific CSS so we remove dupes
+ $('link[href]').each((i, el) => {
+ const srcPath = getSrcPath(id, { astroConfig });
+ const oldHref = getDistPath($(el).attr('href') || '', { astroConfig, srcPath }); // note: this may be a relative URL; transform to absolute to find a buildOutput match
+ const newHref = cssMap.get(oldHref);
+ if (newHref) {
+ // note: link[href] will select too much, however, remote CSS and non-CSS link tags won’t be in cssMap
+ if (pageCSS.has(newHref)) {
+ $(el).remove(); // this is a dupe; remove
+ } else {
+ $(el).attr('href', cssHashes.get(newHref) || ''); // new CSS; update href (important! use cssHashes, not cssMap)
+ pageCSS.add(newHref);
+ }
+ // bonus: add [rel] and [type]. not necessary, but why not?
+ $(el).attr('rel', 'stylesheet');
+ $(el).attr('type', 'text/css');
+ }
+ });
+ (buildState[id] as any).contents = $.html(); // save updated HTML in global buildState
+ })
+ );
+ debug(logging, 'css', `parsed html [${stopTimer(timer.html)}]`);
+}
diff --git a/packages/astro/src/build/bundle/js.ts b/packages/astro/src/build/bundle/js.ts
new file mode 100644
index 000000000..0112f024a
--- /dev/null
+++ b/packages/astro/src/build/bundle/js.ts
@@ -0,0 +1,95 @@
+import type { InputOptions, OutputOptions, OutputChunk } from 'rollup';
+import type { BuildOutput } from '../../@types/astro';
+import type { AstroRuntime } from '../../runtime';
+
+import { fileURLToPath } from 'url';
+import { rollup } from 'rollup';
+import { terser } from 'rollup-plugin-terser';
+import { createBundleStats, addBundleStats, BundleStatsMap } from '../stats.js';
+
+interface BundleOptions {
+ dist: URL;
+ runtime: AstroRuntime;
+}
+
+/** Collect JS imports from build output */
+export function collectJSImports(buildState: BuildOutput): Set<string> {
+ const imports = new Set<string>();
+ for (const id of Object.keys(buildState)) {
+ if (buildState[id].contentType === 'application/javascript') imports.add(id);
+ }
+ return imports;
+}
+
+/** Bundle JS action */
+export async function bundleJS(imports: Set<string>, { runtime, dist }: BundleOptions): Promise<BundleStatsMap> {
+ const ROOT = 'astro:root';
+ const root = `
+ ${[...imports].map((url) => `import '${url}';`).join('\n')}
+`;
+
+ const inputOptions: InputOptions = {
+ input: [...imports],
+ plugins: [
+ {
+ name: 'astro:build',
+ resolveId(source: string, imported?: string) {
+ if (source === ROOT) {
+ return source;
+ }
+ if (source.startsWith('/')) {
+ return source;
+ }
+
+ if (imported) {
+ const outUrl = new URL(source, 'http://example.com' + imported);
+ return outUrl.pathname;
+ }
+
+ return null;
+ },
+ async load(id: string) {
+ if (id === ROOT) {
+ return root;
+ }
+
+ const result = await runtime.load(id);
+
+ if (result.statusCode !== 200) {
+ return null;
+ }
+
+ return result.contents.toString('utf-8');
+ },
+ },
+ ],
+ };
+
+ const build = await rollup(inputOptions);
+
+ const outputOptions: OutputOptions = {
+ dir: fileURLToPath(dist),
+ format: 'esm',
+ exports: 'named',
+ entryFileNames(chunk) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return chunk.facadeModuleId!.substr(1);
+ },
+ plugins: [
+ // We are using terser for the demo, but might switch to something else long term
+ // Look into that rather than adding options here.
+ terser(),
+ ],
+ };
+
+ const stats = createBundleStats();
+ const { output } = await build.write(outputOptions);
+ await Promise.all(
+ output.map(async (chunk) => {
+ const code = (chunk as OutputChunk).code || '';
+ await addBundleStats(stats, code, chunk.fileName);
+ })
+ );
+
+ return stats;
+}
diff --git a/packages/astro/src/build/bundle.ts b/packages/astro/src/build/page.ts
index c8bc37ece..89e2ac2a5 100644
--- a/packages/astro/src/build/bundle.ts
+++ b/packages/astro/src/build/page.ts
@@ -1,28 +1,139 @@
-import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from '../@types/astro';
import type { ImportDeclaration } from '@babel/types';
-import type { InputOptions, OutputOptions, OutputChunk } from 'rollup';
-import type { AstroRuntime } from '../runtime';
+import type { AstroConfig, BuildOutput, RuntimeMode, ValidExtensionPlugins } from '../@types/astro';
+import type { AstroRuntime, LoadResult } from '../runtime';
import type { LogOptions } from '../logger';
-import esbuild from 'esbuild';
-import { promises as fsPromises } from 'fs';
+import fs from 'fs';
+import path from 'path';
+import mime from 'mime';
import { fileURLToPath } from 'url';
+import babelParser from '@babel/parser';
import { parse } from 'astro-parser';
-import { transform } from '../compiler/transform/index.js';
-import { convertMdToAstroSource } from '../compiler/index.js';
-import { getAttrValue } from '../ast.js';
+import esbuild from 'esbuild';
import { walk } from 'estree-walker';
-import babelParser from '@babel/parser';
-import path from 'path';
-import { rollup } from 'rollup';
-import { terser } from 'rollup-plugin-terser';
-import { createBundleStats, addBundleStats } from './stats.js';
-
-const { transformSync } = esbuild;
-const { readFile } = fsPromises;
+import { generateRSS } from './rss.js';
+import { getAttrValue } from '../ast.js';
+import { convertMdToAstroSource } from '../compiler/index.js';
+import { transform } from '../compiler/transform/index.js';
type DynamicImportMap = Map<'vue' | 'react' | 'react-dom' | 'preact' | 'svelte', string>;
+interface PageBuildOptions {
+ astroConfig: AstroConfig;
+ buildState: BuildOutput;
+ logging: LogOptions;
+ filepath: URL;
+ mode: RuntimeMode;
+ resolvePackageUrl: (s: string) => Promise<string>;
+ runtime: AstroRuntime;
+ site?: string;
+}
+
+/** Collection utility */
+export function getPageType(filepath: URL): 'collection' | 'static' {
+ if (/\$[^.]+.astro$/.test(filepath.pathname)) return 'collection';
+ return 'static';
+}
+
+/** Build collection */
+export async function buildCollectionPage({ astroConfig, filepath, logging, mode, runtime, site, resolvePackageUrl, buildState }: PageBuildOptions): Promise<void> {
+ const rel = path.relative(fileURLToPath(astroConfig.astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro
+ const pagePath = `/${rel.replace(/\$([^.]+)\.astro$/, '$1')}`;
+ const srcPath = new URL('pages/' + rel, astroConfig.astroRoot);
+ const builtURLs = new Set<string>(); // !important: internal cache that prevents building the same URLs
+
+ /** Recursively build collection URLs */
+ async function loadCollection(url: string): Promise<LoadResult | undefined> {
+ if (builtURLs.has(url)) return; // this stops us from recursively building the same pages over and over
+ const result = await runtime.load(url);
+ builtURLs.add(url);
+ if (result.statusCode === 200) {
+ const outPath = path.posix.join('/', url, 'index.html');
+ buildState[outPath] = {
+ srcPath,
+ contents: result.contents,
+ contentType: 'text/html',
+ encoding: 'utf8',
+ };
+ }
+ return result;
+ }
+
+ const [result] = await Promise.all([
+ loadCollection(pagePath) as Promise<LoadResult>, // first run will always return a result so assert type here
+ gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }),
+ ]);
+
+ if (result.statusCode >= 500) {
+ throw new Error((result as any).error);
+ }
+ if (result.statusCode === 200 && !result.collectionInfo) {
+ throw new Error(`[${rel}]: Collection page must export createCollection() function`);
+ }
+
+ // note: for pages that require params (/tag/:tag), we will get a 404 but will still get back collectionInfo that tell us what the URLs should be
+ if (result.collectionInfo) {
+ // build subsequent pages
+ await Promise.all(
+ [...result.collectionInfo.additionalURLs].map(async (url) => {
+ // for the top set of additional URLs, we render every new URL generated
+ const addlResult = await loadCollection(url);
+ builtURLs.add(url);
+ if (addlResult && addlResult.collectionInfo) {
+ // believe it or not, we may still have a few unbuilt pages left. this is our last crawl:
+ await Promise.all([...addlResult.collectionInfo.additionalURLs].map(async (url2) => loadCollection(url2)));
+ }
+ })
+ );
+
+ if (result.collectionInfo.rss) {
+ if (!site) throw new Error(`[${rel}] createCollection() tried to generate RSS but "buildOptions.site" missing in astro.config.mjs`);
+ const rss = generateRSS({ ...(result.collectionInfo.rss as any), site }, rel.replace(/\$([^.]+)\.astro$/, '$1'));
+ const feedURL = path.posix.join('/feed', `${pagePath}.xml`);
+ buildState[feedURL] = {
+ srcPath,
+ contents: rss,
+ contentType: 'application/rss+xml',
+ encoding: 'utf8',
+ };
+ }
+ }
+}
+
+/** Build static page */
+export async function buildStaticPage({ astroConfig, buildState, filepath, logging, mode, resolvePackageUrl, runtime }: PageBuildOptions): Promise<void> {
+ const rel = path.relative(fileURLToPath(astroConfig.astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro
+ const pagePath = `/${rel.replace(/\.(astro|md)$/, '')}`;
+
+ let relPath = path.posix.join('/', rel.replace(/\.(astro|md)$/, '.html'));
+ if (!relPath.endsWith('index.html')) {
+ relPath = relPath.replace(/\.html$/, '/index.html');
+ }
+
+ const srcPath = new URL('pages/' + rel, astroConfig.astroRoot);
+
+ // build page in parallel with gathering runtimes
+ await Promise.all([
+ runtime.load(pagePath).then((result) => {
+ if (result.statusCode === 200) {
+ buildState[relPath] = { srcPath, contents: result.contents, contentType: 'text/html', encoding: 'utf8' };
+ }
+ }),
+ gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }),
+ ]);
+}
+
+/** Evaluate mustache expression (safely) */
+function compileExpressionSafe(raw: string): string {
+ let { code } = esbuild.transformSync(raw, {
+ loader: 'tsx',
+ jsxFactory: 'h',
+ jsxFragment: 'Fragment',
+ charset: 'utf8',
+ });
+ return code;
+}
+
/** Add framework runtimes when needed */
async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolvePackageUrl: (s: string) => Promise<string>): Promise<DynamicImportMap> {
const importMap: DynamicImportMap = new Map();
@@ -50,17 +161,6 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
return importMap;
}
-/** Evaluate mustache expression (safely) */
-function compileExpressionSafe(raw: string): string {
- let { code } = transformSync(raw, {
- loader: 'tsx',
- jsxFactory: 'h',
- jsxFragment: 'Fragment',
- charset: 'utf8',
- });
- return code;
-}
-
const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
'.jsx': 'react',
'.tsx': 'react',
@@ -68,39 +168,30 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
'.vue': 'vue',
};
-interface CollectDynamic {
- astroConfig: AstroConfig;
- resolvePackageUrl: (s: string) => Promise<string>;
- logging: LogOptions;
- mode: RuntimeMode;
-}
-
-/** Gather necessary framework runtimes for dynamic components */
-export async function collectDynamicImports(filename: URL, { astroConfig, logging, resolvePackageUrl, mode }: CollectDynamic) {
+/** Gather necessary framework runtimes (React, Vue, Svelte, etc.) for dynamic components */
+async function gatherRuntimes({ astroConfig, buildState, filepath, logging, resolvePackageUrl, mode, runtime }: PageBuildOptions) {
const imports = new Set<string>();
// Only astro files
- if (!filename.pathname.endsWith('.astro') && !filename.pathname.endsWith('.md')) {
+ if (!filepath.pathname.endsWith('.astro') && !filepath.pathname.endsWith('.md')) {
return imports;
}
const extensions = astroConfig.extensions || defaultExtensions;
- let source = await readFile(filename, 'utf-8');
- if (filename.pathname.endsWith('.md')) {
+ let source = await fs.promises.readFile(filepath, 'utf8');
+ if (filepath.pathname.endsWith('.md')) {
source = await convertMdToAstroSource(source);
}
- const ast = parse(source, {
- filename,
- });
+ const ast = parse(source, { filepath });
if (!ast.module) {
return imports;
}
await transform(ast, {
- filename: fileURLToPath(filename),
+ filename: fileURLToPath(filepath),
fileID: '',
compileOptions: {
astroConfig,
@@ -224,13 +315,13 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
}
for (const foundImport of matches.reverse()) {
const name = foundImport[1];
- appendImports(name, filename);
+ appendImports(name, filepath);
}
break;
}
case 'InlineComponent': {
if (/^[A-Z]/.test(node.name)) {
- appendImports(node.name, filename);
+ appendImports(node.name, filepath);
return;
}
@@ -240,82 +331,21 @@ export async function collectDynamicImports(filename: URL, { astroConfig, loggin
},
});
- return imports;
-}
-
-interface BundleOptions {
- runtime: AstroRuntime;
- dist: URL;
- astroConfig: AstroConfig;
-}
-
-/** The primary bundling/optimization action */
-export async function bundle(imports: Set<string>, { runtime, dist }: BundleOptions) {
- const ROOT = 'astro:root';
- const root = `
- ${[...imports].map((url) => `import '${url}';`).join('\n')}
- `;
-
- const inputOptions: InputOptions = {
- input: [...imports],
- plugins: [
- {
- name: 'astro:build',
- resolveId(source: string, imported?: string) {
- if (source === ROOT) {
- return source;
- }
- if (source.startsWith('/')) {
- return source;
- }
-
- if (imported) {
- const outUrl = new URL(source, 'http://example.com' + imported);
- return outUrl.pathname;
- }
-
- return null;
- },
- async load(id: string) {
- if (id === ROOT) {
- return root;
- }
-
- const result = await runtime.load(id);
-
- if (result.statusCode !== 200) {
- return null;
- }
-
- return result.contents.toString('utf-8');
- },
- },
- ],
- };
-
- const build = await rollup(inputOptions);
-
- const outputOptions: OutputOptions = {
- dir: fileURLToPath(dist),
- format: 'esm',
- exports: 'named',
- entryFileNames(chunk) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return chunk.facadeModuleId!.substr(1);
- },
- plugins: [
- // We are using terser for the demo, but might switch to something else long term
- // Look into that rather than adding options here.
- terser(),
- ],
- };
-
- const stats = createBundleStats();
- const {output} = await build.write(outputOptions);
- await Promise.all(output.map(async chunk => {
- const code = (chunk as OutputChunk).code || '';
- await addBundleStats(stats, code, chunk.fileName);
- }));
-
- return stats;
+ // add all imports to build output
+ [...imports].map(async (url) => {
+ // don’t end up in an infinite loop building same URLs over and over
+ const alreadyBuilt = buildState[url];
+ if (alreadyBuilt) return;
+
+ // add new results to buildState
+ const result = await runtime.load(url);
+ if (result.statusCode === 200) {
+ buildState[url] = {
+ srcPath: filepath,
+ contents: result.contents,
+ contentType: result.contentType || mime.getType(url) || '',
+ encoding: 'utf8',
+ };
+ }
+ });
}
diff --git a/packages/astro/src/build/sitemap.ts b/packages/astro/src/build/sitemap.ts
index 1cb3f3e40..5095019c7 100644
--- a/packages/astro/src/build/sitemap.ts
+++ b/packages/astro/src/build/sitemap.ts
@@ -1,14 +1,26 @@
-export interface PageMeta {
- /** (required) The canonical URL of the page */
- canonicalURL: string;
-}
+import type { BuildOutput } from '../@types/astro';
+
+import { canonicalURL } from './util';
/** Construct sitemap.xml given a set of URLs */
-export function generateSitemap(pages: PageMeta[]): string {
+export function generateSitemap(buildState: BuildOutput, site: string): string {
+ const pages: string[] = [];
+
+ // TODO: find way to respect <link rel="canonical"> URLs here
+ // TODO: find way to exclude pages from sitemap
+
+ // look through built pages, only add HTML
+ for (const id of Object.keys(buildState)) {
+ if (buildState[id].contentType !== 'text/html' || id.endsWith('/1/index.html')) continue; // note: exclude auto-generated "page 1" pages (duplicates of index)
+ let url = canonicalURL(id.replace(/index\.html$/, ''), site);
+ pages.push(url);
+ }
+
+ pages.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">`;
- pages.sort((a, b) => a.canonicalURL.localeCompare(b.canonicalURL, 'en', { numeric: true })); // sort alphabetically
for (const page of pages) {
- sitemap += `<url><loc>${page.canonicalURL}</loc></url>`;
+ sitemap += `<url><loc>${page}</loc></url>`;
}
sitemap += `</urlset>\n`;
return sitemap;
diff --git a/packages/astro/src/build/static.ts b/packages/astro/src/build/static.ts
deleted file mode 100644
index af99c33cb..000000000
--- a/packages/astro/src/build/static.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { Element } from 'domhandler';
-import cheerio from 'cheerio';
-
-/** Given an HTML string, collect <link> and <img> tags */
-export function collectStatics(html: string) {
- const statics = new Set<string>();
-
- const $ = cheerio.load(html);
-
- const append = (el: Element, attr: string) => {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const value: string = $(el).attr(attr)!;
- if (value.startsWith('http') || $(el).attr('rel') === 'alternate') {
- return;
- }
- statics.add(value);
- };
-
- $('link[href]').each((i, el) => {
- append(el, 'href');
- });
-
- $('img[src]').each((i, el) => {
- append(el, 'src');
- });
-
- return statics;
-}
diff --git a/packages/astro/src/build/stats.ts b/packages/astro/src/build/stats.ts
index e29409994..909994a4a 100644
--- a/packages/astro/src/build/stats.ts
+++ b/packages/astro/src/build/stats.ts
@@ -1,3 +1,4 @@
+import type { BuildOutput, BundleMap } from '../@types/astro';
import type { LogOptions } from '../logger';
import { info, table } from '../logger.js';
@@ -30,19 +31,48 @@ export async function addBundleStats(bundleStatsMap: BundleStatsMap, code: strin
bundleStatsMap.set(filename, {
size: Buffer.byteLength(code),
- gzipSize: gzsize
+ gzipSize: gzsize,
});
}
-export function mapBundleStatsToURLStats(urlStats: URLStatsMap, importsToUrl: Map<string, Set<string>>, bundleStats: BundleStatsMap) {
- for(let [imp, stats] of bundleStats) {
- for(let url of importsToUrl.get('/' + imp) || []) {
- urlStats.get(url)?.stats.push(stats);
+export function mapBundleStatsToURLStats({ urlStats, depTree, bundleStats }: { urlStats: URLStatsMap; depTree: BundleMap; bundleStats: BundleStatsMap }) {
+ for (let [srcPath, stats] of bundleStats) {
+ for (let url of urlStats.keys()) {
+ if (depTree[url] && depTree[url].js.has('/' + srcPath)) {
+ urlStats.get(url)?.stats.push(stats);
+ }
}
}
}
-export function logURLStats(logging: LogOptions, urlStats: URLStatsMap, builtURLs: string[]) {
+export async function collectBundleStats(buildState: BuildOutput, depTree: BundleMap): Promise<URLStatsMap> {
+ const urlStats = createURLStats();
+
+ await Promise.all(
+ Object.keys(buildState).map(async (id) => {
+ if (!depTree[id]) return;
+ const stats = await Promise.all(
+ [...depTree[id].js, ...depTree[id].css, ...depTree[id].images].map(async (url) => {
+ if (!buildState[url]) return undefined;
+ const stat = {
+ size: Buffer.byteLength(buildState[url].contents),
+ gzipSize: await gzipSize(buildState[url].contents),
+ };
+ return stat;
+ })
+ );
+ urlStats.set(id, {
+ dynamicImports: new Set<string>(),
+ stats: stats.filter((s) => !!s) as any,
+ });
+ })
+ );
+
+ return urlStats;
+}
+
+export function logURLStats(logging: LogOptions, urlStats: URLStatsMap) {
+ const builtURLs = [...urlStats.keys()].map((url) => url.replace(/index\.html$/, ''));
builtURLs.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
info(logging, null, '');
const log = table(logging, [60, 20]);
@@ -51,11 +81,11 @@ export function logURLStats(logging: LogOptions, urlStats: URLStatsMap, builtURL
const lastIndex = builtURLs.length - 1;
builtURLs.forEach((url, index) => {
const sep = index === 0 ? '┌' : index === lastIndex ? '└' : '├';
- const urlPart = (' ' + sep + ' ') + (url === '/' ? url : url + '/');
+ const urlPart = ' ' + sep + ' ' + url;
- const bytes = urlStats.get(url)?.stats.map(s => s.gzipSize).reduce((a, b) => a + b, 0) || 0;
+ const bytes = (urlStats.get(url) || urlStats.get(url + 'index.html'))?.stats.map((s) => s.gzipSize).reduce((a, b) => a + b, 0) || 0;
const kb = (bytes * 0.001).toFixed(2);
const sizePart = kb + ' kB';
log(info, urlPart, sizePart);
});
-} \ No newline at end of file
+}
diff --git a/packages/astro/src/build/util.ts b/packages/astro/src/build/util.ts
index 505e6f183..c22216388 100644
--- a/packages/astro/src/build/util.ts
+++ b/packages/astro/src/build/util.ts
@@ -1,4 +1,7 @@
+import type { AstroConfig } from '../@types/astro';
+
import path from 'path';
+import { fileURLToPath, URL } from 'url';
/** Normalize URL to its canonical form */
export function canonicalURL(url: string, base?: string): string {
@@ -7,3 +10,39 @@ export function canonicalURL(url: string, base?: string): string {
base
).href;
}
+
+/** Sort a Set */
+export function sortSet(set: Set<string>): Set<string> {
+ return new Set([...set].sort((a, b) => a.localeCompare(b, 'en', { numeric: true })));
+}
+
+/** Resolve final output URL */
+export function getDistPath(specifier: string, { astroConfig, srcPath }: { astroConfig: AstroConfig; srcPath: URL }): string {
+ if (specifier[0] === '/') return specifier; // assume absolute URLs are correct
+
+ const fileLoc = path.posix.join(path.posix.dirname(fileURLToPath(srcPath)), specifier);
+ const projectLoc = path.posix.relative(fileURLToPath(astroConfig.astroRoot), fileLoc);
+ const pagesDir = fileURLToPath(new URL('/pages', astroConfig.astroRoot));
+ // if this lives above src/pages, return that URL
+ if (fileLoc.includes(pagesDir)) {
+ const [, publicURL] = projectLoc.split(pagesDir);
+ return publicURL || '/index.html'; // if this is missing, this is the root
+ }
+ // otherwise, return /_astro/* url
+ return '/_astro/' + projectLoc;
+}
+
+/** Given a final output URL, guess at src path (may be inaccurate) */
+export function getSrcPath(url: string, { astroConfig }: { astroConfig: AstroConfig }): URL {
+ if (url.startsWith('/_astro/')) {
+ return new URL(url.replace(/^\/_astro\//, ''), astroConfig.astroRoot);
+ }
+ let srcFile = url.replace(/^\//, '').replace(/\/index.html$/, '.astro');
+ return new URL('./pages/' + srcFile, astroConfig.astroRoot);
+}
+
+/** Stop timer & format time for profiling */
+export function stopTimer(start: number): string {
+ const diff = performance.now() - start;
+ return diff < 750 ? `${Math.round(diff)}ms` : `${(diff / 1000).toFixed(1)}s`;
+}
diff --git a/packages/astro/test/astro-css-bundling.test.js b/packages/astro/test/astro-css-bundling.test.js
new file mode 100644
index 000000000..fe8d82c98
--- /dev/null
+++ b/packages/astro/test/astro-css-bundling.test.js
@@ -0,0 +1,50 @@
+import cheerio from 'cheerio';
+import { suite } from 'uvu';
+import * as assert from 'uvu/assert';
+import { setupBuild } from './helpers.js';
+
+const CSSBundling = suite('CSS Bundling');
+
+setupBuild(CSSBundling, './fixtures/astro-css-bundling');
+
+// note: the hashes should be deterministic, but updating the file contents will change hashes
+// be careful not to test that the HTML simply contains CSS, because it always will! filename and quanity matter here (bundling).
+const EXPECTED_CSS = {
+ '/index.html': ['/_astro/common-ZVuUT3.css', '/_astro/index-Z2jH7pc.css'],
+ '/one/index.html': ['/_astro/common-ZVuUT3.css', '/_astro/one/index-2qFtfN.css'],
+ '/two/index.html': ['/_astro/common-ZVuUT3.css', '/_astro/two/index-2jKE68.css'],
+};
+const UNEXPECTED_CSS = ['/_astro/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css'];
+
+CSSBundling('Bundles CSS', async (context) => {
+ await context.build();
+
+ const builtCSS = new Set();
+
+ // for all HTML files…
+ for (const [filepath, css] of Object.entries(EXPECTED_CSS)) {
+ const html = await context.readFile(filepath);
+ const $ = cheerio.load(html);
+
+ // test 1: assert new bundled CSS is present
+ for (const href of css) {
+ builtCSS.add(href);
+ const link = $(`link[href="${href}"]`);
+ assert.equal(link.length, 1);
+ }
+
+ // test 2: assert old CSS was removed
+ for (const href of UNEXPECTED_CSS) {
+ const link = $(`link[href="${href}"]`);
+ assert.equal(link.length, 0);
+ }
+ }
+
+ // test 3: assert all bundled CSS was built and contains CSS
+ for (const url of builtCSS.keys()) {
+ const css = await context.readFile(url);
+ assert.ok(css, true);
+ }
+});
+
+CSSBundling.run();
diff --git a/packages/astro/test/astro-rss.test.js b/packages/astro/test/astro-rss.test.js
index 1fc70a9a7..055150362 100644
--- a/packages/astro/test/astro-rss.test.js
+++ b/packages/astro/test/astro-rss.test.js
@@ -11,14 +11,8 @@ const snapshot =
']]></description><pubDate>Tue, 19 Oct 1999 00:00:00 GMT</pubDate><itunes:episodeType>music</itunes:episodeType><itunes:duration>259</itunes:duration><itunes:explicit>true</itunes:explicit></item></channel></rss>';
RSS('Generates RSS correctly', async (context) => {
- let rss;
- try {
- await context.build();
- rss = await context.readFile('/feed/episodes.xml');
- assert.ok(true, 'Build successful');
- } catch (err) {
- assert.ok(false, 'Build threw');
- }
+ await context.build();
+ let rss = await context.readFile('/feed/episodes.xml');
assert.match(rss, snapshot);
});
diff --git a/packages/astro/test/astro-sitemap.test.js b/packages/astro/test/astro-sitemap.test.js
index 5e47c5d81..778816929 100644
--- a/packages/astro/test/astro-sitemap.test.js
+++ b/packages/astro/test/astro-sitemap.test.js
@@ -6,18 +6,12 @@ const Sitemap = suite('Sitemap Generation');
setupBuild(Sitemap, './fixtures/astro-rss');
-const snapshot = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://mysite.dev/episode/fazers/</loc></url><url><loc>https://mysite.dev/episode/rap-snitch-knishes/</loc></url><url><loc>https://mysite.dev/episode/rhymes-like-dimes/</loc></url><url><loc>https://mysite.dev/episodes/</loc></url></urlset>`;
+const snapshot = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://mysite.dev/episode/fazers/</loc></url><url><loc>https://mysite.dev/episode/rap-snitch-knishes/</loc></url><url><loc>https://mysite.dev/episode/rhymes-like-dimes/</loc></url><url><loc>https://mysite.dev/episodes/</loc></url></urlset>\n`;
Sitemap('Generates Sitemap correctly', async (context) => {
- let rss;
- try {
- await context.build();
- rss = await context.readFile('/sitemap.xml');
- assert.ok(true, 'Build successful');
- } catch (err) {
- assert.ok(false, 'Build threw');
- }
- assert.match(rss, snapshot);
+ await context.build();
+ let sitemap = await context.readFile('/sitemap.xml');
+ assert.match(sitemap, snapshot);
});
Sitemap.run();
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro
new file mode 100644
index 000000000..deafe668c
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/components/Nav.astro
@@ -0,0 +1,9 @@
+<style>
+.nav {
+ display: block;
+}
+</style>
+
+<nav class=".nav">
+<a href="/">Home</a>
+</nav> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/css/colors.css b/packages/astro/test/fixtures/astro-css-bundling/src/css/colors.css
new file mode 100644
index 000000000..27bafe566
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/css/colors.css
@@ -0,0 +1,4 @@
+:root {
+ --brown: burlywood;
+ --red: crimson;
+}
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/css/page-index.css b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-index.css
new file mode 100644
index 000000000..b553e8e49
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-index.css
@@ -0,0 +1,3 @@
+.page__index {
+ background: var(--red);
+}
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/css/page-one.css b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-one.css
new file mode 100644
index 000000000..3e09a847f
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-one.css
@@ -0,0 +1,3 @@
+.page__one {
+ background: paleturquoise;
+}
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/css/page-two.css b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-two.css
new file mode 100644
index 000000000..dbef8bf34
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/css/page-two.css
@@ -0,0 +1,3 @@
+.page__two {
+ background: var(--brown);
+}
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/css/typography.css b/packages/astro/test/fixtures/astro-css-bundling/src/css/typography.css
new file mode 100644
index 000000000..810acd74a
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/css/typography.css
@@ -0,0 +1,6 @@
+/* Typography.css */
+
+body {
+ font-size: 16px;
+ font-family: sans-serif;
+}
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro
new file mode 100644
index 000000000..9479981c8
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro
@@ -0,0 +1,15 @@
+---
+import Nav from '../components/Nav.astro';
+---
+
+<html>
+ <head>
+ <link rel="stylesheet" href="../css/typography.css" />
+ <link rel="stylesheet" href="../css/colors.css" />
+ <link rel="stylesheet" href="../css/page-index.css" />
+ </head>
+ <body>
+ <Nav />
+ <h1>Index page</h1>
+ </body>
+</html>
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro
new file mode 100644
index 000000000..cd47f0b27
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro
@@ -0,0 +1,14 @@
+---
+import Nav from '../components/Nav.astro';
+---
+
+<html>
+ <head>
+ <link rel="stylesheet" href="../css/typography.css" />
+ <link rel="stylesheet" href="../css/page-one.css" />
+ </head>
+ <body>
+ <Nav />
+ <h1>Page One</h1>
+ </body>
+</html>
diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro
new file mode 100644
index 000000000..cc730f0d1
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro
@@ -0,0 +1,15 @@
+---
+import Nav from '../components/Nav.astro';
+---
+
+<html>
+ <head>
+ <link rel="stylesheet" href="../css/typography.css" />
+ <link rel="stylesheet" href="../css/colors.css" />
+ <link rel="stylesheet" href="../css/page-two.css" />
+ </head>
+ <body>
+ <Nav />
+ <h1>Page Two</h1>
+ </body>
+</html>
diff --git a/packages/astro/test/helpers.js b/packages/astro/test/helpers.js
index 3e8ed6e54..108383d2e 100644
--- a/packages/astro/test/helpers.js
+++ b/packages/astro/test/helpers.js
@@ -52,7 +52,7 @@ export function setupBuild(Suite, fixturePath) {
context.build = build;
context.readFile = async (path) => {
const resolved = fileURLToPath(new URL(`${fixturePath}/${astroConfig.dist}${path}`, import.meta.url));
- return readFile(resolved).then((r) => r.toString('utf-8'));
+ return readFile(resolved).then((r) => r.toString('utf8'));
};
});
diff --git a/packages/create-astro/src/templates/blank/package.json b/packages/create-astro/src/templates/blank/package.json
index e04205726..478970d63 100644
--- a/packages/create-astro/src/templates/blank/package.json
+++ b/packages/create-astro/src/templates/blank/package.json
@@ -6,6 +6,6 @@
"build": "astro build"
},
"devDependencies": {
- "astro": "0.0.9"
+ "astro": "^0.0.12"
}
}
diff --git a/packages/create-astro/src/templates/starter/package.json b/packages/create-astro/src/templates/starter/package.json
index e04205726..478970d63 100644
--- a/packages/create-astro/src/templates/starter/package.json
+++ b/packages/create-astro/src/templates/starter/package.json
@@ -6,6 +6,6 @@
"build": "astro build"
},
"devDependencies": {
- "astro": "0.0.9"
+ "astro": "^0.0.12"
}
}
diff --git a/tools/astro-languageserver/package.json b/tools/astro-languageserver/package.json
index 7572c450a..11f8b3a88 100644
--- a/tools/astro-languageserver/package.json
+++ b/tools/astro-languageserver/package.json
@@ -15,7 +15,7 @@
"dev": "astro-scripts dev 'src/index.ts'"
},
"devDependencies": {
- "astro": "0.0.9",
+ "astro": "^0.0.12",
"astro-scripts": "0.0.1"
},
"dependencies": {
diff --git a/www/package.json b/www/package.json
index 958448d89..06d8484fa 100644
--- a/www/package.json
+++ b/www/package.json
@@ -6,6 +6,6 @@
"build": "astro build ."
},
"devDependencies": {
- "astro": "0.0.11"
+ "astro": "^0.0.12"
}
}
diff --git a/yarn.lock b/yarn.lock
index 3937134a1..96abd37a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1529,6 +1529,11 @@
dependencies:
"@types/unist" "*"
+"@types/mime@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
+ integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
+
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.4"
resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz"
@@ -2204,117 +2209,6 @@ astral-regex@^2.0.0:
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
-astro@0.0.11:
- version "0.0.11"
- resolved "https://registry.yarnpkg.com/astro/-/astro-0.0.11.tgz#a028fdab35f05cf53309865facaf4b686435b4c5"
- integrity sha512-cTS1isXyeQct3G/PFgImhzVuJ6GzjKjjZCFVHfpWFRSF/nIH1kToU91VnAKuRbgMaOlPPQM7BI3LOvmwkxYMCA==
- dependencies:
- "@babel/code-frame" "^7.12.13"
- "@babel/generator" "^7.13.9"
- "@babel/parser" "^7.13.15"
- "@babel/traverse" "^7.13.15"
- "@snowpack/plugin-sass" "^1.4.0"
- "@snowpack/plugin-svelte" "^3.6.1"
- "@snowpack/plugin-vue" "^2.4.0"
- "@vue/server-renderer" "^3.0.10"
- acorn "^7.4.0"
- astro-parser "0.0.9"
- astro-prism "0.0.2"
- autoprefixer "^10.2.5"
- cheerio "^1.0.0-rc.5"
- domhandler "^4.1.0"
- es-module-lexer "^0.4.1"
- esbuild "^0.10.1"
- estree-walker "^3.0.0"
- fast-xml-parser "^3.19.0"
- fdir "^5.0.0"
- find-up "^5.0.0"
- github-slugger "^1.3.0"
- gray-matter "^4.0.2"
- gzip-size "^6.0.0"
- hast-to-hyperscript "~9.0.0"
- kleur "^4.1.4"
- locate-character "^2.0.5"
- magic-string "^0.25.3"
- micromark "^2.11.4"
- micromark-extension-gfm "^0.3.3"
- micromark-extension-mdx-expression "^0.3.2"
- micromark-extension-mdx-jsx "^0.3.3"
- moize "^6.0.1"
- node-fetch "^2.6.1"
- picomatch "^2.2.3"
- postcss "^8.2.8"
- postcss-icss-keyframes "^0.2.1"
- preact "^10.5.13"
- preact-render-to-string "^5.1.18"
- prismjs "^1.23.0"
- react "^17.0.1"
- react-dom "^17.0.1"
- rehype-parse "^7.0.1"
- rollup "^2.43.1"
- rollup-plugin-terser "^7.0.2"
- sass "^1.32.8"
- snowpack "^3.3.7"
- source-map-support "^0.5.19"
- string-width "^5.0.0"
- svelte "^3.35.0"
- unified "^9.2.1"
- vue "^3.0.10"
- yargs-parser "^20.2.7"
-
-astro@0.0.9:
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/astro/-/astro-0.0.9.tgz#c69e05e4d9ecd61f029833738548cd57d3d41933"
- integrity sha512-D+HEH854M22syvp57JT9CLpO2kU35pdYgttpYQiuAFTmZ0N+8r4QqEdOT3PaXAdue6JY0dybVTskVMQ9mK1QbA==
- dependencies:
- "@babel/code-frame" "^7.12.13"
- "@babel/generator" "^7.13.9"
- "@babel/parser" "^7.13.15"
- "@babel/traverse" "^7.13.15"
- "@snowpack/plugin-sass" "^1.4.0"
- "@snowpack/plugin-svelte" "^3.6.1"
- "@snowpack/plugin-vue" "^2.4.0"
- "@vue/server-renderer" "^3.0.10"
- acorn "^7.4.0"
- autoprefixer "^10.2.5"
- cheerio "^1.0.0-rc.5"
- es-module-lexer "^0.4.1"
- esbuild "^0.10.1"
- estree-walker "^3.0.0"
- fast-xml-parser "^3.19.0"
- fdir "^5.0.0"
- find-up "^5.0.0"
- github-slugger "^1.3.0"
- gray-matter "^4.0.2"
- hast-to-hyperscript "^9.0.1"
- kleur "^4.1.4"
- locate-character "^2.0.5"
- magic-string "^0.25.3"
- micromark "^2.11.4"
- micromark-extension-gfm "^0.3.3"
- micromark-extension-mdx-expression "^0.3.2"
- micromark-extension-mdx-jsx "^0.3.3"
- moize "^6.0.1"
- node-fetch "^2.6.1"
- picomatch "^2.2.3"
- postcss "^8.2.8"
- postcss-icss-keyframes "^0.2.1"
- preact "^10.5.13"
- preact-render-to-string "^5.1.18"
- prismjs "^1.23.0"
- react "^17.0.1"
- react-dom "^17.0.1"
- rehype-parse "^7.0.1"
- rollup "^2.43.1"
- rollup-plugin-terser "^7.0.2"
- sass "^1.32.8"
- snowpack "^3.3.7"
- svelte "^3.35.0"
- tiny-glob "^0.2.8"
- unified "^9.2.1"
- vue "^3.0.10"
- yargs-parser "^20.2.7"
-
async-each-series@0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz"
@@ -3000,7 +2894,7 @@ cheerio-select@^1.3.0:
domhandler "^4.2.0"
domutils "^2.6.0"
-cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.5:
+cheerio@^1.0.0-rc.3, cheerio@^1.0.0-rc.6:
version "1.0.0-rc.6"
resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.6.tgz"
integrity sha512-hjx1XE1M/D5pAtMgvWwE21QClmAEeGHOIDfycgmndisdNgI6PE1cGRQkMGBcsbUbmEQyWu5PJLUcAOjtQS8DWw==
@@ -3877,7 +3771,7 @@ del@^2.2.0:
del@^6.0.0:
version "6.0.0"
- resolved "https://registry.npmjs.org/del/-/del-6.0.0.tgz"
+ resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952"
integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==
dependencies:
globby "^11.0.1"
@@ -5556,7 +5450,7 @@ hash-sum@^2.0.0:
resolved "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz"
integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
-hast-to-hyperscript@^9.0.1, hast-to-hyperscript@~9.0.0:
+hast-to-hyperscript@~9.0.0:
version "9.0.1"
resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz"
integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==
@@ -7519,6 +7413,11 @@ mime@1.4.1:
resolved "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
+mime@^2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
+ integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
+
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
@@ -10194,6 +10093,11 @@ shelljs@^0.8.3:
interpret "^1.0.0"
rechoir "^0.6.2"
+shorthash@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/shorthash/-/shorthash-0.0.2.tgz#59b268eecbde59038b30da202bcfbddeb2c4a4eb"
+ integrity sha1-WbJo7sveWQOLMNogK8+93rLEpOs=
+
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"