diff --git a/README.md b/README.md index 17154436..3157dfa4 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,73 @@ The output directory will include a python module that corresponds to the original module. This code depends on the following python modules: - [jsii](https://pypi.org/project/jsii/) -- [publication](https://pypi.org/project/publication/) + +### Java Output + +To produce a Java module from your source, use the `java` option: + +```ts +await srcmak('srcdir', { + java: { + outdir: '/path/to/project/root', + package: 'hello.world' + } +}); +``` + +Or the `--java-*` switches in the CLI: + +```bash +$ jsii-srcmak /src/dir --java-outdir=dir --java-package=hello.world +``` + +* The `outdir`/`--java-outdir` option points to the root directory of your Java project. +* The `package`/`--java-package` option is the java package name. + +The output directory will include a java module that corresponds to the +original module. This code depends on the following maven package (should be defined directly or indirectly in the project's `pom.xml` file): + +- [jsii](https://mvnrepository.com/artifact/software.amazon.jsii) + +The output directory will also include a tarbell `generated@0.0.0.jsii.tgz` that must be bundled in your project. Here is example snippet of how your `pom.xml` can take a dependency on these sources: + +1. Using the `build-helper-maven-plugin` generate source files for your import. + +```xml + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate-sources + + add-source + + + + imports/src/main/k8s/main/java + + + + + +``` + +2. Set additional classpath elements for your import. + +```xml + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + imports/src/main/k8s/main/java + + + +``` ### Entrypoint @@ -115,6 +181,21 @@ Or through the CLI: $ jsii-srcmak /src/dir --dep node_modules/@types/node --dep node_modules/constructs ``` +## Contributing + +To build this project, you must first generate the `package.json`: + +``` +npx projen +``` + +Then you can install your dependencies and build: + +``` +yarn install +yarn build +``` + ## What's with this name? It's a silly little pun that stems from another pun: jsii has `jsii-pacmak` diff --git a/bin/jsii-srcmak.ts b/bin/jsii-srcmak.ts index b385ec57..862da79b 100644 --- a/bin/jsii-srcmak.ts +++ b/bin/jsii-srcmak.ts @@ -9,6 +9,8 @@ async function main() { .option('jsii-path', { desc: 'write .jsii output to this path', type: 'string' }) .option('python-outdir', { desc: 'python output directory (requires --python-module-name)', type: 'string' }) .option('python-module-name', { desc: 'python module name', type: 'string' }) + .option('java-outdir', { desc: 'java output directory (requires --java-package)', type: 'string' }) + .option('java-package', { desc: 'the java package (namespace) to use for all generated types', type: 'string' }) .showHelpOnFail(true) .help(); @@ -27,6 +29,7 @@ async function main() { ...parseDepOption(), ...parseJsiiOptions(), ...parsePythonOptions(), + ...parseJavaOptions(), }); function parseJsiiOptions() { @@ -53,6 +56,20 @@ async function main() { } } + function parseJavaOptions() { + const outdir = argv['java-outdir']; + const packageName = argv['java-package']; + if (!outdir && !packageName) { return undefined; } + if (!outdir) { throw new Error('--java-outdir is required'); } + if (!packageName) { throw new Error('--java-package is required'); } + return { + java: { + outdir: outdir, + package: packageName, + }, + } + } + function parseDepOption() { if (argv.dep?.length === 0) { return undefined; } return { diff --git a/lib/compile.ts b/lib/compile.ts index 57da1962..b5356c23 100644 --- a/lib/compile.ts +++ b/lib/compile.ts @@ -70,6 +70,16 @@ export async function compile(workdir: string, options: Options) { }; } + if (options.java) { + targets.java = { + package: options.java.package, + maven: { + groupId: 'generated', + artifactId: 'generated', + }, + }; + } + await fs.writeFile(path.join(workdir, 'package.json'), JSON.stringify(pkg, undefined, 2)); await exec(compilerModule, args, { diff --git a/lib/options.ts b/lib/options.ts index 30251bcf..2b39682a 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -23,6 +23,13 @@ export interface Options { * @default - python is not generated */ python?: PythonOutputOptions; + + /** + * Produces java code under src/main/yourpackage/main/java/yourpackage/ + * + * @default - java is not generated + */ + java?: JavaOutputOptions; } export interface JsiiOutputOptions { @@ -43,3 +50,15 @@ export interface PythonOutputOptions { */ moduleName: string; } + +export interface JavaOutputOptions { + /** + * Base root directory. + */ + outdir: string; + + /** + * The name of the java package to generate + */ + package: string; +} diff --git a/lib/srcmak.ts b/lib/srcmak.ts index 6d876f52..9402a426 100644 --- a/lib/srcmak.ts +++ b/lib/srcmak.ts @@ -33,5 +33,12 @@ export async function srcmak(srcdir: string, options: Options = { }) { const target = path.join(options.python.outdir, reldir); await fs.move(source, target, { overwrite: true }); } + + if (options.java) { + const reldir = options.java.package.replace(/\./g, '/'); + const source = path.resolve(path.join(workdir, 'dist/java/src/')); + const target = path.join(options.java.outdir, 'src/main', reldir); + await fs.move(source, target, { overwrite: true }); + } }); } diff --git a/test/__snapshots__/cli.test.js.snap b/test/__snapshots__/cli.test.js.snap index 236d5497..22f07d0d 100644 --- a/test/__snapshots__/cli.test.js.snap +++ b/test/__snapshots__/cli.test.js.snap @@ -1,5 +1,264 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`java output 1`] = ` +Object { + "src/main/mypackage/main/java/mypackage/$Module.java": "package mypackage; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.jsii.JsiiModule; + +public final class $Module extends JsiiModule { + private static final Map MODULE_TYPES = load(); + + private static Map load() { + final Map result = new HashMap<>(); + final ClassLoader cl = $Module.class.getClassLoader(); + try (final InputStream is = cl.getResourceAsStream(\\"mypackage/$Module.txt\\"); + final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8); + final BufferedReader br = new BufferedReader(rd)) { + br.lines() + .filter(line -> !line.trim().isEmpty()) + .forEach(line -> { + final String[] parts = line.split(\\"=\\", 2); + final String fqn = parts[0]; + final String className = parts[1]; + result.put(fqn, className); + }); + } + catch (final IOException exception) { + throw new UncheckedIOException(exception); + } + return result; + } + + private final Map> cache = new HashMap<>(); + + public $Module() { + super(\\"generated\\", \\"0.0.0\\", $Module.class, \\"generated@0.0.0.jsii.tgz\\"); + } + + @Override + protected Class resolveClass(final String fqn) throws ClassNotFoundException { + if (!MODULE_TYPES.containsKey(fqn)) { + throw new ClassNotFoundException(\\"Unknown JSII type: \\" + fqn); + } + return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass); + } + + private Class findClass(final String binaryName) { + try { + return Class.forName(binaryName); + } + catch (final ClassNotFoundException exception) { + throw new RuntimeException(exception); + } + } +} +", + "src/main/mypackage/main/java/mypackage/Calculator.java": "package mypackage; + +/** + * A sophisticaed multi-language calculator. + */ + +@software.amazon.jsii.Jsii(module = mypackage.$Module.class, fqn = \\"generated.Calculator\\") +public class Calculator extends software.amazon.jsii.JsiiObject { + + protected Calculator(final software.amazon.jsii.JsiiObjectRef objRef) { + super(objRef); + } + + protected Calculator(final software.amazon.jsii.JsiiObject.InitializationMode initializationMode) { + super(initializationMode); + } + + public Calculator() { + super(software.amazon.jsii.JsiiObject.InitializationMode.JSII); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + /** + * Adds the two operands. + *

+ * @param ops operands. This parameter is required. + */ + public @org.jetbrains.annotations.NotNull java.lang.Number add(final @org.jetbrains.annotations.NotNull mypackage.Operands ops) { + return this.jsiiCall(\\"add\\", java.lang.Number.class, new Object[] { java.util.Objects.requireNonNull(ops, \\"ops is required\\") }); + } + + /** + * Multiplies the two operands. + *

+ * @param ops operands. This parameter is required. + */ + public @org.jetbrains.annotations.NotNull java.lang.Number mul(final @org.jetbrains.annotations.NotNull mypackage.Operands ops) { + return this.jsiiCall(\\"mul\\", java.lang.Number.class, new Object[] { java.util.Objects.requireNonNull(ops, \\"ops is required\\") }); + } + + /** + * Subtracts the two operands. + *

+ * @param ops operands. This parameter is required. + */ + public @org.jetbrains.annotations.NotNull java.lang.Number sub(final @org.jetbrains.annotations.NotNull mypackage.Operands ops) { + return this.jsiiCall(\\"sub\\", java.lang.Number.class, new Object[] { java.util.Objects.requireNonNull(ops, \\"ops is required\\") }); + } +} +", + "src/main/mypackage/main/java/mypackage/Operands.java": "package mypackage; + +/** + * Math operands. + */ + +@software.amazon.jsii.Jsii(module = mypackage.$Module.class, fqn = \\"generated.Operands\\") +@software.amazon.jsii.Jsii.Proxy(Operands.Jsii$Proxy.class) +public interface Operands extends software.amazon.jsii.JsiiSerializable { + + /** + * Left-hand side operand. + */ + @org.jetbrains.annotations.NotNull java.lang.Number getLhs(); + + /** + * Right-hand side operand. + */ + @org.jetbrains.annotations.NotNull java.lang.Number getRhs(); + + /** + * @return a {@link Builder} of {@link Operands} + */ + static Builder builder() { + return new Builder(); + } + /** + * A builder for {@link Operands} + */ + public static final class Builder { + private java.lang.Number lhs; + private java.lang.Number rhs; + + /** + * Sets the value of {@link Operands#getLhs} + * @param lhs Left-hand side operand. This parameter is required. + * @return {@code this} + */ + public Builder lhs(java.lang.Number lhs) { + this.lhs = lhs; + return this; + } + + /** + * Sets the value of {@link Operands#getRhs} + * @param rhs Right-hand side operand. This parameter is required. + * @return {@code this} + */ + public Builder rhs(java.lang.Number rhs) { + this.rhs = rhs; + return this; + } + + /** + * Builds the configured instance. + * @return a new instance of {@link Operands} + * @throws NullPointerException if any required attribute was not provided + */ + public Operands build() { + return new Jsii$Proxy(lhs, rhs); + } + } + + /** + * An implementation for {@link Operands} + */ + final class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements Operands { + private final java.lang.Number lhs; + private final java.lang.Number rhs; + + /** + * Constructor that initializes the object based on values retrieved from the JsiiObject. + * @param objRef Reference to the JSII managed object. + */ + protected Jsii$Proxy(final software.amazon.jsii.JsiiObjectRef objRef) { + super(objRef); + this.lhs = this.jsiiGet(\\"lhs\\", java.lang.Number.class); + this.rhs = this.jsiiGet(\\"rhs\\", java.lang.Number.class); + } + + /** + * Constructor that initializes the object based on literal property values passed by the {@link Builder}. + */ + private Jsii$Proxy(final java.lang.Number lhs, final java.lang.Number rhs) { + super(software.amazon.jsii.JsiiObject.InitializationMode.JSII); + this.lhs = java.util.Objects.requireNonNull(lhs, \\"lhs is required\\"); + this.rhs = java.util.Objects.requireNonNull(rhs, \\"rhs is required\\"); + } + + @Override + public java.lang.Number getLhs() { + return this.lhs; + } + + @Override + public java.lang.Number getRhs() { + return this.rhs; + } + + @Override + public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { + final com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; + final com.fasterxml.jackson.databind.node.ObjectNode data = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + + data.set(\\"lhs\\", om.valueToTree(this.getLhs())); + data.set(\\"rhs\\", om.valueToTree(this.getRhs())); + + final com.fasterxml.jackson.databind.node.ObjectNode struct = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + struct.set(\\"fqn\\", om.valueToTree(\\"generated.Operands\\")); + struct.set(\\"data\\", data); + + final com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + obj.set(\\"$jsii.struct\\", struct); + + return obj; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Operands.Jsii$Proxy that = (Operands.Jsii$Proxy) o; + + if (!lhs.equals(that.lhs)) return false; + return this.rhs.equals(that.rhs); + } + + @Override + public int hashCode() { + int result = this.lhs.hashCode(); + result = 31 * result + (this.rhs.hashCode()); + return result; + } + } +} +", + "src/main/mypackage/main/resources/mypackage/$Module.txt": "generated.Calculator=mypackage.Calculator +generated.Operands=mypackage.Operands +", +} +`; + exports[`jsii output 1`] = ` Object { "author": Object { diff --git a/test/__snapshots__/srcmak.test.js.snap b/test/__snapshots__/srcmak.test.js.snap index 52016b7d..aaf1e81a 100644 --- a/test/__snapshots__/srcmak.test.js.snap +++ b/test/__snapshots__/srcmak.test.js.snap @@ -1,5 +1,229 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`java + different entrypoint 1`] = ` +Object { + "src/main/hello/world/main/java/hello/world/$Module.java": "package hello.world; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; + +import java.nio.charset.StandardCharsets; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.jsii.JsiiModule; + +public final class $Module extends JsiiModule { + private static final Map MODULE_TYPES = load(); + + private static Map load() { + final Map result = new HashMap<>(); + final ClassLoader cl = $Module.class.getClassLoader(); + try (final InputStream is = cl.getResourceAsStream(\\"hello/world/$Module.txt\\"); + final Reader rd = new InputStreamReader(is, StandardCharsets.UTF_8); + final BufferedReader br = new BufferedReader(rd)) { + br.lines() + .filter(line -> !line.trim().isEmpty()) + .forEach(line -> { + final String[] parts = line.split(\\"=\\", 2); + final String fqn = parts[0]; + final String className = parts[1]; + result.put(fqn, className); + }); + } + catch (final IOException exception) { + throw new UncheckedIOException(exception); + } + return result; + } + + private final Map> cache = new HashMap<>(); + + public $Module() { + super(\\"generated\\", \\"0.0.0\\", $Module.class, \\"generated@0.0.0.jsii.tgz\\"); + } + + @Override + protected Class resolveClass(final String fqn) throws ClassNotFoundException { + if (!MODULE_TYPES.containsKey(fqn)) { + throw new ClassNotFoundException(\\"Unknown JSII type: \\" + fqn); + } + return this.cache.computeIfAbsent(MODULE_TYPES.get(fqn), this::findClass); + } + + private Class findClass(final String binaryName) { + try { + return Class.forName(binaryName); + } + catch (final ClassNotFoundException exception) { + throw new RuntimeException(exception); + } + } +} +", + "src/main/hello/world/main/java/hello/world/Hello.java": "package hello.world; + + +@software.amazon.jsii.Jsii(module = hello.world.$Module.class, fqn = \\"generated.Hello\\") +public class Hello extends software.amazon.jsii.JsiiObject { + + protected Hello(final software.amazon.jsii.JsiiObjectRef objRef) { + super(objRef); + } + + protected Hello(final software.amazon.jsii.JsiiObject.InitializationMode initializationMode) { + super(initializationMode); + } + + public Hello() { + super(software.amazon.jsii.JsiiObject.InitializationMode.JSII); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + public @org.jetbrains.annotations.NotNull java.lang.Number add(final @org.jetbrains.annotations.NotNull hello.world.Operands ops) { + return this.jsiiCall(\\"add\\", java.lang.Number.class, new Object[] { java.util.Objects.requireNonNull(ops, \\"ops is required\\") }); + } +} +", + "src/main/hello/world/main/java/hello/world/Operands.java": "package hello.world; + + +@software.amazon.jsii.Jsii(module = hello.world.$Module.class, fqn = \\"generated.Operands\\") +@software.amazon.jsii.Jsii.Proxy(Operands.Jsii$Proxy.class) +public interface Operands extends software.amazon.jsii.JsiiSerializable { + + @org.jetbrains.annotations.NotNull java.lang.Number getLhs(); + + @org.jetbrains.annotations.NotNull java.lang.Number getRhs(); + + /** + * @return a {@link Builder} of {@link Operands} + */ + static Builder builder() { + return new Builder(); + } + /** + * A builder for {@link Operands} + */ + public static final class Builder { + private java.lang.Number lhs; + private java.lang.Number rhs; + + /** + * Sets the value of {@link Operands#getLhs} + * @param lhs the value to be set. This parameter is required. + * @return {@code this} + */ + public Builder lhs(java.lang.Number lhs) { + this.lhs = lhs; + return this; + } + + /** + * Sets the value of {@link Operands#getRhs} + * @param rhs the value to be set. This parameter is required. + * @return {@code this} + */ + public Builder rhs(java.lang.Number rhs) { + this.rhs = rhs; + return this; + } + + /** + * Builds the configured instance. + * @return a new instance of {@link Operands} + * @throws NullPointerException if any required attribute was not provided + */ + public Operands build() { + return new Jsii$Proxy(lhs, rhs); + } + } + + /** + * An implementation for {@link Operands} + */ + final class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements Operands { + private final java.lang.Number lhs; + private final java.lang.Number rhs; + + /** + * Constructor that initializes the object based on values retrieved from the JsiiObject. + * @param objRef Reference to the JSII managed object. + */ + protected Jsii$Proxy(final software.amazon.jsii.JsiiObjectRef objRef) { + super(objRef); + this.lhs = this.jsiiGet(\\"lhs\\", java.lang.Number.class); + this.rhs = this.jsiiGet(\\"rhs\\", java.lang.Number.class); + } + + /** + * Constructor that initializes the object based on literal property values passed by the {@link Builder}. + */ + private Jsii$Proxy(final java.lang.Number lhs, final java.lang.Number rhs) { + super(software.amazon.jsii.JsiiObject.InitializationMode.JSII); + this.lhs = java.util.Objects.requireNonNull(lhs, \\"lhs is required\\"); + this.rhs = java.util.Objects.requireNonNull(rhs, \\"rhs is required\\"); + } + + @Override + public java.lang.Number getLhs() { + return this.lhs; + } + + @Override + public java.lang.Number getRhs() { + return this.rhs; + } + + @Override + public com.fasterxml.jackson.databind.JsonNode $jsii$toJson() { + final com.fasterxml.jackson.databind.ObjectMapper om = software.amazon.jsii.JsiiObjectMapper.INSTANCE; + final com.fasterxml.jackson.databind.node.ObjectNode data = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + + data.set(\\"lhs\\", om.valueToTree(this.getLhs())); + data.set(\\"rhs\\", om.valueToTree(this.getRhs())); + + final com.fasterxml.jackson.databind.node.ObjectNode struct = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + struct.set(\\"fqn\\", om.valueToTree(\\"generated.Operands\\")); + struct.set(\\"data\\", data); + + final com.fasterxml.jackson.databind.node.ObjectNode obj = com.fasterxml.jackson.databind.node.JsonNodeFactory.instance.objectNode(); + obj.set(\\"$jsii.struct\\", struct); + + return obj; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Operands.Jsii$Proxy that = (Operands.Jsii$Proxy) o; + + if (!lhs.equals(that.lhs)) return false; + return this.rhs.equals(that.rhs); + } + + @Override + public int hashCode() { + int result = this.lhs.hashCode(); + result = 31 * result + (this.rhs.hashCode()); + return result; + } + } +} +", + "src/main/hello/world/main/resources/hello/world/$Module.txt": "generated.Hello=hello.world.Hello +generated.Operands=hello.world.Operands +", +} +`; + exports[`outputJsii can be used to look at the jsii file 1`] = ` Object { "author": Object { diff --git a/test/cli.test.ts b/test/cli.test.ts index f5fc6402..989f2762 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -49,6 +49,18 @@ test('fails if only one python option is given', () => { )).toThrow(/--python-module-name is required if --python-outdir is specified/); }); +test('fails if only one java option is given', () => { + expect(() => srcmakcli(srcdir, + '--entrypoint lib/main.ts', + '--java-package mypackage', + )).toThrow(/--java-outdir is required/); + + expect(() => srcmakcli(srcdir, + '--entrypoint lib/main.ts', + '--java-outdir dir', + )).toThrow(/--java-package is required/); +}); + test('python output', async () => { await mkdtemp(async outdir => { srcmakcli(srcdir, @@ -57,7 +69,24 @@ test('python output', async () => { '--python-module-name my.python.module', ); - expect(await snapshotDirectory(outdir, [ 'generated@0.0.0.jsii.tgz' ])).toMatchSnapshot(); + expect(await snapshotDirectory(outdir, { + excludeFiles: [ 'generated@0.0.0.jsii.tgz' ], + })).toMatchSnapshot(); + }); +}); + +test('java output', async () => { + await mkdtemp(async outdir => { + srcmakcli(srcdir, + '--entrypoint lib/main.ts', + `--java-outdir ${outdir}`, + '--java-package mypackage', + ); + + expect(await snapshotDirectory(outdir, { + excludeLines: [ /.*@javax.annotation.Generated.*/ ], + excludeFiles: [ 'generated@0.0.0.jsii.tgz' ], + })).toMatchSnapshot(); }); }); diff --git a/test/srcmak.test.ts b/test/srcmak.test.ts index 15f98caa..1f620844 100644 --- a/test/srcmak.test.ts +++ b/test/srcmak.test.ts @@ -67,7 +67,45 @@ test('python + different entrypoint + submodule', async () => { }, }); - const dir = await snapshotDirectory(target, [ 'generated@0.0.0.jsii.tgz' ]); + const dir = await snapshotDirectory(target, { + excludeFiles: [ 'generated@0.0.0.jsii.tgz' ], + }); + expect(dir).toMatchSnapshot(); + }); + }); +}); + +test('java + different entrypoint', async () => { + await mkdtemp(async source => { + const entry = 'different/entry.ts'; + const ep = path.join(source, entry); + await fs.mkdirp(path.dirname(ep)); + await fs.writeFile(ep, ` + export interface Operands { + readonly lhs: number; + readonly rhs: number; + } + + export class Hello { + public add(ops: Operands): number { + return ops.lhs + ops.rhs; + } + } + `); + + await mkdtemp(async target => { + await srcmak(source, { + entrypoint: 'different/entry.ts', + java: { + outdir: target, + package: 'hello.world', + }, + }); + + const dir = await snapshotDirectory(target, { + excludeLines: [ /.*@javax.annotation.Generated.*/ ], + excludeFiles: [ 'generated@0.0.0.jsii.tgz' ], + }); expect(dir).toMatchSnapshot(); }); }); diff --git a/test/util.ts b/test/util.ts index c5f23a0e..48e6b737 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,15 +1,21 @@ import * as fs from 'fs-extra'; import * as path from 'path'; +interface SnapshotOptions { + excludeLines?: RegExp[]; + excludeFiles?: string[]; +} + /** * Returns a dictionary where keys are relative file names and values are the * file contents. Useful to perform snapshot testing against full directories. */ -export async function snapshotDirectory(basedir: string, exclude: string[] = [], reldir = '.'): Promise> { +export async function snapshotDirectory(basedir: string, excludeOptions: SnapshotOptions = {}, reldir = '.'): Promise> { const result: Record = { }; const absdir = path.join(basedir, reldir); + const { excludeLines, excludeFiles } = excludeOptions; for (const file of await fs.readdir(absdir)) { - if (exclude.includes(file)) { + if (excludeFiles?.includes(file)) { continue; // skip } @@ -17,14 +23,18 @@ export async function snapshotDirectory(basedir: string, exclude: string[] = [], const relpath = path.join(reldir, file); if ((await fs.stat(abspath)).isDirectory()) { - const subdir = await snapshotDirectory(basedir, exclude, relpath); + const subdir = await snapshotDirectory(basedir, excludeOptions, relpath); for (const [k, v] of Object.entries(subdir)) { result[k] = v; } continue; } - const data = await fs.readFile(abspath, 'utf-8'); + let data = await fs.readFile(abspath, 'utf-8'); + for (const excludeLine of excludeLines || []) { + data = data.replace(excludeLine, ''); + } + result[relpath] = data; }