Fix codepaths computation when working dir is nested relative to package.json ()

# Description

When using `tsc` to precompile typescript in a monorepo, we need to work
relative to the location of `package.json`, not where the pulumi program
lives (which is usually nested further down).

## Checklist

- [x] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
  - [x] I have formatted my code using `gofumpt`

<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [ ] I have added tests that prove my fix is effective or that my
feature works
<!--- 
User-facing changes require a CHANGELOG entry.
-->
- [ ] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
This commit is contained in:
Julien P 2024-03-08 17:16:47 +01:00 committed by GitHub
parent d7c4025734
commit 5a19d754d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 586 additions and 18 deletions

View File

@ -0,0 +1,4 @@
changes:
- type: fix
scope: sdk/nodejs
description: Fix codepaths computation when working dir is nested relative to package.json

View File

@ -0,0 +1,9 @@
{
"name": "a-workspace-example-tsc",
"description": "A project that uses npm and yarn workspaces.",
"private": true,
"workspaces": [
"packages/*",
"project/"
]
}

View File

@ -0,0 +1,4 @@
{
"name": "some-package",
"version": "1.0.0"
}

View File

@ -0,0 +1,2 @@
/* eslint-disable header/header */
// This is the pulumi program

View File

@ -0,0 +1,4 @@
{
"name": "project",
"version": "1.0.0"
}

View File

