Skip to content

Commit

Permalink
feat: add declaration task (#666)
Browse files Browse the repository at this point in the history
  • Loading branch information
XGHeaven authored Nov 15, 2024
1 parent 9d79a4e commit 073a6cb
Show file tree
Hide file tree
Showing 19 changed files with 539 additions and 231 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-experts-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ice/pkg': minor
---

feat: add individual declaration task for speed
29 changes: 28 additions & 1 deletion packages/pkg/src/config/userConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
BundleUserConfig,
TransformUserConfig,
TransformTaskConfig,
DeclarationTaskConfig,
DeclarationUserConfig,
} from '../types.js';

function getUserConfig() {
Expand All @@ -24,6 +26,9 @@ function getUserConfig() {
const defaultTransformUserConfig: TransformUserConfig = {
formats: ['esm', 'es2017'],
};
const defaultDeclarationUserConfig: DeclarationUserConfig = {
outputMode: 'multi',
};
const userConfig = [
{
name: 'entry',
Expand Down Expand Up @@ -75,8 +80,30 @@ function getUserConfig() {
},
{
name: 'declaration',
validation: 'boolean',
validation: 'boolean|object',
defaultValue: true,
setConfig: (config: TaskConfig, declaration: UserConfig['declaration']) => {
if (config.type === 'declaration') {
if (declaration === false) {
return config;
}
let taskConfig = config;
const mergedConfig = typeof declaration === 'object' ? {
...defaultDeclarationUserConfig,
...declaration,
} : { ...defaultDeclarationUserConfig };

Object.keys(mergedConfig).forEach((key) => {
taskConfig = mergeValueToTaskConfig<DeclarationTaskConfig>(
taskConfig,
key,
mergedConfig[key],
);
});

return taskConfig;
}
},
},
// TODO: validate values recursively
{
Expand Down
192 changes: 100 additions & 92 deletions packages/pkg/src/helpers/dts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import ts from 'typescript';
import consola from 'consola';
import { performance } from 'perf_hooks';
import { timeFrom, normalizePath } from '../utils.js';
import { createLogger } from './logger.js';
import formatAliasToTSPathsConfig from './formatAliasToTSPathsConfig.js';
import type { TaskConfig } from '../types.js';
import { normalizePath } from '../utils.js';
import { TaskConfig } from '../types.js';
import { prepareSingleFileReplaceTscAliasPaths } from 'tsc-alias';
import fse from 'fs-extra';
import * as path from 'path';
Expand All @@ -23,8 +20,8 @@ export interface DtsInputFile extends File {
dtsPath?: string;
}

const normalizeDtsInput = (file: File, rootDir: string, outputDir: string): DtsInputFile => {
const { filePath, ext } = file;
const normalizeDtsInput = (filePath: string, rootDir: string, outputDir: string): DtsInputFile => {
const ext = path.extname(filePath) as FileExt;
// https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions
// a.js -> a.d.ts
// a.cjs -> a.d.cts
Expand All @@ -34,59 +31,106 @@ const normalizeDtsInput = (file: File, rootDir: string, outputDir: string): DtsI
// a.mts -> a.d.mts
const dtsPath = filePath.replace(path.join(rootDir, 'src'), outputDir).replace(ext, `.d.${/^\.[jt]/.test(ext) ? '' : ext[1]}ts`);
return {
...file,
filePath,
ext,
dtsPath,
};
};

interface DtsCompileOptions {
export interface DtsCompileOptions {
// In watch mode, it only contains the updated file names. In build mode, it contains all file names.
files: File[];
files: string[];
alias: TaskConfig['alias'];
rootDir: string;
outputDir: string;
}

function formatAliasToTSPathsConfig(alias: TaskConfig['alias']) {
const paths: { [from: string]: [string] } = {};

Object.entries(alias || {})
.forEach(([key, value]) => {
const [pathKey, pathValue] = formatPath(key, value);
paths[pathKey] = [pathValue];
});

return paths;
}

export async function dtsCompile({ files, alias, rootDir, outputDir }: DtsCompileOptions): Promise<DtsInputFile[]> {
if (!files.length) {
return;
function formatPath(key: string, value: string) {
if (key.endsWith('$')) {
return [key.replace(/\$$/, ''), value];
}
// abc -> abc/*
// abc/ -> abc/*
return [addWildcard(key), addWildcard(value)];
}

const tsConfig = await getTSConfig(rootDir, outputDir, alias);
function addWildcard(str: string) {
return `${str.endsWith('/') ? str : `${str}/`}*`;
}

const logger = createLogger('dts');
async function getTSConfig(
rootDir: string,
outputDir: string,
alias: TaskConfig['alias'],
) {
const defaultTSCompilerOptions: ts.CompilerOptions = {
allowJs: true,
declaration: true,
emitDeclarationOnly: true,
incremental: true,
skipLibCheck: true,
paths: formatAliasToTSPathsConfig(alias), // default add alias to paths
};
const projectTSConfig = await getProjectTSConfig(rootDir);
const tsConfig: ts.ParsedCommandLine = merge(
{ options: defaultTSCompilerOptions },
projectTSConfig,
{
options: {
outDir: outputDir,
rootDir: path.join(rootDir, 'src'),
},
},
);

logger.debug('Start Compiling typescript declarations...');
return tsConfig;
}

const dtsCompileStart = performance.now();
async function getProjectTSConfig(rootDir: string): Promise<ts.ParsedCommandLine> {
const tsconfigPath = ts.findConfigFile(rootDir, ts.sys.fileExists);
if (tsconfigPath) {
const tsconfigFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(
tsconfigFile.config,
ts.sys,
path.dirname(tsconfigPath),
);
}

const _files = files
.map((file) => normalizeDtsInput(file, rootDir, outputDir))
.map(({ filePath, dtsPath, ...rest }) => ({
...rest,
// Be compatible with Windows env.
filePath: normalizePath(filePath),
dtsPath: normalizePath(dtsPath),
}));
return {
options: {},
fileNames: [],
errors: [],
};
}

const dtsFiles = {};
export async function dtsCompile({ files, rootDir, outputDir, alias }: DtsCompileOptions): Promise<DtsInputFile[]> {
if (!files.length) {
return [];
}

// Create ts host and custom the writeFile and readFile.
const host = ts.createCompilerHost(tsConfig.options);
host.writeFile = (fileName, contents) => {
dtsFiles[fileName] = contents;
};
const tsConfig = await getTSConfig(rootDir, outputDir, alias);

const _readFile = host.readFile;
// Hijack `readFile` to prevent reading file twice
host.readFile = (fileName) => {
const foundItem = files.find((file) => file.filePath === fileName);
if (foundItem && foundItem.srcCode) {
return foundItem.srcCode;
}
return _readFile(fileName);
};
const _files = files
.map((file) => normalizeDtsInput(file, rootDir, outputDir))
.map<DtsInputFile>(({ filePath, dtsPath, ...rest }) => ({
...rest,
// Be compatible with Windows env.
filePath: normalizePath(filePath),
dtsPath: normalizePath(dtsPath),
}));

// In order to only include the update files instead of all the files in the watch mode.
function getProgramRootNames(originalFilenames: string[]) {
Expand All @@ -97,7 +141,13 @@ export async function dtsCompile({ files, alias, rootDir, outputDir }: DtsCompil
return [...needCompileFileNames, ...dtsFilenames];
}

// Create ts program.
const dtsFiles = {};
const host = ts.createCompilerHost(tsConfig.options);

host.writeFile = (fileName, contents) => {
dtsFiles[fileName] = contents;
};

const programOptions: ts.CreateProgramOptions = {
rootNames: getProgramRootNames(tsConfig.fileNames),
options: tsConfig.options,
Expand All @@ -107,8 +157,6 @@ export async function dtsCompile({ files, alias, rootDir, outputDir }: DtsCompil
};
const program = ts.createProgram(programOptions);

logger.debug(`Initializing program takes ${timeFrom(dtsCompileStart)}`);

const emitResult = program.emit();

if (emitResult.diagnostics && emitResult.diagnostics.length > 0) {
Expand All @@ -123,9 +171,17 @@ export async function dtsCompile({ files, alias, rootDir, outputDir }: DtsCompil
});
}

if (!Object.keys(alias).length) {
// no alias config
return _files.map((file) => ({
...file,
dtsContent: dtsFiles[file.dtsPath],
}));
}

// We use tsc-alias to resolve d.ts alias.
// Reason: https://github.com/microsoft/TypeScript/issues/30952#issuecomment-1114225407
const tsConfigLocalPath = path.join(rootDir, 'node_modules/pkg/tsconfig.json');
const tsConfigLocalPath = path.join(rootDir, 'node_modules/.cache/ice-pkg/tsconfig.json');
await fse.ensureFile(tsConfigLocalPath);
await fse.writeJSON(tsConfigLocalPath, {
...tsConfig,
Expand All @@ -142,53 +198,5 @@ export async function dtsCompile({ files, alias, rootDir, outputDir }: DtsCompil
dtsContent: dtsFiles[file.dtsPath] ? runFile({ fileContents: dtsFiles[file.dtsPath], filePath: file.dtsPath }) : '',
}));

logger.debug(`Generating declaration files take ${timeFrom(dtsCompileStart)}`);

return result;
}

async function getTSConfig(
rootDir: string,
outputDir: string,
alias: TaskConfig['alias'],
) {
const defaultTSCompilerOptions: ts.CompilerOptions = {
allowJs: true,
declaration: true,
emitDeclarationOnly: true,
incremental: true,
skipLibCheck: true,
paths: formatAliasToTSPathsConfig(alias), // default add alias to paths
};
const projectTSConfig = await getProjectTSConfig(rootDir);
const tsConfig: ts.ParsedCommandLine = merge(
{ options: defaultTSCompilerOptions },
projectTSConfig,
{
options: {
outDir: outputDir,
rootDir: path.join(rootDir, 'src'),
},
},
);

return tsConfig;
}

async function getProjectTSConfig(rootDir: string): Promise<ts.ParsedCommandLine> {
const tsconfigPath = ts.findConfigFile(rootDir, ts.sys.fileExists);
if (tsconfigPath) {
const tsconfigFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(
tsconfigFile.config,
ts.sys,
path.dirname(tsconfigPath),
);
}

return {
options: {},
fileNames: [],
errors: [],
};
}
25 changes: 0 additions & 25 deletions packages/pkg/src/helpers/formatAliasToTSPathsConfig.ts

This file was deleted.

9 changes: 9 additions & 0 deletions packages/pkg/src/helpers/getBuildTasks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import deepmerge from 'deepmerge';
import path from 'node:path';
import { formatEntry, getTransformDefaultOutputDir } from './getTaskIO.js';
import { getDefaultBundleSwcConfig, getDefaultTransformSwcConfig } from './defaultSwcConfig.js';
import { stringifyObject } from '../utils.js';
Expand Down Expand Up @@ -49,6 +50,14 @@ function getBuildTask(buildTask: BuildTask, context: Context): BuildTask {
defaultTransformSwcConfig,
config.swcCompileOptions || {},
);
} else if (config.type === 'declaration') {
// 这个 output 仅仅用于生成正确的 .d.ts 的 alias,不做实际输出目录
config.outputDir = path.resolve(rootDir, config.transformFormats[0]);
if (config.outputMode === 'unique') {
config.declarationOutputDirs = [path.resolve(rootDir, 'typings')];
} else {
config.declarationOutputDirs = config.transformFormats.map((format) => path.resolve(rootDir, format));
}
} else {
throw new Error('Invalid task type.');
}
Expand Down
14 changes: 1 addition & 13 deletions packages/pkg/src/helpers/getRollupOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import autoprefixer from 'autoprefixer';
import PostcssPluginRpxToVw from 'postcss-plugin-rpx2vw';
import json from '@rollup/plugin-json';
import swcPlugin from '../rollupPlugins/swc.js';
import dtsPlugin from '../rollupPlugins/dts.js';
import minifyPlugin from '../rollupPlugins/minify.js';
import babelPlugin from '../rollupPlugins/babel.js';
import { builtinNodeModules } from './builtinModules.js';
Expand Down Expand Up @@ -44,7 +43,7 @@ export function getRollupOptions(
context: Context,
taskRunnerContext: TaskRunnerContext,
) {
const { pkg, commandArgs, command, userConfig, rootDir } = context;
const { pkg, commandArgs, command, rootDir } = context;
const { name: taskName, config: taskConfig } = taskRunnerContext.buildTask;
const rollupOptions: RollupOptions = {};
const plugins: Plugin[] = [];
Expand Down Expand Up @@ -73,17 +72,6 @@ export function getRollupOptions(
);

if (taskConfig.type === 'transform') {
if (userConfig.declaration) {
plugins.unshift(
dtsPlugin({
rootDir,
entry: taskConfig.entry as Record<string, string>,
generateTypesForJs: userConfig.generateTypesForJs,
alias: taskConfig.alias,
outputDir: taskConfig.outputDir,
}),
);
}
plugins.push(transformAliasPlugin(rootDir, taskConfig.alias));
} else if (taskConfig.type === 'bundle') {
const [external, globals] = getExternalsAndGlobals(taskConfig, pkg as PkgJson);
Expand Down
Loading

0 comments on commit 073a6cb

Please sign in to comment.