or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdcore-bundling.mdhmr.mdindex.mdloaders.mdmodule-federation.mdplugins.md

plugins.mddocs/

0

# Plugin System

1

2

Built-in plugins for optimization, development, asset processing, and build customization using the Tapable hooks system.

3

4

## Capabilities

5

6

### Plugin Architecture

7

8

Base plugin interfaces and creation patterns.

9

10

```typescript { .api }

11

/** Standard plugin interface */

12

interface RspackPluginInstance {

13

/** Apply plugin to compiler */

14

apply(compiler: Compiler): void;

15

/** Optional plugin properties */

16

[key: string]: any;

17

}

18

19

/** Function-based plugin */

20

type RspackPluginFunction = (this: Compiler, compiler: Compiler) => void;

21

22

/** Union type for all plugin types */

23

type Plugin = RspackPluginInstance | RspackPluginFunction | WebpackPluginInstance |

24

WebpackPluginFunction | Falsy;

25

26

type Falsy = false | null | undefined;

27

28

/** Abstract base class for builtin plugins */

29

abstract class RspackBuiltinPlugin implements RspackPluginInstance {

30

abstract name: BuiltinPluginName;

31

apply(compiler: Compiler): void;

32

raw(compiler: Compiler): any;

33

}

34

35

/** Create custom native plugins */

36

function createNativePlugin(options: any): RspackPluginInstance;

37

```

38

39

### Core Development Plugins

40

41

Essential plugins for development workflow and debugging.

42

43

```typescript { .api }

44

/** Enable Hot Module Replacement */

45

class HotModuleReplacementPlugin extends RspackBuiltinPlugin {

46

name: "HotModuleReplacementPlugin";

47

/** No constructor options required */

48

constructor();

49

}

50

51

/** Define global constants at compile time */

52

class DefinePlugin extends RspackBuiltinPlugin {

53

name: "DefinePlugin";

54

constructor(options: DefinePluginOptions);

55

}

56

57

type DefinePluginOptions = Record<string, CodeValue>;

58

type CodeValue = string | boolean | number | null | undefined | RegExp | bigint |

59

CodeValueFunction | CodeValueObject;

60

61

interface CodeValueFunction {

62

(...args: any[]): any;

63

}

64

65

interface CodeValueObject {

66

[key: string]: CodeValue;

67

}

68

69

/** Automatically provide modules when referenced */

70

class ProvidePlugin extends RspackBuiltinPlugin {

71

name: "ProvidePlugin";

72

constructor(options: ProvidePluginOptions);

73

}

74

75

type ProvidePluginOptions = Record<string, string | string[]>;

76

77

/** Show build progress */

78

class ProgressPlugin extends RspackBuiltinPlugin {

79

name: "ProgressPlugin";

80

constructor(options?: ProgressPluginArgument);

81

}

82

83

type ProgressPluginArgument = ProgressPluginOptions |

84

((percentage: number, msg: string, ...args: any[]) => void);

85

86

interface ProgressPluginOptions {

87

activeModules?: boolean;

88

entries?: boolean;

89

handler?: (percentage: number, msg: string, ...args: any[]) => void;

90

modules?: boolean;

91

modulesCount?: number;

92

profile?: boolean;

93

dependencies?: boolean;

94

dependenciesCount?: number;

95

percentBy?: "entries" | "modules" | "dependencies";

96

}

97

98

/** Add banner to bundles */

99

class BannerPlugin extends RspackBuiltinPlugin {

100

name: "BannerPlugin";

101

constructor(options: BannerPluginArgument);

102

}

103

104

type BannerPluginArgument = string | BannerPluginOptions;

105

106

interface BannerPluginOptions {

107

banner: string | ((data: any) => string);

108

entryOnly?: boolean;

109

exclude?: string | RegExp | (string | RegExp)[];

110

include?: string | RegExp | (string | RegExp)[];

111

raw?: boolean;

112

footer?: boolean;

113

stage?: number;

114

test?: string | RegExp | (string | RegExp)[];

115

}

116

```

117

118

**Usage Examples:**

119

120

