Skip to content

Commit b6c7c7e

Browse files
author
Elad Ben-Israel
committed
feat: make this a real thang
1 parent 9604091 commit b6c7c7e

12 files changed

Lines changed: 678 additions & 100 deletions

File tree

.projenrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const project = new TypeScriptLibraryProject({
88
authorName: 'Elad Ben-Israel',
99
authorEmail: 'benisrae@amazon.com',
1010
stability: 'experimental',
11+
bin: {
12+
'jsii-srcmak': 'bin/jsii-srcmak'
13+
},
1114
devDependencies: {
1215
'@types/node': Semver.caret('13.9.8'),
1316
'@types/fs-extra': Semver.caret('9.0.1'),

README.md

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,127 @@
11
# jsii-srcmak
22

3-
> Generates jsii source files for multiple languages from TypeScript.
3+
> Generates [jsii] source files for multiple languages from TypeScript.
4+
5+
[jsii]: https://github.com/aws/jsii
46

57
## Usage
68

7-
Say I have a TypeScript file `source/index.ts`:
9+
This package can be either used as a library or through a CLI.
10+
11+
The library entry point is the `srcmak` function:
812

913
```ts
10-
export interface Operands {
11-
readonly lhs: number;
12-
readonly rhs: number;
13-
}
14-
15-
export class Calc {
16-
public add(ops: Operands): number {
17-
return ops.lhs + ops.rhs;
18-
}
14+
import { srcmak } from 'jsii-srcmak';
15+
await srcmak(srcdir[, options]);
16+
```
17+
18+
The CLI is `jsii-srcmak`:
19+
20+
```bash
21+
$ jsii-srcmak srcdir [OPTIONS]
22+
```
23+
24+
The `srcdir` argument points to a directory tree that includes TypeScript files
25+
which will be translated through jsii to one of the supported languages.
26+
27+
### Compile only
28+
29+
If called with no additional arguments, `srcmak` will only jsii-compile the source. If compilation fails, it will throw an error. This is a nice way to check if generated typescript code is jsii-compatible:
30+
31+
```ts
32+
const srcdir = generateSomeTypeScriptCode();
33+
34+
// verify it is jsii-compatible (throws otherwise)
35+
await srcmak(srcdir);
36+
```
37+
38+
CLI:
39+
40+
```bash
41+
$ jsii-srcmak /source/directory
42+
```
1943

20-
public mul(ops: Operands): number {
21-
return ops.lhs * opts.rhs;
44+
### Python Output
45+
46+
To produce a Python module from your source, use the `python` option:
47+
48+
```ts
49+
await srcmak('srcdir', {
50+
python: {
51+
outdir: '/path/to/project/root',
52+
moduleName: 'name.of.python.module'
2253
}
23-
}
54+
});
55+
```
56+
57+
Or the `--python-*` switches in the CLI:
58+
59+
```bash
60+
$ jsii-srcmak /src/dir --python-outdir=dir --python-module-name=module.name
2461
```
2562

26-
The following invocation will generate `target/my_python_module` with `Calc` in python:
63+
* The `outdir`/`--python-outdir` option points to the root directory of your Python project.
64+
* The `moduleName`/`--python-module-name` option is the python module name. Dots (`.`) delimit submodules.
65+
66+
The output directory will include a python module that corresponds to the
67+
original module. This code depends on the following python modules:
68+
69+
- [jsii](https://pypi.org/project/jsii/)
70+
- [publication](https://pypi.org/project/publication/)
71+
72+
### Entrypoint
73+
74+
The `entrypoint` option can be used to customize the name of the typescript entrypoint (default is `index.ts`).
75+
76+
For example, if the code's entry point is under `/srcdir/foobar/lib/index.ts` then I can specify:
2777

2878
```ts
29-
import { srcmak } from 'jsii-srcmak';
79+
await srcmak('/srcdir', {
80+
entrypoint: 'foobar/lib/index.ts'
81+
});
82+
```
83+
84+
Or through the CLI:
85+
86+
```bash
87+
$ jsii-srcmak /srcdir --entrypoint lib/main.ts
88+
```
3089

31-
await srcmak('source', 'target', {
32-
pythonName: 'my_python_module'
90+
### Dependencies
91+
92+
The `deps` option can be used to specify a list of node module **directories** (must have a `package.json` file) which will be symlinked into the workspace when compiling your code.
93+
94+
This is required if your code references types from other modules.
95+
96+
Use this idiom to resolve a set of modules directories from the calling process:
97+
98+
```ts
99+
const modules = [
100+
'@types/node', // commonly needed
101+
'foobar' // a node module in *my* closure
102+
];
103+
104+
const getModuleDir = m =>
105+
path.dirname(require.resolve(`${m}/package.json`));
106+
107+
await srcmak('srcdir', {
108+
deps: modules.map(getModuleDir)
33109
});
34110
```
35111

112+
Or through the CLI:
113+
114+
```bash
115+
$ jsii-srcmak /src/dir --dep node_modules/@types/node --dep node_modules/constructs
116+
```
117+
118+
## What's with this name?
119+
120+
It's a silly little pun that stems from another pun: jsii has `jsii-pacmak`
121+
which stands for "package maker". That's the tool that takes in a .jsii manifest
122+
and produces language-idiomatic *packages* from it. This tool produces *sources*
123+
from a .jsii manifest. Hence, "source maker". Yeah, it's lame.
124+
36125
## License
37126

38127
Distributed under the [Apache 2.0](./LICENSE) license.

bin/jsii-srcmak

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
require('./jsii-srcmak.js');

bin/jsii-srcmak.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { srcmak } from '../lib';
2+
import * as yargs from 'yargs';
3+
4+
async function main() {
5+
const args = yargs
6+
.usage('$0 SRCDIR [OPTIONS]')
7+
.option('entrypoint', { desc: 'typescript entrypoint (relative to SRCDIR)', default: 'index.ts' })
8+
.option('dep', { desc: 'node module directories to include in compilation', type: 'array', string: true })
9+
.option('jsii-path', { desc: 'write .jsii output to this path', type: 'string' })
10+
.option('python-outdir', { desc: 'python output directory (requires --python-module-name)', type: 'string' })
11+
.option('python-module-name', { desc: 'python module name', type: 'string' })
12+
.showHelpOnFail(true)
13+
.help();
14+
15+
const argv = args.argv;
16+
17+
if (argv._.length !== 1) {
18+
args.showHelp();
19+
console.error();
20+
console.error('Invalid number of arguments. expecting a single positional argument.');
21+
process.exit(1);
22+
}
23+
24+
const srcdir = argv._[0];
25+
await srcmak(srcdir, {
26+
entrypoint: argv.entrypoint,
27+
...parseDepOption(),
28+
...parseJsiiOptions(),
29+
...parsePythonOptions(),
30+
});
31+
32+
function parseJsiiOptions() {
33+
const jsiiPath = argv['jsii-path'];
34+
if (!jsiiPath) { return undefined; }
35+
return {
36+
jsii: {
37+
path: jsiiPath,
38+
},
39+
}
40+
}
41+
42+
function parsePythonOptions() {
43+
const outdir = argv['python-outdir'];
44+
const moduleName = argv['python-module-name'];
45+
if (!outdir && !moduleName) { return undefined; }
46+
if (!outdir) { throw new Error('--python-outdir is required if --python-module-name is specified'); }
47+
if (!moduleName) { throw new Error('--python-module-name is required if --python-outdir is specified'); }
48+
return {
49+
python: {
50+
outdir: outdir,
51+
moduleName: moduleName,
52+
},
53+
}
54+
}
55+
56+
function parseDepOption() {
57+
if (argv.dep?.length === 0) { return undefined; }
58+
return {
59+
deps: argv.dep,
60+
};
61+
}
62+
}
63+
64+
main().catch((e: Error) => {
65+
console.error(e.stack);
66+
process.exit(1);
67+
});
68+

example/lib/main.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Math operands
3+
*/
4+
export interface Operands {
5+
/**
6+
* Left-hand side operand
7+
*/
8+
readonly lhs: number;
9+
10+
/**
11+
* Right-hand side operand
12+
*/
13+
readonly rhs: number;
14+
}
15+
16+
/**
17+
* A sophisticaed multi-language calculator
18+
*/
19+
export class Calculator {
20+
/**
21+
* Adds the two operands
22+
* @param ops operands
23+
*/
24+
public add(ops: Operands) {
25+
return ops.lhs + ops.rhs;
26+
}
27+
28+
/**
29+
* Subtracts the two operands
30+
* @param ops operands
31+
*/
32+
public sub(ops: Operands) {
33+
return ops.lhs - ops.rhs;
34+
}
35+
36+
/**
37+
* Multiplies the two operands
38+
* @param ops operands
39+
*/
40+
public mul(ops: Operands) {
41+
return ops.lhs * ops.rhs
42+
}
43+
}

lib/compile.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ export async function compile(workdir: string, options: Options) {
1616
throw new Error(`jsii entrypoint must be a .ts file: ${entrypoint}`);
1717
}
1818

19+
if (!(await fs.pathExists(path.join(workdir, entrypoint)))) {
20+
throw new Error(`unable to find typescript entrypoint: ${path.join(workdir, entrypoint)}`);
21+
}
22+
1923
// path to entrypoint without extension
2024
const basepath = path.join(path.dirname(entrypoint), path.basename(entrypoint, '.ts'));
2125

2226
// jsii modules to include
23-
const moduleDirs = options.moduleDirs ?? [];
27+
const moduleDirs = options.deps ?? [];
2428

2529
const targets: Record<string, any> = { };
2630

@@ -59,14 +63,13 @@ export async function compile(workdir: string, options: Options) {
5963
peerDependencies: deps,
6064
};
6165

62-
if (options.pythonName) {
66+
if (options.python) {
6367
targets.python = {
6468
distName: 'generated',
65-
module: options.pythonName,
69+
module: options.python.moduleName,
6670
};
6771
}
6872

69-
7073
await fs.writeFile(path.join(workdir, 'package.json'), JSON.stringify(pkg, undefined, 2));
7174

7275
await exec(compilerModule, args, {

lib/options.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface CommonOptions {
1+
export interface Options {
22
/**
33
* The relative path of the .ts entrypoint within the source directory.
44
* @default "index.ts"
@@ -10,20 +10,36 @@ export interface CommonOptions {
1010
* package. For example, if your generated code references some library, you
1111
* should include it's module directory in here.
1212
*/
13-
moduleDirs?: string[];
13+
deps?: string[];
1414

1515
/**
16-
* Path to output the .jsii file output.
17-
* @default - jsii file is not emitted.
16+
* Save .jsii file to an output location.
17+
* @default - jsii manifest is omitted.
1818
*/
19-
outputJsii?: string;
19+
jsii?: JsiiOutputOptions;
20+
21+
/**
22+
* Produce python code.
23+
* @default - python is not generated
24+
*/
25+
python?: PythonOutputOptions;
2026
}
2127

22-
export interface PythonOptions extends CommonOptions {
28+
export interface JsiiOutputOptions {
2329
/**
24-
* The name of the the python module to generate. If omitted python will not be generated.
30+
* Path to save the .jsii output to.
2531
*/
26-
pythonName?: string;
32+
path: string;
2733
}
2834

29-
export type Options = PythonOptions; /* | JavaOptions | DotNetOptions */
35+
export interface PythonOutputOptions {
36+
/**
37+
* Base root directory.
38+
*/
39+
outdir: string;
40+
41+
/**
42+
* The name of the the python module to generate.
43+
*/
44+
moduleName: string;
45+
}

0 commit comments

Comments
 (0)