Skip to content

feat: add individual declaration task #666

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

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Changes from all 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/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
@@ -8,6 +8,8 @@ import type {
BundleUserConfig,
TransformUserConfig,
TransformTaskConfig,
DeclarationTaskConfig,
DeclarationUserConfig,
} from '../types.js';

function getUserConfig() {
@@ -24,6 +26,9 @@ function getUserConfig() {
const defaultTransformUserConfig: TransformUserConfig = {
formats: ['esm', 'es2017'],
};
const defaultDeclarationUserConfig: DeclarationUserConfig = {
outputMode: 'multi',
};
const userConfig = [
{
name: 'entry',
@@ -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
{
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';
@@ -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
@@ -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[]) {
@@ -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,
@@ -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) {
@@ -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,
@@ -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';
@@ -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.');
}
14 changes: 1 addition & 13 deletions packages/pkg/src/helpers/getRollupOptions.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -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[] = [];
@@ -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);
5 changes: 5 additions & 0 deletions packages/pkg/src/helpers/getTaskRunners.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BuildTask, Context, type OutputResult, type TaskRunnerContext } from '../types.js';
import { createTransformTask } from '../tasks/transform.js';
import { createBundleTask } from '../tasks/bundle.js';
import { createDeclarationTask } from '../tasks/declaration.js';
import { Runner } from './runner.js';
import { FSWatcher } from 'chokidar';

@@ -20,6 +21,10 @@ export function getTaskRunners(buildTasks: BuildTask[], context: Context, watche
return createBundleTask(taskRunnerContext);
});
}
case 'declaration': {
const taskRunnerContext: TaskRunnerContext = { mode: 'production', buildTask, buildContext: context, watcher };
return createDeclarationTask(taskRunnerContext);
}
default: {
// @ts-expect-error unreachable
throw new Error(`Unknown task type of ${config.type}`);
116 changes: 116 additions & 0 deletions packages/pkg/src/helpers/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { MessagePort } from 'node:worker_threads';

export type RpcMethods = Record<string, (...args: any[]) => Promise<any>>;

enum RpcMessageType {
Request = 'req',
Response = 'res',
ResponseError = 'resError'
}

interface RpcBaseMessage {
__rpc__: string;
type: RpcMessageType;
}

interface RpcRequestMessage extends RpcBaseMessage {
id: number;
type: RpcMessageType.Request;
method: string;
args: unknown[];
}

interface RpcResponseMessage extends RpcBaseMessage {
id: number;
type: RpcMessageType.Response | RpcMessageType.ResponseError;
data: unknown;
}

type RpcMessage = RpcRequestMessage | RpcResponseMessage;

const RPC_SIGN = 'pkg-rpc';

function isRpcMessage(message: unknown): message is RpcMessage {
return message && typeof message === 'object' && (message as RpcMessage).__rpc__ === RPC_SIGN;
}

export class Rpc<R extends RpcMethods, L extends RpcMethods> {
private requestId = 0;
private requestStore = new Map<number, [resolve: (v: unknown) => void, reject: (e: unknown) => void]>();

constructor(private tunnel: MessagePort, private rpcMethods: L) {
// tunnel.onMessage?.(this.onMessage.bind(this));
this.tunnel.on('message', this.onMessage.bind(this));
}

call<K extends keyof R>(name: K, args: Parameters<R[K]>): ReturnType<R[K]> {
const reqId = ++this.requestId;

this.postMessage({
__rpc__: RPC_SIGN,
type: RpcMessageType.Request,
id: reqId,
method: name as string,
args: args as unknown[],
});

let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});

this.requestStore.set(reqId, [resolve, reject]);

return promise as ReturnType<R[K]>;
}

private onMessage(message: unknown) {
if (isRpcMessage(message)) {
switch (message.type) {
case RpcMessageType.Request: {
const { id, method, args } = message;
const fn = this.rpcMethods[method];
new Promise((resolve, reject) => {
if (fn) {
resolve(fn(...args));
} else {
reject(new Error(`Method ${method} not found`));
}
}).then((returnData) => {
this.postMessage({
__rpc__: RPC_SIGN,
type: RpcMessageType.Response,
id,
data: returnData,
});
}, (error) => {
this.postMessage({
__rpc__: RPC_SIGN,
type: RpcMessageType.ResponseError,
id,
// TODO: stringify error
data: error,
});
});
break;
}
case RpcMessageType.ResponseError:
case RpcMessageType.Response: {
const { id, data } = message;
const fn = this.requestStore.get(id);
if (fn) {
this.requestStore.delete(id);
fn[message.type === RpcMessageType.Response ? 0 : 1](data);
}
break;
}
}
}
}