```typescript

121

import { DefinePlugin, HotModuleReplacementPlugin, ProvidePlugin } from "@rspack/core";

122

123

const config = {

124

plugins: [

125

// Define environment variables

126

new DefinePlugin({

127

"process.env.NODE_ENV": JSON.stringify("production"),

128

"process.env.API_URL": JSON.stringify("https://api.example.com"),

129

PRODUCTION: true,

130

__VERSION__: JSON.stringify("1.0.0")

131

}),

132

133

// Enable HMR

134

new HotModuleReplacementPlugin(),

135

136

// Auto-provide modules

137

new ProvidePlugin({

138

$: "jquery",

139

jQuery: "jquery",

140

React: "react",

141

process: "process/browser"

142

}),

143

144

// Show build progress

145

new ProgressPlugin((percentage, message, ...args) => {

146

console.log(`${Math.round(percentage * 100)}% ${message} ${args.join(" ")}`);

147

})

148

]

149

};

150

```

151

152

### Optimization Plugins

153

154

Plugins for code splitting, minification, and bundle optimization.

155

156

```typescript { .api }

157

/** Configure code splitting */

158

class SplitChunksPlugin extends RspackBuiltinPlugin {

159

name: "SplitChunksPlugin";

160

constructor(options?: OptimizationSplitChunksOptions);

161

}

162

163

/** Extract runtime code into separate chunk */

164

class RuntimeChunkPlugin extends RspackBuiltinPlugin {

165

name: "RuntimeChunkPlugin";

166

constructor(options?: OptimizationRuntimeChunk);

167

}

168

169

/** Limit the number of chunks */

170

class LimitChunkCountPlugin extends RspackBuiltinPlugin {

171

name: "LimitChunkCountPlugin";

172

constructor(options?: { maxChunks?: number; chunkOverhead?: number; entryChunkMultiplicator?: number });

173

}

174

175

/** JavaScript minification using SWC */

176

class SwcJsMinimizerRspackPlugin extends RspackBuiltinPlugin {

177

name: "SwcJsMinimizerRspackPlugin";

178

constructor(options?: SwcJsMinimizerRspackPluginOptions);

179

}

180

181

interface SwcJsMinimizerRspackPluginOptions {

182

test?: AssetConditions;

183

exclude?: AssetConditions;

184

include?: AssetConditions;

185

extractComments?: ExtractCommentsOptions;

186

minimizerOptions?: {

187

minify?: boolean;

188

ecma?: TerserEcmaVersion;

189

compress?: TerserCompressOptions | boolean;

190

mangle?: TerserMangleOptions | boolean;

191

format?: JsFormatOptions;

192

module?: boolean;

193

};

194

}

195

196

type AssetConditions = string | RegExp | (string | RegExp)[];

197

type ExtractCommentsOptions = boolean | string | RegExp | ((node: any, comment: any) => boolean);

198

type TerserEcmaVersion = 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020;

199

200

interface TerserCompressOptions {

201

arguments?: boolean;

202

arrows?: boolean;

203

booleans?: boolean;

204

booleans_as_integers?: boolean;

205

collapse_vars?: boolean;

206

comparisons?: boolean;

207

computed_props?: boolean;

208

conditionals?: boolean;

209

dead_code?: boolean;

210

defaults?: boolean;

211

directives?: boolean;

212

drop_console?: boolean;

213

drop_debugger?: boolean;

214

ecma?: TerserEcmaVersion;

215

evaluate?: boolean;

216

expression?: boolean;

217

global_defs?: Record<string, any>;

218

hoist_funs?: boolean;

219

hoist_props?: boolean;

220

hoist_vars?: boolean;

221

if_return?: boolean;

222

inline?: boolean | number;

223

join_vars?: boolean;

224

keep_classnames?: boolean | RegExp;

225

keep_fargs?: boolean;

226

keep_fnames?: boolean | RegExp;

227

keep_infinity?: boolean;

228

loops?: boolean;

229

negate_iife?: boolean;

230

passes?: number;

231

properties?: boolean;

232

pure_getters?: boolean | "strict";

233

pure_funcs?: string[];

234

reduce_funcs?: boolean;

235

reduce_vars?: boolean;

236

sequences?: boolean;

237

side_effects?: boolean;

238

switches?: boolean;

239

top_retain?: string[] | string | RegExp;

240

toplevel?: boolean;

241

typeofs?: boolean;

242

unsafe?: boolean;

243

unsafe_arrows?: boolean;

244

unsafe_comps?: boolean;

245

unsafe_Function?: boolean;

246

unsafe_math?: boolean;

247

unsafe_symbols?: boolean;

248

unsafe_methods?: boolean;

249

unsafe_proto?: boolean;

250

unsafe_regexp?: boolean;

251

unsafe_undefined?: boolean;

252

unused?: boolean;

253

}

254

255

interface TerserMangleOptions {

256

eval?: boolean;

257

keep_classnames?: boolean | RegExp;

258

keep_fnames?: boolean | RegExp;

259

module?: boolean;

260

properties?: boolean | TerserManglePropertiesOptions;

261

reserved?: string[];

262

safari10?: boolean;

263

toplevel?: boolean;

264

}

265

266

interface TerserManglePropertiesOptions {

267

builtins?: boolean;

268

debug?: boolean;

269

keep_quoted?: boolean | "strict";

270

regex?: RegExp | string;

271

reserved?: string[];

272

}

273

274

interface JsFormatOptions {

275

ascii_only?: boolean;

276

beautify?: boolean;

277

braces?: boolean;

278

comments?: boolean | "all" | "some" | RegExp | ((node: any, comment: any) => boolean);

279

ecma?: TerserEcmaVersion;

280

indent_level?: number;

281

indent_start?: number;

282

inline_script?: boolean;

283

keep_numbers?: boolean;

284

keep_quoted_props?: boolean;

285

max_line_len?: number | false;

286

preamble?: string;

287

preserve_annotations?: boolean;

288

quote_keys?: boolean;

289

quote_style?: 0 | 1 | 2 | 3;

290

semicolons?: boolean;

291

shebang?: boolean;

292

webkit?: boolean;

293

width?: number;

294

wrap_iife?: boolean;

295

wrap_func_args?: boolean;

296

}

297

298

/** CSS minification using Lightning CSS */

299

class LightningCssMinimizerRspackPlugin extends RspackBuiltinPlugin {

300

name: "LightningCssMinimizerRspackPlugin";

301

constructor(options?: LightningCssMinimizerRspackPluginOptions);

302

}

303

304

interface LightningCssMinimizerRspackPluginOptions {

305

errorRecovery?: boolean;

306

minimizerOptions?: any;

307

test?: RegExp | RegExp[];

308

include?: RegExp | RegExp[];

309

exclude?: RegExp | RegExp[];

310

}

311

```

