Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first class Javascript/Typescript support to the Mill build tool #4135

Merged
merged 2 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions example/javascriptlib/testing/1-test-suite/baz/src/calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class Calculator {
add(a, b) {
return a + b;
}
divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
}
12 changes: 12 additions & 0 deletions example/javascriptlib/testing/1-test-suite/baz/src/calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class Calculator {
add(a: number, b: number): number {
return a + b;
}

divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest';
import { Calculator } from 'baz/calculator';
describe('Calculator', () => {
const calculator = new Calculator();
describe('Addition', () => {
test('should return the sum of two numbers', () => {
const result = calculator.add(2, 3);
expect(result).toEqual(5);
});
test('should return the correct sum for negative numbers', () => {
const result = calculator.add(-2, -3);
expect(result).toEqual(-5);
});
});
describe('Division', () => {
test('should return the quotient of two numbers', () => {
const result = calculator.divide(6, 3);
expect(result).toEqual(2);
});
it('should throw an error when dividing by zero', () => {
expect(() => calculator.divide(6, 0)).toThrow("Division by zero is not allowed");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Calculator } from 'baz/calculator';

describe('Calculator', () => {
const calculator = new Calculator();

describe('Addition', () => {
test('should return the sum of two numbers', () => {
const result = calculator.add(2, 3);
expect(result).toEqual(5);
});

test('should return the correct sum for negative numbers', () => {
const result = calculator.add(-2, -3);
expect(result).toEqual(-5);
});
});

describe('Division', () => {
test('should return the quotient of two numbers', () => {
const result = calculator.divide(6, 3);
expect(result).toEqual(2);
});

it('should throw an error when dividing by zero', () => {
expect(() => calculator.divide(6, 0)).toThrow("Division by zero is not allowed");
});
});
});
18 changes: 18 additions & 0 deletions example/javascriptlib/testing/1-test-suite/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ object bar extends TypeScriptModule {
object test extends TypeScriptTests with TestModule.Jest
}

object baz extends TypeScriptModule {
object test extends TypeScriptTests with TestModule.Vitest
}

object foo extends TypeScriptModule {
object test extends TypeScriptTests with TestModule.Mocha
}

object qux extends TypeScriptModule {
object test extends TypeScriptTests with TestModule.Jasmine
}

// Documentation for mill.example.javascriptlib
// This build defines two modules bar and foo with test suites configured to use
// Mocha & Jest resepectively.
Expand All @@ -27,4 +35,14 @@ object foo extends TypeScriptModule {
Test Suites:...1 passed, 1 total...
Tests:...4 passed, 4 total...
...

> mill baz.test
.../calculator.test.ts...
...Test Files 1 passed...
...Tests 4 passed...
...

> mill qux.test
...
4 specs, 0 failures
*/
11 changes: 11 additions & 0 deletions example/javascriptlib/testing/1-test-suite/qux/src/calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class Calculator {
add(a, b) {
return a + b;
}
divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
}
12 changes: 12 additions & 0 deletions example/javascriptlib/testing/1-test-suite/qux/src/calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class Calculator {
add(a: number, b: number): number {
return a + b;
}

divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Calculator } from 'qux/calculator';

describe('Calculator', () => {
const calculator = new Calculator();

describe('Addition', () => {
it('should return the sum of two numbers', () => {
const result = calculator.add(2, 3);
expect(result).toEqual(5);
});

it('should return the correct sum for negative numbers', () => {
const result = calculator.add(-2, -3);
expect(result).toEqual(-5);
});
});

describe('Division', () => {
it('should return the quotient of two numbers', () => {
const result = calculator.divide(6, 3);
expect(result).toEqual(2);
});

it('should throw an error when dividing by zero', () => {
expect(() => calculator.divide(6, 0)).toThrowError("Division by zero is not allowed");
});
});
});
12 changes: 12 additions & 0 deletions example/javascriptlib/testing/1-test-suite/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
environment: 'node',
include: ['**/**/*.test.ts']
},
});
129 changes: 125 additions & 4 deletions javascriptlib/src/mill/javascriptlib/TestModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ object TestModule {
def testConfigSource: T[PathRef] =
Task.Source(Task.workspace / "jest.config.ts")

override def allSources: T[IndexedSeq[PathRef]] = Task {
super.allSources() ++ IndexedSeq(testConfigSource())
}

override def compilerOptions: T[Map[String, ujson.Value]] =
Task { super.compilerOptions() + ("resolveJsonModule" -> ujson.Bool(true)) }

Expand Down Expand Up @@ -144,4 +140,129 @@ object TestModule {
}

}

trait Vitest extends TypeScriptModule with Shared with TestModule {
override def npmDevDeps: T[Seq[String]] =
Task {
Seq(
"@vitest/runner@2.1.8",
"vite@5.4.11",
"vite-tsconfig-paths@3.6.0",
"vitest@2.1.8"
)
}

def testConfigSource: T[PathRef] =
Task.Source(Task.workspace / "vite.config.ts")

override def compilerOptions: T[Map[String, ujson.Value]] =
Task {
super.compilerOptions() + (
"target" -> ujson.Str("ESNext"),
"module" -> ujson.Str("ESNext"),
"moduleResolution" -> ujson.Str("Node"),
"skipLibCheck" -> ujson.Bool(true),
"types" -> ujson.Arr(
s"${npmInstall().path}/node_modules/vitest/globals"
)
)
}

def getConfigFile: T[String] =
Task { (compile()._1.path / "vite.config.ts").toString }

private def copyConfig: Task[Unit] = Task.Anon {
os.copy.over(
testConfigSource().path,
compile()._1.path / "vite.config.ts"
)
}

private def runTest: T[TestResult] = Task {
copyConfig()
os.call(
(
"node",
npmInstall().path / "node_modules/.bin/vitest",
"--run",
"--config",
getConfigFile(),
getPathToTest()
),
stdout = os.Inherit,
env = mkENV(),
cwd = compile()._1.path
)
()
}

protected def testTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon {
runTest()
}

}

trait Jasmine extends TypeScriptModule with Shared with TestModule {
override def npmDevDeps: T[Seq[String]] =
Task {
Seq(
"@types/jasmine@5.1.2",
"jasmine@5.1.0",
"ts-node@10.9.1",
"tsconfig-paths@4.2.0",
"typescript@5.2.2"
)
}

override def compilerOptions: T[Map[String, ujson.Value]] =
Task {
super.compilerOptions() + (
"target" -> ujson.Str("ES5"),
"module" -> ujson.Str("commonjs"),
"moduleResolution" -> ujson.Str("node"),
"allowJs" -> ujson.Bool(true)
)
}

def configBuilder: T[PathRef] = Task {
val path = compile()._1.path / "jasmine.json"
os.write(
path,
ujson.write(
ujson.Obj(
"spec_dir" -> ujson.Str("typescript/src"),
"spec_files" -> ujson.Arr(ujson.Str("**/*.test.ts")),
"stopSpecOnExpectationFailure" -> ujson.Bool(false),
"random" -> ujson.Bool(false)
)
)
)
PathRef(path)
}

private def runTest: T[Unit] = Task {
configBuilder()
val jasmine = npmInstall().path / "node_modules/jasmine/bin/jasmine.js"
val tsnode = npmInstall().path / "node_modules/ts-node/register/transpile-only.js"
val tsconfigPath = npmInstall().path / "node_modules/tsconfig-paths/register.js"
os.call(
(
"node",
jasmine,
"--config=jasmine.json",
s"--require=$tsnode",
s"--require=$tsconfigPath"
),
stdout = os.Inherit,
env = mkENV(),
cwd = compile()._1.path
)
()
}

protected def testTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon {
runTest()
}

}
}
20 changes: 10 additions & 10 deletions javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ trait TypeScriptModule extends Module { outer =>
Task.traverse(moduleDeps)(_.npmDevDeps)().flatten ++ npmDevDeps()
}

def npmInstall: Target[PathRef] = Task {
def npmInstall: T[PathRef] = Task {
os.call((
"npm",
"install",
"--save-dev",
"@types/node@22.7.8",
"typescript@5.6.3",
"@types/node@22.10.2",
"typescript@5.7.2",
"ts-node@^10.9.2",
"esbuild@0.24.0",
"@esbuild-plugins/tsconfig-paths@0.1.2",
Expand All @@ -36,13 +36,13 @@ trait TypeScriptModule extends Module { outer =>
PathRef(Task.dest)
}

def sources: Target[PathRef] = Task.Source(millSourcePath / "src")
def sources: T[PathRef] = Task.Source(millSourcePath / "src")

def allSources: Target[IndexedSeq[PathRef]] =
def allSources: T[IndexedSeq[PathRef]] =
Task { os.walk(sources().path).filter(_.ext == "ts").map(PathRef(_)) }

// specify tsconfig.compilerOptions
def compilerOptions: Task[Map[String, ujson.Value]] = Task {
def compilerOptions: Task[Map[String, ujson.Value]] = Task.Anon {
Map(
"esModuleInterop" -> ujson.Bool(true),
"declaration" -> ujson.Bool(true),
Expand Down Expand Up @@ -89,15 +89,15 @@ trait TypeScriptModule extends Module { outer =>
)
)

os.call(npmInstall().path / "node_modules/typescript/bin/tsc")
os.call(npmInstall().path / "node_modules/.bin/tsc")
os.copy.over(millSourcePath, Task.dest / "typescript")

(PathRef(Task.dest), PathRef(Task.dest / "typescript"))
}

def mainFileName: Target[String] = Task { s"${millSourcePath.last}.ts" }
def mainFileName: T[String] = Task { s"${millSourcePath.last}.ts" }

def mainFilePath: Target[Path] = Task { compile()._2.path / "src" / mainFileName() }
def mainFilePath: T[Path] = Task { compile()._2.path / "src" / mainFileName() }

def mkENV: T[Map[String, String]] =
Task {
Expand Down Expand Up @@ -158,7 +158,7 @@ trait TypeScriptModule extends Module { outer =>

}

def bundle: Target[PathRef] = Task {
def bundle: T[PathRef] = Task {
val env = mkENV()
val tsnode = npmInstall().path / "node_modules/.bin/ts-node"
val bundleScript = compile()._1.path / "build.ts"
Expand Down
Loading