@ -25,10 +25,22 @@ import (
var ErrNotInWorkspace = errors.New("not in a workspace")
// FindWorkspaceRoot determines if we are in a yarn/npm workspace setup and
// returns the root directory of the workspace. If the programDirectory is
// returns the root directory of the workspace. If the startingPath is
// not in a workspace, it returns ErrNotInWorkspace.
func FindWorkspaceRoot(programDirectory string) (string, error) {
currentDir := filepath.Dir(programDirectory)
func FindWorkspaceRoot(startingPath string) (string, error) {
stat, err := os.Stat(startingPath)
if err != nil {
return "", err
}
if !stat.IsDir() {
startingPath = filepath.Dir(startingPath)
}
// We start at the location of the first `package.json` we find.
packageJSONDir, err := searchup(startingPath, "package.json")
if err != nil {
return "", fmt.Errorf("did not find package.json in %s: %w", startingPath, err)
}
currentDir := packageJSONDir
nextDir := filepath.Dir(currentDir)
for currentDir != nextDir { // We're at the root when the nextDir is the same as the currentDir.
p := filepath.Join(currentDir, "package.json")
@ -52,15 +64,12 @@ func FindWorkspaceRoot(programDirectory string) (string, error) {
if err != nil {
return "", err
}
if paths != nil && slices.Contains(paths, filepath.Join(programDirectory, "package.json")) {
if paths != nil && slices.Contains(paths, filepath.Join(packageJSONDir, "package.json")) {
return currentDir, nil
}
}
// None of the workspace globs matched the program directory, so we're
// in the slightly weird situation where a parent directory has a
// package.json with workspaces set up, but the program directory is
// not part of this.
return "", ErrNotInWorkspace
currentDir = nextDir
nextDir = filepath.Dir(currentDir)
}
return "", ErrNotInWorkspace
}
@ -104,3 +113,14 @@ func parseWorkspaces(p string) ([]string, error) {
}
return pkgExtended.Workspaces.Packages, nil
}
func searchup(currentDir, fileToFind string) (string, error) {
if _, err := os.Stat(filepath.Join(currentDir, fileToFind)); err == nil {
return currentDir, nil
}
parentDir := filepath.Dir(currentDir)
if currentDir == parentDir {
return "", nil
}
return searchup(parentDir, fileToFind)
}

View File

@ -45,3 +45,22 @@ func TestFindWorkspaceRootYarnExtended(t *testing.T) {
require.NoError(t, err)
require.Equal(t, filepath.Join("testdata", "workspace-extended"), root)
}
func TestFindWorkspaceRootNested(t *testing.T) {
t.Parallel()
root, err := FindWorkspaceRoot(filepath.Join("testdata", "workspace-nested", "project", "dist"))
require.NoError(t, err)
require.Equal(t, filepath.Join("testdata", "workspace-nested"), root)
}
func TestFindWorkspaceRootFileArgument(t *testing.T) {
t.Parallel()
// Use a file as the argument to FindWorkspaceRoot instead of a directory.
root, err := FindWorkspaceRoot(filepath.Join("testdata", "workspace-nested", "project", "dist", "index.js"))
require.NoError(t, err)
require.Equal(t, filepath.Join("testdata", "workspace-nested"), root)
}

View File

@ -182,12 +182,22 @@ function searchUp(currentDir: string, fileToFind: string): string | null {
}
/**
* @internal
* findWorkspaceRoot detects if we are in a yarn/npm workspace setup, and
* returns the root of the workspace. If we are not in a workspace setup, it
* returns null.
*/
async function findWorkspaceRoot(programDirectory: string): Promise<string | null> {
let currentDir = upath.dirname(programDirectory);
export async function findWorkspaceRoot(startingPath: string): Promise<string | null> {
const stat = fs.statSync(startingPath);
if (!stat.isDirectory()) {
startingPath = upath.dirname(startingPath);
}
const packageJSONDir = searchUp(startingPath, "package.json");
if (packageJSONDir === null) {
return null;
}
// We start at the location of the first `package.json` we find.
let currentDir = packageJSONDir;
let nextDir = upath.dirname(currentDir);
while (currentDir !== nextDir) {
const p = upath.join(currentDir, "package.json");
@ -199,12 +209,13 @@ async function findWorkspaceRoot(programDirectory: string): Promise<string | nul
const workspaces = parseWorkspaces(p);
for (const workspace of workspaces) {
const files = await pGlob(upath.join(currentDir, workspace, "package.json"));
const normalized = upath.normalizeTrim(upath.join(programDirectory, "package.json"));
const normalized = upath.normalizeTrim(upath.join(packageJSONDir, "package.json"));
if (files.map((f) => upath.normalizeTrim(f)).includes(normalized)) {
return currentDir;
}
}
return null;
currentDir = nextDir;
nextDir = upath.dirname(currentDir);
}
return null;
}
@ -234,10 +245,7 @@ async function allFoldersForPackages(
// searching up from the current directory
throw new ResourceError("Failed to find package.json.", logResource);
}
// Ensure workingDir is a relative path so we get relative paths in the
// output. If we have absolute paths, AWS lambda might not find the
// dependencies.
workingDir = upath.relative(upath.resolve("."), workingDir);
workingDir = upath.resolve(workingDir);
// This is the core starting point of the algorithm. We read the
// package.json information for this project, and then we start by walking
@ -252,7 +260,10 @@ async function allFoldersForPackages(
}
// Find the workspace root, fallback to current working directory if we are not in a workspaces setup.
let workspaceRoot = (await findWorkspaceRoot(upath.resolve("."))) || workingDir;
let workspaceRoot = (await findWorkspaceRoot(workingDir)) || workingDir;
// Ensure workingDir is a relative path so we get relative paths in the
// output. If we have absolute paths, AWS lambda might not find the
// dependencies.
workspaceRoot = upath.relative(upath.resolve("."), workspaceRoot);
// Read package tree from the workspace root to ensure we can find all

View File

@ -0,0 +1,50 @@
// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as assert from "assert";
import * as path from "path";
import { findWorkspaceRoot } from "../../runtime/closure/codePaths";
const testdata = (...p: string[]) => path.join(__dirname, "..", "..", "..", "tests", "runtime", "testdata", ...p);
describe("findWorkspaceRoot", () => {
it("finds the root of a workspace", async () => {
const root = await findWorkspaceRoot(testdata("workspace", "project"));
assert.notStrictEqual(root, null);
assert.strictEqual(root, testdata("workspace"));
});
it("returns null if we are not in a workspace", async () => {
const root = await findWorkspaceRoot(testdata("nested", "project"));
assert.strictEqual(root, null);
});
it("finds the root of a workspace when using yarn's extended declaration", async () => {
const root = await findWorkspaceRoot(testdata("workspace-extended", "project"));
assert.notStrictEqual(root, null);
assert.strictEqual(root, testdata("workspace-extended"));
});
it("finds the root of a workspace when in a nested directory", async () => {
const root = await findWorkspaceRoot(testdata("workspace-nested", "project", "dist"));
assert.notStrictEqual(root, null);
assert.strictEqual(root, testdata("workspace-nested"));
});
it("finds the root of a workspace passing a file as argument", async () => {
const root = await findWorkspaceRoot(testdata("workspace-nested", "project", "dist", "index.js"));
assert.notStrictEqual(root, null);
assert.strictEqual(root, testdata("workspace-nested"));
});
});

View File

@ -0,0 +1,4 @@
{
"name": "nested-project",
"version": "1.0.0"
}

View File

@ -0,0 +1,15 @@
// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This is where the pulumi program lives.

View File

@ -0,0 +1,11 @@
{
"name": "a-workspace-example",
"description": "A project that uses yarn workspaces using the extended configuration in package.json",
"private": true,
"workspaces": {
"packages": [
"packages/*",
"project/"
]
}
}

View File

@ -0,0 +1,4 @@
{
"name": "some-package",
"version": "1.0.0"
}

View File

@ -0,0 +1,4 @@
{
"name": "project",
"version": "1.0.0"
}

View File

@ -0,0 +1,9 @@
{
"name": "a-workspace-example-tsc",
"description": "A project that uses npm and yarn workspaces.",
"private": true,
"workspaces": [
"packages/*",
"project/"
]
}

View File

@ -0,0 +1,4 @@
{
"name": "some-package",
"version": "1.0.0"
}

View File

@ -0,0 +1,2 @@
/* eslint-disable header/header */
// This is the pulumi program

View File

@ -0,0 +1,4 @@
{
"name": "project",
"version": "1.0.0"
}

View File

@ -0,0 +1,9 @@
{
"name": "a-workspace-example",
"description": "A project that uses npm and yarn workspaces.",
"private": true,
"workspaces": [
"packages/*",
"project/"
]
}

View File

@ -0,0 +1,4 @@
{
"name": "some-package",
"version": "1.0.0"
}

View File

@ -0,0 +1,4 @@
{
"name": "project",
"version": "1.0.0"
}

View File

@ -100,6 +100,7 @@
"tests/util.ts",
"tests/runtime/asyncIterableUtil.spec.ts",
"tests/runtime/closureLoader.spec.ts",
"tests/runtime/findWorkspaceRoot.spec.ts",
"tests/runtime/registrations.spec.ts",
"tests/runtime/tsClosureCases.ts",
"tests/runtime/package.spec.ts",
@ -107,6 +108,7 @@
"tests/runtime/settings.spec.ts",
"tests/runtime/langhost/run.spec.ts",
"tests/runtime/langhost/cases/069.ambiguous_entrypoints/index.ts",
"tests/runtime/testdata/nested/project/index.ts",
"tests/automation/cmd.spec.ts",
"tests/automation/localWorkspace.spec.ts",

View File

@ -1457,6 +1457,16 @@ func TestCodePaths(t *testing.T) {
})
}
//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestCodePathsTSC(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join("nodejs", "codepaths-tsc"),
Dependencies: []string{"@pulumi/pulumi"},
Quick: true,
RunBuild: true,
})
}
//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestCodePathsNested(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
@ -1477,6 +1487,17 @@ func TestCodePathsWorkspace(t *testing.T) {
})
}
//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestCodePathsWorkspaceTSC(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join("nodejs", "codepaths-workspaces-tsc"),
Dependencies: []string{"@pulumi/pulumi"},
Quick: true,
RunBuild: true,
RelativeWorkDir: "infra",
})
}
// Test that the resource stopwatch doesn't contain a negative time.
func TestNoNegativeTimingsOnRefresh(t *testing.T) {
if runtime.GOOS == WindowsOS {

View File

@ -0,0 +1 @@
!/bin

View File

@ -0,0 +1,7 @@
name: codepaths-tsc
description: Test case for codepaths using tsc to pre-compile
main: bin/index.js
runtime:
name: nodejs
options:
typescript: false

View File

@ -0,0 +1,21 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Dummy file to keep ProgramTest happy. We need ProjectInfo.main to exist already.
// This will be overwritten by the compiled main file in the test after running
// `yarn run build`.
throw new Error(
`This file should not be required in tests. It should be overwritten by the tsc output during testing.`
);

View File

@ -0,0 +1,44 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as runtime from "@pulumi/pulumi/runtime"
(async function () {
const deps = await runtime.computeCodePaths() as Map<string, string>;
// Deps might include more than just the direct dependencies, but the
// precise results depend on how the packages are hoisted within
// node_modules. This can change based on the version of the package
// manager and the dependencies of @pulumi/pulumi.
//
// For example for this nesting:
//
// node_modules
// └─┬ semver
// └── lru-cache
//
// deps only includes `node_modules/semver`. However if they are siblings,
// it will include both `node_modules/semver` and `node_modules/lru-cache`.
//
// We only assert that the direct dependencies are included, which are
// guaranteed to be stable.
const directDependencies = [`../node_modules/semver`]
const depPaths = [...deps.keys()]
for (const expected of directDependencies) {
const depPath = depPaths.find((path) => path.includes(expected));
if (!depPath) {
throw new Error(`Expected to find a path matching ${expected}, got ${depPaths}`)
}
}
})();

View File

@ -0,0 +1,16 @@
{
"name": "test-codepaths-tsc",
"description": "Test case for codepaths using tsc to pre-compile",
"main": "bin/index.js",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@pulumi/pulumi": "latest",
"typescript": "^3.8.3",
"semver": "^7.6.0"
},
"devDependencies": {
"@types/node": "^14.14.20"
}
}

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": ["index.ts"],
"outDir": "bin"
}