312

313

### Asset Processing Plugins

314

315

Plugins for handling CSS, HTML, and other assets.

316

317

```typescript { .api }

318

/** Extract CSS into separate files */

319

class CssExtractRspackPlugin extends RspackBuiltinPlugin {

320

name: "CssExtractRspackPlugin";

321

constructor(options?: CssExtractRspackPluginOptions);

322

}

323

324

interface CssExtractRspackPluginOptions {

325

filename?: string;

326

chunkFilename?: string;

327

ignoreOrder?: boolean;

328

insert?: string | ((linkTag: HTMLLinkElement) => void);

329

attributes?: Record<string, string>;

330

linkType?: string | false;

331

runtime?: boolean;

332

}

333

334

/** Generate HTML files with bundle injection */

335

class HtmlRspackPlugin extends RspackBuiltinPlugin {

336

name: "HtmlRspackPlugin";

337

constructor(options?: HtmlRspackPluginOptions);

338

}

339

340

interface HtmlRspackPluginOptions {

341

title?: string;

342

filename?: string;

343

template?: string;

344

templateContent?: string | ((templateParameters: any) => string) | false;

345

templateParameters?: any;

346

inject?: boolean | "head" | "body";

347

publicPath?: string;

348

scriptLoading?: "blocking" | "defer" | "module";

349

favicon?: string;

350

meta?: Record<string, any>;

351

base?: string | Record<string, any> | false;

352

minify?: boolean | any;

353

hash?: boolean;

354

cache?: boolean;

355

showErrors?: boolean;

356

chunks?: string[];

357

chunksSortMode?: string | ((a: any, b: any) => number);

358

excludeChunks?: string[];

359

xhtml?: boolean;

360

}

361

362

/** Copy files during build */

363

class CopyRspackPlugin extends RspackBuiltinPlugin {

364

name: "CopyRspackPlugin";

365

constructor(options: CopyRspackPluginOptions);

366

}

367

368

interface CopyRspackPluginOptions {

369

patterns: CopyPattern[];

370

options?: CopyGlobalOptions;

371

}

372

373

interface CopyPattern {

374

from: string;

375

to?: string;

376

context?: string;

377

globOptions?: any;

378

filter?: (filepath: string) => boolean;

379

transform?: (content: Buffer, path: string) => string | Buffer;

380

transformPath?: (targetPath: string, absoluteFrom: string) => string;

381

noErrorOnMissing?: boolean;

382

info?: any;

383

}

384

385

interface CopyGlobalOptions {

386

concurrency?: number;

387

}

388

```

