From ba38147ccc88db2930d9e15af955ea264d75240e Mon Sep 17 00:00:00 2001 From: Jonathan Neal Date: Tue, 9 Nov 2021 12:57:43 -0500 Subject: [PATCH] Support tsconfig.json & jsconfig.json aliases (#1747) * Resolve paths from tsconfig or jsconfig https://code.visualstudio.com/docs/languages/jsconfig https://nextjs.org/docs/advanced-features/module-path-aliases * edit: rename plugin to `@astrojs/vite-plugin-tsconfig-alias` Co-authored-by: Nate Moore * edit: switch from `ps` to `path.posix` * edit: move sanitization of paths to loop * edit: rename `resolveConfigPaths` to `configAliasVitePlugin` * edit: update implementation based on feedback * prettier * edit: rename `matchTailingAsterisk` to `matchTrailingAsterisk` * edit: cleanup with comments * edit: spellcheck `condition` to `conditionally` * edit: refactor based on feedback * edit: Update README.md * edit: cleanup baseUrl transformation and add explainer comments * edit: cleanup resolutions and add commenting * yarn lint Co-authored-by: Nate Moore --- packages/astro/package.json | 1 + packages/astro/src/core/create-vite.ts | 2 + .../src/vite-plugin-config-alias/README.md | 26 +++++ .../src/vite-plugin-config-alias/index.ts | 105 ++++++++++++++++++ yarn.lock | 23 +++- 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 packages/astro/src/vite-plugin-config-alias/README.md create mode 100644 packages/astro/src/vite-plugin-config-alias/index.ts diff --git a/packages/astro/package.json b/packages/astro/package.json index c43e6a9d5336..a84b295e38c2 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -96,6 +96,7 @@ "strip-indent": "^4.0.0", "supports-esm": "^1.0.0", "tiny-glob": "^0.2.8", + "tsconfig-resolver": "^3.0.1", "vite": "^2.6.10", "yargs-parser": "^20.2.9", "zod": "^3.8.1" diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index b49f664dc515..c883333da41f 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'url'; import vite from './vite.js'; import astroVitePlugin from '../vite-plugin-astro/index.js'; import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js'; +import configAliasVitePlugin from '../vite-plugin-config-alias/index.js'; import markdownVitePlugin from '../vite-plugin-markdown/index.js'; import jsxVitePlugin from '../vite-plugin-jsx/index.js'; import fetchVitePlugin from '../vite-plugin-fetch/index.js'; @@ -47,6 +48,7 @@ export async function createVite(inlineConfig: ViteConfigWithSSR, { astroConfig, entries: ['src/**/*'], // Try and scan a user’s project (won’t catch everything), }, plugins: [ + configAliasVitePlugin({ config: astroConfig }), astroVitePlugin({ config: astroConfig, devServer }), markdownVitePlugin({ config: astroConfig, devServer }), jsxVitePlugin({ config: astroConfig, logging }), diff --git a/packages/astro/src/vite-plugin-config-alias/README.md b/packages/astro/src/vite-plugin-config-alias/README.md new file mode 100644 index 000000000000..85971c90aa0a --- /dev/null +++ b/packages/astro/src/vite-plugin-config-alias/README.md @@ -0,0 +1,26 @@ +# vite-plugin-config-alias + +This adds aliasing support to Vite from `tsconfig.json` or `jsconfig.json` files. + +Consider the following example configuration: + +``` +{ + "compilerOptions": { + "baseUrl": "src", + "paths": { + "components:*": ["components/*.astro"] + } + } +} +``` + +With this configuration, the following imports would map to the same location. + +```js +import Test from '../components/Test.astro' + +import Test from 'components/Test.astro' + +import Test from 'components:Test' +``` diff --git a/packages/astro/src/vite-plugin-config-alias/index.ts b/packages/astro/src/vite-plugin-config-alias/index.ts new file mode 100644 index 000000000000..8154730f4a5b --- /dev/null +++ b/packages/astro/src/vite-plugin-config-alias/index.ts @@ -0,0 +1,105 @@ +import * as tsr from 'tsconfig-resolver'; +import * as path from 'path'; +import * as url from 'url'; + +import type * as vite from 'vite'; + +/** Result of successfully parsed tsconfig.json or jsconfig.json. */ +export declare interface 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 the results of a config file if it exists, otherwise null. */ +const getExistingConfig = (searchName: string, cwd: string | undefined): tsr.TsConfigResultSuccess | null => { + const config = tsr.tsconfigResolverSync({ cwd, searchName }); + + return config.exists ? config : null; +}; + +/** Returns a list of compiled aliases. */ +const getConfigAlias = (cwd: string | undefined): Alias[] | null => { + /** Closest tsconfig.json or jsconfig.json */ + const config = getExistingConfig('tsconfig.json', cwd) || getExistingConfig('jsconfig.json', cwd); + + // if no config was found, return null + if (!config) return null; + + /** Compiler options from tsconfig.json or jsconfig.json. */ + const compilerOptions = Object(config.config.compilerOptions); + + // if no compilerOptions.baseUrl was defined, return null + if (!compilerOptions.baseUrl) return null; + + // resolve the base url from the configuration file directory + const baseUrl = path.posix.resolve(path.posix.dirname(normalize(config.path)), normalize(compilerOptions.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); + + /** Regular Expression used to match a given path. */ + const find = new RegExp(`^${[...alias].map((segment) => (segment === '*' ? '(.+)' : segment.replace(/[\\^$*+?.()|[\]{}]/, '\\$&'))).join('')}$`); + + /** Internal index used to calculate the matching id in a replacement. */ + let matchId = 0; + + for (let value of values) { + /** String used to replace a matched path. */ + const replacement = [...path.posix.resolve(baseUrl, value)].map((segment) => (segment === '*' ? `$${++matchId}` : segment === '$' ? '$$' : segment)).join(''); + + aliases.push({ find, replacement }); + } + } + + // compile the baseUrl expression and push it to the list + // - `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`, + }); + + return aliases; +}; + +/** Returns a Vite plugin used to alias pathes from tsconfig.json and jsconfig.json. */ +export default function configAliasVitePlugin(astroConfig: { projectRoot?: URL; [key: string]: unknown }): vite.PluginOption { + /** Aliases from the tsconfig.json or jsconfig.json configuration. */ + const configAlias = getConfigAlias(astroConfig.projectRoot && url.fileURLToPath(astroConfig.projectRoot)); + + // if no config alias was found, bypass this plugin + if (!configAlias) return {} as vite.PluginOption; + + return { + name: '@astrojs/vite-plugin-config-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; + + // conditionally resolve the source ID from any matching alias or baseUrl + 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; + } + } + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 5de4d7366e1e..e0d2acfd9754 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1898,6 +1898,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/json5@^0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" + integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== + "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -1990,7 +1995,7 @@ dependencies: "@types/node" "*" -"@types/resolve@^1.20.1": +"@types/resolve@^1.17.0", "@types/resolve@^1.20.1": version "1.20.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.1.tgz#3727e48042fda81e374f5d5cf2fa92288bf698f8" integrity sha512-Ku5+GPFa12S3W26Uwtw+xyrtIpaZsGYHH6zxNbZlstmlvMYSZRzOwzwsXbxlVUbHyUucctSyuFtu6bNxwYomIw== @@ -6458,7 +6463,7 @@ json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= -json5@^2.1.2: +json5@^2.1.2, json5@^2.1.3: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== @@ -9527,7 +9532,7 @@ resolve-path@^1.4.0: http-errors "~1.6.2" path-is-absolute "1.0.1" -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.13.1, resolve@^1.20.0, resolve@^1.3.2: +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.3.2: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -10608,6 +10613,18 @@ ts-morph@^12.0.0: "@ts-morph/common" "~0.11.0" code-block-writer "^10.1.1" +tsconfig-resolver@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tsconfig-resolver/-/tsconfig-resolver-3.0.1.tgz#c9e62e328ecfbeaae4a4f1131a92cdbed12350c4" + integrity sha512-ZHqlstlQF449v8glscGRXzL6l2dZvASPCdXJRWG4gHEZlUVx2Jtmr+a2zeVG4LCsKhDXKRj5R3h0C/98UcVAQg== + dependencies: + "@types/json5" "^0.0.30" + "@types/resolve" "^1.17.0" + json5 "^2.1.3" + resolve "^1.17.0" + strip-bom "^4.0.0" + type-fest "^0.13.1" + tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"