View File

@ -0,0 +1 @@
!/infra/bin

View File

@ -0,0 +1,7 @@
name: codepaths-workspaces-tsc
description: A project that uses npm and yarn workspaces and tsc. CodePaths computation should discover dependencies across the workspaces.
main: bin/index.js
runtime:
name: nodejs
options:
typescript: false

View File

@ -0,0 +1,21 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Dummy file to keep ProgramTest happy. We need ProjectInfo.main to exist already.
// This will be overwritten by the compiled main file in the test after running
// `yarn run build`.
throw new Error(
`This file should not be required in tests. It should be overwritten by the tsc output during testing.`
);

View File

@ -0,0 +1,39 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as runtime from "@pulumi/pulumi/runtime" // @pulumi dependency is not included
import * as semver from "semver" // npm dependency
import * as myRandom from "my-random" // workspace dependency
import * as myDynamicProvider from "my-dynamic-provider" // workspace dependency
const random = new myRandom.MyRandom("plop", {});
export const id = random.randomID;
export const version = semver.parse("1.2.3");
const dynamicProviderResource = new myDynamicProvider.MyDynamicProviderResource("prov", {});
(async function () {
const deps = await runtime.computeCodePaths() as Map<string, string>;
const directDependencies = [`node_modules/semver`, `node_modules/my-random`, `node_modules/my-dynamic-provider`]
const depPaths = [...deps.keys()]
for (const expected of directDependencies) {
const depPath = depPaths.find((path) => path.includes(expected));
if (!depPath) {
throw new Error(`Expected to find a path matching ${expected}, got ${depPaths}`)
}
}
})();

