Refactor: remove all legacy build logic from codebase (#3141)

* refactor: remove legacy build flag

* refactor: remove legacy style maps

* chore: changeset

* refactor: nuke vite-plugin-build-html

* deps: remove parse5

* tests: restore user provided doctype support!

* deps: remove @web/parse5-utils

* deps: change srcset-parse to dev dep

* chore: remove unused utils

* refactor: remove unused style mapping

* unrelated fix: add .test to astro-markdown-css

* refactor: remove unused astro-style with test update

* chore: remove unused buildTime var
This commit is contained in:
Ben Holmes 2022-04-19 13:52:15 -04:00 committed by GitHub
parent 9e00f6d546
commit 0247b54270
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 55 additions and 1086 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Astro internals: remove all legacy build logic from the codebase, now that the legacy build flag has been removed

View file

@ -86,7 +86,6 @@
"@babel/traverse": "^7.17.9",
"@proload/core": "^0.2.2",
"@proload/plugin-tsm": "^0.1.1",
"@web/parse5-utils": "^1.3.0",
"ast-types": "^0.14.2",
"boxen": "^6.2.1",
"ci-info": "^3.3.0",
@ -109,7 +108,6 @@
"micromorph": "^0.1.2",
"mime": "^3.0.0",
"ora": "^6.1.0",
"parse5": "^6.0.1",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.0",
"postcss": "^8.4.12",
@ -128,7 +126,6 @@
"sirv": "^2.0.2",
"slash": "^4.0.0",
"sourcemap-codec": "^1.4.8",
"srcset-parse": "^1.1.0",
"string-width": "^5.1.2",
"strip-ansi": "^7.0.1",
"supports-esm": "^1.0.0",
@ -163,7 +160,8 @@
"chai": "^4.3.6",
"cheerio": "^1.0.0-rc.10",
"mocha": "^9.2.2",
"sass": "^1.50.0"
"sass": "^1.50.0",
"srcset-parse": "^1.1.0"
},
"engines": {
"node": "^14.15.0 || >=16.0.0",

View file

@ -983,7 +983,6 @@ export interface SSRElement {
export interface SSRMetadata {
renderers: SSRLoadedRenderer[];
pathname: string;
legacyBuild: boolean;
}
export interface SSRResult {

View file

@ -48,7 +48,6 @@ function printAstroHelp() {
['--host [optional IP]', 'Expose server on network'],
['--config <path>', 'Specify the path to the Astro config file.'],
['--root <path>', 'Specify the path to the project root folder.'],
['--legacy-build', 'Use the build strategy prior to 0.24.0'],
['--drafts', 'Include markdown draft pages in the build.'],
['--verbose', 'Enable verbose logging'],
['--silent', 'Disable logging'],

View file

@ -78,7 +78,6 @@ export class App {
const scripts = createModuleScriptElementWithSrcSet(info.scripts, manifest.site);
const result = await render({
legacyBuild: false,
links,
logging: this.#logging,
markdown: manifest.markdown,

View file

@ -193,7 +193,6 @@ async function generatePath(
const ssr = isBuildingToSSR(opts.astroConfig);
const url = new URL(opts.astroConfig.base + removeLeadingForwardSlash(pathname), origin);
const options: RenderOptions = {
legacyBuild: false,
links,
logging,
markdown: astroConfig.markdown,

View file

@ -213,7 +213,6 @@ class AstroBuilder {
pageCount: number;
buildMode: 'static' | 'ssr';
}) {
const buildTime = performance.now() - timeStart;
const total = getTimeStat(timeStart, performance.now());
let messages: string[] = [];

View file

@ -30,24 +30,6 @@ export interface BuildInternals {
* A map for page-specific information by a client:only component
*/
pagesByClientOnly: Map<string, Set<PageBuildData>>;
/**
* chunkToReferenceIdMap maps them to a hash id used to find the final file.
* @deprecated This Map is only used for the legacy build.
*/
chunkToReferenceIdMap: Map<string, string>;
/**
* This is a mapping of pathname to the string source of all collected inline <style> for a page.
* @deprecated This Map is only used for the legacy build.
*/
astroStyleMap: Map<string, string>;
/**
* This is a virtual JS module that imports all dependent styles for a page.
* @deprecated This Map is only used for the legacy build.
*/
astroPageStyleMap: Map<string, string>;
}
/**
@ -71,9 +53,6 @@ export function createBuildInternals(): BuildInternals {
return {
pureCSSChunks,
chunkToReferenceIdMap,
astroStyleMap,
astroPageStyleMap,
hoistedScriptIdToHoistedMap,
entrySpecifierToBundleMap: new Map<string, string>(),

View file

@ -151,7 +151,6 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
vitePluginPages(opts, internals),
rollupPluginAstroBuildCSS({
internals,
legacy: false,
target: 'server',
}),
...(viteConfig.plugins || []),
@ -222,7 +221,6 @@ async function clientBuild(
vitePluginHoistedScripts(astroConfig, internals),
rollupPluginAstroBuildCSS({
internals,
legacy: false,
target: 'client',
}),
...(viteConfig.plugins || []),

View file

@ -38,11 +38,3 @@ export function startsWithDotSlash(path: string) {
export function isRelativePath(path: string) {
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
}
export function prependDotSlash(path: string) {
if (isRelativePath(path)) {
return path;
}
return './' + path;
}

View file

@ -66,7 +66,6 @@ export async function getParamsAndProps(
}
export interface RenderOptions {
legacyBuild: boolean;
logging: LogOptions;
links: Set<SSRElement>;
markdown: MarkdownRenderingOptions;
@ -87,7 +86,6 @@ export async function render(
opts: RenderOptions
): Promise<{ type: 'html'; html: string } | { type: 'response'; response: Response }> {
const {
legacyBuild,
links,
logging,
origin,
@ -128,7 +126,6 @@ export async function render(
throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
const result = createResult({
legacyBuild,
links,
logging,
markdown,

View file

@ -102,18 +102,13 @@ export async function render(
routeCache,
viteServer,
} = ssrOpts;
// TODO: clean up "legacy" flag passed through helper functions
const isLegacyBuild = false;
// Add hoisted script tags
const scripts = createModuleScriptElementWithSrcSet(
!isLegacyBuild && mod.hasOwnProperty('$$metadata')
? Array.from(mod.$$metadata.hoistedScriptPaths())
: []
mod.hasOwnProperty('$$metadata') ? Array.from(mod.$$metadata.hoistedScriptPaths()) : []
);
// Inject HMR scripts
if (mod.hasOwnProperty('$$metadata') && mode === 'development' && !isLegacyBuild) {
if (mod.hasOwnProperty('$$metadata') && mode === 'development') {
scripts.add({
props: { type: 'module', src: '/@vite/client' },
children: '',
@ -138,29 +133,25 @@ export async function render(
// Pass framework CSS in as link tags to be appended to the page.
let links = new Set<SSRElement>();
if (!isLegacyBuild) {
[...(await getStylesForURL(filePath, viteServer))].forEach((href) => {
if (mode === 'development' && svelteStylesRE.test(href)) {
scripts.add({
props: { type: 'module', src: href },
children: '',
});
} else {
links.add({
props: {
rel: 'stylesheet',
href,
'data-astro-injected': true,
},
children: '',
});
}
});
}
[...(await getStylesForURL(filePath, viteServer))].forEach((href) => {
if (mode === 'development' && svelteStylesRE.test(href)) {
scripts.add({
props: { type: 'module', src: href },
children: '',
});
} else {
links.add({
props: {
rel: 'stylesheet',
href,
'data-astro-injected': true,
},
children: '',
});
}
});
let content = await coreRender({
// TODO: Remove this flag once legacyBuild support is removed
legacyBuild: isLegacyBuild,
links,
logging,
markdown: astroConfig.markdown,
@ -197,41 +188,6 @@ export async function render(
// inject tags
const tags: vite.HtmlTagDescriptor[] = [];
// dev only: inject Astro HMR client
if (mode === 'development' && isLegacyBuild) {
tags.push({
tag: 'script',
attrs: { type: 'module' },
// HACK: inject the direct contents of our `astro/runtime/client/hmr.js` to ensure
// `import.meta.hot` is properly handled by Vite
children: await getHmrScript(),
injectTo: 'head',
});
}
// inject CSS
if (isLegacyBuild) {
[...(await getStylesForURL(filePath, viteServer))].forEach((href) => {
if (mode === 'development' && svelteStylesRE.test(href)) {
tags.push({
tag: 'script',
attrs: { type: 'module', src: href },
injectTo: 'head',
});
} else {
tags.push({
tag: 'link',
attrs: {
rel: 'stylesheet',
href,
'data-astro-injected': true,
},
injectTo: 'head',
});
}
});
}
// add injected tags
let html = injectTags(content.html, tags);

View file

@ -22,7 +22,6 @@ function onlyAvailableInSSR(name: string) {
export interface CreateResultArgs {
ssr: boolean;
legacyBuild: boolean;
logging: LogOptions;
origin: string;
markdown: MarkdownRenderingOptions;
@ -101,7 +100,7 @@ class Slots {
let renderMarkdown: any = null;
export function createResult(args: CreateResultArgs): SSRResult {
const { legacyBuild, markdown, params, pathname, renderers, request, resolve, site } = args;
const { markdown, params, pathname, renderers, request, resolve, site } = args;
const url = new URL(request.url);
const canonicalURL = createCanonicalURL('.' + pathname, site ?? url.origin);
@ -138,16 +137,15 @@ export function createResult(args: CreateResultArgs): SSRResult {
}
: onlyAvailableInSSR('Astro.redirect'),
resolve(path: string) {
if (!legacyBuild) {
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
if (isCSSRequest(path)) {
extra = `It looks like you are resolving styles. If you are adding a link tag, replace with this:
let extra = `This can be replaced with a dynamic import like so: await import("${path}")`;
if (isCSSRequest(path)) {
extra = `It looks like you are resolving styles. If you are adding a link tag, replace with this:
---
import "${path}";
---
`;
} else if (isScriptRequest(path)) {
extra = `It looks like you are resolving scripts. If you are adding a script tag, replace with this:
} else if (isScriptRequest(path)) {
extra = `It looks like you are resolving scripts. If you are adding a script tag, replace with this:
<script type="module" src={(await import("${path}?url")).default}></script>
@ -157,21 +155,18 @@ or consider make it a module like so:
import MyModule from "${path}";
</script>
`;
}
warn(
args.logging,
`deprecation`,
`${bold(
'Astro.resolve()'
)} is deprecated. We see that you are trying to resolve ${path}.
${extra}`
);
// Intentionally return an empty string so that it is not relied upon.
return '';
}
return astroGlobal.resolve(path);
warn(
args.logging,
`deprecation`,
`${bold(
'Astro.resolve()'
)} is deprecated. We see that you are trying to resolve ${path}.
${extra}`
);
// Intentionally return an empty string so that it is not relied upon.
return '';
},
slots: astroSlots,
} as unknown as AstroGlobal;
@ -206,7 +201,6 @@ ${extra}`
_metadata: {
renderers,
pathname,
legacyBuild,
},
};

View file

@ -543,16 +543,7 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
// styles and scripts into the head.
export async function renderHead(result: SSRResult): Promise<string> {
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => {
// TODO: clean up legacyBuild from metadata
const styleChildren = !result._metadata.legacyBuild ? '' : style.children;
return renderElement('style', {
children: styleChildren,
props: { ...style.props, 'astro-style': true },
});
});
const styles = [];
let needsHydrationStyles = false;
const scripts = Array.from(result.scripts)
.filter(uniqueElements)
@ -568,7 +559,7 @@ export async function renderHead(result: SSRResult): Promise<string> {
if (needsHydrationStyles) {
styles.push(
renderElement('style', {
props: { 'astro-style': true },
props: {},
children: 'astro-root, astro-fragment { display: contents; }',
})
);

View file

@ -19,27 +19,6 @@ const ASTRO_STYLE_PREFIX = '@astro-inline-style';
const ASTRO_PAGE_STYLE_PREFIX = '@astro-page-all-styles';
export function getAstroPageStyleId(pathname: string) {
let styleId = ASTRO_PAGE_STYLE_PREFIX + pathname;
if (styleId.endsWith('/')) {
styleId += 'index';
}
styleId += '.js';
return styleId;
}
export function getAstroStyleId(pathname: string) {
let styleId = ASTRO_STYLE_PREFIX + pathname;
if (styleId.endsWith('/')) {
styleId += 'index';
}
return styleId;
}
export function getAstroStylePathFromId(id: string) {
return id.substr(ASTRO_STYLE_PREFIX.length + 1);
}
function isStyleVirtualModule(id: string) {
return id.startsWith(ASTRO_STYLE_PREFIX);
}
@ -54,12 +33,11 @@ function isRawOrUrlModule(id: string) {
interface PluginOptions {
internals: BuildInternals;
legacy: boolean;
target: 'client' | 'server';
}
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
const { internals, legacy } = options;
const { internals } = options;
const styleSourceMap = new Map<string, string>();
function* walkStyles(
@ -153,16 +131,6 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
return undefined;
},
async load(id) {
if (isPageStyleVirtualModule(id)) {
return internals.astroPageStyleMap.get(id) || null;
}
if (isStyleVirtualModule(id)) {
return internals.astroStyleMap.get(id) || null;
}
return null;
},
async transform(value, id) {
if (isStyleVirtualModule(id)) {
styleSourceMap.set(id, value);
@ -203,7 +171,6 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
source: minifiedCSS,
});
internals.chunkToReferenceIdMap.set(chunk.fileName, referenceId);
if (chunk.type === 'chunk') {
const fileName = this.getFileName(referenceId);
for (const pageData of getPageDatasByChunk(internals, chunk)) {
@ -236,26 +203,10 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
);
// Crawl the module graph to find CSS chunks to create
if (!legacy) {
await addStyles.call(this);
}
await addStyles.call(this);
for (const [chunkId, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk') {
// This find shared chunks of CSS and adds them to the main CSS chunks,
// so that shared CSS is added to the page.
for (const { css: cssSet } of getPageDatasByChunk(internals, chunk)) {
for (const imp of chunk.imports) {
if (internals.chunkToReferenceIdMap.has(imp) && !pureChunkFilenames.has(imp)) {
const referenceId = internals.chunkToReferenceIdMap.get(imp)!;
const fileName = this.getFileName(referenceId);
if (!cssSet.has(fileName)) {
cssSet.add(fileName);
}
}
}
}
// Removes imports for pure CSS chunks.
if (hasPureCSSChunks) {
if (internals.pureCSSChunks.has(chunk) && !chunk.exports.length) {

View file

@ -1,43 +0,0 @@
import { InputOptions } from 'rollup';
function fromEntries<V>(entries: [string, V][]) {
const obj: Record<string, V> = {};
for (const [k, v] of entries) {
obj[k] = v;
}
return obj;
}
export function addRollupInput(inputOptions: InputOptions, newInputs: string[]): InputOptions {
// Add input module ids to existing input option, whether it's a string, array or object
// this way you can use multiple html plugins all adding their own inputs
if (!inputOptions.input) {
return { ...inputOptions, input: newInputs };
}
if (typeof inputOptions.input === 'string') {
return {
...inputOptions,
input: [inputOptions.input, ...newInputs],
};
}
if (Array.isArray(inputOptions.input)) {
return {
...inputOptions,
input: [...inputOptions.input, ...newInputs],
};
}
if (typeof inputOptions.input === 'object') {
return {
...inputOptions,
input: {
...inputOptions.input,
...fromEntries(newInputs.map((i) => [i.split('/').slice(-1)[0].split('.')[0], i])),
},
};
}
throw new Error(`Unknown rollup input type. Supported inputs are string, array and object.`);
}

View file

@ -1,229 +0,0 @@
import { Document, Element, Node } from 'parse5';
import npath from 'path';
import { findElements, getTagName, getAttribute, findNodes, hasAttribute } from '@web/parse5-utils';
import adapter from 'parse5/lib/tree-adapters/default.js';
const hashedLinkRels = ['stylesheet', 'preload'];
const linkRels = [...hashedLinkRels, 'icon', 'manifest', 'apple-touch-icon', 'mask-icon'];
const windowsPathRE = /^[A-Z]:\//;
function getSrcSetUrls(srcset: string) {
if (!srcset) {
return [];
}
const srcsetParts = srcset.includes(',') ? srcset.split(',') : [srcset];
const urls = srcsetParts
.map((url) => url.trim())
.map((url) => (url.includes(' ') ? url.split(' ')[0] : url));
return urls;
}
function extractFirstUrlOfSrcSet(node: Element) {
const srcset = getAttribute(node, 'srcset');
if (!srcset) {
return '';
}
const urls = getSrcSetUrls(srcset);
return urls[0];
}
function isAsset(node: Element) {
let path = '';
switch (getTagName(node)) {
case 'img':
path = getAttribute(node, 'src') ?? '';
break;
case 'source':
path = extractFirstUrlOfSrcSet(node) ?? '';
break;
case 'link':
if (linkRels.includes(getAttribute(node, 'rel') ?? '')) {
path = getAttribute(node, 'href') ?? '';
}
break;
case 'meta':
if (getAttribute(node, 'property') === 'og:image' && getAttribute(node, 'content')) {
path = getAttribute(node, 'content') ?? '';
}
break;
case 'script':
if (getAttribute(node, 'type') !== 'module' && getAttribute(node, 'src')) {
path = getAttribute(node, 'src') ?? '';
}
break;
default:
return false;
}
if (!path) {
return false;
}
// Windows fix: if path starts with C:/, avoid URL parsing
if (windowsPathRE.test(path)) {
return true;
}
try {
new URL(path);
return false;
} catch (e) {
return true;
}
}
function isInlineScript(node: Element): boolean {
switch (getTagName(node)) {
case 'script':
if (getAttribute(node, 'type') === 'module' && !getAttribute(node, 'src')) {
return true;
}
return false;
default:
return false;
}
}
function isExternalScript(node: Element): boolean {
switch (getTagName(node)) {
case 'script':
if (hasAttribute(node, 'src')) {
return true;
}
return false;
default:
return false;
}
}
function isInlineStyle(node: Element): boolean {
return getTagName(node) === 'style';
}
export function isStylesheetLink(node: Element): boolean {
return getTagName(node) === 'link' && getAttribute(node, 'rel') === 'stylesheet';
}
export function isHashedAsset(node: Element) {
switch (getTagName(node)) {
case 'img':
return true;
case 'source':
return true;
case 'script':
return true;
case 'link':
return hashedLinkRels.includes(getAttribute(node, 'rel')!);
case 'meta':
return true;
default:
return false;
}
}
export function resolveAssetFilePath(
browserPath: string,
htmlDir: string,
projectRootDir: string,
absolutePathPrefix?: string
) {
const _browserPath =
absolutePathPrefix && browserPath[0] === '/'
? '/' + npath.posix.relative(absolutePathPrefix, browserPath)
: browserPath;
return npath.join(
_browserPath.startsWith('/') ? projectRootDir : htmlDir,
_browserPath.split('/').join(npath.sep)
);
}
export function getSourceAttribute(node: Element) {
switch (getTagName(node)) {
case 'img': {
return 'src';
}
case 'source': {
return 'srcset';
}
case 'link': {
return 'href';
}
case 'script': {
return 'src';
}
case 'meta': {
return 'content';
}
default:
throw new Error(`Unknown node with tagname ${getTagName(node)}`);
}
}
export interface Location {
start: number;
end: number;
}
export function getSourcePaths(node: Element) {
const key = getSourceAttribute(node);
let location: Location = { start: 0, end: 0 };
const src = getAttribute(node, key);
if (node.sourceCodeLocation) {
let loc = node.sourceCodeLocation.attrs?.[key];
if (loc) {
location.start = loc.startOffset;
location.end = loc.endOffset;
}
}
if (typeof key !== 'string' || src === '') {
throw new Error(`Missing attribute ${key} in element ${node.nodeName}`);
}
let paths: { path: string; location: Location }[] = [];
if (src && key === 'srcset') {
paths = getSrcSetUrls(src).map((path) => ({
path,
location,
}));
} else if (src) {
paths.push({
path: src,
location,
});
}
return paths;
}
export function getTextContent(node: Node): string {
if (adapter.isCommentNode(node)) {
return node.data || '';
}
if (adapter.isTextNode(node)) {
return node.value || '';
}
const subtree = findNodes(node, (n) => adapter.isTextNode(n));
return subtree.map(getTextContent).join('');
}
export function getAttributes(node: Element): Record<string, any> {
return Object.fromEntries(node.attrs.map((attr) => [attr.name, attr.value]));
}
export function findAssets(document: Document) {
return findElements(document, isAsset);
}
export function findInlineScripts(document: Document) {
return findElements(document, isInlineScript);
}
export function findExternalScripts(document: Document) {
return findElements(document, isExternalScript);
}
export function findInlineStyles(document: Document) {
return findElements(document, isInlineStyle);
}
export function findStyleLinks(document: Document) {
return findElements(document, isStylesheetLink);
}

View file

@ -1,537 +0,0 @@
import {
createElement,
createScript,
getAttribute,
hasAttribute,
insertBefore,
remove,
setAttribute,
} from '@web/parse5-utils';
import { promises as fs } from 'fs';
import parse5 from 'parse5';
import * as npath from 'path';
import type { OutputChunk, PluginContext, PreRenderedChunk } from 'rollup';
import srcsetParse from 'srcset-parse';
import type { Plugin as VitePlugin, ViteDevServer } from 'vite';
import type { AstroConfig } from '../@types/astro';
import type { BuildInternals } from '../core/build/internal';
import type { AllPagesData } from '../core/build/types';
import type { LogOptions } from '../core/logger/core.js';
import { prependDotSlash } from '../core/path.js';
import { render as ssrRender } from '../core/render/dev/index.js';
import { RouteCache } from '../core/render/route-cache.js';
import { getOutputFilename } from '../core/util.js';
import { getAstroPageStyleId, getAstroStyleId } from '../vite-plugin-build-css/index.js';
import { addRollupInput } from './add-rollup-input.js';
import {
findAssets,
findExternalScripts,
findInlineScripts,
findInlineStyles,
getAttributes,
getTextContent,
} from './extract-assets.js';
import {
hasSrcSet,
isBuildableImage,
isBuildableLink,
isHoistedScript,
isInSrcDirectory,
} from './util.js';
import { createRequest } from '../core/request.js';
// This package isn't real ESM, so have to coerce it
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;
const PLUGIN_NAME = '@astro/rollup-plugin-build';
const ASTRO_PAGE_PREFIX = '@astro-page';
const ASTRO_SCRIPT_PREFIX = '@astro-script';
const ASTRO_EMPTY = '@astro-empty';
interface PluginOptions {
astroConfig: AstroConfig;
internals: BuildInternals;
logging: LogOptions;
allPages: AllPagesData;
pageNames: string[];
origin: string;
routeCache: RouteCache;
viteServer: ViteDevServer;
}
function relativePath(from: string, to: string): string {
const rel = npath.posix.relative(from, to);
return prependDotSlash(rel);
}
export function rollupPluginAstroScanHTML(options: PluginOptions): VitePlugin {
const { astroConfig, internals, logging, origin, allPages, routeCache, viteServer, pageNames } =
options;
// The filepath root of the src folder
const srcRoot = astroConfig.srcDir.pathname;
// The web path of the src folter
const srcRootWeb = srcRoot.substr(astroConfig.root.pathname.length - 1);
// A map of pages to rendered HTML
const renderedPageMap = new Map<string, string>();
//
const astroScriptMap = new Map<string, string>();
const astroPageMap = new Map<string, string>();
const astroAssetMap = new Map<string, Promise<Buffer>>();
const cssChunkMap = new Map<string, string[]>();
const pageStyleImportOrder: string[] = [];
return {
name: PLUGIN_NAME,
enforce: 'pre',
async options(inputOptions) {
const htmlInput: Set<string> = new Set();
const assetInput: Set<string> = new Set();
const jsInput: Set<string> = new Set();
for (const [component, pageData] of Object.entries(allPages)) {
const [renderers, mod] = pageData.preload;
// Hydrated components are statically identified.
for (const path of mod.$$metadata.hydratedComponentPaths()) {
jsInput.add(path);
}
for (const pathname of pageData.paths) {
pageNames.push(pathname.replace(/\/?$/, '/').replace(/^\//, ''));
const id = ASTRO_PAGE_PREFIX + pathname;
const response = await ssrRender(renderers, mod, {
astroConfig,
filePath: new URL(`./${component}`, astroConfig.root),
logging,
request: createRequest({
url: new URL(origin + pathname),
headers: new Headers(),
logging,
ssr: false,
}),
mode: 'production',
origin,
pathname,
route: pageData.route,
routeCache,
viteServer,
});
if (response.type !== 'html') {
continue;
}
const html = response.html;
renderedPageMap.set(id, html);
const document = parse5.parse(html, {
sourceCodeLocationInfo: true,
});
const frontEndImports = [];
for (const script of findInlineScripts(document)) {
const astroScript = getAttribute(script, 'astro-script');
if (astroScript) {
const js = getTextContent(script);
const scriptId = ASTRO_SCRIPT_PREFIX + astroScript;
frontEndImports.push(scriptId);
astroScriptMap.set(scriptId, js);
}
}
for (const script of findExternalScripts(document)) {
if (isHoistedScript(script)) {
const astroScript = getAttribute(script, 'astro-script');
const src = getAttribute(script, 'src');
if (astroScript) {
const js = `import '${src}';`;
const scriptId = ASTRO_SCRIPT_PREFIX + astroScript;
frontEndImports.push(scriptId);
astroScriptMap.set(scriptId, js);
}
} else if (isInSrcDirectory(script, 'src', srcRoot, srcRootWeb)) {
const src = getAttribute(script, 'src');
if (src) jsInput.add(src);
}
}
const assetImports = [];
const styleId = getAstroStyleId(pathname);
let styles = 0;
for (const node of findInlineStyles(document)) {
if (hasAttribute(node, 'astro-style')) {
const style = getTextContent(node) || ' '; // If an empty node, add whitespace
const thisStyleId = `${styleId}/${++styles}.css`;
internals.astroStyleMap.set(thisStyleId, style);
assetImports.push(thisStyleId);
}
}
for (let node of findAssets(document)) {
if (isBuildableLink(node, srcRoot, srcRootWeb)) {
const href = getAttribute(node, 'href')!;
assetImports.push(href);
}
if (isBuildableImage(node, srcRoot, srcRootWeb)) {
const src = getAttribute(node, 'src');
if (src?.startsWith(srcRoot) && !astroAssetMap.has(src)) {
astroAssetMap.set(src, fs.readFile(new URL(`file://${src}`)));
} else if (src?.startsWith(srcRootWeb) && !astroAssetMap.has(src)) {
const resolved = new URL('.' + src, astroConfig.root);
astroAssetMap.set(src, fs.readFile(resolved));
}
}
if (hasSrcSet(node)) {
const candidates = matchSrcset(getAttribute(node, 'srcset')!);
for (const { url } of candidates) {
if (url.startsWith(srcRoot) && !astroAssetMap.has(url)) {
astroAssetMap.set(url, fs.readFile(new URL(`file://${url}`)));
} else if (url.startsWith(srcRootWeb) && !astroAssetMap.has(url)) {
const resolved = new URL('.' + url, astroConfig.root);
astroAssetMap.set(url, fs.readFile(resolved));
}
}
}
}
if (frontEndImports.length) {
htmlInput.add(id);
const jsSource = frontEndImports.map((sid) => `import '${sid}';`).join('\n');
astroPageMap.set(id, jsSource);
}
if (assetImports.length) {
const pageStyleId = getAstroPageStyleId(pathname);
const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n');
internals.astroPageStyleMap.set(pageStyleId, jsSource);
assetInput.add(pageStyleId);
// preserve asset order in the order we encounter them
for (const assetHref of assetImports) {
if (!pageStyleImportOrder.includes(assetHref)) pageStyleImportOrder.push(assetHref);
}
}
}
}
const allInputs = new Set([...jsInput, ...htmlInput, ...assetInput]);
// You always need at least 1 input, so add an placeholder just so we can build HTML/CSS
if (!allInputs.size) {
allInputs.add(ASTRO_EMPTY);
}
const outOptions = addRollupInput(inputOptions, Array.from(allInputs));
return outOptions;
},
async resolveId(id) {
switch (true) {
case astroScriptMap.has(id):
case astroPageMap.has(id):
case id === ASTRO_EMPTY: {
return id;
}
}
return undefined;
},
async load(id) {
// Load pages
if (astroPageMap.has(id)) {
return astroPageMap.get(id)!;
}
// Load scripts
if (astroScriptMap.has(id)) {
return astroScriptMap.get(id)!;
}
// Give this module actual code so it doesnt warn about an empty chunk
if (id === ASTRO_EMPTY) {
return 'console.log("empty");';
}
return null;
},
outputOptions(outputOptions) {
Object.assign(outputOptions, {
entryFileNames(chunk: PreRenderedChunk) {
// Removes the `@astro-page` prefix from JS chunk names.
if (chunk.name.startsWith(ASTRO_PAGE_PREFIX)) {
let pageName = chunk.name.substr(ASTRO_PAGE_PREFIX.length + 1);
if (!pageName) {
pageName = 'index';
}
return `assets/${pageName}.[hash].js`;
}
return 'assets/[name].[hash].js';
},
});
return outputOptions;
},
async generateBundle(this: PluginContext, _options, bundle) {
const facadeIdMap = new Map<string, string>();
for (const [chunkId, output] of Object.entries(bundle)) {
if (output.type === 'chunk') {
const chunk = output as OutputChunk;
const id = chunk.facadeModuleId;
if (id === ASTRO_EMPTY) {
delete bundle[chunkId];
} else if (id) {
facadeIdMap.set(id, chunk.fileName);
}
}
}
// Emit assets (images, etc)
const assetIdMap = new Map<string, string>();
for (const [assetPath, dataPromise] of astroAssetMap) {
const referenceId = this.emitFile({
type: 'asset',
name: npath.basename(assetPath),
source: await dataPromise,
});
assetIdMap.set(assetPath, referenceId);
}
// Sort CSS in order of appearance in HTML (pageStyleImportOrder)
// This is the “global ordering” used below
const sortedCSSChunks = [...internals.pureCSSChunks];
sortedCSSChunks.sort((a, b) => {
let aIndex = Math.min(
...Object.keys(a.modules).map((id) => {
const i = pageStyleImportOrder.findIndex((url) => id.endsWith(url));
return i >= 0 ? i : Infinity; // if -1 is encountered (unknown order), move to the end (Infinity)
})
);
let bIndex = Math.min(
...Object.keys(b.modules).map((id) => {
const i = pageStyleImportOrder.findIndex((url) => id.endsWith(url));
return i >= 0 ? i : Infinity;
})
);
return aIndex - bIndex;
});
const sortedChunkNames = sortedCSSChunks.map(({ fileName }) => fileName);
// Create a mapping of chunks to dependent chunks, used to add the proper
// link tags for CSS.
for (const chunk of sortedCSSChunks) {
const chunkModules = [chunk.fileName, ...chunk.imports];
// For each HTML output, sort CSS in HTML order Note: here we actually
// want -1 to be first. Since the last CSS “wins”, we want to load
// “unknown” (-1) CSS ordering first, followed by “known” ordering at
// the end so it takes priority
chunkModules.sort((a, b) => sortedChunkNames.indexOf(a) - sortedChunkNames.indexOf(b));
const referenceIDs: string[] = [];
for (const chunkID of chunkModules) {
const referenceID = internals.chunkToReferenceIdMap.get(chunkID);
if (referenceID) referenceIDs.push(referenceID);
}
for (const id of Object.keys(chunk.modules)) {
cssChunkMap.set(id, referenceIDs);
}
}
// Keep track of links added so we don't do so twice.
const linkChunksAdded = new Set<string>();
const appendStyleChunksBefore = (
ref: parse5.Element,
pathname: string,
referenceIds: string[] | undefined,
attrs: Record<string, any> = {}
) => {
let added = false;
if (referenceIds) {
const lastNode = ref;
for (const referenceId of referenceIds) {
const chunkFileName = this.getFileName(referenceId);
const relPath = relativePath(pathname, '/' + chunkFileName);
// This prevents added links more than once per type.
const key = pathname + relPath + attrs.rel || 'stylesheet';
if (!linkChunksAdded.has(key)) {
linkChunksAdded.add(key);
insertBefore(
lastNode.parentNode,
createElement('link', {
rel: 'stylesheet',
...attrs,
href: relPath,
}),
lastNode
);
added = true;
}
}
}
return added;
};
for (const [id, html] of renderedPageMap) {
const pathname = id.substr(ASTRO_PAGE_PREFIX.length);
const document = parse5.parse(html, {
sourceCodeLocationInfo: true,
});
// This is the module for the page-level bundle which includes
// hoisted scripts and hydrated components.
const pageAssetId = facadeIdMap.get(id);
const bundlePath = '/' + pageAssetId;
// Update scripts
let pageBundleAdded = false;
// Update inline scripts. These could be hydrated component scripts or hoisted inline scripts
for (let script of findInlineScripts(document)) {
if (getAttribute(script, 'astro-script') && typeof pageAssetId === 'string') {
if (!pageBundleAdded) {
pageBundleAdded = true;
const relPath = relativePath(pathname, bundlePath);
insertBefore(
script.parentNode,
createScript({
type: 'module',
src: relPath,
}),
script
);
}
remove(script);
}
}
// Update external scripts. These could be hoisted or in the src folder.
for (let script of findExternalScripts(document)) {
if (getAttribute(script, 'astro-script') && typeof pageAssetId === 'string') {
if (!pageBundleAdded) {
pageBundleAdded = true;
const relPath = relativePath(pathname, bundlePath);
insertBefore(
script.parentNode,
createScript({
type: 'module',
src: relPath,
}),
script
);
}
remove(script);
} else if (isInSrcDirectory(script, 'src', srcRoot, srcRootWeb)) {
let src = getAttribute(script, 'src');
// If this is projectRoot relative, get the fullpath to match the facadeId.
if (src?.startsWith(srcRootWeb)) {
src = new URL('.' + src, astroConfig.root).pathname;
}
// On windows the facadeId doesn't start with / but does not Unix :/
if (src && (facadeIdMap.has(src) || facadeIdMap.has(src.substr(1)))) {
const assetRootPath = '/' + (facadeIdMap.get(src) || facadeIdMap.get(src.substr(1)));
const relPath = relativePath(pathname, assetRootPath);
const attrs = getAttributes(script);
insertBefore(
script.parentNode,
createScript({
...attrs,
src: relPath,
}),
script
);
remove(script);
}
}
}
const styleId = getAstroPageStyleId(pathname);
let pageCSSAdded = false;
for (const node of findAssets(document)) {
if (isBuildableLink(node, srcRoot, srcRootWeb)) {
const rel = getAttribute(node, 'rel');
switch (rel) {
case 'stylesheet': {
if (!pageCSSAdded) {
const attrs = getAttributes(node);
delete attrs['data-astro-injected'];
pageCSSAdded = appendStyleChunksBefore(
node,
pathname,
cssChunkMap.get(styleId),
attrs
);
}
remove(node);
break;
}
case 'preload': {
if (getAttribute(node, 'as') === 'style') {
const attrs = getAttributes(node);
appendStyleChunksBefore(node, pathname, cssChunkMap.get(styleId), attrs);
remove(node);
}
}
}
}
if (isBuildableImage(node, srcRoot, srcRootWeb)) {
const src = getAttribute(node, 'src')!;
const referenceId = assetIdMap.get(src);
if (referenceId) {
const fileName = this.getFileName(referenceId);
const relPath = relativePath(pathname, '/' + fileName);
setAttribute(node, 'src', relPath);
}
}
// Could be a `source` or an `img`.
if (hasSrcSet(node)) {
const srcset = getAttribute(node, 'srcset')!;
let changedSrcset = srcset;
const urls = matchSrcset(srcset).map((c) => c.url);
for (const url of urls) {
if (assetIdMap.has(url)) {
const referenceId = assetIdMap.get(url)!;
const fileName = this.getFileName(referenceId);
const relPath = relativePath(pathname, '/' + fileName);
changedSrcset = changedSrcset.replace(url, relPath);
}
}
// If anything changed, update it
if (changedSrcset !== srcset) {
setAttribute(node, 'srcset', changedSrcset);
}
}
}
// Page styles for <style> usage, if not already appended via links.
for (const style of findInlineStyles(document)) {
if (hasAttribute(style, 'astro-style')) {
if (!pageCSSAdded) {
pageCSSAdded = appendStyleChunksBefore(style, pathname, cssChunkMap.get(styleId));
}
remove(style);
}
}
const outHTML = parse5.serialize(document);
const name = pathname.substr(1);
const outPath = getOutputFilename(astroConfig, name);
this.emitFile({
fileName: outPath,
source: outHTML,
type: 'asset',
});
}
},
};
}

View file

@ -1,64 +0,0 @@
import { getAttribute, hasAttribute, getTagName } from '@web/parse5-utils';
import parse5 from 'parse5';
import { isStylesheetLink } from './extract-assets.js';
const tagsWithSrcSet = new Set(['img', 'source']);
function startsWithSrcRoot(pathname: string, srcRoot: string, srcRootWeb: string): boolean {
return (
pathname.startsWith(srcRoot) || // /Users/user/project/src/styles/main.css
pathname.startsWith(srcRootWeb) || // /src/styles/main.css
`/${pathname}`.startsWith(srcRoot)
); // Windows fix: some paths are missing leading "/"
}
export function isInSrcDirectory(
node: parse5.Element,
attr: string,
srcRoot: string,
srcRootWeb: string
): boolean {
const value = getAttribute(node, attr);
return value ? startsWithSrcRoot(value, srcRoot, srcRootWeb) : false;
}
export function isAstroInjectedLink(node: parse5.Element): boolean {
return isStylesheetLink(node) && getAttribute(node, 'data-astro-injected') === '';
}
export function isBuildableLink(
node: parse5.Element,
srcRoot: string,
srcRootWeb: string
): boolean {
if (isAstroInjectedLink(node)) {
return true;
}
const href = getAttribute(node, 'href');
if (typeof href !== 'string' || !href.length) {
return false;
}
return startsWithSrcRoot(href, srcRoot, srcRootWeb);
}
export function isBuildableImage(
node: parse5.Element,
srcRoot: string,
srcRootWeb: string
): boolean {
if (getTagName(node) === 'img') {
const src = getAttribute(node, 'src');
return src ? startsWithSrcRoot(src, srcRoot, srcRootWeb) : false;
}
return false;
}
export function hasSrcSet(node: parse5.Element): boolean {
return tagsWithSrcSet.has(getTagName(node)) && !!getAttribute(node, 'srcset');
}
export function isHoistedScript(node: parse5.Element): boolean {
return getTagName(node) === 'script' && hasAttribute(node, 'hoist');
}

View file

@ -291,8 +291,8 @@ describe('CSS', function () {
});
it('resolves Astro styles', async () => {
const style = $('style[astro-style]');
expect(style.length).to.not.equal(0);
const astroPageCss = $('link[rel=stylesheet][href^=/src/pages/index.astro?astro&type=style]');
expect(astroPageCss.length).to.equal(4, 'The index.astro page should generate 4 stylesheets, 1 for each <style> tag on the page.')
});
it('resolves Styles from React', async () => {

View file

@ -24,14 +24,12 @@ describe('Doctype', () => {
expect(html).to.match(/^<!DOCTYPE html>/i);
});
// Note: parse5 converts this to <!DOCTYPE html> (HTML5). Uncomment if we want to support legacy doctypes.
//
// it('Preserves user provided doctype', async () => {
// const html = await fixture.readFile('/preserve/index.html');
it('Preserves user provided doctype', async () => {
const html = await fixture.readFile('/preserve/index.html');
// // test that Doctype included was preserved
// expect(html).to.match(new RegExp('^<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">', 'i'));
// });
// test that Doctype included was preserved
expect(html).to.match(new RegExp('^<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">', 'i'));
});
it('User provided doctype is case insensitive', async () => {
const html = await fixture.readFile('/capital/index.html');

View file

@ -479,7 +479,6 @@ importers:
'@types/send': ^0.17.1
'@types/unist': ^2.0.6
'@types/yargs-parser': ^21.0.0
'@web/parse5-utils': ^1.3.0
ast-types: ^0.14.2
astro-scripts: workspace:*
boxen: ^6.2.1
@ -506,7 +505,6 @@ importers:
mime: ^3.0.0
mocha: ^9.2.2
ora: ^6.1.0
parse5: ^6.0.1
path-browserify: ^1.0.1
path-to-regexp: ^6.2.0
postcss: ^8.4.12
@ -546,7 +544,6 @@ importers:
'@babel/traverse': 7.17.9
'@proload/core': 0.2.2
'@proload/plugin-tsm': 0.1.1_@proload+core@0.2.2
'@web/parse5-utils': 1.3.0
ast-types: 0.14.2
boxen: 6.2.1
ci-info: 3.3.0
@ -569,7 +566,6 @@ importers:
micromorph: 0.1.2
mime: 3.0.0
ora: 6.1.0
parse5: 6.0.1
path-browserify: 1.0.1
path-to-regexp: 6.2.0
postcss: 8.4.12
@ -588,7 +584,6 @@ importers:
sirv: 2.0.2
slash: 4.0.0
sourcemap-codec: 1.4.8
srcset-parse: 1.1.0
string-width: 5.1.2
strip-ansi: 7.0.1
supports-esm: 1.0.0
@ -623,6 +618,7 @@ importers:
cheerio: 1.0.0-rc.10
mocha: 9.2.2
sass: 1.50.0
srcset-parse: 1.1.0
packages/astro-prism:
specifiers:
@ -4488,14 +4484,6 @@ packages:
/@vue/shared/3.2.31:
resolution: {integrity: sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==}
/@web/parse5-utils/1.3.0:
resolution: {integrity: sha512-Pgkx3ECc8EgXSlS5EyrgzSOoUbM6P8OKS471HLAyvOBcP1NCBn0to4RN/OaKASGq8qa3j+lPX9H14uA5AHEnQg==}
engines: {node: '>=10.0.0'}
dependencies:
'@types/parse5': 6.0.3
parse5: 6.0.1
dev: false
/@webcomponents/template-shadowroot/0.1.0:
resolution: {integrity: sha512-ry84Vft6xtRBbd4M/ptRodbOLodV5AD15TYhyRghCRgIcJJKmYmJ2v2BaaWxygENwh6Uq3zTfGPmlckKT/GXsQ==}
dev: false
@ -9497,7 +9485,7 @@ packages:
/srcset-parse/1.1.0:
resolution: {integrity: sha512-JWp4cG2eybkvKA1QUHGoNK6JDEYcOnSuhzNGjZuYUPqXreDl/VkkvP2sZW7Rmh+icuCttrR9ccb2WPIazyM/Cw==}
dev: false
dev: true
/statuses/2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}