389

390

### Development and Debugging Plugins

391

392

Plugins for development workflow, debugging, and diagnostics.

393

394

```typescript { .api }

395

/** Generate source maps */

396

class SourceMapDevToolPlugin extends RspackBuiltinPlugin {

397

name: "SourceMapDevToolPlugin";

398

constructor(options?: SourceMapDevToolPluginOptions);

399

}

400

401

interface SourceMapDevToolPluginOptions {

402

filename?: string;

403

append?: string;

404

publicPath?: string;

405

fileContext?: string;

406

test?: string | RegExp | (string | RegExp)[];

407

include?: string | RegExp | (string | RegExp)[];

408

exclude?: string | RegExp | (string | RegExp)[];

409

columns?: boolean;

410

lineToLine?: boolean;

411

noSources?: boolean;

412

namespace?: string;

413

}

414

415

/** Eval-based source maps for development */

416

class EvalSourceMapDevToolPlugin extends RspackBuiltinPlugin {

417

name: "EvalSourceMapDevToolPlugin";

418

constructor(options?: any);

419

}

420

421

class EvalDevToolModulePlugin extends RspackBuiltinPlugin {

422

name: "EvalDevToolModulePlugin";

423

constructor(options?: EvalDevToolModulePluginOptions);

424

}

425

426

interface EvalDevToolModulePluginOptions {

427

namespace?: string;

428

moduleFilenameTemplate?: string;

429

}

430

431

/** Detect circular dependencies */

432

class CircularDependencyRspackPlugin extends RspackBuiltinPlugin {

433

name: "CircularDependencyRspackPlugin";

434

constructor(options?: CircularDependencyRspackPluginOptions);

435

}

436

437

interface CircularDependencyRspackPluginOptions {

438

exclude?: RegExp;

439

include?: RegExp;

440

failOnError?: boolean;

441

allowAsyncCycles?: boolean;

442

cwd?: string;

443

}

444

```

445

446

### Environment and External Plugins

447

448

Plugins for different target environments and external dependencies.

449

450

```typescript { .api }

451

/** Configure externals */

452

class ExternalsPlugin extends RspackBuiltinPlugin {

453

name: "ExternalsPlugin";

454

constructor(type: string, externals: Externals);

455

}

456

457

type Externals = string | RegExp | ExternalsObjectElement | ExternalsArrayElement |

458

ExternalsFunction;

459

460

interface ExternalsObjectElement {

461

[key: string]: string | boolean | string[] | ExternalsObjectElement;

462

}

463

464

type ExternalsArrayElement = string | RegExp | ExternalsObjectElement | ExternalsFunction;

465

type ExternalsFunction = (

466

context: string,

467

request: string,

468

callback: (err?: Error, result?: string) => void

469

) => void;

470

471

/** Node.js target support */

472

class NodeTargetPlugin extends RspackBuiltinPlugin {

473

name: "NodeTargetPlugin";

474

constructor();

475

}

476

477

/** Electron target support */

478

class ElectronTargetPlugin extends RspackBuiltinPlugin {

479

name: "ElectronTargetPlugin";

480

constructor(context: "main" | "preload" | "renderer");

481

}

482

483

/** Web Worker template */

484

class WebWorkerTemplatePlugin extends RspackBuiltinPlugin {

485

name: "WebWorkerTemplatePlugin";

486

constructor();

487

}

488

489

/** Load environment variables */

490

class EnvironmentPlugin extends RspackBuiltinPlugin {

491

name: "EnvironmentPlugin";

492

constructor(keys: string[] | Record<string, any>);

493

}

494

```