View File

@ -0,0 +1,18 @@
{
"name": "infra",
"main": "bin/index.js",
"version": "1.0.0",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@pulumi/pulumi": "latest",
"my-dynamic-provider": "1.0.0",
"my-random": "1.0.0",
"semver": "^7.5.2",
"typescript": "^3.8.3"
},
"devDependencies": {
"@types/node": "^14.14.20"
}
}

View File

@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"files": ["index.ts"],
"compilerOptions": {
"outDir": "bin"
}
}

View File

@ -0,0 +1,12 @@
{
"name": "codepaths-workspaces-tsc",
"description": "A project that uses npm and yarn workspaces and tsc. CodePaths computation should discover dependencies across the workspaces.",
"private": true,
"scripts": {
"build": "yarn workspaces run build"
},
"workspaces": [
"packages/*",
"infra/"
]
}

View File

@ -0,0 +1,31 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as pulumi from "@pulumi/pulumi"; // @pulumi dependency is not included;
import * as pathExists from "path-exists"; // npm dependency
import * as relative from "./relative"; // local dependency
const dynamicProvider: pulumi.dynamic.ResourceProvider = {
async create(inputs) {
return {
id: `dyn-${Math.ceil(Math.random() * 1000)}`, outs: { isFinite: isFinite(42), magic: relative.fun() }
};
}
}
export class MyDynamicProviderResource extends pulumi.dynamic.Resource {
constructor(name: string, opts?: pulumi.CustomResourceOptions) {
super(dynamicProvider, name, {}, opts);
}
}

View File

@ -0,0 +1,16 @@
{
"name": "my-dynamic-provider",
"main": "bin/index.js",
"version": "1.0.0",
"dependencies": {
"@pulumi/pulumi": "latest",
"path-exists": "^4.0.0"
},
"devDependencies": {
"typescript": "^3.8.3",
"@types/node": "^14.14.20"
},
"scripts": {
"build": "tsc"
}
}

View File

@ -0,0 +1,19 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const magic = 42;
export const fun = () => {
return magic;
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"files": ["index.ts"],
"compilerOptions": {
"outDir": "bin"
}
}

View File

@ -0,0 +1,25 @@
// Copyright 2024-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as pulumi from "@pulumi/pulumi";
export class MyRandom extends pulumi.ComponentResource {
public readonly randomID: pulumi.Output<string>;
constructor(name: string, opts: pulumi.ResourceOptions) {
super("pkg:index:MyRandom", name, {}, opts);
this.randomID = pulumi.output(`${name}-${Math.floor(Math.random() * 1000)}`);
this.registerOutputs({ randomID: this.randomID });
}
}

View File

@ -0,0 +1,15 @@
{
"name": "my-random",
"main": "bin/index.js",
"version": "1.0.0",
"dependencies": {
"@pulumi/pulumi": "latest"
},
"devDependencies": {
"typescript": "^3.8.3",
"@types/node": "^14.14.20"
},
"scripts": {
"build": "tsc"
}
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"files": ["index.ts"],
"compilerOptions": {
"outDir": "bin"
}
}

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": ["index.ts"],
"outDir": "bin"
}