Skip to content

Commit 3f1e16b

Browse files
perezdbterlson
andauthored
Adding package versions + kind configuration support for PackageDirectory (#347)
Co-authored-by: Brian Terlson <[email protected]>
1 parent c22f1e9 commit 3f1e16b

File tree

9 files changed

+273
-22
lines changed

9 files changed

+273
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@alloy-js/typescript"
5+
---
6+
7+
The PackageDirectory component can now specify what kind of dependency to create for package dependencies added by reference using the `packageDependencyKinds` prop.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@alloy-js/typescript"
5+
---
6+
7+
The PackageDirectory component can now specify what version of a dependency to create for package dependencies added by reference using the `packageVersions` prop.

packages/typescript/src/components/PackageDirectory.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import {
99
SourceDirectoryContext,
1010
splitProps,
1111
useContext,
12+
Wrap,
1213
} from "@alloy-js/core";
1314
import { join } from "pathe";
15+
import { PackageMetadataContext } from "../context/package-metadata.js";
16+
import { ExternalPackage, getPackageScope } from "../create-package.js";
1417
import { TSPackageScope } from "../symbols/index.js";
1518
import { modulePath } from "../utils.js";
1619
import { PackageJsonFile, PackageJsonFileProps } from "./PackageJson.js";
@@ -20,6 +23,20 @@ export interface PackageDirectoryProps extends PackageJsonFileProps {
2023
tsConfig?: { outDir?: string };
2124
children?: Children;
2225
path?: string;
26+
27+
/**
28+
* Optional configuration values for referenced external packages.
29+
* By default, external packages are added as regular dependencies.
30+
* However you can specify `peerDependencies` or `devDependencies` to change this behavior.
31+
* Version can also be specified here to override the configured package version.
32+
*/
33+
packages?: [
34+
ExternalPackage,
35+
{
36+
version?: string;
37+
kind?: "dependencies" | "peerDependencies" | "devDependencies";
38+
},
39+
][];
2340
}
2441

2542
export const PackageContext: ComponentContext<PackageContext> =
@@ -58,13 +75,38 @@ export function PackageDirectory(props: PackageDirectoryProps) {
5875
"devDependencies",
5976
]);
6077

