Astro.resolve (#1085)

* add: Astro.resolve

* Add docs and tests for Astro.resolve

* Add warnings when using string literals

* Prevent windows errors

* Adds a changeset

* Use the astro logger to log the warning

* Use the .js extension

* Dont warn for data urls

* Rename nonRelative and better match

Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com>
This commit is contained in:
Matthew Phillips 2021-08-16 16:43:06 -04:00 committed by GitHub
parent 47025a7c7d
commit 78b5bde14c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 17 deletions

View file

@ -0,0 +1,20 @@
---
'docs': patch
'astro': patch
---
Adds support for Astro.resolve
`Astro.resolve()` helps with creating URLs relative to the current Astro file, allowing you to reference files within your `src/` folder.
Astro *does not* resolve relative links within HTML, such as images:
```html
<img src="../images/penguin.png" />
```
The above will be sent to the browser as-is and the browser will resolve it relative to the current __page__. If you want it to be resolved relative to the .astro file you are working in, use `Astro.resolve`:
```astro
<img src={Astro.resolve('../images/penguin.png')} />
```

View file

@ -64,6 +64,30 @@ const data = Astro.fetchContent('../pages/post/*.md'); // returns an array of po
`Astro.site` returns a `URL` made from `buildOptions.site` in your Astro config. If undefined, this will return a URL generated from `localhost`.
```astro
---
const path = Astro.site.pathname;
---
<h1>Welcome to {path}</h1>
```
### `Astro.resolve()`
`Astro.resolve()` helps with creating URLs relative to the current Astro file, allowing you to reference files within your `src/` folder.
Astro *does not* resolve relative links within HTML, such as images:
```html
<img src="../images/penguin.png" />
```
The above will be sent to the browser as-is and the browser will resolve it relative to the current __page__. If you want it to be resolved relative to the .astro file you are working in, use `Astro.resolve`:
```astro
<img src={Astro.resolve('../images/penguin.png')} />
```
## `getStaticPaths()`
If a page uses dynamic params in the filename, that component will need to export a `getStaticPaths()` function.

View file

@ -2,7 +2,7 @@ import type { Ast, Script, Style, TemplateNode, Expression } from '@astrojs/pars
import type { CompileOptions } from '../../@types/compiler';
import type { AstroConfig, AstroMarkdownOptions, TransformResult, ComponentInfo, Components } from '../../@types/astro';
import type { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier, ImportDefaultSpecifier } from '@babel/types';
import type { Attribute } from './interfaces';
import eslexer from 'es-module-lexer';
import esbuild from 'esbuild';
import path from 'path';
@ -15,6 +15,7 @@ import * as babelTraverse from '@babel/traverse';
import { error, warn, parseError } from '../../logger.js';
import { yellow } from 'kleur/colors';
import { isComponentTag, isCustomElementTag, positionAt } from '../utils.js';
import { warnIfRelativeStringLiteral } from './utils.js';
import { renderMarkdown } from '@astrojs/markdown-support';
import { camelCase } from 'camel-case';
import { transform } from '../transform/index.js';
@ -32,15 +33,6 @@ const { transformSync } = esbuild;
const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible', 'client:media']);
interface Attribute {
start: number;
end: number;
type: 'Attribute' | 'Spread';
name: string;
value: TemplateNode[] | boolean;
expression?: Expression;
}
interface CodeGenOptions {
compileOptions: CompileOptions;
filename: string;
@ -67,8 +59,10 @@ function findHydrationAttributes(attrs: Record<string, string>): HydrationAttrib
return { method, value };
}
/** Retrieve attributes from TemplateNode */
async function getAttributes(attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> {
async function getAttributes(nodeName: string, attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> {
let result: Record<string, string> = {};
for (const attr of attrs) {
if (attr.type === 'Spread') {
@ -118,9 +112,13 @@ async function getAttributes(attrs: Attribute[], state: CodegenState, compileOpt
}
continue;
}
case 'Text':
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
case 'Text': {
let text = getTextFromAttribute(val);
warnIfRelativeStringLiteral(compileOptions.logging, nodeName, attr, text);
result[attr.name] = JSON.stringify(text);
continue;
}
case 'AttributeShorthand':
result[attr.name] = '(' + attr.name + ')';
continue;
@ -641,7 +639,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
throw new Error('AHHHH');
}
try {
const attributes = await getAttributes(node.attributes, state, compileOptions);
const attributes = await getAttributes(name, node.attributes, state, compileOptions);
const hydrationAttributes = findHydrationAttributes(attributes);
buffers.out += buffers.out === '' ? '' : ',';

View file

@ -0,0 +1,10 @@
import type { Expression, TemplateNode } from '@astrojs/parser';
export interface Attribute {
start: number;
end: number;
type: 'Attribute' | 'Spread';
name: string;
value: TemplateNode[] | boolean;
expression?: Expression;
}

View file

@ -3,6 +3,9 @@
*/
import type { VariableDeclarator, CallExpression } from '@babel/types';
import type { Attribute } from './interfaces';
import type { LogOptions } from '../../logger';
import { warn } from '../../logger.js';
/** Is this an import.meta.* built-in? You can pass an optional 2nd param to see if the name matches as well. */
export function isImportMetaDeclaration(declaration: VariableDeclarator, metaName?: string): boolean {
@ -18,3 +21,21 @@ export function isImportMetaDeclaration(declaration: VariableDeclarator, metaNam
if (metaName && (init.callee.property.type !== 'Identifier' || init.callee.property.name !== metaName)) return false;
return true;
}
const warnableRelativeValues = new Set([
'img+src',
'a+href',
'script+src',
'link+href',
'source+srcset'
]);
const matchesRelative = /^(?![A-Za-z][+-.0-9A-Za-z]*:|\/)/;
export function warnIfRelativeStringLiteral(logging: LogOptions, nodeName: string, attr: Attribute, value: string) {
let key = nodeName + '+' + attr.name;
if(warnableRelativeValues.has(key) && matchesRelative.test(value)) {
let message = `This value will be resolved relative to the page: <${nodeName} ${attr.name}="${value}">`;
warn(logging, 'relative-link', message);
}
}

View file

@ -107,8 +107,13 @@ interface CompileComponentOptions {
/** Compiles an Astro component */
export async function compileComponent(source: string, { compileOptions, filename, projectRoot }: CompileComponentOptions): Promise<CompileResult> {
const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
const { mode } = compileOptions;
const { hostname, port } = compileOptions.astroConfig.devOptions;
const site = compileOptions.astroConfig.buildOptions.site || `http://${hostname}:${port}`;
const devSite = `http://${hostname}:${port}`;
const site = compileOptions.astroConfig.buildOptions.site || devSite;
const fileID = path.join('/_astro', path.relative(projectRoot, filename));
const fileURL = new URL('.' + fileID, mode === 'production' ? site : devSite);
// return template
let moduleJavaScript = `
@ -123,6 +128,12 @@ ${/* Global Astro Namespace (shadowed & extended by the scoped namespace inside
const __TopLevelAstro = {
site: new URL(${JSON.stringify(site)}),
fetchContent: (globResult) => fetchContent(globResult, import.meta.url),
resolve(...segments) {
return segments.reduce(
(url, segment) => new URL(segment, url),
new URL(${JSON.stringify(fileURL)})
).pathname
},
};
const Astro = __TopLevelAstro;
@ -158,10 +169,14 @@ async function __render(props, ...children) {
value: (props[__astroInternal] && props[__astroInternal].isPage) || false,
enumerable: true
},
resolve: {
value: (props[__astroContext] && props[__astroContext].resolve) || {},
enumerable: true
},
request: {
value: (props[__astroContext] && props[__astroContext].request) || {},
enumerable: true
}
},
});
${result.script}
@ -186,6 +201,7 @@ export async function __renderPage({request, children, props, css}) {
value: {
pageCSS: css,
request,
resolve: __TopLevelAstro.resolve,
createAstroRootUID(seed) { return seed + astroRootUIDCounter++; },
},
writable: false,

View file

@ -0,0 +1,23 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const GlobalBuild = suite('Astro.* built');
setup(GlobalBuild, './fixtures/astro-global', {
runtimeOptions: {
mode: 'production'
}
});
GlobalBuild('Astro.resolve in the build', async (context) => {
const result = await context.runtime.load('/resolve');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('img').attr('src'), '/blog/_astro/src/images/penguin.png');
});
GlobalBuild.run();

View file

@ -41,4 +41,13 @@ Global('Astro.site', async (context) => {
assert.equal($('#site').attr('href'), 'https://mysite.dev/blog/');
});
Global.run();
Global('Astro.resolve in development', async (context) => {
const result = await context.runtime.load('/resolve');
assert.ok(!result.error, `build error: ${result.error}`);
const html = result.contents;
const $ = doc(html);
assert.equal($('img').attr('src'), '/_astro/src/images/penguin.png');
});
Global.run();

View file

@ -0,0 +1,5 @@
---
const penguinUrl = Astro.resolve('../images/penguin.png');
---
<img src={penguinUrl} />
<img src="../images/penguin.png" />

View file

@ -0,0 +1,12 @@
---
import Child from '../components/ChildResolve.astro';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<Child />
</body>
</html>