diff --git a/README.md b/README.md index 9877c7899f..e6e94c7b18 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,7 @@ __jsii__ allows code in any language to naturally interact with JavaScript classes. -> NOTE: Due to performance of the hosted JavaScript engine and marshaling costs, -__jsii__ modules are likely to be used for development and build tools, as -oppose to performance-sensitive runtime behavior. - -For example: +For example, consider the following TypeScript class: ```ts export class HelloJsii { @@ -18,10 +14,19 @@ export class HelloJsii { } ``` -Now, we can use this class from Java: +By compiling our source module using __jsii__, we can now package it as modules +in one of the supported target languages. Each target module has the exact same +API as the source. This allows users of that target language to use `HelloJsii` +like any other class. + +> NOTE: Due to performance of the hosted JavaScript engine and marshaling costs, +__jsii__ modules are likely to be used for development and build tools, as +oppose to performance-sensitive runtime behavior. + +From Java: ```java -const hello = new HelloJsii(); +HelloJsii hello = new HelloJsii(); hello.sayHello("World"); // => Hello, World! ``` @@ -46,17 +51,32 @@ hello = HelloJsii.new hello.say_hello 'World' # => Hello, World! ``` -# Getting Started +[Here's](#what-kind-of-sorcery-is-this) how it works. + +## Getting Started Let's create our first jsii TypeScript module. +### Initialize the project + Define a `package.json`: ```json { "name": "hello-jsii", - "main": "index.js", - "types": "index.d.ts", + "version": "1.0.0", + "license": "Apache-2.0", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "jsii", + "watch": "jsii -w", + "package": "jsii-pacmak -v" + }, + "devDependencies": { + "jsii": "^0.6.0", + "jsii-pacmak": "^0.6.0" + }, "jsii": { "outdir": "dist", "targets": { @@ -72,18 +92,232 @@ Define a `package.json`: }, "sphinx": { } } + } +} +``` + +What's going on here? + +#### `jsii` + +The `jsii` section in your `package.json` is the [jsii +configuration](#configuration) for your module. It tells jsii which target +languages to package, and includes additional required information for the +jsii packager (also known as "pacmak"). + +#### `npm run build`` + +The `build` script script uses `jsii` to compile your code. + +`jsii` wraps the TypeScript compiler (`tsc`) and will compile your .ts files into .js files. + +Note that `jsii` is rather opinionated about how the TypeScript compiler is configured (for now), +so it will generate a `tsconfig.json` file for you with the correct options so you an spin up +a TypeScript-supporting IDE. No need to check-in this file. + +The `watch` script (optional) will invoke `tsc -w` which will monitor your +filesystem for changes and recompile your .ts files to .js (note that jsii +errors will not be reported in this mode) + +#### `npm run package` + +The `package` script invokes `jsii-pacmak`, which is the __jsii packager__. + +Pacmak will will generate _and compile_ your package to all target languages. The output packages +will be emitted to `outdir` (in the above case `dist`). + +The `-v` switch tells pacmak to print some messages. + +Run `jsii-pacmak --help` for more options. + +#### Required package.json fields + +* `license` is required (with SPDX license identifier). +* `main` and `types` are also required and specify the javascript and typescript + declarations entry points of the module. +* `version` is required. + +### Module Code + +Okay, we are ready to write some code. Create a `lib/index.ts` file: + +```ts +export class HelloJsii { + public sayHello(name: string) { + return `Hello, ${name}!`; + } +} +``` + +### Build + +And build your module: + +```console +$ npm run build +``` + +If build succeeds, you will see the resulting .js file (`lib/index.js`) produced by the +TypeScript compiler. + +You should also see a `.jsii` file in the root: + +```json +{ + "fingerprint": "HB39Oy4HWtsnwdRnAFYl+qlmy8Z2tmaGM2KDDe9/hHo=", + "license": "Apache-2.0", + "name": "hello-jsii", + "schema": "jsii/1.0", + "targets": { + "dotnet": { + "namespace": "Acme.Hello" + }, + "java": { + "maven": { + "artifactId": "hello-jsii", + "groupId": "com.acme.hello" + }, + "package": "com.acme.hello" + }, + "js": { + "npm": "hello-jsii" + } }, - "scripts": { - "build": "jsii", - "pacmak": "jsii-pacmak" + "types": { + "hello-jsii.HelloJsii": { + "assembly": "hello-jsii", + "fqn": "hello-jsii.HelloJsii", + "initializer": { + "initializer": true + }, + "kind": "class", + "methods": [ + { + "name": "sayHello", + "parameters": [ + { + "name": "name", + "type": { + "primitive": "string" + } + } + ], + "returns": { + "primitive": "string" + } + } + ], + "name": "HelloJsii", + "namespace": "hello-jsii" + } }, - "devDependencies": { - "jsii": "^0.5.0-beta", - "@jsii/pacmak": "^0.5.0-beta" + "version": "1.0.0" +} +``` + +This file includes all the information needed in order to package your module into every +jsii-supported language. It contains the module metadata from `package.json` and a full declaration +of your module's public API. + +### Packaging + +Okay, now the magic happens: + +```console +$ npm run package +[jsii-pacmak] [INFO] Building hello-jsii (java,dotnet,sphinx,npm) into dist +``` + +Now, if you check out the contents of `dist`, you'll find: + +``` +├── dotnet +│   └── Acme.Hello +│   ├── Acme.Hello.csproj +│   ├── AssemblyInfo.cs +│   ├── HelloJsii.cs +│   └── hello-jsii-1.0.0.tgz +├── java +│   └── com +│   └── acme +│   └── hello +│   └── hello-jsii +│   ├── 1.0.0 +│   │   ├── hello-jsii-1.0.0-javadoc.jar +│   │   ├── hello-jsii-1.0.0-javadoc.jar.md5 +│   │   ├── hello-jsii-1.0.0-javadoc.jar.sha1 +│   │   ├── hello-jsii-1.0.0-sources.jar +│   │   ├── hello-jsii-1.0.0-sources.jar.md5 +│   │   ├── hello-jsii-1.0.0-sources.jar.sha1 +│   │   ├── hello-jsii-1.0.0.jar +│   │   ├── hello-jsii-1.0.0.jar.md5 +│   │   ├── hello-jsii-1.0.0.jar.sha1 +│   │   ├── hello-jsii-1.0.0.pom +│   │   ├── hello-jsii-1.0.0.pom.md5 +│   │   └── hello-jsii-1.0.0.pom.sha1 +│   ├── maven-metadata.xml +│   ├── maven-metadata.xml.md5 +│   └── maven-metadata.xml.sha1 +├── js +│   └── hello-jsii@1.0.0.jsii.tgz +└── sphinx + └── hello-jsii.rst +``` + +These files are ready-to-publish artifacts for each target language. You can +see the npm tarball under `js`, the Maven repo under `java`, the Sphinx .rst file +under `sphinx`, etc. + +That's it. You are ready to rock! + +## Configuration + +jsii configuration is read from the module's `package.json` and includes the following options: + + * `targets` - the list of target languages this module will be packaged for. For each + target, you would need to specify some naming information such as namespaces, package manager + coordinates, etc. See [supported targets](#targets) for details. + * `outdir` - the default output directory (relative to package root) for + __jsii-pacmak__. This is where target artifacts are emitted during packaging. Each artifact + will be emitted under `/` (e.g. `dist/java`, `dist/js`, etc). + +### Targets + +The following targets are currently supported: + + * `js` - implicit - every module will always have a "js" target (dah!). + * `java` - packages the module as in Java/Maven package. Requires the following config: + +```json +{ + "java": { + "package": "com.acme.hello", + "maven": { + "groupId": "com.acme.hello", + "artifactId": "hello-jsii" + } } } ``` +* `dotnet` - packages the module as a .NET/NuGet package. Requires the following config: + +```json +{ + "dotnet": { + "namespace": "Acme.Hello" + } +} +``` + +* `sphinx` - produces sphinx documentation for the module. No config is required, but an empty + entry will be needed in order to package this target: + +```json +{ + "sphinx": { } +} +``` ## Features @@ -245,7 +479,7 @@ The script will do the following: 3. Create a single .zip file under `dist/jsii-.zip` with all the tarballs. 4. TODO: release to GitHub. -# Language Support +# Adding Target Languages jsii Language support consists of: @@ -281,7 +515,7 @@ library artifacts in a way that they can consumed locally. ## License -Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -See [LICENSE](./LICENSE.md) file for license terms. +__jsii__ is distributed under the +[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). +See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for more information. \ No newline at end of file diff --git a/packages/jsii-pacmak/bin/jsii-pacmak.ts b/packages/jsii-pacmak/bin/jsii-pacmak.ts index e717b99993..ccd9bfe64d 100644 --- a/packages/jsii-pacmak/bin/jsii-pacmak.ts +++ b/packages/jsii-pacmak/bin/jsii-pacmak.ts @@ -8,6 +8,7 @@ import logging = require('../lib/logging'); import { Target } from '../lib/target'; import { resolveDependencyDirectory, shell } from '../lib/util'; import { VERSION } from '../lib/version'; +import { SPEC_FILE_NAME } from '../node_modules/jsii-spec'; (async function main() { const targetConstructors = await Target.findAll(); @@ -64,6 +65,11 @@ import { VERSION } from '../lib/version'; desc: 'clean up temporary files upon success (use --no-clean to disable)', default: true, }) + .option('npmignore', { + type: 'boolean', + desc: 'Auto-update .npmignore to exclude the output directory and include the .jsii file', + default: true + }) .version(VERSION) .argv; @@ -105,10 +111,16 @@ import { VERSION } from '../lib/version'; logging.info(`Building ${pkg.name} (${targets.join(',')}) into ${path.relative(process.cwd(), outDir)}`); - // if outdir is coming from package.json, verify it is excluded by .npmignore. if it is explicitly - // defined via --out, don't perform this verification. - const npmIgnoreExclude = argv.outdir ? undefined : outDir; - const tarball = await npmPack(packageDir, npmIgnoreExclude); + if (argv.npmignore) { + // if outdir is coming from package.json, verify it is excluded by .npmignore. if it is explicitly + // defined via --out, don't perform this verification. + const npmIgnoreExclude = argv.outdir ? undefined : outDir; + + // updates .npmignore to exclude the output directory and include the .jsii file + await updateNpmIgnore(packageDir, npmIgnoreExclude); + } + + const tarball = await npmPack(packageDir); try { for (const targetName of targets) { // if we are targeting a single language, output to outdir, otherwise outdir/ @@ -161,22 +173,7 @@ import { VERSION } from '../lib/version'; process.exit(1); }); -async function npmPack(packageDir: string, excludeOutDir?: string): Promise { - // if excludeOutdir is defined, verify that it is excluded by .npmignore - if (excludeOutDir) { - const npmIgnorePath = path.join(packageDir, '.npmignore'); - const npmIgnoreLine = path.relative(packageDir, excludeOutDir); - let outDirIgnored = false; - if (await fs.pathExists(npmIgnorePath)) { - const contents = (await fs.readFile(npmIgnorePath)).toString().split('\n'); - outDirIgnored = contents.indexOf(npmIgnoreLine) !== -1; - } - - if (!outDirIgnored) { - throw new Error(`${npmIgnorePath} is expected to include the jsii output directory "${npmIgnoreLine}"`); - } - } - +async function npmPack(packageDir: string): Promise { logging.debug(`Running "npm pack" in ${packageDir}`); const args = [ 'pack' ]; if (logging.level >= logging.LEVEL_VERBOSE) { @@ -185,3 +182,53 @@ async function npmPack(packageDir: string, excludeOutDir?: string): Promise(); + let modified = false; + if (await fs.pathExists(npmIgnorePath)) { + lines = (await fs.readFile(npmIgnorePath)).toString().split('\n'); + } + + // if this is a fresh .npmignore, we can be a bit more opinionated + // otherwise, we add just add stuff that's critical + if (lines.length === 0) { + excludePattern('Exclude typescript source and config', '*.ts', 'tsconfig.json'); + includePattern('Include javascript files and typescript declarations', '*.js', '*.d.ts'); + } + + if (excludeOutdir) { + excludePattern('Exclude jsii outdir', path.relative(packageDir, excludeOutdir)); + } + + includePattern('Include .jsii', SPEC_FILE_NAME); + + if (modified) { + await fs.writeFile(npmIgnorePath, lines.join('\n') + '\n'); + logging.info('Updated .npmignre'); + } + + function includePattern(comment: string, ...patterns: string[]) { + excludePattern(comment, ...patterns.map(p => `!${p}`)); + } + + function excludePattern(comment: string, ...patterns: string[]) { + let first = true; + for (const pattern of patterns) { + if (lines.indexOf(pattern) !== -1) { + return; // already in .npmignore + } + + modified = true; + + if (first) { + lines.push(''); + lines.push(`# ${comment}`); + first = false; + } + + lines.push(pattern); + } + } +} \ No newline at end of file