78+
let pkgMeta: PackageMetadataContext | undefined = undefined;
79+
if (props.packages) {
80+
pkgMeta = {
81+
versionSpecifiers: new Map(),
82+
dependencyType: new Map(),
83+
};
84+
for (const [pkg, config] of props.packages) {
85+
if (!config) {
86+
throw new Error("Package configuration must be provided");
87+
}
88+
if (config.version) {
89+
pkgMeta.versionSpecifiers.set(getPackageScope(pkg), config.version);
90+
}
91+
if (config.kind) {
92+
pkgMeta.dependencyType.set(getPackageScope(pkg), config.kind);
93+
}
94+
}
95+
}
96+
6197
return (
6298
<SourceDirectory path={props.path ?? "."}>
6399
<PackageContext.Provider value={packageContext}>
64100
<Scope value={packageContext.scope}>
65-
<PackageJsonFile {...pkgJsonProps} devDependencies={devDeps} />
66-
<TSConfigJson {...props.tsConfig} />
67-
{props.children}
101+
<Wrap
102+
when={!!pkgMeta}
103+
with={PackageMetadataContext.Provider}
104+
props={{ value: pkgMeta }}
105+
>
106+
<PackageJsonFile {...pkgJsonProps} devDependencies={devDeps} />
107+
<TSConfigJson {...props.tsConfig} />
108+
{props.children}
109+
</Wrap>
68110
</Scope>
69111
</PackageContext.Provider>
70112
</SourceDirectory>

packages/typescript/src/components/PackageJson.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { memo, SourceFile } from "@alloy-js/core";
1+
import { memo, SourceFile, useContext } from "@alloy-js/core";
2+
import { PackageMetadataContext } from "../context/package-metadata.js";
23
import { modulePath } from "../utils.js";
34
import { usePackage } from "./PackageDirectory.js";
45

@@ -73,8 +74,32 @@ export interface PackageExports {
7374
*/
7475
export function PackageJsonFile(props: PackageJsonFileProps) {
7576
const pkg = usePackage();
77+
const pkgMeta = useContext(PackageMetadataContext);
78+
79+
const dependencies = memo(() => {
80+
const kinds = {
81+
dependencies: props.dependencies,
82+
devDependencies: props.devDependencies,
83+
peerDependencies: props.peerDependencies,
84+
};
85+
86+
if (pkg) {
87+
for (const dependency of pkg.scope.dependencies) {
88+
const kind = pkgMeta?.dependencyType.get(dependency) ?? "dependencies";
89+
const versionSpecifier =
90+
pkgMeta?.versionSpecifiers.get(dependency) ?? dependency.version;
91+
92+
kinds[kind] ??= {};
93+
kinds[kind][dependency.name] = versionSpecifier;
94+
}
95+
}
96+
97+
return kinds;
98+
});
7699

77100
const jsonContent = memo(() => {
101+
const deps = dependencies();
102+
78103
const pkgJson = {
79104
name: props.name,
80105
version: props.version,
@@ -83,20 +108,9 @@ export function PackageJsonFile(props: PackageJsonFileProps) {
83108
license: props.license,
84109
homepage: props.homepage,
85110
type: props.type ?? "module",
86-
dependencies:
87-
props.dependencies || (pkg && pkg.scope.dependencies.size > 0) ?
88-
Object.fromEntries([
89-
...Object.entries(props.dependencies ?? {}),
90-
...(pkg ?
91-
Array.from(pkg.scope.dependencies).map((dependency) => [
92-
dependency.name,
93-
dependency.version,
94-
])
95-
: []),
96-
])
97-
: undefined,
98-
devDependencies: props.devDependencies,
99-
peerDependencies: props.peerDependencies,
111+
dependencies: deps.dependencies,
112+
devDependencies: deps.devDependencies,
113+
peerDependencies: deps.peerDependencies,
100114
scripts: props.scripts,
101115
exports: undefined as any,
102116
};

packages/typescript/src/components/TypeDeclaration.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const TypeDeclaration = ensureTypeRefContext(
1313
<JSDoc children={props.doc} />
1414
<hbr />
1515
</Show>
16-
<Declaration {...props} nameKind="type">
16+
<Declaration {...props} kind="type" nameKind="type">
1717
type <Name /> = {props.children};
1818
</Declaration>
1919
</>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
// type ref context isn't exported because it conflicts with the component of the same name.
2+
export * from "./package-metadata.js";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ComponentContext, createNamedContext } from "@alloy-js/core";
2+
import { TSPackageScope } from "../symbols/ts-package-scope.js";
3+
4+
export interface PackageMetadataContext {
5+
versionSpecifiers: Map<TSPackageScope, string>;
6+
dependencyType: Map<
7+
TSPackageScope,
8+
"dependencies" | "peerDependencies" | "devDependencies"
9+
>;
10+
}
11+
12+
export const PackageMetadataContext: ComponentContext<PackageMetadataContext> =
13+
createNamedContext<PackageMetadataContext>("PackageMetadataContext");

packages/typescript/src/create-package.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,19 @@ function assignMembers(
165165
}
166166
}
167167

168+
const packageScopeSymbol: unique symbol = Symbol();
169+
/**
170+
* Retrieve the package scope associated with an external package created via
171+
* createPackage.
172+
*/
173+
export function getPackageScope(pkg: ExternalPackage) {
174+
return (pkg as any)[packageScopeSymbol];
175+
}
176+
168177
function createSymbols(
169178
binder: Binder,
170179
props: CreatePackageProps<PackageDescriptor>,
171-
refkeys: Record<string, any>,
180+
refkeys: Record<string | typeof packageScopeSymbol, any>,
172181
) {
173182
const pkgScope = new TSPackageScope(
174183
props.name,
@@ -180,6 +189,8 @@ function createSymbols(
180189
},
181190
);
182191

192+
refkeys[packageScopeSymbol] = pkgScope;
193+
183194
for (const [path, symbols] of Object.entries(props.descriptor)) {
184195
const keys = path === "." ? refkeys : refkeys[path];
185196
const moduleScope = new TSModuleScope(path, pkgScope, {
@@ -252,13 +263,20 @@ function createRefkeysForMembers(
252263
}
253264
}
254265

266+
export interface ExternalPackage {
267+
[externalPackageSymbol]: true;
268+
}
269+
270+
const externalPackageSymbol: unique symbol = Symbol("ExternalPackageSymbol");
271+
255272
export function createPackage<const T extends PackageDescriptor>(
256273
props: CreatePackageProps<T>,
257-
): PackageRefkeys<T> & SymbolCreator {
274+
): PackageRefkeys<T> & SymbolCreator & ExternalPackage {
258275
const refkeys: any = {
259276
[getSymbolCreatorSymbol()](binder: Binder) {
260277
createSymbols(binder, props, refkeys);
261278
},
279+
[externalPackageSymbol]: true,
262280
};
263281

264282
for (const [path, symbols] of Object.entries(props.descriptor)) {

packages/typescript/test/externals.test.tsx

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Output } from "@alloy-js/core";
1+
import { Output, render } from "@alloy-js/core";
22
import { expect, it } from "vitest";
33
import { fs } from "../src/builtins/node.js";
44
import {
@@ -187,3 +187,152 @@ it("can import static members", () => {
187187
`,
188188
});
189189
});
190+
191+
it("can specify packages as dev dependencies", () => {
192+
const testLib = createPackage({
193+
name: "testLib",
194+
version: "1.0.0",
195+
descriptor: {
196+
".": {
197+
named: ["foo"],
198+
},
199+
},
200+
});
201+
202+
expect(
203+
<Output externals={[testLib, fs]}>
204+
<PackageDirectory
205+
path="."
206+
name="test"
207+
version="1.0.0"
208+
packages={[[testLib, { version: "2.0.0", kind: "devDependencies" }]]}
209+
>
210+
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
211+
</PackageDirectory>
212+
</Output>,
213+
).toRenderTo({
214+
"package.json": `
215+
{
216+
"name": "test",
217+
"version": "1.0.0",
218+
"type": "module",
219+
"devDependencies": {
220+
"typescript": "^5.5.2",
221+
"testLib": "2.0.0"
222+
}
223+
}
224+
`,
225+
"tsconfig.json": expect.anything(),
226+
"index.ts": expect.anything(),
227+
});
228+
});
229+
230+
it("can specify packages as peer dependencies", () => {
231+
const testLib = createPackage({
232+
name: "testLib",
233+
version: "1.0.0",
234+
descriptor: {
235+
".": {
236+
named: ["foo"],
237+
},
238+
},
239+
});
240+
241+
expect(
242+
<Output externals={[testLib, fs]}>
243+
<PackageDirectory
244+
path="."
245+
name="test"
246+
version="1.0.0"
247+
packages={[[testLib, { version: "2.0.0", kind: "peerDependencies" }]]}
248+
>
249+
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
250+
</PackageDirectory>
251+
</Output>,
252+
).toRenderTo({
253+
"package.json": `
254+
{
255+
"name": "test",
256+
"version": "1.0.0",
257+
"type": "module",
258+
"devDependencies": {
259+
"typescript": "^5.5.2"
260+
},
261+
"peerDependencies": {
262+
"testLib": "2.0.0"
263+
}
264+
}
265+
`,
266+
"tsconfig.json": expect.anything(),
267+
"index.ts": expect.anything(),
268+
});
269+
});
270+
271+
it("can inherit package version as peer dependency", () => {
272+
const testLib = createPackage({
273+
name: "testLib",
274+
version: "1.0.0",
275+
descriptor: {
276+
".": {
277+
named: ["foo"],
278+
},
279+
},
280+
});
281+
282+
expect(
283+
<Output externals={[testLib, fs]}>
284+
<PackageDirectory
285+
path="."
286+
name="test"
287+
version="1.0.0"
288+
packages={[[testLib, { kind: "peerDependencies" }]]}
289+
>
290+
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
291+
</PackageDirectory>
292+
</Output>,
293+
).toRenderTo({
294+
"package.json": `
295+
{
296+
"name": "test",
297+
"version": "1.0.0",
298+
"type": "module",
299+
"devDependencies": {
300+
"typescript": "^5.5.2"
301+
},
302+
"peerDependencies": {
303+
"testLib": "1.0.0"
304+
}
305+
}
306+
`,
307+
"tsconfig.json": expect.anything(),
308+
"index.ts": expect.anything(),
309+
});
310+
});
311+
312+
it("must throw an error if package configuration is not provided", () => {
313+
const testLib = createPackage({
314+
name: "testLib",
315+
version: "1.0.0",
316+
descriptor: {
317+
".": {
318+
named: ["foo"],
319+
},
320+
},
321+
});
322+
323+
expect(() =>
324+
render(
325+
<Output externals={[testLib, fs]}>
326+
<PackageDirectory
327+
path="."
328+
name="test"
329+
version="1.0.0"
330+
// @ts-expect-error explicitly testing missing package config.
331+
packages={[[testLib]]}
332+
>
333+
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
334+
</PackageDirectory>
335+
</Output>,
336+
),
337+
).toThrowError("Package configuration must be provided");
338+
});

0 commit comments

Comments
 (0)