Skip to content
Open
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions .chronus/changes/peer-deps-2025-11-10-17-1-32.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/typescript"
---

The PackageDirectory component can now specify what kind of dependency to create for package dependencies added by reference using the `packageDependencyKinds` prop.
7 changes: 7 additions & 0 deletions .chronus/changes/peer-deps-2025-11-10-17-2-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/typescript"
---

The PackageDirectory component can now specify what version of a dependency to create for package dependencies added by reference using the `packageVersions` prop.
52 changes: 49 additions & 3 deletions packages/typescript/src/components/PackageDirectory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {
SourceDirectoryContext,
splitProps,
useContext,
Wrap,
} from "@alloy-js/core";
import { join } from "pathe";
import { PackageMetadataContext } from "../context/package-metadata.js";
import { ExternalPackage, getPackageScope } from "../create-package.js";
import { TSPackageScope } from "../symbols/index.js";
import { modulePath } from "../utils.js";
import { PackageJsonFile, PackageJsonFileProps } from "./PackageJson.js";
Expand All @@ -20,6 +23,23 @@ export interface PackageDirectoryProps extends PackageJsonFileProps {
tsConfig?: { outDir?: string };
children?: Children;
path?: string;

/**
* The versions to use for external packages referenced within this package.
* By default, the version specified when the external package was created is
* used.
*/
packageVersions?: [ExternalPackage, string][];

/**
* The package dependency kinds to use for external packages referenced within
* this package. By default, all external packages are added as
* "dependencies".
*/
packageDependencyKinds?: [
ExternalPackage,
"dependencies" | "peerDependencies" | "devDependencies",
][];
}

export const PackageContext: ComponentContext<PackageContext> =
Expand Down Expand Up @@ -58,13 +78,39 @@ export function PackageDirectory(props: PackageDirectoryProps) {
"devDependencies",
]);

let pkgMeta: PackageMetadataContext | undefined = undefined;
if (props.packageVersions || props.packageDependencyKinds) {
pkgMeta = {
versionSpecifiers: new Map(),
dependencyType: new Map(),
};

if (props.packageVersions) {
for (const [pkg, version] of props.packageVersions) {
pkgMeta.versionSpecifiers.set(getPackageScope(pkg), version);
}
}

if (props.packageDependencyKinds) {
for (const [pkg, kind] of props.packageDependencyKinds) {
pkgMeta.dependencyType.set(getPackageScope(pkg), kind);
}
}
}

return (
<SourceDirectory path={props.path ?? "."}>
<PackageContext.Provider value={packageContext}>
<Scope value={packageContext.scope}>
<PackageJsonFile {...pkgJsonProps} devDependencies={devDeps} />
<TSConfigJson {...props.tsConfig} />
{props.children}
<Wrap
when={!!pkgMeta}
with={PackageMetadataContext.Provider}
props={{ value: pkgMeta }}
>
<PackageJsonFile {...pkgJsonProps} devDependencies={devDeps} />
<TSConfigJson {...props.tsConfig} />
{props.children}
</Wrap>
</Scope>
</PackageContext.Provider>
</SourceDirectory>
Expand Down
44 changes: 29 additions & 15 deletions packages/typescript/src/components/PackageJson.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo, SourceFile } from "@alloy-js/core";
import { memo, SourceFile, useContext } from "@alloy-js/core";
import { PackageMetadataContext } from "../context/package-metadata.js";
import { modulePath } from "../utils.js";
import { usePackage } from "./PackageDirectory.js";

