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

New handling for define:vars scripts and styles #3976

Merged
merged 13 commits into from
Jul 22, 2022
Merged
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
"@astrojs/compiler": "^0.20.0",
"@astrojs/compiler": "^0.21.0",
"@astrojs/language-server": "^0.20.0",
"@astrojs/markdown-remark": "^0.12.0",
"@astrojs/prism": "0.6.1",
Expand Down
47 changes: 20 additions & 27 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,6 @@ export async function renderComponent(
if (Component && (Component as any).isAstroComponentFactory) {
async function* renderAstroComponentInline(): AsyncGenerator<string, void, undefined> {
let iterable = await renderToIterable(result, Component as any, _props, slots);
// If this component added any define:vars styles and the head has already been
// sent out, we need to include those inline.
if (result.styles.size && alreadyHeadRenderedResults.has(result)) {
let styles = Array.from(result.styles);
result.styles.clear();
for (const style of styles) {
if ('define:vars' in style.props) {
// We only want to render the property value and not the full stylesheet
// which is bundled in the head.
style.children = '';
yield markHTMLString(renderElement('style', style));
}
}
}
yield* iterable;
}

Expand Down Expand Up @@ -583,7 +569,7 @@ Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the
}

// support object styles for better JSX compat
if (key === 'style' && typeof value === 'object') {
if (key === 'style' && !(value instanceof HTMLString) && typeof value === 'object') {
return markHTMLString(` ${key}="${toStyleString(value)}"`);
}

Expand Down Expand Up @@ -633,19 +619,31 @@ export function spreadAttributes(
}

// Adds CSS variables to an inline style tag
export function defineStyleVars(selector: string, vars: Record<any, any>) {
let output = '\n';
for (const [key, value] of Object.entries(vars)) {
output += ` --${key}: ${value};\n`;
export function defineStyleVars(defs: Record<any, any>|Record<any, any>[]) {
let output = '';
let arr = !Array.isArray(defs) ? [defs] : defs;
for (const vars of arr) {
for (const [key, value] of Object.entries(vars)) {
if (value || value === 0) {
output += `--${key}: ${value};`;
}
}
}
return markHTMLString(`${selector} {${output}}`);
return markHTMLString(output);
}

// converts (most) arbitrary strings to valid JS identifiers
const toIdent = (k: string) =>
k.trim().replace(/(?:(?<!^)\b\w|\s+|[^\w]+)/g, (match, index) => {
if (/[^\w]|\s/.test(match)) return '';
return index === 0 ? match : match.toUpperCase();
});

// Adds variables to an inline script.
export function defineScriptVars(vars: Record<any, any>) {
export async function defineScriptVars(vars: Record<any, any>) {
natemoo-re marked this conversation as resolved.
Show resolved Hide resolved
let output = '';
for (const [key, value] of Object.entries(vars)) {
output += `let ${key} = ${JSON.stringify(value)};\n`;
output += `let ${toIdent(key)} = ${JSON.stringify(value)};\n`;
}
return markHTMLString(output);
}
Expand Down Expand Up @@ -945,11 +943,6 @@ function renderElement(
const { lang: _, 'data-astro-id': astroId, 'define:vars': defineVars, ...props } = _props;
if (defineVars) {
if (name === 'style') {
if (props['is:global']) {
children = defineStyleVars(`:root`, defineVars) + '\n' + children;
} else {
children = defineStyleVars(`.astro-${astroId}`, defineVars) + '\n' + children;
}
delete props['is:global'];
delete props['is:scoped'];
}
Expand Down
35 changes: 26 additions & 9 deletions packages/astro/src/vite-plugin-astro/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PluginContext } from 'rollup';
import type { PluginContext, SourceDescription } from 'rollup';
import type * as vite from 'vite';
import type { AstroConfig } from '../@types/astro';
import type { LogOptions } from '../core/logger/core.js';
Expand Down Expand Up @@ -175,17 +175,29 @@ export default function astro({ config, logging }: AstroPluginOptions): vite.Plu
}
}

return {
code:
hoistedScript.type === 'inline'
? hoistedScript.code!
: `import "${hoistedScript.src!}";`,
let result: SourceDescription & { meta: any } = {
code: '',
meta: {
vite: {
lang: 'ts',
},
},
lang: 'ts'
}
}
};

switch (hoistedScript.type) {
case 'inline': {
let { code, map } = hoistedScript
result.code = appendSourceMap(code, map);
break;
}
case 'external': {
const { src } = hoistedScript
result.code = `import "${src}"`;
break;
}
}

return result
}
default:
return null;
Expand Down Expand Up @@ -349,3 +361,8 @@ ${source}
},
};
}

