diff --git a/.changeset/seven-singers-mate.md b/.changeset/seven-singers-mate.md
new file mode 100644
index 000000000000..1daa0ad3df02
--- /dev/null
+++ b/.changeset/seven-singers-mate.md
@@ -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
+
+```
+
+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
+
+```
\ No newline at end of file
diff --git a/docs/src/pages/reference/api-reference.md b/docs/src/pages/reference/api-reference.md
index 1482416912a1..2f73cb30c9fd 100644
--- a/docs/src/pages/reference/api-reference.md
+++ b/docs/src/pages/reference/api-reference.md
@@ -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;
+---
+
+
Welcome to {path}
+```
+
+### `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
+
+```
+
+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
+
+```
+
## `getStaticPaths()`
If a page uses dynamic params in the filename, that component will need to export a `getStaticPaths()` function.
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index 9c4f3bf50884..2557f8b54bec 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -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): HydrationAttrib
return { method, value };
}
+
+
/** Retrieve attributes from TemplateNode */
-async function getAttributes(attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise> {
+async function getAttributes(nodeName: string, attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise> {
let result: Record = {};
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 === '' ? '' : ',';
diff --git a/packages/astro/src/compiler/codegen/interfaces.ts b/packages/astro/src/compiler/codegen/interfaces.ts
new file mode 100644
index 000000000000..a487b85e9de3
--- /dev/null
+++ b/packages/astro/src/compiler/codegen/interfaces.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/astro/src/compiler/codegen/utils.ts b/packages/astro/src/compiler/codegen/utils.ts
index 8183f9142cbb..a4e66fbe8ab5 100644
--- a/packages/astro/src/compiler/codegen/utils.ts
+++ b/packages/astro/src/compiler/codegen/utils.ts
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts
index b824e65a9b63..a84abf97fba2 100644
--- a/packages/astro/src/compiler/index.ts
+++ b/packages/astro/src/compiler/index.ts
@@ -107,8 +107,13 @@ interface CompileComponentOptions {
/** Compiles an Astro component */
export async function compileComponent(source: string, { compileOptions, filename, projectRoot }: CompileComponentOptions): Promise {
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,
diff --git a/packages/astro/test/astro-global-build.test.js b/packages/astro/test/astro-global-build.test.js
new file mode 100644
index 000000000000..6acdfd4284f6
--- /dev/null
+++ b/packages/astro/test/astro-global-build.test.js
@@ -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();
\ No newline at end of file
diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js
index 556bf66ada65..9a80973aee73 100644
--- a/packages/astro/test/astro-global.test.js
+++ b/packages/astro/test/astro-global.test.js
@@ -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();
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-global/src/components/ChildResolve.astro b/packages/astro/test/fixtures/astro-global/src/components/ChildResolve.astro
new file mode 100644
index 000000000000..185a1fb1c092
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-global/src/components/ChildResolve.astro
@@ -0,0 +1,5 @@
+---
+const penguinUrl = Astro.resolve('../images/penguin.png');
+---
+
+
\ No newline at end of file
diff --git a/packages/astro/test/fixtures/astro-global/src/pages/resolve.astro b/packages/astro/test/fixtures/astro-global/src/pages/resolve.astro
new file mode 100644
index 000000000000..df2b5f5cb364
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-global/src/pages/resolve.astro
@@ -0,0 +1,12 @@
+---
+import Child from '../components/ChildResolve.astro';
+---
+
+
+
+ Testing
+
+
+
+
+
\ No newline at end of file