summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/metal-guests-tickle.md5
-rw-r--r--packages/astro/src/vite-plugin-config-alias/index.ts141
-rw-r--r--packages/astro/test/alias-tsconfig.test.js85
-rw-r--r--packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte4
-rw-r--r--packages/astro/test/fixtures/alias-tsconfig/src/pages/index.astro7
5 files changed, 174 insertions, 68 deletions
diff --git a/.changeset/metal-guests-tickle.md b/.changeset/metal-guests-tickle.md
new file mode 100644
index 000000000..545742583
--- /dev/null
+++ b/.changeset/metal-guests-tickle.md
@@ -0,0 +1,5 @@
+---
+'astro': minor
+---
+
+Support tsconfig aliases in CSS `@import`
diff --git a/packages/astro/src/vite-plugin-config-alias/index.ts b/packages/astro/src/vite-plugin-config-alias/index.ts
index ab4547cc6..71081c521 100644
--- a/packages/astro/src/vite-plugin-config-alias/index.ts
+++ b/packages/astro/src/vite-plugin-config-alias/index.ts
@@ -1,47 +1,27 @@
-import * as path from 'path';
+import path from 'path';
import type { AstroSettings } from '../@types/astro';
+import { normalizePath, type ResolvedConfig, type Plugin as VitePlugin } from 'vite';
-import type * as vite from 'vite';
-
-/** Result of successfully parsed tsconfig.json or jsconfig.json. */
-export declare interface Alias {
+type Alias = {
find: RegExp;
replacement: string;
-}
-
-/** Returns a path with its slashes replaced with posix slashes. */
-const normalize = (pathname: string) => String(pathname).split(path.sep).join(path.posix.sep);
+};
/** Returns a list of compiled aliases. */
const getConfigAlias = (settings: AstroSettings): Alias[] | null => {
- /** Closest tsconfig.json or jsconfig.json */
- const config = settings.tsConfig;
- const configPath = settings.tsConfigPath;
-
- // if no config was found, return null
- if (!config || !configPath) return null;
+ const { tsConfig, tsConfigPath } = settings;
+ if (!tsConfig || !tsConfigPath || !tsConfig.compilerOptions) return null;
- /** Compiler options from tsconfig.json or jsconfig.json. */
- const compilerOptions = Object(config.compilerOptions);
-
- // if no compilerOptions.baseUrl was defined, return null
- if (!compilerOptions.baseUrl) return null;
+ const { baseUrl, paths } = tsConfig.compilerOptions;
+ if (!baseUrl || !paths) return null;
// resolve the base url from the configuration file directory
- const baseUrl = path.posix.resolve(
- path.posix.dirname(normalize(configPath).replace(/^\/?/, '/')),
- normalize(compilerOptions.baseUrl)
- );
+ const resolvedBaseUrl = path.resolve(path.dirname(tsConfigPath), baseUrl);
- /** List of compiled alias expressions. */
const aliases: Alias[] = [];
// compile any alias expressions and push them to the list
- for (let [alias, values] of Object.entries(
- Object(compilerOptions.paths) as { [key: string]: string[] }
- )) {
- values = [].concat(values as never);
-
+ for (const [alias, values] of Object.entries(paths)) {
/** Regular Expression used to match a given path. */
const find = new RegExp(
`^${[...alias]
@@ -54,9 +34,9 @@ const getConfigAlias = (settings: AstroSettings): Alias[] | null => {
/** Internal index used to calculate the matching id in a replacement. */
let matchId = 0;
- for (let value of values) {
+ for (const value of values) {
/** String used to replace a matched path. */
- const replacement = [...path.posix.resolve(baseUrl, value)]
+ const replacement = [...normalizePath(path.resolve(resolvedBaseUrl, value))]
.map((segment) => (segment === '*' ? `$${++matchId}` : segment === '$' ? '$$' : segment))
.join('');
@@ -68,8 +48,10 @@ const getConfigAlias = (settings: AstroSettings): Alias[] | null => {
// - `baseUrl` changes the way non-relative specifiers are resolved
// - if `baseUrl` exists then all non-relative specifiers are resolved relative to it
aliases.push({
- find: /^(?!\.*\/)(.+)$/,
- replacement: `${[...baseUrl].map((segment) => (segment === '$' ? '$$' : segment)).join('')}/$1`,
+ find: /^(?!\.*\/|\w:)(.+)$/,
+ replacement: `${[...normalizePath(resolvedBaseUrl)]
+ .map((segment) => (segment === '$' ? '$$' : segment))
+ .join('')}/$1`,
});
return aliases;
@@ -80,40 +62,79 @@ export default function configAliasVitePlugin({
settings,
}: {
settings: AstroSettings;
-}): vite.PluginOption {
- const { config } = settings;
- /** Aliases from the tsconfig.json or jsconfig.json configuration. */
+}): VitePlugin | null {
const configAlias = getConfigAlias(settings);
+ if (!configAlias) return null;
- // if no config alias was found, bypass this plugin
- if (!configAlias) return {} as vite.PluginOption;
-
- return {
+ const plugin: VitePlugin = {
name: 'astro:tsconfig-alias',
enforce: 'pre',
- async resolveId(sourceId: string, importer, options) {
- /** Resolved ID conditionally handled by any other resolver. (this gives priority to all other resolvers) */
- const resolvedId = await this.resolve(sourceId, importer, { skipSelf: true, ...options });
-
- // if any other resolver handles the file, return that resolution
- if (resolvedId) return resolvedId;
+ configResolved(config) {
+ patchCreateResolver(config, plugin);
+ },
+ async resolveId(id, importer, options) {
+ if (isVirtualId(id)) return;
- // conditionally resolve the source ID from any matching alias or baseUrl
+ // Handle aliases found from `compilerOptions.paths`. Unlike Vite aliases, tsconfig aliases
+ // are best effort only, so we have to manually replace them here, instead of using `vite.resolve.alias`
for (const alias of configAlias) {
- if (alias.find.test(sourceId)) {
- /** Processed Source ID with our alias applied. */
- const aliasedSourceId = sourceId.replace(alias.find, alias.replacement);
-
- /** Resolved ID conditionally handled by any other resolver. (this also gives priority to all other resolvers) */
- const resolvedAliasedId = await this.resolve(aliasedSourceId, importer, {
- skipSelf: true,
- ...options,
- });
-
- // if the existing resolvers find the file, return that resolution
- if (resolvedAliasedId) return resolvedAliasedId;
+ if (alias.find.test(id)) {
+ const updatedId = id.replace(alias.find, alias.replacement);
+ const resolved = await this.resolve(updatedId, importer, { skipSelf: true, ...options });
+ if (resolved) return resolved;
}
}
},
};
+
+ return plugin;
+}
+
+/**
+ * Vite's `createResolver` is used to resolve various things, including CSS `@import`.
+ * However, there's no way to extend this resolver, besides patching it. This function
+ * patches and adds a Vite plugin whose `resolveId` will be used to resolve before the
+ * internal plugins in `createResolver`.
+ *
+ * Vite may simplify this soon: https://github.com/vitejs/vite/pull/10555
+ */
+function patchCreateResolver(config: ResolvedConfig, prePlugin: VitePlugin) {
+ const _createResolver = config.createResolver;
+ // @ts-expect-error override readonly property intentionally
+ config.createResolver = function (...args1: any) {
+ const resolver = _createResolver.apply(config, args1);
+ return async function (...args2: any) {
+ const id: string = args2[0];
+ const importer: string | undefined = args2[1];
+ const ssr: boolean | undefined = args2[3];
+
+ // fast path so we don't run this extensive logic in prebundling
+ if (importer?.includes('node_modules')) {
+ return resolver.apply(_createResolver, args2);
+ }
+
+ const fakePluginContext = {
+ resolve: (_id: string, _importer?: string) => resolver(_id, _importer, false, ssr),
+ };
+ const fakeResolveIdOpts = {
+ assertions: {},
+ isEntry: false,
+ ssr,
+ };
+
+ // @ts-expect-error resolveId exists
+ const resolved = await prePlugin.resolveId.apply(fakePluginContext, [
+ id,
+ importer,
+ fakeResolveIdOpts,
+ ]);
+ if (resolved) return resolved;
+
+ return resolver.apply(_createResolver, args2);
+ };
+ };
+}
+
+function isVirtualId(id: string) {
+ return id.includes('\0') || id.startsWith('virtual:') || id.startsWith('astro:');
}
diff --git a/packages/astro/test/alias-tsconfig.test.js b/packages/astro/test/alias-tsconfig.test.js
index 07dd1fe23..84dd50c2f 100644
--- a/packages/astro/test/alias-tsconfig.test.js
+++ b/packages/astro/test/alias-tsconfig.test.js
@@ -1,18 +1,38 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
-import { isWindows, loadFixture } from './test-utils.js';
+import { loadFixture } from './test-utils.js';
describe('Aliases with tsconfig.json', () => {
let fixture;
+ /**
+ * @param {string} html
+ * @returns {string[]}
+ */
+ function getLinks(html) {
+ let $ = cheerio.load(html);
+ let out = [];
+ $('link[rel=stylesheet]').each((i, el) => {
+ out.push($(el).attr('href'));
+ });
+ return out;
+ }
+
+ /**
+ * @param {string} href
+ * @returns {Promise<{ href: string; css: string; }>}
+ */
+ async function getLinkContent(href, f = fixture) {
+ const css = await f.readFile(href);
+ return { href, css };
+ }
+
before(async () => {
fixture = await loadFixture({
root: './fixtures/alias-tsconfig/',
});
});
- if (isWindows) return;
-
describe('dev', () => {
let devServer;
@@ -50,13 +70,66 @@ describe('Aliases with tsconfig.json', () => {
expect($('#namespace').text()).to.equal('namespace');
});
- // TODO: fix this https://github.com/withastro/astro/issues/6551
- it.skip('works in css @import', async () => {
+ it('works in css @import', async () => {
const html = await fixture.fetch('/').then((res) => res.text());
- console.log(html);
// imported css should be bundled
expect(html).to.include('#style-red');
expect(html).to.include('#style-blue');
});
+
+ it('works in components', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerio.load(html);
+
+ expect($('#alias').text()).to.equal('foo');
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('can load client components', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ // Should render aliased element
+ expect($('#client').text()).to.equal('test');
+
+ const scripts = $('script').toArray();
+ expect(scripts.length).to.be.greaterThan(0);
+ });
+
+ it('can load via baseUrl', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('#foo').text()).to.equal('foo');
+ expect($('#constants-foo').text()).to.equal('foo');
+ });
+
+ it('can load namespace packages with @* paths', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('#namespace').text()).to.equal('namespace');
+ });
+
+ it('works in css @import', async () => {
+ const html = await fixture.readFile('/index.html');
+ const content = await Promise.all(getLinks(html).map((href) => getLinkContent(href)));
+ const [{ css }] = content;
+ // imported css should be bundled
+ expect(css).to.include('#style-red');
+ expect(css).to.include('#style-blue');
+ });
+
+ it('works in components', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('#alias').text()).to.equal('foo');
+ });
});
});
diff --git a/packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte b/packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte
new file mode 100644
index 000000000..066885bd9
--- /dev/null
+++ b/packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte
@@ -0,0 +1,4 @@
+<script>
+ import { foo } from 'src/utils/constants';
+</script>
+<div id="alias">{foo}</div>
diff --git a/packages/astro/test/fixtures/alias-tsconfig/src/pages/index.astro b/packages/astro/test/fixtures/alias-tsconfig/src/pages/index.astro
index 00dc44b92..a3feb613b 100644
--- a/packages/astro/test/fixtures/alias-tsconfig/src/pages/index.astro
+++ b/packages/astro/test/fixtures/alias-tsconfig/src/pages/index.astro
@@ -2,10 +2,10 @@
import Client from '@components/Client.svelte'
import Foo from 'src/components/Foo.astro';
import StyleComp from 'src/components/Style.astro';
+import Alias from '@components/Alias.svelte';
import { namespace } from '@test/namespace-package'
import { foo } from 'src/utils/constants';
-// TODO: support alias in @import https://github.com/withastro/astro/issues/6551
-// import '@styles/main.css';
+import '@styles/main.css';
---
<html lang="en">
<head>
@@ -18,8 +18,11 @@ import { foo } from 'src/utils/constants';
<Client client:load />
<Foo />
<StyleComp />
+ <Alias client:load />
<p id="namespace">{namespace}</p>
<p id="constants-foo">{foo}</p>
+ <p id="style-red">style-red</p>
+ <p id="style-blue">style-blue</p>
</main>
</body>
</html>