Skip to content

Commit 01975c5

Browse files
committed
improve rollup DSL
1 parent 2de4f44 commit 01975c5

10 files changed

Lines changed: 683 additions & 367 deletions

File tree

packages/rollup-util/src/BuildContext.ts

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,56 @@ import { join } from "node:path";
22
import { pathToFileURL } from "node:url";
33

44
import { discoverModule, discoverWorkspace, getDependencies } from "@calmdownval/workspaces-util";
5+
import { rollup, type InputOptions, type OutputOptions } from "rollup";
6+
7+
import type { Configurator } from "./Entity";
58

69
export interface BuildContext {
710
readonly cwd: string;
811
readonly moduleName: string;
912
readonly targetEnv: TargetEnv;
10-
11-
/** @internal */
12-
addBuildTask(block: BuildTask): void;
1313
}
1414

1515
export type TargetEnv =
16-
| "development"
17-
| "staging"
18-
| "production";
16+
| "dev"
17+
| "stag"
18+
| "prod";
19+
20+
export function inEnvs(...envs: TargetEnv[]): Configurator<any> {
21+
return (_, context) => envs.includes(context.targetEnv);
22+
}
23+
1924

2025
/** @internal */
21-
export interface BuildTask {
22-
(context: BuildContext): Promise<void>;
26+
export interface BuildTarget {
27+
readonly input: InputOptions;
28+
readonly outputs: readonly OutputOptions[];
2329
}
2430

31+
/** @internal */
32+
export interface BuildTask {
33+
(context: BuildContext): Promise<readonly BuildTarget[]>;
34+
}
2535

26-
let currentContext: BuildContext | null = null;
36+
let currentTasks: BuildTask[] | null = null;
2737

2838
/** @internal */
29-
export function runBuild(block: BuildTask) {
30-
if (!currentContext) {
31-
throw new Error('Could not get build context. Please use the rollup-build command.');
39+
export function buildTask(task: BuildTask) {
40+
if (!currentTasks) {
41+
throw new Error("Build has not been initiated. Please use the rollup-build command.");
3242
}
3343

34-
currentContext.addBuildTask(block);
44+
currentTasks.push(task);
3545
}
3646

47+
3748
const ENV_MAP: { readonly [K in string]?: TargetEnv } = {
38-
dev: "development",
39-
development: "development",
40-
stag: "staging",
41-
staging: "staging",
42-
prod: "production",
43-
production: "production",
49+
dev: "dev",
50+
development: "dev",
51+
stag: "stag",
52+
staging: "stag",
53+
prod: "prod",
54+
production: "prod",
4455
};
4556

4657
export async function build(cwd: string = process.cwd()) {
@@ -62,39 +73,47 @@ export async function build(cwd: string = process.cwd()) {
6273

6374
// get the target environment
6475
const targetEnv: TargetEnv = process.env.BUILD_ENV
65-
? ENV_MAP[process.env.BUILD_ENV] ?? "production"
66-
: "production";
76+
? ENV_MAP[process.env.BUILD_ENV] ?? "prod"
77+
: "prod";
6778

6879
// build!
6980
for (const currentModule of moduleQueue) {
70-
const taskQueue: BuildTask[] = [];
71-
currentContext = {
81+
const context: BuildContext = {
7282
cwd: currentModule.baseDir,
7383
moduleName: currentModule.declaration.name,
7484
targetEnv,
75-
addBuildTask: block => {
76-
taskQueue.push(block);
77-
},
7885
};
7986

8087
// import the build.targets.mjs definition file
81-
process.chdir(currentContext.cwd);
88+
currentTasks = [];
89+
process.chdir(context.cwd);
8290
try {
83-
const url = pathToFileURL(join(currentContext.cwd, "build.targets.mjs")).href;
91+
const url = pathToFileURL(join(context.cwd, "build.targets.mjs")).href;
8492
await import(url);
8593
}
8694
catch {
87-
// likely no such file exists, skip it...
95+
// likely no such file exists, skip it...?
8896
continue;
8997
}
9098

9199
// run queued tasks
92-
for (const task of taskQueue) {
93-
await task(currentContext);
100+
const targets = (
101+
await Promise.all(
102+
currentTasks.map(task => task(context)),
103+
)
104+
)
105+
.flat(1);
106+
107+
// build the targets sequentially
108+
for (const target of targets) {
109+
const bundle = await rollup(target.input);
110+
for (const output of target.outputs) {
111+
await bundle.write(output);
112+
}
94113
}
95114
}
96115
}
97116
finally {
98-
currentContext = null;
117+
currentTasks = null;
99118
}
100119
}

packages/rollup-util/src/Entity.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { BuildContext } from "./BuildContext";
2+
3+
export type Entity<TName extends string, TConfig extends object, TBase = {}> = TBase & {
4+
readonly name: TName;
5+
6+
/** @internal */
7+
isFinal: boolean;
8+
9+
/** @internal */
10+
getEnabled: Configurator<boolean>;
11+
12+
/** @internal */
13+
getConfig: Configurator<TConfig>;
14+
15+
/** @internal */
16+
finalize(): Entity<TName, TConfig, TBase>;
17+
18+
enable(
19+
configurator: Configurator<boolean>,
20+
): Entity<TName, TConfig, TBase>;
21+
22+
disable(
23+
configurator: Configurator<boolean>,
24+
): Entity<TName, TConfig, TBase>;
25+
26+
configure(
27+
configurator: Configurator<TConfig>,
28+
): Entity<TName, TConfig, TBase>;
29+
}
30+
31+
export interface Configurator<TConfig> {
32+
(currentConfig: TConfig | undefined, context: BuildContext): Promise<TConfig | undefined> | TConfig | undefined;
33+
}
34+
35+
export type AnyEntity = (
36+
Entity<any, any>
37+
);
38+
39+
export type NameOf<TEntity extends AnyEntity> = (
40+
TEntity extends Entity<infer TName, any, any> ? TName : string
41+
);
42+
43+
export type ConfigOf<TEntity extends AnyEntity> = (
44+
TEntity extends Entity<any, infer TConfig, any> ? TConfig : unknown
45+
);
46+
47+
export function createEntity<TName extends string, TConfig extends object, TBase>(
48+
name: TName,
49+
base: TBase,
50+
): Entity<TName, TConfig, TBase> {
51+
return {
52+
name,
53+
isFinal: false,
54+
getEnabled: defaultGetEnabled,
55+
getConfig: defaultGetConfig,
56+
finalize: onFinalize,
57+
enable: onEnable,
58+
disable: onDisable,
59+
configure: onConfigure,
60+
...base,
61+
} satisfies Entity<TName, TConfig, {}> as any;
62+
}
63+
64+
function defaultGetEnabled() {
65+
return true;
66+
}
67+
68+
function defaultGetConfig(config?: any) {
69+
return config;
70+
}
71+
72+
function onFinalize(
73+
this: AnyEntity,
74+
): AnyEntity {
75+
return this.isFinal
76+
? this
77+
: {
78+
...this,
79+
isFinal: true,
80+
};
81+
}
82+
83+
function onEnable(
84+
this: AnyEntity,
85+
configurator: Configurator<boolean>,
86+
): AnyEntity {
87+
const prev = this.getEnabled;
88+
const next: Configurator<boolean> = async (currentConfig, context) => (
89+
configurator(await prev(currentConfig, context), context)
90+
);
91+
92+
if (this.isFinal) {
93+
this.getEnabled = next;
94+
return this;
95+
}
96+
97+
return {
98+
...this,
99+
getEnabled: next,
100+
};
101+
}
102+
103+
function onDisable(
104+
this: AnyEntity,
105+
configurator: Configurator<boolean>,
106+
): AnyEntity {
107+
const prev = this.getEnabled;
108+
const next: Configurator<boolean> = async (currentConfig, context) => (
109+
!(await configurator(!(await prev(currentConfig, context)), context))
110+
);
111+
112+
if (this.isFinal) {
113+
this.getEnabled = next;
114+
return this;
115+
}
116+
117+
return {
118+
...this,
119+
getEnabled: next,
120+
};
121+
}
122+
123+
function onConfigure(
124+
this: AnyEntity,
125+
configurator: Configurator<any>,
126+
): AnyEntity {
127+
const prev = this.getConfig;
128+
const next: Configurator<any> = async (currentConfig, context) => (
129+
configurator(await prev(currentConfig, context), context)
130+
);
131+
132+
if (this.isFinal) {
133+
this.getConfig = next;
134+
return this;
135+
}
136+
137+
return {
138+
...this,
139+
getConfig: next,
140+
};
141+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { AnyEntity, NameOf } from "./Entity";
2+
3+
export interface EntityContainer<TEntity extends AnyEntity, TEntities extends EntityMap<TEntity> = EntityMap<TEntity>> {
4+
readonly entityKind: string;
5+
readonly entityMap: TEntities;
6+
readonly entityOrder: readonly (keyof TEntities)[];
7+
isFinal: boolean;
8+
9+
finalize(): EntityContainer<TEntity, TEntities>;
10+
11+
add<T extends TEntity>(
12+
entity: T,
13+
): EntityContainer<TEntity, TEntities & { [K in NameOf<TEntity>]: T }>;
14+
15+
collect<T>(
16+
block: (entity: TEntity) => Promise<T | null | undefined>,
17+
): Promise<T[]>;
18+
}
19+
20+
export type EntityMap<TEntity extends AnyEntity> = {
21+
readonly [TName in string]: TEntity;
22+
};
23+
24+
export function createEntityContainer<TEntity extends AnyEntity>(kind: string): EntityContainer<TEntity, {}> {
25+
return {
26+
entityKind: kind,
27+
entityMap: {},
28+
entityOrder: [],
29+
isFinal: false,
30+
finalize: onFinalize,
31+
add: onAdd,
32+
collect: onCollect,
33+
};
34+
}
35+
36+
type Mutable<T> = (
37+
{ -readonly [K in keyof T]: T[K] }
38+
);
39+
40+
function onFinalize(
41+
this: EntityContainer<AnyEntity>,
42+
): EntityContainer<any, any> {
43+
return {
44+
...this,
45+
isFinal: true,
46+
entityMap: this.entityOrder.reduce<Mutable<EntityMap<AnyEntity>>>((map, key) => {
47+
map[key] = this.entityMap[key].finalize();
48+
return map;
49+
}, {}),
50+
};
51+
}
52+
53+
function onAdd(
54+
this: EntityContainer<AnyEntity>,
55+
entity: AnyEntity,
56+
): EntityContainer<any, any> {
57+
if (this.entityMap[entity.name] !== undefined) {
58+
throw new Error(`${this.entityKind} '${entity.name}' has already been added.`);
59+
}
60+
61+
if (this.isFinal) {
62+
(this.entityMap as Mutable<EntityMap<AnyEntity>>)[entity.name] = entity;
63+
(this.entityOrder as string[]).push(entity.name);
64+
return this;
65+
}
66+
67+
return {
68+
...this,
69+
entityMap: {
70+
...this.entityMap,
71+
[entity.name]: entity,
72+
},
73+
entityOrder: [
74+
...this.entityOrder,
75+
entity.name,
76+
],
77+
};
78+
}
79+
80+
function onCollect<T>(
81+
this: EntityContainer<AnyEntity>,
82+
block: (entity: any) => Promise<T | null | undefined>,
83+
): Promise<T[]> {
84+
return Promise
85+
.all(this.entityOrder.map(name => block(this.entityMap[name])))
86+
.then(result => result.filter(it => it !== null && it !== undefined));
87+
}

0 commit comments

Comments
 (0)