Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use tsconfck instead of tsconfig-resolver #8798

Merged
merged 10 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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': patch
---

Fixed `tsconfig.json`'s new array format for `extends` not working
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 = inputConfig.tsconfigFile.split('/').pop();
Princesseuh marked this conversation as resolved.
Show resolved Hide resolved

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
Loading