Improve asset resolution in Astro (#437)
* Improve asset resolution in Astro Fixes #96 * Add docs, changeset * Fix collection resolution
This commit is contained in:
parent
0c39c27ef5
commit
272769d723
12 changed files with 111 additions and 32 deletions
5
.changeset/loud-carrots-trade.md
Normal file
5
.changeset/loud-carrots-trade.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve asset resolution
|
|
@ -147,6 +147,39 @@ Inside of an expression, you must wrap multiple elements in a Fragment. Fragment
|
||||||
| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
|
| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
|
||||||
| Attributes | `dash-case` | `camelCase` |
|
| Attributes | `dash-case` | `camelCase` |
|
||||||
|
|
||||||
|
### URL resolution
|
||||||
|
|
||||||
|
It’s important to note that Astro **won’t** transform HTML references for you. For example, consider an `<img>` tag with a relative `src` attribute inside `src/pages/about.astro`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Incorrect: will try and load `/about/thumbnail.png` -->
|
||||||
|
<img src="./thumbnail.png" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Since `src/pages/about.astro` will build to `/about/index.html`, you may not have expected that image to live at `/about/thumbnail.png`. So to fix this, choose either of two options:
|
||||||
|
|
||||||
|
#### Option 1: Absolute URLs
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ✅ Correct: references public/thumbnail.png -->
|
||||||
|
<img src="/thumbnail.png" />
|
||||||
|
```
|
||||||
|
|
||||||
|
The recommended approach is to place files within `public/*`. This references a file it `public/thumbnail.png`, which will resolve to `/thumbnail.png` at the final build (since `public/` ends up at `/`).
|
||||||
|
|
||||||
|
#### Option 2: Asset import references
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
---
|
||||||
|
// ✅ Correct: references src/thumbnail.png
|
||||||
|
import thumbnailSrc from './thumbnail.png';
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src={thumbnailSrc} />
|
||||||
|
```
|
||||||
|
|
||||||
|
If you’d prefer to organize assets alongside Astro components, you may import the file in JavaScript inside the component script. This works as intended but this makes `thumbnail.png` harder to reference in other parts of your app, as its final URL isn’t easily-predictable (unlike assets in `public/*`, where the final URL is guaranteed to never change).
|
||||||
|
|
||||||
### TODO: Composition (Slots)
|
### TODO: Composition (Slots)
|
||||||
|
|
||||||
[code-ext]: https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode
|
[code-ext]: https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode
|
||||||
|
|
|
@ -91,18 +91,13 @@ export async function buildCollectionPage({ astroConfig, filepath, runtime, site
|
||||||
export async function buildStaticPage({ astroConfig, buildState, filepath, runtime }: PageBuildOptions): Promise<void> {
|
export async function buildStaticPage({ astroConfig, buildState, filepath, runtime }: PageBuildOptions): Promise<void> {
|
||||||
const { pages: pagesRoot } = astroConfig;
|
const { pages: pagesRoot } = astroConfig;
|
||||||
const url = filepath.pathname.replace(pagesRoot.pathname, '/').replace(/(index)?\.(astro|md)$/, '');
|
const url = filepath.pathname.replace(pagesRoot.pathname, '/').replace(/(index)?\.(astro|md)$/, '');
|
||||||
|
const result = await runtime.load(url);
|
||||||
// build page in parallel
|
if (result.statusCode !== 200) throw new Error((result as any).error);
|
||||||
await Promise.all([
|
const outFile = path.posix.join(url, '/index.html');
|
||||||
runtime.load(url).then((result) => {
|
buildState[outFile] = {
|
||||||
if (result.statusCode !== 200) throw new Error((result as any).error);
|
srcPath: filepath,
|
||||||
const outFile = path.posix.join(url, '/index.html');
|
contents: result.contents,
|
||||||
buildState[outFile] = {
|
contentType: 'text/html',
|
||||||
srcPath: filepath,
|
encoding: 'utf8',
|
||||||
contents: result.contents,
|
};
|
||||||
contentType: 'text/html',
|
|
||||||
encoding: 'utf8',
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,21 @@ export function getDistPath(specifier: string, { astroConfig, srcPath }: { astro
|
||||||
|
|
||||||
const fileLoc = new URL(specifier, srcPath);
|
const fileLoc = new URL(specifier, srcPath);
|
||||||
const projectLoc = fileLoc.pathname.replace(projectRoot.pathname, '');
|
const projectLoc = fileLoc.pathname.replace(projectRoot.pathname, '');
|
||||||
|
const ext = path.extname(fileLoc.pathname);
|
||||||
|
|
||||||
const isPage = fileLoc.pathname.includes(pagesRoot.pathname);
|
const isPage = fileLoc.pathname.includes(pagesRoot.pathname) && (ext === '.astro' || ext === '.md');
|
||||||
// if this lives above src/pages, return that URL
|
// if this lives in src/pages, return that URL
|
||||||
if (isPage) {
|
if (isPage) {
|
||||||
const [, publicURL] = projectLoc.split(pagesRoot.pathname);
|
const [, publicURL] = projectLoc.split(pagesRoot.pathname);
|
||||||
return publicURL || '/index.html'; // if this is missing, this is the root
|
return publicURL || '/index.html'; // if this is missing, this is the root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if this is in public/, use that as final URL
|
||||||
|
const isPublicAsset = fileLoc.pathname.includes(astroConfig.public.pathname);
|
||||||
|
if (isPublicAsset) {
|
||||||
|
return fileLoc.pathname.replace(astroConfig.public.pathname, '/');
|
||||||
|
}
|
||||||
|
|
||||||
// otherwise, return /_astro/* url
|
// otherwise, return /_astro/* url
|
||||||
return '/_astro/' + projectLoc;
|
return '/_astro/' + projectLoc;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ configureSnowpackLogger(snowpackLogger);
|
||||||
/** Pass a URL to Astro to resolve and build */
|
/** Pass a URL to Astro to resolve and build */
|
||||||
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
||||||
const { logging, snowpackRuntime, snowpack } = config;
|
const { logging, snowpackRuntime, snowpack } = config;
|
||||||
const { pages: pagesRoot, buildOptions, devOptions } = config.astroConfig;
|
const { buildOptions, devOptions } = config.astroConfig;
|
||||||
|
|
||||||
let origin = buildOptions.site ? new URL(buildOptions.site).origin : `http://localhost:${devOptions.port}`;
|
let origin = buildOptions.site ? new URL(buildOptions.site).origin : `http://localhost:${devOptions.port}`;
|
||||||
const fullurl = new URL(rawPathname || '/', origin);
|
const fullurl = new URL(rawPathname || '/', origin);
|
||||||
|
@ -63,7 +63,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
||||||
const reqPath = decodeURI(fullurl.pathname);
|
const reqPath = decodeURI(fullurl.pathname);
|
||||||
info(logging, 'access', reqPath);
|
info(logging, 'access', reqPath);
|
||||||
|
|
||||||
const searchResult = searchForPage(fullurl, pagesRoot);
|
const searchResult = searchForPage(fullurl, config.astroConfig);
|
||||||
if (searchResult.statusCode === 404) {
|
if (searchResult.statusCode === 404) {
|
||||||
try {
|
try {
|
||||||
const result = await snowpack.loadUrl(reqPath);
|
const result = await snowpack.loadUrl(reqPath);
|
||||||
|
@ -332,7 +332,6 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO
|
||||||
};
|
};
|
||||||
|
|
||||||
const mountOptions = {
|
const mountOptions = {
|
||||||
[fileURLToPath(pagesRoot)]: '/_astro/pages',
|
|
||||||
...(existsSync(astroConfig.public) ? { [fileURLToPath(astroConfig.public)]: '/' } : {}),
|
...(existsSync(astroConfig.public) ? { [fileURLToPath(astroConfig.public)]: '/' } : {}),
|
||||||
[fileURLToPath(frontendPath)]: '/_astro_frontend',
|
[fileURLToPath(frontendPath)]: '/_astro_frontend',
|
||||||
[fileURLToPath(projectRoot)]: '/_astro', // must be last (greediest)
|
[fileURLToPath(projectRoot)]: '/_astro', // must be last (greediest)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { AstroConfig } from './@types/astro';
|
||||||
|
|
||||||
import 'source-map-support/register.js';
|
import 'source-map-support/register.js';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -9,13 +11,14 @@ interface PageLocation {
|
||||||
snowpackURL: string;
|
snowpackURL: string;
|
||||||
}
|
}
|
||||||
/** findAnyPage and return the _astro candidate for snowpack */
|
/** findAnyPage and return the _astro candidate for snowpack */
|
||||||
function findAnyPage(candidates: Array<string>, pagesRoot: URL): PageLocation | false {
|
function findAnyPage(candidates: Array<string>, astroConfig: AstroConfig): PageLocation | false {
|
||||||
for (let candidate of candidates) {
|
for (let candidate of candidates) {
|
||||||
const url = new URL(`./${candidate}`, pagesRoot);
|
const url = new URL(`./${candidate}`, astroConfig.pages);
|
||||||
if (existsSync(url)) {
|
if (existsSync(url)) {
|
||||||
|
const pagesPath = astroConfig.pages.pathname.replace(astroConfig.projectRoot.pathname, '');
|
||||||
return {
|
return {
|
||||||
fileURL: url,
|
fileURL: url,
|
||||||
snowpackURL: `/_astro/pages/${candidate}.js`,
|
snowpackURL: `/_astro/${pagesPath}${candidate}.js`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,14 +42,14 @@ type SearchResult =
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Given a URL, attempt to locate its source file (similar to Snowpack’s load()) */
|
/** Given a URL, attempt to locate its source file (similar to Snowpack’s load()) */
|
||||||
export function searchForPage(url: URL, pagesRoot: URL): SearchResult {
|
export function searchForPage(url: URL, astroConfig: AstroConfig): SearchResult {
|
||||||
const reqPath = decodeURI(url.pathname);
|
const reqPath = decodeURI(url.pathname);
|
||||||
const base = reqPath.substr(1);
|
const base = reqPath.substr(1);
|
||||||
|
|
||||||
// Try to find index.astro/md paths
|
// Try to find index.astro/md paths
|
||||||
if (reqPath.endsWith('/')) {
|
if (reqPath.endsWith('/')) {
|
||||||
const candidates = [`${base}index.astro`, `${base}index.md`];
|
const candidates = [`${base}index.astro`, `${base}index.md`];
|
||||||
const location = findAnyPage(candidates, pagesRoot);
|
const location = findAnyPage(candidates, astroConfig);
|
||||||
if (location) {
|
if (location) {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -57,7 +60,7 @@ export function searchForPage(url: URL, pagesRoot: URL): SearchResult {
|
||||||
} else {
|
} else {
|
||||||
// Try to find the page by its name.
|
// Try to find the page by its name.
|
||||||
const candidates = [`${base}.astro`, `${base}.md`];
|
const candidates = [`${base}.astro`, `${base}.md`];
|
||||||
let location = findAnyPage(candidates, pagesRoot);
|
let location = findAnyPage(candidates, astroConfig);
|
||||||
if (location) {
|
if (location) {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -69,7 +72,7 @@ export function searchForPage(url: URL, pagesRoot: URL): SearchResult {
|
||||||
|
|
||||||
// Try to find name/index.astro/md
|
// Try to find name/index.astro/md
|
||||||
const candidates = [`${base}/index.astro`, `${base}/index.md`];
|
const candidates = [`${base}/index.astro`, `${base}/index.md`];
|
||||||
const location = findAnyPage(candidates, pagesRoot);
|
const location = findAnyPage(candidates, astroConfig);
|
||||||
if (location) {
|
if (location) {
|
||||||
return {
|
return {
|
||||||
statusCode: 301,
|
statusCode: 301,
|
||||||
|
@ -81,7 +84,7 @@ export function searchForPage(url: URL, pagesRoot: URL): SearchResult {
|
||||||
// Try and load collections (but only for non-extension files)
|
// Try and load collections (but only for non-extension files)
|
||||||
const hasExt = !!path.extname(reqPath);
|
const hasExt = !!path.extname(reqPath);
|
||||||
if (!location && !hasExt) {
|
if (!location && !hasExt) {
|
||||||
const collection = loadCollection(reqPath, pagesRoot);
|
const collection = loadCollection(reqPath, astroConfig);
|
||||||
if (collection) {
|
if (collection) {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -109,8 +112,8 @@ export function searchForPage(url: URL, pagesRoot: URL): SearchResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** load a collection route */
|
/** load a collection route */
|
||||||
function loadCollection(url: string, pagesRoot: URL): { currentPage?: number; location: PageLocation } | undefined {
|
function loadCollection(url: string, astroConfig: AstroConfig): { currentPage?: number; location: PageLocation } | undefined {
|
||||||
const pages = glob('**/$*.astro', { cwd: fileURLToPath(pagesRoot), filesOnly: true });
|
const pages = glob('**/$*.astro', { cwd: fileURLToPath(astroConfig.pages), filesOnly: true });
|
||||||
for (const pageURL of pages) {
|
for (const pageURL of pages) {
|
||||||
const reqURL = new RegExp('^/' + pageURL.replace(/\$([^/]+)\.astro/, '$1') + '/?(.*)');
|
const reqURL = new RegExp('^/' + pageURL.replace(/\$([^/]+)\.astro/, '$1') + '/?(.*)');
|
||||||
const match = url.match(reqURL);
|
const match = url.match(reqURL);
|
||||||
|
@ -125,10 +128,11 @@ function loadCollection(url: string, pagesRoot: URL): { currentPage?: number; lo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const pagesPath = astroConfig.pages.pathname.replace(astroConfig.projectRoot.pathname, '');
|
||||||
return {
|
return {
|
||||||
location: {
|
location: {
|
||||||
fileURL: new URL(`./${pageURL}`, pagesRoot),
|
fileURL: new URL(`./${pageURL}`, astroConfig.pages),
|
||||||
snowpackURL: `/_astro/pages/${pageURL}.js`,
|
snowpackURL: `/_astro/${pagesPath}${pageURL}.js`,
|
||||||
},
|
},
|
||||||
currentPage,
|
currentPage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { setup, setupBuild } from './helpers.js';
|
||||||
const Components = suite('Components tests');
|
const Components = suite('Components tests');
|
||||||
|
|
||||||
setup(Components, './fixtures/astro-components');
|
setup(Components, './fixtures/astro-components');
|
||||||
setupBuild(Components, './fixtures/astro-components');
|
|
||||||
|
|
||||||
Components('Astro components are able to render framework components', async ({ runtime }) => {
|
Components('Astro components are able to render framework components', async ({ runtime }) => {
|
||||||
let result = await runtime.load('/');
|
let result = await runtime.load('/');
|
||||||
|
|
19
packages/astro/test/astro-resolve.test.js
Normal file
19
packages/astro/test/astro-resolve.test.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { suite } from 'uvu';
|
||||||
|
import * as assert from 'uvu/assert';
|
||||||
|
import { setupBuild } from './helpers.js';
|
||||||
|
|
||||||
|
const Resolution = suite('Astro Resolution');
|
||||||
|
|
||||||
|
setupBuild(Resolution, './fixtures/astro-resolve');
|
||||||
|
|
||||||
|
Resolution('Assets', async (context) => {
|
||||||
|
await context.build();
|
||||||
|
|
||||||
|
// public/ asset resolved
|
||||||
|
assert.ok(await context.readFile('/svg.svg'));
|
||||||
|
|
||||||
|
// asset in src/pages resolved (and didn’t overwrite /svg.svg)
|
||||||
|
assert.ok(await context.readFile('/_astro/src/pages/svg.svg'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Resolution.run();
|
1
packages/astro/test/fixtures/astro-resolve/public/svg.svg
vendored
Normal file
1
packages/astro/test/fixtures/astro-resolve/public/svg.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg></svg>
|
After Width: | Height: | Size: 12 B |
3
packages/astro/test/fixtures/astro-resolve/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-resolve/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"workspaceRoot": "../../../../../"
|
||||||
|
}
|
12
packages/astro/test/fixtures/astro-resolve/src/pages/index.astro
vendored
Normal file
12
packages/astro/test/fixtures/astro-resolve/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- public/ -->
|
||||||
|
<img src="/svg.svg" />
|
||||||
|
|
||||||
|
<!-- src/ -->
|
||||||
|
<img src="./svg.svg" />
|
||||||
|
</body>
|
||||||
|
</html>
|
1
packages/astro/test/fixtures/astro-resolve/src/pages/svg.svg
vendored
Normal file
1
packages/astro/test/fixtures/astro-resolve/src/pages/svg.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg></svg>
|
After Width: | Height: | Size: 12 B |
Loading…
Reference in a new issue