495

496

### Module ID Plugins

497

498

Plugins for controlling module and chunk ID generation.

499

500

```typescript { .api }

501

/** Named module IDs for development */

502

class NamedModuleIdsPlugin extends RspackBuiltinPlugin {

503

name: "NamedModuleIdsPlugin";

504

constructor();

505

}

506

507

/** Named chunk IDs for development */

508

class NamedChunkIdsPlugin extends RspackBuiltinPlugin {

509

name: "NamedChunkIdsPlugin";

510

constructor();

511

}

512

513

/** Deterministic module IDs for production */

514

class DeterministicModuleIdsPlugin extends RspackBuiltinPlugin {

515

name: "DeterministicModuleIdsPlugin";

516

constructor();

517

}

518

519

/** Deterministic chunk IDs for production */

520

class DeterministicChunkIdsPlugin extends RspackBuiltinPlugin {

521

name: "DeterministicChunkIdsPlugin";

522

constructor();

523

}

524

525

/** Natural module IDs (numeric) */

526

class NaturalModuleIdsPlugin extends RspackBuiltinPlugin {

527

name: "NaturalModuleIdsPlugin";

528

constructor();

529

}

530

531

/** Natural chunk IDs (numeric) */

532

class NaturalChunkIdsPlugin extends RspackBuiltinPlugin {

533

name: "NaturalChunkIdsPlugin";

534

constructor();

535

}

536

537

/** Occurrence-based chunk IDs */

538

class OccurrenceChunkIdsPlugin extends RspackBuiltinPlugin {

539

name: "OccurrenceChunkIdsPlugin";

540

constructor();

541

}

542

```

543

544

### Utility Plugins

545

546

Additional utility plugins for specific use cases.

547

548

```typescript { .api }

549

/** Ignore files during compilation */

550

class IgnorePlugin extends RspackBuiltinPlugin {

551

name: "IgnorePlugin";

552

constructor(options: IgnorePluginOptions);

553

}

554

555

interface IgnorePluginOptions {

556

checkResource?: (resource: string) => boolean;

557

checkContext?: (context: string) => boolean;

558

resourceRegExp?: RegExp;

559

contextRegExp?: RegExp;

560

}

561

562

/** Prevent emitting on errors */

563

class NoEmitOnErrorsPlugin extends RspackBuiltinPlugin {

564

name: "NoEmitOnErrorsPlugin";

565

constructor();

566

}

567

568

/** Warn about case-sensitive modules */

569

class WarnCaseSensitiveModulesPlugin extends RspackBuiltinPlugin {

570

name: "WarnCaseSensitiveModulesPlugin";

571

constructor();

572

}

573

574

/** Replace modules based on context */

575

class ContextReplacementPlugin extends RspackBuiltinPlugin {

576

name: "ContextReplacementPlugin";

577

constructor(

578

resourceRegExp: RegExp,

579

newContentResource?: string,

580

newContentRecursive?: boolean,

581

newContentRegExp?: RegExp

582

);

583

}

584

585

/** Replace normal modules */

586

class NormalModuleReplacementPlugin extends RspackBuiltinPlugin {

587

name: "NormalModuleReplacementPlugin";

588

constructor(

589

resourceRegExp: RegExp,

590

newResource: string | ((resource: any) => string)

591

);

592

}

593

```

594

595

**Plugin Usage Example:**

596

597

```typescript

598

import {

599

DefinePlugin,

600

HtmlRspackPlugin,

601

CssExtractRspackPlugin,

602

SwcJsMinimizerRspackPlugin,

603

ProgressPlugin

604

} from "@rspack/core";

605

606

const config = {

607

plugins: [

608

new DefinePlugin({

609

"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development")

610

}),

611

612

new HtmlRspackPlugin({

613

title: "My App",

614

template: "./src/index.html",

615

filename: "index.html",

616

inject: true,

617

minify: process.env.NODE_ENV === "production"

618

}),

619

620

new CssExtractRspackPlugin({

621

filename: "[name].[contenthash].css",

622

chunkFilename: "[id].[contenthash].css"

623

}),

624

625

new ProgressPlugin(),

626

627

...(process.env.NODE_ENV === "production" ? [

628

new SwcJsMinimizerRspackPlugin({

629

minimizerOptions: {

630

compress: true,

631

mangle: true

632

}

633

})

634

] : [])

635

]

636

};

637

```

