Support tsconfig aliases in CSS @import
(#6816)
This commit is contained in:
parent
c464bf258f
commit
8539eb1643
5 changed files with 174 additions and 68 deletions
5
.changeset/metal-guests-tickle.md
Normal file
5
.changeset/metal-guests-tickle.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Support tsconfig aliases in CSS `@import`
|
|
@ -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;
|
||||
const { tsConfig, tsConfigPath } = settings;
|
||||
if (!tsConfig || !tsConfigPath || !tsConfig.compilerOptions) return null;
|
||||
|
||||
// if no config was found, return null
|
||||
if (!config || !configPath) 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 });
|
||||
configResolved(config) {
|
||||
patchCreateResolver(config, plugin);
|
||||
},
|
||||
async resolveId(id, importer, options) {
|
||||
if (isVirtualId(id)) return;
|
||||
|
||||
// if any other resolver handles the file, return that resolution
|
||||
if (resolvedId) return resolvedId;
|
||||
|
||||
// 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:');
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
4
packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte
vendored
Normal file
4
packages/astro/test/fixtures/alias-tsconfig/src/components/Alias.svelte
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
<script>
|
||||
import { foo } from 'src/utils/constants';
|
||||
</script>
|
||||
<div id="alias">{foo}</div>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue