Skip to content

Commit

Permalink
feat: use tsconfck instead of tsconfig-resolver (#8798)
Browse files Browse the repository at this point in the history
  • Loading branch information
Princesseuh authored Oct 11, 2023
1 parent f999365 commit f369fa2
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 112 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-dolphins-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Fixed `tsconfig.json`'s new array format for `extends` not working. This was done by migrating Astro to use [`tsconfck`](https://github.com/dominikg/tsconfck) instead of [`tsconfig-resolver`](https://github.com/ifiokjr/tsconfig-resolver) to find and parse `tsconfig.json` files.
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
"shiki": "^0.14.3",
"string-width": "^6.1.0",
"strip-ansi": "^7.1.0",
"tsconfig-resolver": "^3.0.1",
"tsconfck": "3.0.0-next.9",
"unist-util-visit": "^4.1.2",
"vfile": "^5.3.7",
"vite": "^4.4.9",
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import type * as babel from '@babel/core';
import type { OutgoingHttpHeaders } from 'node:http';
import type { AddressInfo } from 'node:net';
import type * as rollup from 'rollup';
import type { TsConfigJson } from 'tsconfig-resolver';
import type * as vite from 'vite';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
import type { SerializedSSRManifest } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
import type { TSConfig } from '../core/config/tsconfig.js';
import type { AstroCookies } from '../core/cookies/index.js';
import type { ResponseWithEncoding } from '../core/endpoint/index.js';
import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
Expand Down Expand Up @@ -1503,7 +1503,7 @@ export interface AstroSettings {
* Map of directive name (e.g. `load`) to the directive script code
*/
clientDirectives: Map<string, string>;
tsConfig: TsConfigJson | undefined;
tsConfig: TSConfig | undefined;
tsConfigPath: string | undefined;
watchFiles: string[];
timer: AstroTimer;
Expand Down
26 changes: 16 additions & 10 deletions packages/astro/src/cli/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,25 +848,31 @@ async function updateTSConfig(
return UpdateResult.none;
}

const inputConfig = loadTSConfig(cwd, false);
const configFileName = inputConfig.exists ? inputConfig.path.split('/').pop() : 'tsconfig.json';
let inputConfig = await loadTSConfig(cwd);
let inputConfigText = '';

if (inputConfig.reason === 'invalid-config') {
if (inputConfig === 'invalid-config' || inputConfig === 'unknown-error') {
return UpdateResult.failure;
}

if (inputConfig.reason === 'not-found') {
} else if (inputConfig === 'missing-config') {
logger.debug('add', "Couldn't find tsconfig.json or jsconfig.json, generating one");
inputConfig = {
tsconfig: defaultTSConfig,
tsconfigFile: path.join(cwd, 'tsconfig.json'),
rawConfig: { tsconfig: defaultTSConfig, tsconfigFile: path.join(cwd, 'tsconfig.json') },
};
} else {
inputConfigText = JSON.stringify(inputConfig.rawConfig.tsconfig, null, 2);
}

const configFileName = path.basename(inputConfig.tsconfigFile);

const outputConfig = updateTSConfigForFramework(
inputConfig.exists ? inputConfig.config : defaultTSConfig,
inputConfig.rawConfig.tsconfig,
firstIntegrationWithTSSettings
);

const input = inputConfig.exists ? JSON.stringify(inputConfig.config, null, 2) : '';
const output = JSON.stringify(outputConfig, null, 2);
const diff = getDiffContent(input, output);
const diff = getDiffContent(inputConfigText, output);

if (!diff) {
return UpdateResult.none;
Expand Down Expand Up @@ -906,7 +912,7 @@ async function updateTSConfig(
}

if (await askToContinue({ flags })) {
await fs.writeFile(inputConfig?.path ?? path.join(cwd, 'tsconfig.json'), output, {
await fs.writeFile(inputConfig.tsconfigFile, output, {
encoding: 'utf-8',
});
logger.debug('add', `Updated ${configFileName} file`);
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function getViteConfig(inlineConfig: UserConfig) {
level: 'info',
});
const { astroConfig: config } = await resolveConfig({}, cmd);
const settings = createSettings(config, inlineConfig.root);
const settings = await createSettings(config, inlineConfig.root);
await runHookConfigSetup({ settings, command: cmd, logger });
const viteConfig = await createVite(
{
Expand Down
16 changes: 8 additions & 8 deletions packages/astro/src/content/server-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function attachContentServerListeners({
contentPaths.contentDir.href.replace(settings.config.root.href, '')
)} for changes`
);
const maybeTsConfigStats = getTSConfigStatsWhenAllowJsFalse({ contentPaths, settings });
const maybeTsConfigStats = await getTSConfigStatsWhenAllowJsFalse({ contentPaths, settings });
if (maybeTsConfigStats) warnAllowJsIsFalse({ ...maybeTsConfigStats, logger });
await attachListeners();
} else {
Expand Down Expand Up @@ -96,7 +96,7 @@ See ${bold('https://www.typescriptlang.org/tsconfig#allowJs')} for more informat
);
}

function getTSConfigStatsWhenAllowJsFalse({
async function getTSConfigStatsWhenAllowJsFalse({
contentPaths,
settings,
}: {
Expand All @@ -108,15 +108,15 @@ function getTSConfigStatsWhenAllowJsFalse({
);
if (!isContentConfigJsFile) return;

const inputConfig = loadTSConfig(fileURLToPath(settings.config.root), false);
const tsConfigFileName = inputConfig.exists && inputConfig.path.split(path.sep).pop();
const inputConfig = await loadTSConfig(fileURLToPath(settings.config.root));
if (typeof inputConfig === 'string') return;

const tsConfigFileName = inputConfig.tsconfigFile.split(path.sep).pop();
if (!tsConfigFileName) return;

const contentConfigFileName = contentPaths.config.url.pathname.split(path.sep).pop()!;
const allowJSOption = inputConfig?.config?.compilerOptions?.allowJs;
const hasAllowJs =
allowJSOption === true || (tsConfigFileName === 'jsconfig.json' && allowJSOption !== false);
if (hasAllowJs) return;
const allowJSOption = inputConfig.tsconfig.compilerOptions?.allowJs;
if (allowJSOption) return;

return { tsConfigFileName, contentConfigFileName };
}
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function build(
const { userConfig, astroConfig } = await resolveConfig(inlineConfig, 'build');
telemetry.record(eventCliSession('build', userConfig));

const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));

const builder = new AstroBuilder(settings, {
...options,
Expand Down
18 changes: 12 additions & 6 deletions packages/astro/src/core/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,24 @@ export function createBaseSettings(config: AstroConfig): AstroSettings {
};
}

export function createSettings(config: AstroConfig, cwd?: string): AstroSettings {
const tsconfig = loadTSConfig(cwd);
export async function createSettings(config: AstroConfig, cwd?: string): Promise<AstroSettings> {
const tsconfig = await loadTSConfig(cwd);
const settings = createBaseSettings(config);

const watchFiles = tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [];

let watchFiles = [];
if (cwd) {
watchFiles.push(fileURLToPath(new URL('./package.json', pathToFileURL(cwd))));
}

settings.tsConfig = tsconfig?.config;
settings.tsConfigPath = tsconfig?.path;
if (typeof tsconfig !== 'string') {
watchFiles.push(
...[tsconfig.tsconfigFile, ...(tsconfig.extended ?? []).map((e) => e.tsconfigFile)]
);
settings.tsConfig = tsconfig.tsconfig;
settings.tsConfigPath = tsconfig.tsconfigFile;
}

settings.watchFiles = watchFiles;

return settings;
}
135 changes: 98 additions & 37 deletions packages/astro/src/core/config/tsconfig.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import * as tsr from 'tsconfig-resolver';
import {
TSConfckParseError,
find,
parse,
type TSConfckParseOptions,
type TSConfckParseResult,
} from 'tsconfck';
import type { CompilerOptions, TypeAcquisition } from 'typescript';

export const defaultTSConfig: tsr.TsConfigJson = { extends: 'astro/tsconfigs/base' };
export const defaultTSConfig: TSConfig = { extends: 'astro/tsconfigs/base' };

export type frameworkWithTSSettings = 'vue' | 'react' | 'preact' | 'solid-js';
// The following presets unfortunately cannot be inside the specific integrations, as we need
// them even in cases where the integrations are not installed
export const presets = new Map<frameworkWithTSSettings, tsr.TsConfigJson>([
export const presets = new Map<frameworkWithTSSettings, TSConfig>([
[
'vue', // Settings needed for template intellisense when using Volar
{
Expand Down Expand Up @@ -45,52 +51,78 @@ export const presets = new Map<frameworkWithTSSettings, tsr.TsConfigJson>([
],
]);

// eslint-disable-next-line @typescript-eslint/ban-types
type TSConfigResult<T = {}> = Promise<
(TSConfckParseResult & T) | 'invalid-config' | 'missing-config' | 'unknown-error'
>;

/**
* Load a tsconfig.json or jsconfig.json is the former is not found
* @param cwd Directory to start from
* @param resolve Determine if the function should go up directories like TypeScript would
* @param root The root directory to search in, defaults to `process.cwd()`.
* @param findUp Whether to search for the config file in parent directories, by default only the root directory is searched.
*/
export function loadTSConfig(cwd: string | undefined, resolve = true): tsr.TsConfigResult {
cwd = cwd ?? process.cwd();
let config = tsr.tsconfigResolverSync({
cwd,
filePath: resolve ? undefined : cwd,
ignoreExtends: !resolve,
});

// When a direct filepath is provided to `tsconfigResolver`, it'll instead return invalid-config even when
// the file does not exists. We'll manually handle this so we can provide better errors to users
if (!resolve && config.reason === 'invalid-config' && !existsSync(join(cwd, 'tsconfig.json'))) {
config = { reason: 'not-found', path: undefined, exists: false };
export async function loadTSConfig(
root: string | undefined,
findUp = false
): Promise<TSConfigResult<{ rawConfig: TSConfckParseResult }>> {
const safeCwd = root ?? process.cwd();

const [jsconfig, tsconfig] = await Promise.all(
['jsconfig.json', 'tsconfig.json'].map((configName) =>
// `tsconfck` expects its first argument to be a file path, not a directory path, so we'll fake one
find(join(safeCwd, './dummy.txt'), {
root: findUp ? undefined : root,
configName: configName,
})
)
);

// If we have both files, prefer tsconfig.json
if (tsconfig) {
const parsedConfig = await safeParse(tsconfig, { root: root });

if (typeof parsedConfig === 'string') {
return parsedConfig;
}

return { ...parsedConfig, rawConfig: parsedConfig.extended?.[0] ?? parsedConfig.tsconfig };
}

// If we couldn't find a tsconfig.json, try to load a jsconfig.json instead
if (config.reason === 'not-found') {
const jsconfig = tsr.tsconfigResolverSync({
cwd,
filePath: resolve ? undefined : cwd,
searchName: 'jsconfig.json',
ignoreExtends: !resolve,
});

if (
!resolve &&
jsconfig.reason === 'invalid-config' &&
!existsSync(join(cwd, 'jsconfig.json'))
) {
return { reason: 'not-found', path: undefined, exists: false };
if (jsconfig) {
const parsedConfig = await safeParse(jsconfig, { root: root });

if (typeof parsedConfig === 'string') {
return parsedConfig;
}

return jsconfig;
return { ...parsedConfig, rawConfig: parsedConfig.extended?.[0] ?? parsedConfig.tsconfig };
}

return config;
return 'missing-config';
}

async function safeParse(tsconfigPath: string, options: TSConfckParseOptions = {}): TSConfigResult {
try {
const parseResult = await parse(tsconfigPath, options);

if (parseResult.tsconfig == null) {
return 'missing-config';
}

return parseResult;
} catch (e) {
if (e instanceof TSConfckParseError) {
return 'invalid-config';
}

return 'unknown-error';
}
}

export function updateTSConfigForFramework(
target: tsr.TsConfigJson,
target: TSConfig,
framework: frameworkWithTSSettings
): tsr.TsConfigJson {
): TSConfig {
if (!presets.has(framework)) {
return target;
}
Expand Down Expand Up @@ -120,3 +152,32 @@ function deepMergeObjects<T extends Record<string, any>>(a: T, b: T): T {

return merged;
}

// The code below is adapted from `pkg-types`
// `pkg-types` offer more types and utilities, but since we only want the TSConfig type, we'd rather avoid adding a dependency.
// https://github.com/unjs/pkg-types/blob/78328837d369d0145a8ddb35d7fe1fadda4bfadf/src/types/tsconfig.ts
// See https://github.com/unjs/pkg-types/blob/78328837d369d0145a8ddb35d7fe1fadda4bfadf/LICENSE for license information

export type StripEnums<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends boolean
? T[K]
: T[K] extends string
? T[K]
: T[K] extends object
? T[K]
: T[K] extends Array<any>
? T[K]
: T[K] extends undefined
? undefined
: any;
};

export interface TSConfig {
compilerOptions?: StripEnums<CompilerOptions>;
compileOnSave?: boolean;
extends?: string;
files?: string[];
include?: string[];
exclude?: string[];
typeAcquisition?: TypeAcquisition;
}
4 changes: 2 additions & 2 deletions packages/astro/src/core/dev/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function restartContainer(container: Container): Promise<Container

try {
const { astroConfig } = await resolveConfig(container.inlineConfig, 'dev', container.fs);
const settings = createSettings(astroConfig, fileURLToPath(existingSettings.config.root));
const settings = await createSettings(astroConfig, fileURLToPath(existingSettings.config.root));
await close();
return await createRestartedContainer(container, settings);
} catch (_err) {
Expand Down Expand Up @@ -105,7 +105,7 @@ export async function createContainerWithAutomaticRestart({
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'dev', fs);
telemetry.record(eventCliSession('dev', userConfig));

const settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));

const initialContainer = await createContainer({ settings, logger: logger, inlineConfig, fs });

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/preview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default async function preview(inlineConfig: AstroInlineConfig): Promise<
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'preview');
telemetry.record(eventCliSession('preview', userConfig));

const _settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const _settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));

const settings = await runHookConfigSetup({
settings: _settings,
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default async function sync(
const { userConfig, astroConfig } = await resolveConfig(inlineConfig ?? {}, 'sync');
telemetry.record(eventCliSession('sync', userConfig));

const _settings = createSettings(astroConfig, fileURLToPath(astroConfig.root));
const _settings = await createSettings(astroConfig, fileURLToPath(astroConfig.root));

const settings = await runHookConfigSetup({
settings: _settings,
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/vite-plugin-config-alias/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import type { CompilerOptions } from 'typescript';
import { normalizePath, type ResolvedConfig, type Plugin as VitePlugin } from 'vite';
import type { AstroSettings } from '../@types/astro.js';

Expand All @@ -12,7 +13,7 @@ const getConfigAlias = (settings: AstroSettings): Alias[] | null => {
const { tsConfig, tsConfigPath } = settings;
if (!tsConfig || !tsConfigPath || !tsConfig.compilerOptions) return null;

const { baseUrl, paths } = tsConfig.compilerOptions;
const { baseUrl, paths } = tsConfig.compilerOptions as CompilerOptions;
if (!baseUrl) return null;

// resolve the base url from the configuration file directory
Expand Down
Loading

0 comments on commit f369fa2

Please sign in to comment.