Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ export class Assembler implements Emitter {
bundled: this.projectInfo.bundleDependencies,
types: Object.fromEntries(this._types),
submodules: noEmptyDict(toSubmoduleDeclarations(this.mySubmodules())),
targets: this.projectInfo.targets,

// Force this into shape
targets: this.projectInfo.targets as spec.Assembly['targets'],

metadata: {
...this.projectInfo.metadata,

Expand Down
82 changes: 78 additions & 4 deletions src/project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ export type TSCompilerOptions = Partial<
>
>;

/**
* The assembly targets have some typing in `spec.PackageJson`, extract it.
*
* The types in the upstream spec library:
*
* - Missing the user-visible `go` target
* - Missing the synthetic `js` target
*/
export type AssemblyTargets = spec.PackageJson['jsii']['targets'] & {
js?: {
npm: string;
};
go?: {
moduleName: string;
packageName: string;
};
[otherLanguage: string]: unknown;
};

export interface ProjectInfo {
readonly projectRoot: string;
readonly packageJson: PackageJson;
Expand All @@ -65,7 +84,7 @@ export interface ProjectInfo {
readonly peerDependencies: { readonly [name: string]: string };
readonly dependencyClosure: readonly spec.Assembly[];
readonly bundleDependencies?: { readonly [name: string]: string };
readonly targets: spec.AssemblyTargets;
readonly targets: AssemblyTargets;
readonly metadata?: { readonly [key: string]: any };
readonly jsiiVersionFormat: 'short' | 'full';
readonly diagnostics?: { readonly [code: string]: ts.DiagnosticCategory };
Expand All @@ -85,6 +104,12 @@ export interface ProjectInfo {
readonly validateTsconfig?: TypeScriptConfigValidationRuleSet;
}

/**
* A type representing the contents of a `package.json` file.
*
* Note that there is also a `PackageJson` type in `@jsii/spec`, and this one is
* not the same. Do not ask me why.
*/
export interface PackageJson {
readonly description?: string;
readonly homepage?: string;
Expand Down Expand Up @@ -121,7 +146,7 @@ export interface PackageJson {
// main jsii config
readonly diagnostics?: { readonly [id: string]: 'error' | 'warning' | 'suggestion' | 'message' };
readonly metadata?: { readonly [key: string]: unknown };
readonly targets?: { readonly [name: string]: unknown };
readonly targets?: AssemblyTargets;
readonly versionFormat?: 'short' | 'full';

// Either user-provided config ...
Expand Down Expand Up @@ -241,8 +266,10 @@ export function loadProjectInfo(projectRoot: string): ProjectInfoResult {
dependencyClosure: transitiveDependencies,
bundleDependencies,
targets: {
..._required(pkg.jsii, 'The "package.json" file must specify the "jsii" attribute').targets,
js: { npm: pkg.name },
...validateTargets(
_required(pkg.jsii, 'The "package.json" file must specify the "jsii" attribute').targets as AssemblyTargets,
),
js: { npm: pkg.name! },
},
metadata,
jsiiVersionFormat: _validateVersionFormat(pkg.jsii?.versionFormat ?? 'full'),
Expand Down Expand Up @@ -278,6 +305,53 @@ export function loadProjectInfo(projectRoot: string): ProjectInfoResult {
return { projectInfo, diagnostics };
}

/**
* Validate the values of the `.jsii.targets` field
*/
function validateTargets(targets: AssemblyTargets | undefined): AssemblyTargets | undefined {
if (!targets) {
return undefined;
}

// Go package names must be valid identifiers
if (targets.go) {
if (!isIdentifier(targets.go.packageName)) {
throw new JsiiError(`jsii.targets.go.packageName contains non-identifier characters: ${targets.go.packageName}`);
}
}

if (targets.dotnet) {
if (!targets.dotnet.namespace.split('.').every(isIdentifier)) {
throw new JsiiError(
`jsii.targets.dotnet.namespace contains non-identifier characters: ${targets.dotnet.namespace}`,
);
}
}

if (targets.java) {
if (!targets.java.package.split('.').every(isIdentifier)) {
throw new JsiiError(`jsii.targets.java.package contains non-identifier characters: ${targets.java.package}`);
}
}

if (targets.python) {
if (!targets.python.module.split('.').every(isIdentifier)) {
throw new JsiiError(`jsii.targets.python.module contains non-identifier characters: ${targets.python.module}`);
}
}

return targets;

/**
* Regexp-check for matching an identifier
*
* Conveniently, all identifiers look more or less the same in all languages.
*/
function isIdentifier(x: string) {
return /^[\w_][\w\d_]*$/u.test(x);
}
}

function _guessRepositoryType(url: string): string {
if (url.endsWith('.git')) {
return 'git';
Expand Down
62 changes: 57 additions & 5 deletions test/project-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { writeAssembly } from '@jsii/spec';
import * as clone from 'clone';
import * as ts from 'typescript';

import { loadProjectInfo } from '../src/project-info';
import { loadProjectInfo, PackageJson } from '../src/project-info';
import { TypeScriptConfigValidationRuleSet } from '../src/tsconfig';
import { VERSION } from '../src/version';

Expand All @@ -28,7 +28,7 @@ const BASE_PROJECT = {
},
dependencies: { 'jsii-test-dep': '^1.2.3' } as { [name: string]: string },
peerDependencies: { 'jsii-test-dep': '^1.2.3' } as { [name: string]: string },
};
} satisfies PackageJson;

describe('loadProjectInfo', () => {
test('loads valid project', () =>
Expand Down Expand Up @@ -236,7 +236,7 @@ describe('loadProjectInfo', () => {
diagCode2: 'warning',
diagCode3: 'suggestion',
diagCode4: 'message',
};
} as const;
info.jsii.diagnostics = diagnostics;
},
);
Expand All @@ -248,13 +248,58 @@ describe('loadProjectInfo', () => {
(info) => {
const diagnostics = {
diagCode1: 'invalid-category',
};
} as any;
info.jsii.diagnostics = diagnostics;
},
);
});
});

describe('invalid target configuration', () => {
test('invalid Go packageName is rejected', () => {
expectProjectLoadError(/contains non-identifier characters/, (proj) => {
proj.jsii.targets.go = {
moduleName: 'asdf',
packageName: 'as-df',
};
});
});

test('invalid .NET namespace is rejected', () => {
expectProjectLoadError(/contains non-identifier characters/, (proj) => {
proj.jsii.targets.dotnet = {
namespace: 'a-x',
packageId: 'asdf',
};
});
});

test('invalid Java package is rejected', () => {
expectProjectLoadError(/contains non-identifier characters/, (proj) => {
proj.jsii.targets.java = {
package: 'as-df',
maven: {
artifactId: 'asdf',
groupId: 'asdf',
},
};
});
});

test('invalid Python module is rejected', () => {
expectProjectLoadError(/contains non-identifier characters/, (proj) => {
proj.jsii.targets.python = {
module: 'as-df',
distName: 'as-df',
};
});
});

function expectProjectLoadError(error: RegExp, gremlin: Parameters<typeof _withTestProject>[1]) {
_withTestProject((root) => expect(() => loadProjectInfo(root)).toThrow(error), gremlin);
}
});

describe('user-provided tsconfig', () => {
test('can set a user-provided config', () => {
return _withTestProject(
Expand Down Expand Up @@ -354,6 +399,11 @@ const TEST_DEP_DEP_ASSEMBLY: spec.Assembly = {
fingerprint: 'F1NG3RPR1N7',
};

/**
* A type to represent the package.json type that gets mutated by testproject gremlins
*/
type HalfFilledPackageJson = DeepWriteable<PackageJson> & typeof BASE_PROJECT;

/**
* Creates a throw-away directory with a ``package.json`` file. Cleans up after itself.
*
Expand All @@ -364,7 +414,7 @@ const TEST_DEP_DEP_ASSEMBLY: spec.Assembly = {
*/
function _withTestProject<T>(
cb: (projectRoot: string) => T,
gremlin?: (packageInfo: any) => void,
gremlin?: (packageInfo: HalfFilledPackageJson) => void,
compressAssembly = false,
): T {
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), path.basename(__filename)));
Expand Down Expand Up @@ -430,3 +480,5 @@ function _stripUndefined(obj: { [key: string]: any } | undefined): { [key: strin
}
return obj;
}

type DeepWriteable<T> = T extends object ? { -readonly [P in keyof T]: DeepWriteable<T[P]> } : T;