Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSS injection, fix portfolio example #1648

Merged
merged 4 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions examples/portfolio/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -190,28 +190,19 @@ const featuredProject = projects[0];
height="1131"
class="img"
src="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75"
srcSet="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 800w,
https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 1200w,
https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1600&q=75 1600w,
https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=2400&q=75 2400w,"
srcSet="https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 800w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1200&q=75 1200w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=1600&q=75 1600w,https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=2400&q=75 2400w"
sizes="(max-width: 800px) 800px, (max-width: 1200px) 1200px, (max-width: 1600px) 1600px, (max-width: 2400px) 2400px, 1200px"
/>
<div class="gradient" />
<div class="gradient2" />
>
<div class="gradient"></div>
<div class="gradient2"></div>
<div class="overlay">
<h1 class="title">
<small class="subtitle">The personal site of </small>Jeanine White
</h1>
<div>
<span class="role">
👩‍💻 Developer <span class="invert">👩‍💻 Developer</span>
</span>&nbsp;
<span class="role">
🎤 Speaker <span class="invert">🎤 Speaker</span>
</span>&nbsp;
<span class="role">
✏️ Writer <span class="invert">✏️ Writer</span>
</span>
<span class="role">👩‍💻 Developer <span class="invert">👩‍💻 Developer</span></span>&nbsp;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here are just cleanup

<span class="role">🎤 Speaker <span class="invert">🎤 Speaker</span></span>&nbsp;
<span class="role">✏️ Writer <span class="invert">✏️ Writer</span></span>
</div>
<p class="desc">Lover of dogs, roadtrips, and poetry.</p>
</div>
Expand All @@ -233,8 +224,7 @@ const featuredProject = projects[0];
<span>Hello!</span> I’m Jeanine, and this is my website. It was made using{' '}
<a href="https://github.com/snowpackjs/astro" target="_blank" rel="nofollow">
Astro
</a>
, a new way to build static sites. This is just an example template for you to modify.
</a>, a new way to build static sites. This is just an example template for you to modify.
</p>
<p>
<a href="/about">Read more</a>
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"author": "Skypack",
"license": "MIT",
"type": "module",
"types": "./dist/types/@types/astro-public.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/snowpackjs/astro.git",
Expand Down Expand Up @@ -53,7 +52,7 @@
"test": "mocha --parallel --timeout 15000"
},
"dependencies": {
"@astrojs/compiler": "^0.2.16",
"@astrojs/compiler": "^0.2.17",
"@astrojs/language-server": "^0.7.16",
"@astrojs/markdown-remark": "^0.3.1",
"@astrojs/markdown-support": "0.3.1",
Expand All @@ -73,13 +72,14 @@
"estree-util-value-to-estree": "^1.2.0",
"fast-xml-parser": "^3.19.0",
"html-entities": "^2.3.2",
"htmlparser2": "^7.1.2",
"kleur": "^4.1.4",
"mime": "^2.5.2",
"morphdom": "^2.6.1",
"node-fetch": "^2.6.5",
"path-to-regexp": "^6.2.0",
"remark-slug": "^7.0.0",
"sass": "^1.43.2",
"sass": "^1.43.3",
"semver": "^7.3.5",
"send": "^0.17.1",
"shiki": "^0.9.10",
Expand Down
1 change: 0 additions & 1 deletion packages/astro/src/@types/astro-public.ts

This file was deleted.

64 changes: 64 additions & 0 deletions packages/astro/src/core/ssr/css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type vite from '../../../vendor/vite';

import path from 'path';
import htmlparser2 from 'htmlparser2';

// https://vitejs.dev/guide/features.html#css-pre-processors
export const STYLE_EXTENSIONS = new Set(['.css', '.pcss', '.scss', '.sass', '.styl', '.stylus', '.less']);
export const PREPROCESSOR_EXTENSIONS = new Set(['.pcss', '.scss', '.sass', '.styl', '.stylus', '.less']);

/** find unloaded styles */
export function getStylesForID(id: string, viteServer: vite.ViteDevServer): Set<string> {
const css = new Set<string>();
const { idToModuleMap } = viteServer.moduleGraph;
const moduleGraph = idToModuleMap.get(id);
if (!moduleGraph) return css;

// recursively crawl module graph to get all style files imported by parent id
function crawlCSS(entryModule: string, scanned = new Set<string>()) {
const moduleName = idToModuleMap.get(entryModule);
if (!moduleName) return;
for (const importedModule of moduleName.importedModules) {
if (!importedModule.id || scanned.has(importedModule.id)) return;
const ext = path.extname(importedModule.id.toLowerCase());

if (STYLE_EXTENSIONS.has(ext)) {
css.add(importedModule.id); // if style file, add to list
} else {
crawlCSS(importedModule.id, scanned); // otherwise, crawl file to see if it imports any CSS
}
scanned.add(importedModule.id);
}
}
crawlCSS(id);

return css;
}

/** add CSS <link> tags to HTML */
export function addLinkTagsToHTML(html: string, styles: Set<string>): string {
let output = html;

try {
// get position of </head>
let headEndPos = -1;
const parser = new htmlparser2.Parser({
Copy link
Member Author

@drwpow drwpow Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using htmlparser2 to inject this in the right place. I tested a lot of Node DOM options and this is RSF: really stinkin’ fast™. It can parse average-sized HTML in about 1ms or less (cold start). It’s over 7x faster than Cheerio, and it may be even faster than a WASM action (when you include interop cost).

But the real seller here is it’s more reliable than RegEx, and gives us assurance we’re not messing up the HTML (can handle comments, etc. etc.)

onclosetag(tagname) {
if (tagname === 'head') {
headEndPos = parser.startIndex;
}
},
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
});
parser.write(html);
parser.end();

// update html
if (headEndPos !== -1) {
output = html.substring(0, headEndPos) + [...styles].map((href) => `<link rel="stylesheet" type="text/css" href="${href}">`).join('') + html.substring(headEndPos);
}
} catch (err) {
// on invalid HTML, do nothing
}

return output;
}
12 changes: 10 additions & 2 deletions packages/astro/src/core/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fs from 'fs';
import path from 'path';
import { renderPage, renderSlot } from '../../runtime/server/index.js';
import { canonicalURL as getCanonicalURL, codeFrame } from '../util.js';
import { addLinkTagsToHTML, getStylesForID } from './css.js';
import { generatePaginateFunction } from './paginate.js';
import { getParams, validateGetStaticPathsModule, validateGetStaticPathsResult } from './routing.js';

Expand Down Expand Up @@ -148,18 +149,25 @@ export async function ssr({ astroConfig, filePath, logging, mode, origin, pathna
({ default: render } = await import(render));
}
const { code } = await render(content, { ...renderOpts, ...(opts ?? {}) });
return code
}
return code;
},
} as unknown as AstroGlobal;
},
_metadata: { renderers },
};

