Skip to content

Commit 158e2dd

Browse files
author
Elad Ben-Israel
committed
feat: initial version
1 parent f34eddd commit 158e2dd

12 files changed

Lines changed: 4128 additions & 41 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ jspm_packages/
4646
# exclude typescript compiler outputs
4747
*.d.ts
4848
*.js
49+
# synthesized by projen
50+
/.eslintrc.json
4951
!# synthesized by projen, but committed to git
5052
!/LICENSE
5153
!/.projenrc.js

.projenrc.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { TypeScriptLibraryProject, Semver } = require('projen');
1+
const { TypeScriptLibraryProject, Semver, Jest, Eslint } = require('projen');
22

33
const project = new TypeScriptLibraryProject({
44
name: 'jsii-srcmak',
@@ -9,13 +9,18 @@ const project = new TypeScriptLibraryProject({
99
authorEmail: 'benisrae@amazon.com',
1010
stability: 'experimental',
1111
devDependencies: {
12-
'@types/node': Semver.caret('10.0.0')
12+
'@types/node': Semver.caret('13.9.8'),
13+
'@types/fs-extra': Semver.caret('9.0.1'),
1314
},
1415
dependencies: {
1516
'jsii': Semver.pinned('1.1.0'),
1617
'jsii-pacmak': Semver.pinned('1.1.0'),
18+
'fs-extra': Semver.caret('9.0.0')
1719
},
1820
releaseToNpm: true
1921
});
2022

23+
new Jest(project);
24+
new Eslint(project);
25+
2126
project.synth();

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# jsii-srcmak
2+
3+
> Generates jsii source files for multiple languages from TypeScript.
4+
5+
## Usage
6+
7+
Say I have a TypeScript file `source/index.ts`:
8+
9+
```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+
}
19+
20+
public mul(ops: Operands): number {
21+
return ops.lhs * opts.rhs;
22+
}
23+
}
24+
```
25+
26+
The following invocation will generate `target/my_python_module` with `Calc` in python:
27+
28+
```ts
29+
import { srcmak } from 'jsii-srcmak';
30+
31+
await srcmak('source', 'target', {
32+
pythonName: 'my_python_module'
33+
});
34+
```
35+
36+
## License
37+
38+
Distributed under the [Apache 2.0](./LICENSE) license.

lib/compile.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import { exec } from './util';
4+
import { Options } from './options';
5+
6+
const compilerModule = require.resolve('jsii/bin/jsii');
7+
8+
/**
9+
* Compiles the source files in `workdir` with jsii.
10+
*/
11+
export async function compile(workdir: string, options: Options) {
12+
const args = [ '--silence-warnings', 'reserved-word' ];
13+
const entrypoint = options.entrypoint ?? 'index.ts';
14+
15+
if (path.extname(entrypoint) !== '.ts') {
16+
throw new Error(`jsii entrypoint must be a .ts file: ${entrypoint}`);
17+
}
18+
19+
// path to entrypoint without extension
20+
const basepath = path.join(path.dirname(entrypoint), path.basename(entrypoint, '.ts'));
21+
22+
// jsii modules to include
23+
const modules = options.modules ?? [];
24+
25+
const targets: Record<string, any> = { };
26+
27+
const deps: Record<string, string> = { };
28+
for (const mod of modules) {
29+
if (mod.startsWith('@types/')) {
30+
continue;
31+
}
32+
33+
deps[mod] = '*';
34+
}
35+
36+
const pkg = {
37+
name: 'generated',
38+
version: '0.0.0',
39+
author: 'generated@generated.com',
40+
main: `${basepath}.js`,
41+
types: `${basepath}.d.ts`,
42+
license: 'Apache-2.0',
43+
repository: { url: 'http://generated', type: 'git' },
44+
jsii: {
45+
outdir: 'dist',
46+
targets: targets,
47+
},
48+
dependencies: deps,
49+
peerDependencies: deps,
50+
};
51+
52+
if (options.pythonName) {
53+
targets.python = {
54+
distName: 'generated',
55+
module: options.pythonName,
56+
};
57+
}
58+
59+
for (const mod of modules) {
60+
const sourcedir = path.dirname(require.resolve(`${mod}/package.json`));
61+
await fs.mkdirp(path.join(workdir, path.join('node_modules', path.dirname(mod))));
62+
await fs.ensureSymlink(sourcedir, path.join(workdir, 'node_modules', mod));
63+
}
64+
65+
await fs.writeFile(path.join(workdir, 'package.json'), JSON.stringify(pkg, undefined, 2));
66+
67+
await exec(compilerModule, args, {
68+
cwd: workdir,
69+
});
70+
}

lib/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
console.log('hi');
1+
export * from './srcmak';
2+
export * from './compile';

lib/options.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface Options {
2+
/**
3+
* The relative path of the .ts entrypoint within the source directory.
4+
* @default "index.ts"
5+
*/
6+
entrypoint?: string;
7+
8+
/**
9+
* The name of the the python module to generate. If omitted python will not be generated.
10+
*/
11+
pythonName?: string;
12+
13+
/**
14+
* List of module names to compile against. These modules must be resolvable
15+
* against the current executable and their version will be the same version.
16+
*/
17+
modules?: string[];
18+
}

lib/srcmak.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import { withTempDir, exec } from './util';
4+
import { compile } from './compile';
5+
import { Options } from './options';
6+
7+
const pacmakModule = require.resolve('jsii-pacmak/bin/jsii-pacmak');
8+
9+
export async function srcmak(srcdir: string, targetdir: string, options: Options) {
10+
srcdir = path.resolve(srcdir);
11+
targetdir = path.resolve(targetdir);
12+
13+
await withTempDir('jsii-codemak', async () => {
14+
// copy sources to temp directory
15+
await fs.copy(srcdir, '.');
16+
17+
// perform jsii compilation
18+
await compile('.', options);
19+
20+
// run pacmak to generate code
21+
await exec(pacmakModule, [ '--code-only' ]);
22+
23+
// extract code based on selected languages
24+
if (options.pythonName) {
25+
const reldir = options.pythonName.replace(/\./g, '/'); // replace "." with "/"
26+
const source = path.resolve(`dist/python/src/${reldir}`);
27+
const target = path.join(targetdir, reldir);
28+
await fs.move(source, target, { overwrite: true });
29+
}
30+
});
31+
}

lib/util.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import * as os from 'os';
4+
import { spawn, SpawnOptions } from 'child_process';
5+
6+
export async function withTempDir(dirname: string, closure: (dir: string) => Promise<void>) {
7+
const prevdir = process.cwd();
8+
const parent = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk8s.'));
9+
const workdir = path.join(parent, dirname);
10+
await fs.mkdirp(workdir);
11+
try {
12+
process.chdir(workdir);
13+
await closure(workdir);
14+
15+
if (!process.env.WITH_TEMP_DIR_RETAIN) {
16+
await fs.remove(parent);
17+
} else {
18+
console.error(`retained temp dir: ${parent}`);
19+
}
20+
} catch(e) {
21+
console.error(`retained temp dir due to an error: ${parent}`);
22+
throw e;
23+
} finally {
24+
process.chdir(prevdir);
25+
}
26+
}
27+
28+
export async function exec(moduleName: string, args: string[] = [], options: SpawnOptions = { }) {
29+
return new Promise((ok, fail) => {
30+
31+
const opts: SpawnOptions = {
32+
...options,
33+
stdio: [ 'inherit', 'pipe', 'pipe' ],
34+
};
35+
const child = spawn(process.execPath, [ moduleName, ...args ], opts);
36+
37+
const data = new Array<Buffer>();
38+
child.stdout?.on('data', chunk => data.push(chunk));
39+
child.stderr?.on('data', chunk => data.push(chunk));
40+
41+
const newError = (message: string) => new Error([
42+
message,
43+
`COMMAND: ${moduleName} ${args.join(' ')}`,
44+
`WORKDIR: ${path.resolve(options.cwd ?? '.')}`,
45+
'------------------------------------------------------------------------------------',
46+
Buffer.concat(data).toString('utf-8'),
47+
'------------------------------------------------------------------------------------',
48+
].join('\n'));
49+
50+
child.once('error', err => {
51+
throw newError(`jsii compilation failed. error: ${err.message}`);
52+
});
53+
54+
child.once('exit', code => {
55+
if (code === 0) {
56+
return ok();
57+
}
58+
else {
59+
return fail(newError(`jsii compilation failed with non-zero exit code: ${code}`));
60+
}
61+
});
62+
});
63+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`python + different entrypoint + submodule 1`] = `
4+
Object {
5+
"my_python_module/submodule/__init__.py": "import abc
6+
import builtins
7+
import datetime
8+
import enum
9+
import typing
10+
11+
import jsii
12+
import jsii.compat
13+
import publication
14+
15+
__jsii_assembly__ = jsii.JSIIAssembly.load(\\"generated\\", \\"0.0.0\\", __name__, \\"generated@0.0.0.jsii.tgz\\")
16+
17+
18+
class Hello(metaclass=jsii.JSIIMeta, jsii_type=\\"generated.Hello\\"):
19+
def __init__(self) -> None:
20+
jsii.create(Hello, self, [])
21+
22+
@jsii.member(jsii_name=\\"add\\")
23+
def add(self, *, lhs: jsii.Number, rhs: jsii.Number) -> jsii.Number:
24+
\\"\\"\\"
25+
:param lhs: -
26+
:param rhs: -
27+
\\"\\"\\"
28+
ops = Operands(lhs=lhs, rhs=rhs)
29+
30+
return jsii.invoke(self, \\"add\\", [ops])
31+
32+
33+
@jsii.data_type(jsii_type=\\"generated.Operands\\", jsii_struct_bases=[], name_mapping={'lhs': 'lhs', 'rhs': 'rhs'})
34+
class Operands():
35+
def __init__(self, *, lhs: jsii.Number, rhs: jsii.Number):
36+
\\"\\"\\"
37+
:param lhs: -
38+
:param rhs: -
39+
\\"\\"\\"
40+
self._values = {
41+
'lhs': lhs,
42+
'rhs': rhs,
43+
}
44+
45+
@builtins.property
46+
def lhs(self) -> jsii.Number:
47+
return self._values.get('lhs')
48+
49+
@builtins.property
50+
def rhs(self) -> jsii.Number:
51+
return self._values.get('rhs')
52+
53+
def __eq__(self, rhs) -> bool:
54+
return isinstance(rhs, self.__class__) and rhs._values == self._values
55+
56+
def __ne__(self, rhs) -> bool:
57+
return not (rhs == self)
58+
59+
def __repr__(self) -> str:
60+
return 'Operands(%s)' % ', '.join(k + '=' + repr(v) for k, v in self._values.items())
61+
62+
63+
__all__ = [\\"Hello\\", \\"Operands\\", \\"__jsii_assembly__\\"]
64+
65+
publication.publish()
66+
",
67+
"my_python_module/submodule/_jsii/__init__.py": "import abc
68+
import builtins
69+
import datetime
70+
import enum
71+
import typing
72+
73+
import jsii
74+
import jsii.compat
75+
import publication
76+
__all__ = []
77+
78+
publication.publish()
79+
",
80+
"my_python_module/submodule/py.typed": "
81+
",
82+
}
83+
`;

0 commit comments

Comments
 (0)