Skip to content

Commit

Permalink
Astro.resolve (#1085)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
matthewp and Jonathan Neal committed Aug 16, 2021
1 parent 47025a7 commit 78b5bde
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 17 deletions.
20 changes: 20 additions & 0 deletions .changeset/seven-singers-mate.md
Original file line number Diff line number Diff line change
@@ -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')} />
```
24 changes: 24 additions & 0 deletions docs/src/pages/reference/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 12 additions & 14 deletions packages/astro/src/compiler/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand All @@ -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') {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 === '' ? '' : ',';
Expand Down
10 changes: 10 additions & 0 deletions packages/astro/src/compiler/codegen/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 21 additions & 0 deletions packages/astro/src/compiler/codegen/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
20 changes: 18 additions & 2 deletions packages/astro/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:https://${hostname}:${port}`;
const devSite = `http:https://${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 = `
Expand All @@ -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;
Expand Down Expand Up @@ -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}
Expand All @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions packages/astro/test/astro-global-build.test.js
Original file line number Diff line number Diff line change
@@ -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();
11 changes: 10 additions & 1 deletion packages/astro/test/astro-global.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const penguinUrl = Astro.resolve('../images/penguin.png');
---
<img src={penguinUrl} />
<img src="../images/penguin.png" />
12 changes: 12 additions & 0 deletions packages/astro/test/fixtures/astro-global/src/pages/resolve.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import Child from '../components/ChildResolve.astro';
---

<html>
<head>
<title>Testing</title>
</head>
<body>
<Child />
</body>
</html>

0 comments on commit 78b5bde

Please sign in to comment.