private postMessage(data: RpcMessage) {
this.tunnel.postMessage(data);
}
}
42 changes: 42 additions & 0 deletions packages/pkg/src/helpers/runnerGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Runner, RunnerStatus } from './runner.js';
import { WatchChangedFile } from '../types.js';
import { concurrentPromiseAll } from '../utils.js';
import { RunnerReporter } from './runnerReporter.js';

export class RunnerGroup<T> {
private parallelRunners: Array<Runner<T>> = [];
private concurrentRunners: Array<Runner<T>> = [];

constructor(public runners: Array<Runner<T>>, public reporter: RunnerReporter) {
for (const runner of runners) {
if (runner.isParallel) {
this.parallelRunners.push(runner);
} else {
this.concurrentRunners.push(runner);
}
runner.on('status', () => {
if (runner.isRunning) {
this.reporter.onRunnerStart(runner);
} else if (runner.isFinished) {
this.reporter.onRunnerEnd(runner);
}
});
}
}

async run(changedFiles?: WatchChangedFile[]): Promise<T[]> {
const startTime = Date.now();
const parallelPromise = Promise.all(this.parallelRunners.map((runner) => runner.run(changedFiles)));
const concurrentPromise = concurrentPromiseAll(this.concurrentRunners.map((runner) => () => runner.run(changedFiles)), 1);

const [parallelResults, concurrentResults] = await Promise.all([parallelPromise, concurrentPromise]);
const stopTime = Date.now();
this.reporter.onStop({
startTime,
stopTime,
cost: stopTime - startTime,
runners: this.runners,
});
return [...parallelResults, ...concurrentResults];
}
}
11 changes: 10 additions & 1 deletion packages/pkg/src/plugins/component.ts
Original file line number Diff line number Diff line change
@@ -14,8 +14,9 @@ const plugin: Plugin = (api) => {

registerUserConfig(config.getUserConfig());
registerCliOption(config.getCliOptions());
const transformFormats = userConfig.transform?.formats || ['esm', 'es2017'];
// TODO: Move default value to userConfig defaultValue
(userConfig.transform?.formats || ['esm', 'es2017']).forEach((format) => {
transformFormats.forEach((format) => {
registerTask(`transform-${format}`, {
type: 'transform',
});
@@ -35,6 +36,14 @@ const plugin: Plugin = (api) => {
});
}
}

if ((userConfig.declaration ?? true) && transformFormats.length) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
registerTask(TaskName.DECLARATION, {
type: 'declaration',
transformFormats,
});
}
};

export default plugin;
95 changes: 0 additions & 95 deletions packages/pkg/src/rollupPlugins/dts.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/pkg/src/tasks/declaration.rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DtsCompileOptions } from '../helpers/dts.js';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DeclarationMainMethods = {
};

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DeclarationWorkerMethods = {
/**
* @param outputDirs 输出到的目录,支持多个目录
* @param options 编译配置,这里面的 outputDir 没有任何用处
*/
run: (outputDirs: string[], options: DtsCompileOptions) => Promise<void>;
};
69 changes: 69 additions & 0 deletions packages/pkg/src/tasks/declaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import path from 'node:path';
import { Worker, MessagePort } from 'node:worker_threads';
import { fileURLToPath } from 'node:url';
import { DeclarationTaskConfig, OutputResult, TaskRunnerContext, WatchChangedFile } from '../types.js';
import globby from 'globby';
import { Runner } from '../helpers/runner.js';
import { Rpc } from '../helpers/rpc.js';
import { DeclarationMainMethods, DeclarationWorkerMethods } from './declaration.rpc.js';
import { getExistedChangedFilesPath } from '../helpers/watcher.js';
import { getTransformEntryDirs } from '../helpers/getTaskIO.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));

export function createDeclarationTask(context: TaskRunnerContext) {
return new DeclarationRunner(context);
}