638

639

## Critical Missing Plugins

640

641

The following critical plugins are available in @rspack/core but not yet documented above:

642

643

### Module Federation Plugins

644

645

```typescript { .api }

646

/** Main Module Federation plugin for complete micro-frontend setup */

647

class ModuleFederationPlugin extends RspackBuiltinPlugin {

648

name: "ModuleFederationPlugin";

649

constructor(options: ModuleFederationPluginOptions);

650

}

651

652

interface ModuleFederationPluginOptions {

653

name: string;

654

filename?: string;

655

exposes?: Exposes;

656

remotes?: Remotes;

657

shared?: Shared;

658

runtimePlugins?: string[];

659

shareStrategy?: "version-first" | "loaded-first";

660

}

661

662

/** Container plugin for exposing modules */

663

class ContainerPlugin extends RspackBuiltinPlugin {

664

name: "ContainerPlugin";

665

constructor(options: ContainerPluginOptions);

666

}

667

668

interface ContainerPluginOptions {

669

name: string;

670

library?: LibraryOptions;

671

filename?: string;

672

runtime?: string | false;

673

shareScope?: string;

674

exposes: Exposes;

675

}

676

677

/** Container reference plugin for consuming remote modules */

678

class ContainerReferencePlugin extends RspackBuiltinPlugin {

679

name: "ContainerReferencePlugin";

680

constructor(options: ContainerReferencePluginOptions);

681

}

682

683

interface ContainerReferencePluginOptions {

684

remoteType: string;

685

shareScope?: string;

686

remotes: Remotes;

687

}

688

```

689

690

### DLL Plugins

691

692

```typescript { .api }

693

/** Create a DLL bundle for commonly used dependencies */

694

class DllPlugin extends RspackBuiltinPlugin {

695

name: "DllPlugin";

696

constructor(options: DllPluginOptions);

697

}

698

699

interface DllPluginOptions {

700

context?: string;

701

format?: boolean;

702

name?: string;

703

path: string;

704

entryOnly?: boolean;

705

type?: string;

706

}

707

708

/** Reference a pre-built DLL bundle */

709

class DllReferencePlugin extends RspackBuiltinPlugin {

710

name: "DllReferencePlugin";

711

constructor(options: DllReferencePluginOptions);

712

}

713

714

interface DllReferencePluginOptions {

715

context?: string;

716

extensions?: string[];

717

manifest: DllReferencePluginOptionsManifest;

718

name?: string;

719

scope?: string;

720

sourceType?: DllReferencePluginOptionsSourceType;

721

type?: string;

722

}

723

724

type DllReferencePluginOptionsSourceType = "var" | "assign" | "this" | "window" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "umd" | "umd2" | "jsonp" | "system";

725

726

interface DllReferencePluginOptionsManifest {

727

name?: string;

728

type?: string;

729

content: DllReferencePluginOptionsContent;

730

}

731

732

type DllReferencePluginOptionsContent = Record<string, { id: string | number; buildMeta?: Record<string, any>; }>;

733

```

734

735

### Entry and Core Plugins

736

737

```typescript { .api }

738

/** Add static entry points */

739

class EntryPlugin extends RspackBuiltinPlugin {

740

name: "EntryPlugin";

741

constructor(context: string, entry: string, options?: EntryOptions | string);

742

}

743

744

/** Add dynamic entry points at runtime */

745

class DynamicEntryPlugin extends RspackBuiltinPlugin {

746

name: "DynamicEntryPlugin";

747

constructor(context: string, entry: () => Promise<EntryNormalized> | EntryNormalized);

748

}

749

750

/** Security plugin for Subresource Integrity */

751

class SubresourceIntegrityPlugin extends RspackBuiltinPlugin {

752

name: "SubresourceIntegrityPlugin";

753

constructor(options?: SubresourceIntegrityPluginOptions);

754

}

755

756

interface SubresourceIntegrityPluginOptions {

757

hashFuncNames?: ("sha256" | "sha384" | "sha512")[];

758

enabled?: boolean;

759

}

760

```