let html = await renderPage(result, Component, pageProps, null);

// run transformIndexHtml() in development to add HMR client to the page.
if (mode === 'development') {
html = await viteServer.transformIndexHtml(fileURLToPath(filePath), html, pathname);
}

// insert CSS imported from Astro and JS components
const styles = getStylesForID(fileURLToPath(filePath), viteServer);
const relativeStyles = new Set<string>([...styles].map((url) => url.replace(fileURLToPath(astroConfig.projectRoot), '/')));
html = addLinkTagsToHTML(html, relativeStyles);
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved

return html;
} catch (e: any) {
viteServer.ssrFixStacktrace(e);
Expand Down
28 changes: 12 additions & 16 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ function extractDirectives(inputProps: Record<string | number, any>): ExtractedP
}
} else if (key === 'class:list') {
// support "class" from an expression passed into a component (#782)
extracted.props[key.slice(0, -5)] = serializeListValue(value)
extracted.props[key.slice(0, -5)] = serializeListValue(value);
} else {
extracted.props[key] = value;
}
Expand Down Expand Up @@ -308,29 +308,25 @@ export function spreadAttributes(values: Record<any, any>) {
}

function serializeListValue(value: any) {
const hash: Record<string, any> = {}
const hash: Record<string, any> = {};

push(value)
push(value);

return Object.keys(hash).join(' ');

function push(item: any) {
// push individual iteratables
if (item && typeof item.forEach === 'function') item.forEach(push)

if (item && typeof item.forEach === 'function') item.forEach(push);
// otherwise, push object value keys by truthiness
else if (item === Object(item)) Object.keys(item).forEach(
name => {
if (item[name]) push(name)
}
)

else if (item === Object(item))
Object.keys(item).forEach((name) => {
if (item[name]) push(name);
});
// otherwise, push any other values as a string
else if (item = item != null && String(item).trim()) item.split(/\s+/).forEach(
(name: string) => {
hash[name] = true
}
)
else if ((item = item != null && String(item).trim()))
Copy link
Member Author

@drwpow drwpow Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prettifier added parens here… is this correct? Why the self-assignment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this was the result of some code golf. Pinging @jonathantneal!

Copy link
Contributor

@jonathantneal jonathantneal Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct. I would let it happen. Prettier likes to wrap expressions in extra parentheses.

For those who seek a deeper answer — the 'expression' is for whether item is not null and is not a spacey string. To check if it’s a spacey string, we also need to perceive the item as a string; thus the internal re-assignment. Re-assignment of a param is a pattern used multiple times elsewhere in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code golf is a good term here! Yes the code seems to work, but it took me several reads to understand what was going on here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I wouldn't block this PR based on this, but would love if you could follow up to make this function a bit clearer, @jonathantneal!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative here, if we feel it’s a hold up. https://github.com/snowpackjs/astro/pull/1648/files#r735873641

item.split(/\s+/).forEach((name: string) => {
hash[name] = true;
});
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
35 changes: 30 additions & 5 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TransformResult } from '@astrojs/compiler';
import type { SourceMapInput } from 'rollup';
import type vite from '../core/vite';
import type { AstroConfig } from '../@types/astro-core';

Expand All @@ -15,6 +16,20 @@ interface AstroPluginOptions {
devServer?: AstroDevServer;
}

// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726
function isSSR(options: undefined | boolean | { ssr: boolean }): boolean {
if (options === undefined) {
return false;
}
if (typeof options === 'boolean') {
return options;
}
if (typeof options == 'object') {
return !!options.ssr;
}
return false;
}

/** Transform .astro files for Vite */
export default function astro({ config, devServer }: AstroPluginOptions): vite.Plugin {
let viteTransform: TransformHook;
Expand All @@ -25,7 +40,7 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
viteTransform = getViteTransform(resolvedConfig);
},
// note: don’t claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.globEager, etc.)
async load(id) {
async load(id, opts) {
if (!id.endsWith('.astro')) {
return null;
}
Expand All @@ -47,12 +62,20 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
internalURL: 'astro/internal',
preprocessStyle: async (value: string, attrs: Record<string, string>) => {
if (!attrs || !attrs.lang) return null;
const result = await transformWithVite(value, attrs, id, viteTransform);
const result = await transformWithVite({ value, attrs, id, transformHook: viteTransform, ssr: isSSR(opts) });
if (!result) {
// TODO: compiler supports `null`, but types don't yet
return result as any;
}
return { code: result.code, map: result.map?.toString() };
let map: SourceMapInput | undefined;
if (result.map) {
if (typeof result.map === 'string') {
map = result.map;
} else if (result.map.mappings) {
map = result.map.toString();
}
}
return { code: result.code, map };
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
},
});
// Compile `.ts` to `.js`
Expand All @@ -63,12 +86,14 @@ export default function astro({ config, devServer }: AstroPluginOptions): vite.P
map,
};
} catch (err: any) {
// if esbuild threw the error, find original code source to display
// if esbuild threw the error, find original code source to display (if it’s mapped)
if (err.errors && tsResult?.map) {
const json = JSON.parse(tsResult.map);
const mappings = decode(json.mappings);
const focusMapping = mappings[err.errors[0].location.line + 1];
err.sourceLoc = { file: id, line: (focusMapping[0][2] || 0) + 1, column: (focusMapping[0][3] || 0) + 1 };
if (Array.isArray(focusMapping) && focusMapping.length) {
err.sourceLoc = { file: id, line: (focusMapping[0][2] || 0) + 1, column: (focusMapping[0][3] || 0) + 1 };
}
}
throw err;
}
Expand Down
24 changes: 16 additions & 8 deletions packages/astro/src/vite-plugin-astro/styles.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type vite from '../core/vite';