class DeclarationRunner extends Runner<OutputResult> {
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
override get isParallel() {
return true;
}

async doRun(changedFiles?: WatchChangedFile[]) {
const { context } = this;
// getTransformEntryDirs
// TODO: 应该使用和 transform 一致的目录
let files: string[];

if (changedFiles) {
files = getExistedChangedFilesPath(changedFiles);
} else {
const entryDirs = getTransformEntryDirs(context.buildContext.rootDir, context.buildTask.config.entry as Record<string, string>);
const result = await Promise.all(entryDirs.map((entry) => globby('**/*.{ts,tsx,mts,cts}', {
cwd: entry,
onlyFiles: true,
ignore: ['**/*.d.{ts,mts,cts}'],
absolute: true,
})));
// unique files
const filesSet = new Set<string>();
for (const item of result) {
for (const file of item) {
filesSet.add(file);
}
}
files = Array.from(filesSet);
}
const worker = new Worker(path.join(dirname, './declaration.worker.js'));
const rpc = new Rpc<DeclarationWorkerMethods, DeclarationMainMethods>(worker as unknown as MessagePort, {
});

const buildConfig = context.buildTask.config as DeclarationTaskConfig;
await rpc.call('run', [buildConfig.declarationOutputDirs, {
files,
rootDir: context.buildContext.rootDir,
outputDir: buildConfig.outputDir,
alias: buildConfig.alias,
}]);

await worker.terminate();

return {
taskName: context.buildTask.name,
outputs: [],
outputFiles: [],
};
}
}
25 changes: 25 additions & 0 deletions packages/pkg/src/tasks/declaration.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from 'fs-extra';
import path from 'node:path';
import { parentPort } from 'node:worker_threads';
import { dtsCompile } from '../helpers/dts.js';
import { Rpc } from '../helpers/rpc.js';
import { DeclarationMainMethods, DeclarationWorkerMethods } from './declaration.rpc.js';

const rpc = new Rpc<DeclarationMainMethods, DeclarationWorkerMethods>(parentPort, {
run: async (outputDirs, options) => {
const dtsFiles = await dtsCompile(options);

await Promise.all(outputDirs.map(async (dir) => {
await fs.ensureDir(dir);
for (const file of dtsFiles) {
if (!file.dtsContent) {
continue;
}
const relDtsPath = path.relative(options.outputDir, file.dtsPath);
const dtsPath = path.join(dir, relDtsPath);
await fs.ensureDir(path.dirname(dtsPath));
await fs.writeFile(dtsPath, file.dtsContent);
}
}));
},
});
28 changes: 26 additions & 2 deletions packages/pkg/src/types.ts
Original file line number Diff line number Diff line change
@@ -103,6 +103,17 @@ export interface BundleUserConfig {
browser?: boolean;
}


export interface DeclarationUserConfig {
/**
* How to output declaration files.
* - 'multi' output .d.ts to every transform format folder, like esm/es2017
* - 'unique' output .d.ts to `typings` folder of the root
* @default 'multi'
*/
outputMode?: 'multi' | 'unique';
}

export interface UserConfig {
/**
* Entry for a task
@@ -134,7 +145,7 @@ export interface UserConfig {
* Generate .d.ts files from TypeScript files in your project.
* @default true
*/
declaration?: boolean;
declaration?: boolean | DeclarationUserConfig;

/**
* Configure JSX transform type.
@@ -248,7 +259,19 @@ export interface TransformTaskConfig extends _TaskConfig, TransformUserConfig {
define?: Record<string, string>;
}

export type TaskConfig = BundleTaskConfig | TransformTaskConfig;
export interface DeclarationTaskConfig extends _TaskConfig, DeclarationUserConfig {
type: 'declaration';
/**
* 记录 transform 配置的 format 用于计算实际的输出目录
*/
transformFormats?: TransformUserConfig['formats'];
/**
* 实际的输出目录,可以同时输出到 esm、es2017 内等
*/
declarationOutputDirs?: string[];
}

export type TaskConfig = BundleTaskConfig | TransformTaskConfig | DeclarationTaskConfig;

export type BuildTask = _BuildTask<TaskConfig, TaskName>;

@@ -273,6 +296,7 @@ export enum TaskName {
'TRANSFORM_ES2017' = 'transform-es2017',
'BUNDLE_ES5' = 'bundle-es5',
'BUNDLE_ES2017' = 'bundle-es2017',
'DECLARATION' = 'declaration'
}
type TaskKey = keyof typeof TaskName;
// TODO: The type name should be renamed to TaskName.
54 changes: 54 additions & 0 deletions packages/pkg/tests/helpers/rpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import { Rpc, RpcMethods } from '../../src/helpers/rpc';
import { MessageChannel } from 'node:worker_threads';

interface TestMethods extends RpcMethods {
testMethod(arg: string): Promise<string>;
}

const serverMethods: TestMethods = {
testMethod: async (arg: string) => `result-${arg}`,
};