Expand Down Expand Up @@ -73,8 +74,32 @@ export interface PackageExports {
*/
export function PackageJsonFile(props: PackageJsonFileProps) {
const pkg = usePackage();
const pkgMeta = useContext(PackageMetadataContext);

const dependencies = memo(() => {
const kinds = {
dependencies: props.dependencies,
devDependencies: props.devDependencies,
peerDependencies: props.peerDependencies,
};

if (pkg) {
for (const dependency of pkg.scope.dependencies) {
const kind = pkgMeta?.dependencyType.get(dependency) ?? "dependencies";
const versionSpecifier =
pkgMeta?.versionSpecifiers.get(dependency) ?? dependency.version;

kinds[kind] ??= {};
kinds[kind][dependency.name] = versionSpecifier;
}
}

return kinds;
});

const jsonContent = memo(() => {
const deps = dependencies();

const pkgJson = {
name: props.name,
version: props.version,
Expand All @@ -83,20 +108,9 @@ export function PackageJsonFile(props: PackageJsonFileProps) {
license: props.license,
homepage: props.homepage,
type: props.type ?? "module",
dependencies:
props.dependencies || (pkg && pkg.scope.dependencies.size > 0) ?
Object.fromEntries([
...Object.entries(props.dependencies ?? {}),
...(pkg ?
Array.from(pkg.scope.dependencies).map((dependency) => [
dependency.name,
dependency.version,
])
: []),
])
: undefined,
devDependencies: props.devDependencies,
peerDependencies: props.peerDependencies,
dependencies: deps.dependencies,
devDependencies: deps.devDependencies,
peerDependencies: deps.peerDependencies,
scripts: props.scripts,
exports: undefined as any,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript/src/components/TypeDeclaration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const TypeDeclaration = ensureTypeRefContext(
<JSDoc children={props.doc} />
<hbr />
</Show>
<Declaration {...props} nameKind="type">
<Declaration {...props} kind="type" nameKind="type">
type <Name /> = {props.children};
</Declaration>
</>
Expand Down
1 change: 1 addition & 0 deletions packages/typescript/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
// type ref context isn't exported because it conflicts with the component of the same name.
export * from "./package-metadata.js";
13 changes: 13 additions & 0 deletions packages/typescript/src/context/package-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createNamedContext } from "@alloy-js/core";
import { TSPackageScope } from "../symbols/ts-package-scope.js";

export interface PackageMetadataContext {
versionSpecifiers: Map<TSPackageScope, string>;
dependencyType: Map<
TSPackageScope,
"dependencies" | "peerDependencies" | "devDependencies"
>;
}

export const PackageMetadataContext =
createNamedContext<PackageMetadataContext>("PackageMetadataContext");
22 changes: 20 additions & 2 deletions packages/typescript/src/create-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,19 @@ function assignMembers(
}
}

const packageScopeSymbol: unique symbol = Symbol();
/**
* Retrieve the package scope associated with an external package created via
* createPackage.
*/
export function getPackageScope(pgk: ExternalPackage) {
return (pgk as any)[packageScopeSymbol];
}

function createSymbols(
binder: Binder,
props: CreatePackageProps<PackageDescriptor>,
refkeys: Record<string, any>,
refkeys: Record<string | typeof packageScopeSymbol, any>,
) {
const pkgScope = new TSPackageScope(
props.name,
Expand All @@ -180,6 +189,8 @@ function createSymbols(
},
);

refkeys[packageScopeSymbol] = pkgScope;

for (const [path, symbols] of Object.entries(props.descriptor)) {
const keys = path === "." ? refkeys : refkeys[path];
const moduleScope = new TSModuleScope(path, pkgScope, {
Expand Down Expand Up @@ -252,13 +263,20 @@ function createRefkeysForMembers(
}
}

export interface ExternalPackage {
[externalPackageSymbol]: true;
}

const externalPackageSymbol: unique symbol = Symbol("ExternalPackageSymbol");

export function createPackage<const T extends PackageDescriptor>(
props: CreatePackageProps<T>,
): PackageRefkeys<T> & SymbolCreator {
): PackageRefkeys<T> & SymbolCreator & ExternalPackage {
const refkeys: any = {
[getSymbolCreatorSymbol()](binder: Binder) {
createSymbols(binder, props, refkeys);
},
[externalPackageSymbol]: true,
};

for (const [path, symbols] of Object.entries(props.descriptor)) {
Expand Down
82 changes: 82 additions & 0 deletions packages/typescript/test/externals.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,85 @@ it("can import static members", () => {
`,
});
});

it("can specify packages as dev dependencies", () => {
const testLib = createPackage({
name: "testLib",
version: "1.0.0",
descriptor: {
".": {
named: ["foo"],
},
},
});

expect(
<Output externals={[testLib, fs]}>
<PackageDirectory
path="."
name="test"
version="1.0.0"
packageVersions={[[testLib, "2.0.0"]]}
packageDependencyKinds={[[testLib, "devDependencies"]]}
>
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
</PackageDirectory>
</Output>,
).toRenderTo({
"package.json": `
{
"name": "test",
"version": "1.0.0",
"type": "module",
"devDependencies": {
"typescript": "^5.5.2",
"testLib": "2.0.0"
}
}
`,
"tsconfig.json": expect.anything(),
"index.ts": expect.anything(),
});
});

it("can specify packages as peer dependencies", () => {
const testLib = createPackage({
name: "testLib",
version: "1.0.0",
descriptor: {
".": {
named: ["foo"],
},
},
});

expect(
<Output externals={[testLib, fs]}>
<PackageDirectory
path="."
name="test"
version="1.0.0"
packageVersions={[[testLib, "2.0.0"]]}
packageDependencyKinds={[[testLib, "peerDependencies"]]}
>
<SourceFile path="index.ts">{testLib.foo};</SourceFile>
</PackageDirectory>
</Output>,
).toRenderTo({
"package.json": `
{
"name": "test",
"version": "1.0.0",
"type": "module",
"devDependencies": {
"typescript": "^5.5.2"
},
"peerDependencies": {
"testLib": "2.0.0"
}
}
`,
"tsconfig.json": expect.anything(),
"index.ts": expect.anything(),
});
});
Loading