function appendSourceMap(content: string, map?: string) {
if (!map) return content;
return `${content}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(map).toString('base64')}`
}
45 changes: 23 additions & 22 deletions packages/astro/test/astro-directives.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,36 @@ describe('Directives', async () => {
const html = await fixture.readFile('/define-vars/index.html');
const $ = cheerio.load(html);

expect($('script#inline')).to.have.lengthOf(1);
expect($('script#inline').toString()).to.include('let foo = "bar"');
expect($('script')).to.have.lengthOf(3);

let i = 0;
for (const script of $('script').toArray()) {
// Wrap script in scope ({}) to avoid redeclaration errors
expect($(script).text().at(0)).to.equal('{');
expect($(script).text().at(-1)).to.equal('}');
if (i < 2) {
// Inline defined variables
expect($(script).toString()).to.include('let foo = "bar"');
} else {
// Convert invalid keys to valid identifiers
expect($(script).toString()).to.include('let dashCase = "bar"');
}
i++;
}
});

it('Passes define:vars to style elements', async () => {
const html = await fixture.readFile('/define-vars/index.html');
const $ = cheerio.load(html);

expect($('style')).to.have.lengthOf(2);
expect($('style').toString()).to.include('--bg: white;');
expect($('style').toString()).to.include('--fg: black;');

const scopedClass = $('html')
.attr('class')
.split(' ')
.find((name) => /^astro-[A-Za-z0-9-]+/.test(name));

expect($($('style').get(0)).toString().replace(/\s+/g, '')).to.equal(
`<style>.${scopedClass}{--bg:white;--fg:black;}body{background:var(--bg);color:var(--fg)}</style>`
);

const scopedTitleClass = $('.title')
.attr('class')
.split(' ')
.find((name) => /^astro-[A-Za-z0-9-]+/.test(name));

expect($($('style').get(1)).toString().replace(/\s+/g, '')).to.equal(
`<style>.${scopedTitleClass}{--textColor:red;}</style>`
);

// Inject style attribute on top-level element in page
expect($('html').attr('style').toString()).to.include('--bg: white;');
expect($('html').attr('style').toString()).to.include('--fg: black;');

// Inject style attribute on top-level elements in component
expect($('h1').attr('style').toString()).to.include('--textColor: red;');
});

it('set:html', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
const textColor = 'red'
const textColor = 'red'
---

<h1 class="title">hello there</h1>

<style define:vars={{textColor: textColor}}>
<style define:vars={{ textColor: textColor }}>
.title {
color: var(--textColor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ let fg = 'black'

<html>
<head>
<style define:vars={{bg, fg}}>
body {
<style define:vars={{bg}}>
h1 {
background: var(--bg);
}
</style>
<style define:vars={{fg}}>
h1 {
color: var(--fg);
}
</style>
Expand All @@ -18,6 +22,12 @@ let fg = 'black'
<script id="inline" define:vars={{ foo }}>
console.log(foo);
</script>
<script id="inline-2" define:vars={{ foo }}>
console.log(foo);
</script>
<script id="inline-3" define:vars={{ 'dash-case': foo }}>
console.log(foo);
</script>

<Title />
</body>
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.