describe('Rpc', () => {
it('should handle call method correctly', async () => {
const channel = new MessageChannel();
const clientRpc = new Rpc<TestMethods, {}>(channel.port1, {});
const serverRpc = new Rpc<{}, TestMethods>(channel.port2, serverMethods);
const resultPromise = clientRpc.call('testMethod', ['arg1']);
const result = await resultPromise;
expect(result)
.toBe('result-arg1');
});

it('should handle errors in the server method', async () => {
const channel = new MessageChannel();
const serverMethodsWithError: TestMethods = {
testMethod: async (arg: string) => {
if (arg === 'error') {
throw new Error('Server error');
}
return `result-${arg}`;
},
};
const clientRpc = new Rpc<TestMethods, {}>(channel.port1, {});
const serverRpc = new Rpc<{}, TestMethods>(channel.port2, serverMethodsWithError);
try {
await clientRpc.call('testMethod', ['error']);
} catch (error) {
expect(error.message)
.toBe('Server error');
}
});

it('should throw error for non-existent method', async () => {
const channel = new MessageChannel();
const serverRpc = new Rpc<{}, TestMethods>(channel.port2, serverMethods);
const clientRpc = new Rpc<TestMethods, {}>(channel.port1, {});
try {
await clientRpc.call('nonExistentMethod', []);
} catch (error) {
expect(error.message).toBe('Method nonExistentMethod not found');
}
});
});
2 changes: 1 addition & 1 deletion packages/pkg/tests/helpers/runner.test.ts
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ describe('Runner Tests', () => {
{
const metric = runner.getMetric('testMark');
expect(metric.delta.length).toEqual(2)
expect(metric.cost).toBeGreaterThan(50)
expect(metric.cost).toBeGreaterThanOrEqual(50)
}
});

26 changes: 26 additions & 0 deletions packages/pkg/tests/projects/__snapshots__/default.test.ts.snap
Original file line number Diff line number Diff line change
@@ -143,6 +143,32 @@ exports[`Run config default > esm structure 1`] = `
}
`;

exports[`Run config no-declaration > cjs structure 1`] = `null`;

exports[`Run config no-declaration > dist structure 1`] = `null`;

exports[`Run config no-declaration > es2017 structure 1`] = `
{
"files": [
{
"name": "index.js",
},
],
"name": "es2017",
}
`;

exports[`Run config no-declaration > esm structure 1`] = `
{
"files": [
{
"name": "index.js",
},
],
"name": "esm",
}
`;

exports[`Run config sourcemap-enable > cjs structure 1`] = `null`;

exports[`Run config sourcemap-enable > dist structure 1`] = `null`;
9 changes: 8 additions & 1 deletion packages/pkg/tests/projects/default.test.ts
Original file line number Diff line number Diff line change
@@ -60,5 +60,12 @@ runProjectTest('default', [
config: {
sourceMaps: true
}
}
},
{
name: 'no-declaration',
snapshot: 'structure',
config: {
declaration: false
}
},
])

Unchanged files with check annotations Beta

const { getESLintConfig } = require('@iceworks/spec');

Check warning on line 1 in .eslintrc.js

GitHub Actions / CI (18)

A `require()` style import is forbidden
module.exports = getESLintConfig('common-ts', {
rules: {
// if (config.type === 'transform') {
// return;
// }
config.extensions = [

Check warning on line 11 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
'.js',
'.json',
'.jsx',
'.tsx',
'.html',
];
config.entry = {

Check warning on line 19 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
avatar: './src/Avatar/index',
button: './src/Button/index',
};
// config.sourcemap = true;
config.alias = { ...config.alias };

Check warning on line 24 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
config.externals = { react: 'React', 'react-dom': 'ReactDOM' };

Check warning on line 25 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
config.modifyStylesOptions ??= [];

Check warning on line 27 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
config.modifyStylesOptions.push((options) => {
return options;
});
config.modifySwcCompileOptions = (originOptions) => {

Check warning on line 31 in examples/plugin/src/index.ts

GitHub Actions / CI (18)

Assignment to property of function parameter 'config'
return originOptions;
};
};
import { createElement } from 'rax';

Check warning on line 1 in examples/rax-component/src/components/Header/index.tsx

GitHub Actions / CI (18)

'createElement' is defined but never used
import View from 'rax-view';
import './index.css';
import { createElement, Fragment } from 'rax';

Check warning on line 1 in examples/rax-component/src/index.tsx

GitHub Actions / CI (18)

'createElement' is defined but never used

Check warning on line 1 in examples/rax-component/src/index.tsx

GitHub Actions / CI (18)

'Fragment' is defined but never used
import styles from './index.module.css';
import Header from './components/Header';