export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>;
import { PREPROCESSOR_EXTENSIONS } from '../core/ssr/css.js';

// https://vitejs.dev/guide/features.html#css-pre-processors
const SUPPORTED_PREPROCESSORS = new Set(['scss', 'sass', 'styl', 'stylus', 'less']);
export type TransformHook = (code: string, id: string, ssr?: boolean) => Promise<vite.TransformResult>;

/** Load vite:css’ transform() hook */
export function getViteTransform(viteConfig: vite.ResolvedConfig): TransformHook {
Expand All @@ -13,10 +12,19 @@ export function getViteTransform(viteConfig: vite.ResolvedConfig): TransformHook
return viteCSSPlugin.transform.bind(null as any) as any;
}

interface TransformWithViteOptions {
value: string;
attrs: Record<string, string>;
id: string;
transformHook: TransformHook;
ssr?: boolean;
}

/** Transform style using Vite hook */
export async function transformWithVite(value: string, attrs: Record<string, string>, id: string, transformHook: TransformHook): Promise<vite.TransformResult | null> {
const lang = (attrs.lang || '').toLowerCase(); // don’t be case-sensitive
if (!SUPPORTED_PREPROCESSORS.has(lang)) return null; // only preprocess the above
const result = await transformHook(value, id.replace(/\.astro$/, `.${lang}`));
return result || null;
export async function transformWithVite({ value, attrs, transformHook, id, ssr }: TransformWithViteOptions): Promise<vite.TransformResult | null> {
const lang = (`.${attrs.lang}` || '').toLowerCase(); // add leading "."; don’t be case-sensitive
if (!PREPROCESSOR_EXTENSIONS.has(lang)) {
return null; // only preprocess langs supported by Vite
}
return transformHook(value, id + `?astro&type=style&lang${lang}`, ssr);
}
Loading