From abdee9027d7f44631a7570caaf163eeb8dccf32c Mon Sep 17 00:00:00 2001 From: Monye David Onoh Date: Mon, 10 Mar 2025 07:16:07 +0100 Subject: [PATCH] JavaScriptLib Restructure (#4647) The goal of the pr is to allow existing Ts projects run with mill, it takes into considerations the fluidity of Ts project structure and removes the reliance on `ts-paths` as the only means to access `module` dependencies. This pr refactors `TscModule ` formerly (`TypeScriptModule `), it aims to introduce the features: - [x] `package` RootModule with TscModule - [x] relative paths - [x] esm support; use swc-core for compiling Key changes: - `compile` task has been modified, these modifications: - change how sources, mod-sources, resources and generated sources are grouped, allowing for the use of relative paths - these changes also taking into consideration `RootModule` use, the top level src feature now works along side `TscModule` - Due to this restructuring, implementation of `PublishModule` has been significantly altered as `TscModule` compilations output now prepares source files internal and external (source files generated via a task that would end up in the `out/`) in a manner that nodes `npm` publishing tool will understand and work with. - The restructuring also consequentially fixes the need for `TscModule` to violate the new `writing to path during execution phase` restrictions. As the previous implementation would require files (mostly test config files and build scripts) to be created within or moved to `compile.dest` during execution phases. - It also provides minimal support of esm. Additional examples - [x] `module/7-root-module` for RootModule base pr for #4425 @lihaoyi --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../basic/3-custom-build-logic/build.mill | 8 +- .../basic/3-custom-build-logic/foo/src/foo.ts | 2 +- .../foo/test/src/foo/foo.test.ts | 2 +- .../server/src/server.ts | 2 +- .../server/src/server.ts | 3 +- .../module/1-common-config/build.mill | 6 +- .../module/2-custom-tasks/build.mill | 8 +- .../module/3-override-tasks/build.mill | 8 +- .../module/3-override-tasks/foo/readme.md | 1 - .../module/5-resources/foo/src/foo.ts | 2 +- .../module/7-root-module/build.mill | 51 ++ .../module/7-root-module/src/foo.ts | 33 + .../module/7-root-module/test/src/foo.test.ts | 64 ++ .../bar/test/src/bar/calculator.test.ts | 2 +- .../baz/test/src/baz/calculator.test.ts | 2 +- .../server/src/server.ts | 2 +- .../server/src/server.ts | 2 +- .../mill/javascriptlib/PublishModule.scala | 222 +----- .../javascriptlib/ReactScriptsModule.scala | 12 +- .../src/mill/javascriptlib/TestModule.scala | 400 +++++------ .../src/mill/javascriptlib/TsLintModule.scala | 3 +- .../mill/javascriptlib/TypeScriptModule.scala | 641 ++++++++++++++---- .../extending/example-typescript-support.adoc | 2 +- .../pages/javascriptlib/module-config.adoc | 4 + 24 files changed, 890 insertions(+), 592 deletions(-) delete mode 100644 example/javascriptlib/module/3-override-tasks/foo/readme.md create mode 100644 example/javascriptlib/module/7-root-module/build.mill create mode 100644 example/javascriptlib/module/7-root-module/src/foo.ts create mode 100644 example/javascriptlib/module/7-root-module/test/src/foo.test.ts diff --git a/example/javascriptlib/basic/3-custom-build-logic/build.mill b/example/javascriptlib/basic/3-custom-build-logic/build.mill index 5e60530428c..b11bdfc8596 100644 --- a/example/javascriptlib/basic/3-custom-build-logic/build.mill +++ b/example/javascriptlib/basic/3-custom-build-logic/build.mill @@ -5,8 +5,12 @@ import mill._, javascriptlib._ object foo extends TypeScriptModule { /** Total number of lines in module source files */ - def lineCount = Task { - allSources().map(f => os.read.lines(f.path).size).sum + def lineCount: T[Int] = Task { + sources() + .flatMap(pathRef => os.walk(pathRef.path)) + .filter(_.ext == "ts") + .map(os.read.lines(_).size) + .sum } /** Generate resource using lineCount of sources */ diff --git a/example/javascriptlib/basic/3-custom-build-logic/foo/src/foo.ts b/example/javascriptlib/basic/3-custom-build-logic/foo/src/foo.ts index c8dfa026363..b1687112d55 100644 --- a/example/javascriptlib/basic/3-custom-build-logic/foo/src/foo.ts +++ b/example/javascriptlib/basic/3-custom-build-logic/foo/src/foo.ts @@ -1,6 +1,6 @@ import * as fs from 'fs/promises'; -const Resources: string = process.env.RESOURCESDEST || "@foo/resources.dest" // `RESOURCES` is generated on bundle +const Resources: string = process.env.RESOURCESDEST || "@foo/resources" // `RESOURCES` is generated on bundle const LineCount = require.resolve(`${Resources}/line-count.txt`); export default class Foo { diff --git a/example/javascriptlib/basic/3-custom-build-logic/foo/test/src/foo/foo.test.ts b/example/javascriptlib/basic/3-custom-build-logic/foo/test/src/foo/foo.test.ts index 9b30e44134c..9f76b893a5b 100644 --- a/example/javascriptlib/basic/3-custom-build-logic/foo/test/src/foo/foo.test.ts +++ b/example/javascriptlib/basic/3-custom-build-logic/foo/test/src/foo/foo.test.ts @@ -1,4 +1,4 @@ -import Foo from 'foo/foo'; +import Foo from '../../../src/foo'; describe('Foo.getLineCount', () => { beforeEach(() => { diff --git a/example/javascriptlib/basic/5-client-server-hello/server/src/server.ts b/example/javascriptlib/basic/5-client-server-hello/server/src/server.ts index 92909dc8bfe..53a14b2ead7 100644 --- a/example/javascriptlib/basic/5-client-server-hello/server/src/server.ts +++ b/example/javascriptlib/basic/5-client-server-hello/server/src/server.ts @@ -2,7 +2,7 @@ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; -const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle +const Resources: string = (process.env.RESOURCESDEST || "@server/resources") + "/build" // `RESOURCES` is generated on bundle const Client = require.resolve(`${Resources}/index.html`); const server = http.createServer((req, res) => { diff --git a/example/javascriptlib/basic/6-client-server-realistic/server/src/server.ts b/example/javascriptlib/basic/6-client-server-realistic/server/src/server.ts index 59b73126f07..a8b7fa82af5 100644 --- a/example/javascriptlib/basic/6-client-server-realistic/server/src/server.ts +++ b/example/javascriptlib/basic/6-client-server-realistic/server/src/server.ts @@ -1,9 +1,8 @@ import express, {Express} from 'express'; import cors from 'cors'; -import path from 'path' import api from "./api" -const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle +const Resources: string = (process.env.RESOURCESDEST || "@server/resources") + "/build" // `RESOURCES` is generated on bundle const Client = require.resolve(`${Resources}/index.html`); const BuildPath = Client.replace(/index\.html$/, ""); const app: Express = express(); diff --git a/example/javascriptlib/module/1-common-config/build.mill b/example/javascriptlib/module/1-common-config/build.mill index 05656f379e5..e76d0d66137 100644 --- a/example/javascriptlib/module/1-common-config/build.mill +++ b/example/javascriptlib/module/1-common-config/build.mill @@ -5,11 +5,9 @@ import mill.javascriptlib._ object foo extends TypeScriptModule { - def customSource = Task { - Seq(PathRef(moduleDir / "custom-src/foo2.ts")) - } + def customSource = Task.Sources("custom-src/foo2.ts") - def allSources = super.allSources() ++ customSource() + def sources = Task { super.sources() ++ customSource() } def resources = super.resources() ++ Seq(PathRef(moduleDir / "custom-resources")) diff --git a/example/javascriptlib/module/2-custom-tasks/build.mill b/example/javascriptlib/module/2-custom-tasks/build.mill index 7c966afa10b..1197471a260 100644 --- a/example/javascriptlib/module/2-custom-tasks/build.mill +++ b/example/javascriptlib/module/2-custom-tasks/build.mill @@ -5,8 +5,12 @@ import mill._, javascriptlib._ object foo extends TypeScriptModule { /** Total number of lines in module source files */ - def lineCount = Task { - allSources().map(f => os.read.lines(f.path).size).sum + def lineCount: T[Int] = Task { + sources() + .flatMap(pathRef => os.walk(pathRef.path)) + .filter(_.ext == "ts") + .map(os.read.lines(_).size) + .sum } def generatedSources = Task { diff --git a/example/javascriptlib/module/3-override-tasks/build.mill b/example/javascriptlib/module/3-override-tasks/build.mill index 963f80f310e..ceb886fa441 100644 --- a/example/javascriptlib/module/3-override-tasks/build.mill +++ b/example/javascriptlib/module/3-override-tasks/build.mill @@ -3,7 +3,9 @@ package build import mill._ import mill.javascriptlib._ -object foo extends TypeScriptModule { +object `package` extends RootModule with TypeScriptModule { + def moduleName = "foo" + def sources = Task { val srcPath = Task.dest / "src" val filePath = srcPath / "foo.ts" @@ -18,7 +20,7 @@ object foo extends TypeScriptModule { """.stripMargin ) - PathRef(Task.dest) + Seq(PathRef(Task.dest)) } def compile = Task { @@ -46,7 +48,7 @@ object foo extends TypeScriptModule { /** Usage -> mill foo.run "added tags" +> mill run "added tags" Compiling... Hello World! Running... added tags diff --git a/example/javascriptlib/module/3-override-tasks/foo/readme.md b/example/javascriptlib/module/3-override-tasks/foo/readme.md deleted file mode 100644 index 2d5728c3d00..00000000000 --- a/example/javascriptlib/module/3-override-tasks/foo/readme.md +++ /dev/null @@ -1 +0,0 @@ -Generate source directory from build.mill \ No newline at end of file diff --git a/example/javascriptlib/module/5-resources/foo/src/foo.ts b/example/javascriptlib/module/5-resources/foo/src/foo.ts index 54b1381cf3e..e3d09ecd5f9 100644 --- a/example/javascriptlib/module/5-resources/foo/src/foo.ts +++ b/example/javascriptlib/module/5-resources/foo/src/foo.ts @@ -17,4 +17,4 @@ if (process.env.NODE_ENV !== "test") { console.error('Error:', err); } })(); -} \ No newline at end of file +} diff --git a/example/javascriptlib/module/7-root-module/build.mill b/example/javascriptlib/module/7-root-module/build.mill new file mode 100644 index 00000000000..c6f0c2b06a9 --- /dev/null +++ b/example/javascriptlib/module/7-root-module/build.mill @@ -0,0 +1,51 @@ +// You can use ``object `package` extends RootModule`` to use a `Module` +// as the root module of the file: + +package build + +import mill._, javascriptlib._ + +object `package` extends RootModule with TypeScriptModule { + def moduleName = "foo" + def npmDeps = Seq("immutable@4.3.7") + object test extends TypeScriptTests with TestModule.Jest +} + +// Since our ``object `package` extends RootModule``, its files live in a +// top-level `src/` folder. + +// Mill will ordinarily use the name of the singleton object, as +// the default value for the `moduleName` task. + +// For exmaple: + +// The `moduleName` for the singleton `bar` defined as +// `object bar extends TypeScriptModule` would be `bar`, +// with the expected source directory in `bar/src`. + +// For this example, since we use the `RootModule` we would need to manually define our +// `moduleName`. +// +// The `moduleName` is used in the generated `tsconfig` files `compilerOptions['paths']` +// as the modules `src/` path mapping and to define the default `mainFileName`, which is +// the modules entry file. +// The generated mapping for this example would be `"foo/*": [...]`. and its expected main file +// woudld be `foo.ts` located in top-level `src/` + +/** Usage +> mill run James Bond prof +Hello James Bond Professor + +> mill test +PASS .../foo.test.ts +... +Test Suites:...1 passed, 1 total... +Tests:...3 passed, 3 total... +... + +> mill show bundle +Build succeeded! + +> node out/bundle.dest/bundle.js James Bond prof +Hello James Bond Professor +*/ diff --git a/example/javascriptlib/module/7-root-module/src/foo.ts b/example/javascriptlib/module/7-root-module/src/foo.ts new file mode 100644 index 00000000000..cda766f1e8a --- /dev/null +++ b/example/javascriptlib/module/7-root-module/src/foo.ts @@ -0,0 +1,33 @@ +import {Map} from 'immutable'; + +interface User { + firstName: string + lastName: string + role: string +} + +export const defaultRoles: Map = Map({prof: "Professor"}); + +/** + * Generate a user object based on command-line arguments + * @param args Command-line arguments + * @returns User object + */ +export function generateUser(args: string[]): User { + return { + firstName: args[0] || "unknown", + lastName: args[1] || "unknown", + role: defaultRoles.get(args[2], ""), + }; +} + +// Main CLI logic +if (process.env.NODE_ENV !== "test") { + const args = process.argv.slice(2); // Skip 'node' and script name + const user = generateUser(args); + + console.log(defaultRoles.toObject()); + console.log(args[2]); + console.log(defaultRoles.get(args[2])); + console.log("Hello " + user.firstName + " " + user.lastName + " " + user.role); +} \ No newline at end of file diff --git a/example/javascriptlib/module/7-root-module/test/src/foo.test.ts b/example/javascriptlib/module/7-root-module/test/src/foo.test.ts new file mode 100644 index 00000000000..0a359977e7c --- /dev/null +++ b/example/javascriptlib/module/7-root-module/test/src/foo.test.ts @@ -0,0 +1,64 @@ +import {generateUser, defaultRoles} from "foo/foo"; +import {Map} from 'immutable'; + +// Define the type roles object +type RoleKeys = "admin" | "user"; +type Roles = { + [key in RoleKeys]: string; +}; + +// Mock `defaultRoles` as a global variable for testing +const mockDefaultRoles = Map({ + admin: "Administrator", + user: "User", +}); + +describe("generateUser function", () => { + beforeAll(() => { + process.env.NODE_ENV = "test"; // Set NODE_ENV for all tests + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should generate a user with all specified fields", () => { + // Override the `defaultRoles` map for testing + (defaultRoles as any).get = mockDefaultRoles.get.bind(mockDefaultRoles); + + const args = ["John", "Doe", "admin"]; + const user = generateUser(args); + + expect(user).toEqual({ + firstName: "John", + lastName: "Doe", + role: "Administrator", + }); + }); + + test("should default lastName and role when they are not provided", () => { + (defaultRoles as any).get = mockDefaultRoles.get.bind(mockDefaultRoles); + + const args = ["Jane"]; + const user = generateUser(args); + + expect(user).toEqual({ + firstName: "Jane", + lastName: "unknown", + role: "", + }); + }); + + test("should default all fields when args is empty", () => { + (defaultRoles as any).get = mockDefaultRoles.get.bind(mockDefaultRoles); + + const args: string[] = []; + const user = generateUser(args); + + expect(user).toEqual({ + firstName: "unknown", + lastName: "unknown", + role: "", + }); + }); +}); \ No newline at end of file diff --git a/example/javascriptlib/testing/1-test-suite/bar/test/src/bar/calculator.test.ts b/example/javascriptlib/testing/1-test-suite/bar/test/src/bar/calculator.test.ts index 3aeb3cf9324..b04d8ade03e 100644 --- a/example/javascriptlib/testing/1-test-suite/bar/test/src/bar/calculator.test.ts +++ b/example/javascriptlib/testing/1-test-suite/bar/test/src/bar/calculator.test.ts @@ -1,4 +1,4 @@ -import {Calculator} from 'bar/calculator'; +import {Calculator} from '../../../src/calculator'; describe('Calculator', () => { const calculator = new Calculator(); diff --git a/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.ts b/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.ts index 09bc7d16b00..b04d8ade03e 100644 --- a/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.ts +++ b/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.ts @@ -1,4 +1,4 @@ -import { Calculator } from 'baz/calculator'; +import {Calculator} from '../../../src/calculator'; describe('Calculator', () => { const calculator = new Calculator(); diff --git a/example/javascriptlib/testing/3-integration-suite-cypress/server/src/server.ts b/example/javascriptlib/testing/3-integration-suite-cypress/server/src/server.ts index d98689f6cb3..b0171e88913 100644 --- a/example/javascriptlib/testing/3-integration-suite-cypress/server/src/server.ts +++ b/example/javascriptlib/testing/3-integration-suite-cypress/server/src/server.ts @@ -1,7 +1,7 @@ import express, {Express} from 'express'; import cors from 'cors'; -const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle +const Resources: string = (process.env.RESOURCESDEST || "@server/resources") + "/build" // `RESOURCES` is generated on bundle const Client = require.resolve(`${Resources}/index.html`); const app: Express = express(); diff --git a/example/javascriptlib/testing/3-integration-suite-playwright/server/src/server.ts b/example/javascriptlib/testing/3-integration-suite-playwright/server/src/server.ts index d98689f6cb3..b0171e88913 100644 --- a/example/javascriptlib/testing/3-integration-suite-playwright/server/src/server.ts +++ b/example/javascriptlib/testing/3-integration-suite-playwright/server/src/server.ts @@ -1,7 +1,7 @@ import express, {Express} from 'express'; import cors from 'cors'; -const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle +const Resources: string = (process.env.RESOURCESDEST || "@server/resources") + "/build" // `RESOURCES` is generated on bundle const Client = require.resolve(`${Resources}/index.html`); const app: Express = express(); diff --git a/javascriptlib/src/mill/javascriptlib/PublishModule.scala b/javascriptlib/src/mill/javascriptlib/PublishModule.scala index 3177d916120..43db6dcb3b1 100644 --- a/javascriptlib/src/mill/javascriptlib/PublishModule.scala +++ b/javascriptlib/src/mill/javascriptlib/PublishModule.scala @@ -20,11 +20,9 @@ trait PublishModule extends TypeScriptModule { private def pubDeclarationOut: T[String] = Task { "declarations" } - override def mainFileName: T[String] = Task { s"${moduleDir.last}.js" } - // main file; defined with mainFileName def pubMain: T[String] = - Task { pubBundledOut() + "/src/" + mainFileName() } + Task { pubBundledOut() + "/src/" + mainFileName().replaceAll("\\.ts", ".js") } private def pubMainType: T[String] = Task { pubMain().replaceFirst(pubBundledOut(), pubDeclarationOut()).replaceAll("\\.js", ".d.ts") @@ -42,7 +40,7 @@ trait PublishModule extends TypeScriptModule { } private def pubTypesVersion: T[Map[String, Seq[String]]] = Task { - pubAllSources().map { source => + tscAllSources().map { source => val dist = source.replaceFirst("typescript", pubBundledOut()) val declarations = source.replaceFirst("typescript", pubDeclarationOut()) ("./" + dist).replaceAll("\\.ts", "") -> Seq(declarations.replaceAll("\\.ts", ".d.ts")) @@ -50,15 +48,14 @@ trait PublishModule extends TypeScriptModule { } // build package.json from publishMeta - // mv to publishDir.dest - def pubbPackageJson: T[PathRef] = Task { // PathRef + // mv to compileDir.dest + def pubPackageJson: T[PathRef] = Task { // PathRef def splitDeps(input: String): (String, String) = { input.split("@", 3).toList match { case first :: second :: tail if input.startsWith("@") => ("@" + first + "@" + second, tail.mkString) case first :: tail => (first, tail.mkString) - case _ => ??? } } @@ -74,156 +71,14 @@ trait PublishModule extends TypeScriptModule { devDependencies = transitiveNpmDeps().map { deps => splitDeps(deps) }.toMap ).toJsonClean - os.write.over(Task.dest / "package.json", updatedJson) + os.write.over(T.dest / "package.json", updatedJson) - PathRef(Task.dest) + PathRef(T.dest) } // Package.Json construction // Compilation Options - override def modulePaths: Task[Seq[(String, String)]] = Task.Anon { - val module = moduleDir.last - - Seq((s"$module/*", "typescript/src" + ":" + s"${pubDeclarationOut()}")) ++ - resources().map { rp => - val resourceRoot = rp.path.last - val result = ( - s"@$module/$resourceRoot/*", - resourceRoot match { - case s if s.contains(".dest") => rp.path.toString - case _ => s"typescript/$resourceRoot" - } - ) - result - } - } - - private def pubModDeps: T[Seq[String]] = Task { - moduleDeps.map { _.moduleDir.subRelativeTo(Task.workspace).segments.head }.distinct - } - - private def pubModDepsSources: T[Seq[PathRef]] = Task { - Task.traverse(moduleDeps)(_.sources)() - } - - private def pubBaseModeGenSources: T[Seq[PathRef]] = Task { - for { - pr <- generatedSources() - file <- os.walk(pr.path) - if file.ext == "ts" - } yield PathRef(file) - } - - private def pubModDepsGenSources: T[Seq[PathRef]] = Task { - Task.traverse(moduleDeps)(_.generatedSources)().flatMap { modSource => - val fileExt: Path => Boolean = _.ext == "ts" - for { - pr <- modSource - file <- os.walk(pr.path) - if fileExt(file) - } yield PathRef(file) - } - } - - // mv generated sources for base mod and its deps - private def pubGenSources: T[Unit] = Task { - val allGeneratedSources = pubBaseModeGenSources() ++ pubModDepsGenSources() - allGeneratedSources.foreach { target => - os.checker.withValue(os.Checker.Nop) { - val destination = publishDir().path / "typescript/generatedSources" / target.path.last - os.makeDir.all(destination / os.up) - os.copy.over(target.path, destination) - } - } - } - - private def pubCopyModDeps: T[Unit] = Task { - val targets = pubModDeps() - - targets.foreach { target => - val destination = publishDir().path / "typescript" / target - os.checker.withValue(os.Checker.Nop) { - os.makeDir.all(destination / os.up) - os.copy(Task.workspace / target, destination, mergeFolders = true) - } - } - } - - override def resources: T[Seq[PathRef]] = Task { - val modDepsResources = moduleDeps.map { x => PathRef(x.moduleDir / "resources") } - Seq(PathRef(moduleDir / "resources")) ++ modDepsResources - } - - /** - * Generate sources relative to publishDir / "typescript" - */ - private def pubAllSources: T[IndexedSeq[String]] = Task { - val project = Task.workspace.toString - val fileExt: Path => Boolean = _.ext == "ts" - (for { - source <- - os.walk(sources().path) ++ pubModDepsSources().toIndexedSeq.flatMap(pr => - os.walk(pr.path) - ).filter(fileExt) - } yield source.toString - .replaceFirst(moduleDir.toString, "typescript") - .replaceFirst( - project, - "typescript" - )) ++ (pubBaseModeGenSources() ++ pubModDepsGenSources()).map(pr => - "typescript/generatedSources/" + pr.path.last - ) - - } - - override def generatedSourcesPathsBuilder: T[Seq[(String, String)]] = Task { - Seq("@generated/*" -> "typescript/generatedSources") - } - - override def upstreamPathsBuilder: Task[Seq[(String, String)]] = Task.Anon { - - val upstreams = (for { - (res, mod) <- Task.traverse(moduleDeps)(_.resources)().zip(moduleDeps) - } yield { - val relative = mod.moduleDir.subRelativeTo(Task.workspace) - Seq(( - mod.moduleDir.subRelativeTo(Task.workspace).toString + "/*", - s"typescript/$relative/src:${pubDeclarationOut()}" - )) ++ - res.map { rp => - val resourceRoot = rp.path.last - val modName = mod.moduleDir.subRelativeTo(Task.workspace).toString - // nb: resources are be moved in bundled stage - ( - s"@$modName/$resourceRoot/*", - resourceRoot match { - case s if s.contains(".dest") => - rp.path.toString - case _ => s"typescript/$modName/$resourceRoot" - } - ) - } - - }).flatten - - upstreams - } - - override def typeRoots: T[ujson.Value] = Task { - ujson.Arr( - "node_modules/@types", - "declarations" - ) - } - - override def declarationDir: T[ujson.Value] = Task { - ujson.Str("declarations") - } - - override def compilerOptionsPaths: Task[Map[String, String]] = - Task.Anon { Map.empty[String, String] } - override def compilerOptions: T[Map[String, ujson.Value]] = Task { Map( "declarationMap" -> ujson.Bool(true), @@ -254,40 +109,17 @@ trait PublishModule extends TypeScriptModule { () } - private def pubSymLink: Task[Unit] = Task { + private def pubSymLink: Task[Unit] = Task.Anon { pubTsPatchInstall() // patch typescript compiler => use custom transformers - os.checker.withValue(os.Checker.Nop) { - os.symlink(publishDir().path / "node_modules", npmInstall().path / "node_modules") - if (os.exists(npmInstall().path / ".npmrc")) - os.symlink(publishDir().path / ".npmrc", npmInstall().path / ".npmrc") - } + if (os.exists(npmInstall().path / ".npmrc")) + os.symlink(T.dest / ".npmrc", npmInstall().path / ".npmrc") } override def compile: T[(PathRef, PathRef)] = Task { pubSymLink() - os.checker.withValue(os.Checker.Nop) { - os.write( - publishDir().path / "tsconfig.json", - ujson.Obj( - "compilerOptions" -> ujson.Obj.from( - compilerOptionsBuilder().toSeq ++ Seq("typeRoots" -> typeRoots()) - ), - "files" -> pubAllSources() - ) - ) - os.copy(moduleDir, publishDir().path / "typescript", mergeFolders = true) - pubCopyModDeps() - pubGenSources() - // Run type check, build declarations - os.call( - ("node", npmInstall().path / "node_modules/typescript/bin/tsc"), - cwd = publishDir().path - ) - } - (publishDir(), PathRef(publishDir().path / "typescript")) + super.compile() } - // Compilation Options // EsBuild - Copying Resources @@ -306,7 +138,7 @@ trait PublishModule extends TypeScriptModule { s""" copyStaticFiles({ | src: ${ujson.Str(rp.toString)}, | dest: ${ujson.Str( - publishDir().path.toString + "/" + pubBundledOut() + "/" + rp.last + compile()._1.path.toString + "/" + pubBundledOut() + "/" + rp.last )}, | dereference: true, | preserveTimestamps: true, @@ -348,38 +180,20 @@ trait PublishModule extends TypeScriptModule { } - override def bundle: T[PathRef] = Task { - val tsnode = npmInstall().path / "node_modules/.bin/ts-node" - val bundleScript = compile()._1.path / "build.ts" - val bundle = Task.dest / "bundle.js" - - os.write.over( - bundleScript, - bundleScriptBuilder() - ) - - os.call( - (tsnode, bundleScript), - stdout = os.Inherit, - cwd = compile()._1.path - ) - PathRef(bundle) - } - // EsBuild - END - // publishDir; is used to process and compile files for publishing - def publishDir: T[PathRef] = Task { PathRef(Task.dest) } - def publish(): Command[Unit] = Task.Command { // build package.json - os.move(pubbPackageJson().path / "package.json", publishDir().path / "package.json") + os.copy.over(pubPackageJson().path / "package.json", T.dest / "package.json") + + // bundled code for publishing + val bundled = bundle().path / os.up - // bundle code for publishing - bundle() + os.walk(bundled, skip = p => p.last == "node_modules" || p.last == "package-lock.json") + .foreach(p => os.copy.over(p, T.dest / p.relativeTo(bundled), createFolders = true)) // run npm publish - os.call(("npm", "publish"), stdout = os.Inherit, cwd = publishDir().path) + os.call(("npm", "publish"), stdout = os.Inherit, cwd = T.dest) () } diff --git a/javascriptlib/src/mill/javascriptlib/ReactScriptsModule.scala b/javascriptlib/src/mill/javascriptlib/ReactScriptsModule.scala index 0e7bc408453..26b245f69c6 100644 --- a/javascriptlib/src/mill/javascriptlib/ReactScriptsModule.scala +++ b/javascriptlib/src/mill/javascriptlib/ReactScriptsModule.scala @@ -27,7 +27,7 @@ trait ReactScriptsModule extends TypeScriptModule { ) } - override def sources: Target[PathRef] = Task.Source(moduleDir) + override def sources: Target[Seq[PathRef]] = Task.Sources(moduleDir) def packageJestOptions: Target[ujson.Obj] = Task { ujson.Obj( @@ -64,14 +64,14 @@ trait ReactScriptsModule extends TypeScriptModule { ) } - override def compilerOptionsPaths: Task[Map[String, String]] = - Task.Anon { Map("app/*" -> "src/app/*") } + override def compilerOptionsPaths: T[Map[String, String]] = + Task { Map("app/*" -> "src/app/*") } override def compilerOptionsBuilder: Task[Map[String, ujson.Value]] = Task.Anon { val npm = npmInstall().path val combinedPaths = compilerOptionsPaths() ++ Seq( "*" -> npm / "node_modules", - "typescript" -> npm / "node_modules/typescript" + "typescript" -> npm / "node_modules" / "typescript" ) val combinedCompilerOptions: Map[String, ujson.Value] = compilerOptions() ++ Map( @@ -84,7 +84,7 @@ trait ReactScriptsModule extends TypeScriptModule { // build react project via react scripts override def compile: T[(PathRef, PathRef)] = Task { // copy src files - os.copy(sources().path, Task.dest, mergeFolders = true) + sources().foreach(source => os.copy(source.path, Task.dest, mergeFolders = true)) copyNodeModules() // mk tsconfig.json @@ -92,7 +92,7 @@ trait ReactScriptsModule extends TypeScriptModule { Task.dest / "tsconfig.json", ujson.Obj( "compilerOptions" -> ujson.Obj.from(compilerOptionsBuilder().toSeq), - "include" -> ujson.Arr((sources().path / "src").toString) + "include" -> ujson.Arr(sources().map(source => (source.path / "src").toString)) ) ) diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index c58b0af0cb7..6f9093a35c6 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -38,21 +38,9 @@ object TestModule { coverageTask(Task.Anon { args })() } - // = '/out'; allow coverage resolve distributed source files. - // & define coverage files relative to . - private[TestModule] def coverageSetupSymlinks: Task[Unit] = Task.Anon { - os.checker.withValue(os.Checker.Nop) { - os.symlink(Task.workspace / "out/node_modules", npmInstall().path / "node_modules") - os.symlink(Task.workspace / "out/tsconfig.json", compile()._1.path / "tsconfig.json") - if (os.exists(compile()._1.path / ".nycrc")) - os.symlink(Task.workspace / "out/.nycrc", compile()._1.path / ".nycrc") - } - } - def istanbulNycrcConfigBuilder: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path val fileName = ".nycrc" - val config = compiled / fileName + val config = T.dest / fileName val customConfig = Task.workspace / fileName val content = @@ -67,12 +55,11 @@ object TestModule { |} |""".stripMargin - os.checker.withValue(os.Checker.Nop) { - if (!os.exists(customConfig)) os.write.over(config, content) - else os.copy.over(customConfig, config) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) + + os.write.over(config, content) - os.write.over(config, content) - } PathRef(config) } @@ -86,7 +73,7 @@ object TestModule { // coverage files - returnn coverage files directory def coverageFiles: T[PathRef] = Task { - val dir = Task.workspace / "out" / s"${moduleDeps.head}_coverage" + val dir = compile()._1.path / s"${moduleDeps.head}_coverage" println(s"coverage files: $dir") PathRef(dir) } @@ -96,16 +83,25 @@ object TestModule { override def upstreamPathsBuilder: T[Seq[(String, String)]] = Task { val stuUpstreams = for { - ((_, ts), mod) <- Task.traverse(moduleDeps)(_.compile)().zip(moduleDeps) - } yield ( - mod.moduleDir.subRelativeTo(Task.workspace).toString + "/test/utils/*", - (ts.path / "test/src/utils").toString - ) + mod <- recModuleDeps + } yield { + val prefix = mod.moduleName.replaceAll("\\.", "/") + ( + prefix + "/test/utils/*", + if (prefix == primeMod) + s"typescript/test/src/utils" + ":" + s"typescript/test/utils" + else s"typescript/$mod/test/src/utils" + ":" + s"typescript/$mod/test/utils" + ) + } - stuUpstreams ++ super.upstreamPathsBuilder() + stuUpstreams } - def getPathToTest: T[String] = Task { compile()._2.path.toString } + private def primeMod: String = outerModuleName.getOrElse("") + + def testDir: String = this.toString.split("\\.").tail.mkString + + def getPathToTest: T[String] = Task { compile()._2.path.toString + s"/$testDir" } } trait IntegrationSuite extends TypeScriptModule { @@ -128,13 +124,12 @@ object TestModule { override def compilerOptions: T[Map[String, ujson.Value]] = Task { - super.compilerOptions() + ("resolveJsonModule" -> ujson.Bool(true)) + Map("resolveJsonModule" -> ujson.Bool(true)) } def conf: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path val fileName = "jest.config.ts" - val config = compiled / fileName + val config = T.dest / fileName val customConfig = Task.workspace / fileName val content = @@ -152,38 +147,46 @@ object TestModule { | }, {}); | |export default { + |roots: ["/typescript"], |preset: 'ts-jest', |testEnvironment: 'node', - | testMatch: ['/**/**/**/*.test.ts', '/**/**/**/*.test.js'], + |testMatch: ["/typescript/$testDir/**/?(*.)+(spec|test).[jt]s?(x)"], |transform: ${ujson.Obj("^.+\\.(ts|tsx)$" -> ujson.Arr.from(Seq( ujson.Str("ts-jest"), ujson.Obj("tsconfig" -> "tsconfig.json") )))}, |moduleFileExtensions: ${ujson.Arr.from(Seq("ts", "tsx", "js", "jsx", "json", "node"))}, - |moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps) + |moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps, {prefix: ""}) |} |""".stripMargin - os.checker.withValue(os.Checker.Nop) { - if (!os.exists(customConfig)) os.write.over(config, content) - else os.copy.over(customConfig, config) - } + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) PathRef(config) } + override def compile: T[(PathRef, PathRef)] = Task { + conf() + coverageConf() + symLink() + os.copy(super.compile()._1.path, T.dest, mergeFolders = true) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) + } + private def runTest: T[TestResult] = Task { + val compileDir = compile()._1.path os.call( ( - "node", - npmInstall().path / "node_modules/jest/bin/jest.js", + "node_modules/.bin/jest", "--config", - conf().path, + compileDir / "jest.config.ts", getPathToTest() ), stdout = os.Inherit, env = forkEnv(), - cwd = compile()._1.path + cwd = compileDir ) () } @@ -194,9 +197,8 @@ object TestModule { // with coverage def coverageConf: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path - val fileName = "jest.config.ts" - val config = compiled / fileName + val fileName = "jest.config.coverage.ts" + val config = T.dest / fileName val customConfig = Task.workspace / fileName val content = @@ -214,18 +216,16 @@ object TestModule { | }, {}); | |export default { - |rootDir: ${ujson.Str((Task.workspace / "out").toString)}, + |roots: ["/typescript"], |preset: 'ts-jest', |testEnvironment: 'node', - |testMatch: [${ujson.Str( - s"/${compile()._2.path.subRelativeTo(Task.workspace / "out") / "src"}/**/*.test.ts" - )}], + |testMatch: ["/typescript/$testDir/**/?(*.)+(spec|test).[jt]s?(x)"], |transform: ${ujson.Obj("^.+\\.(ts|tsx)$" -> ujson.Arr.from(Seq( ujson.Str("ts-jest"), ujson.Obj("tsconfig" -> "tsconfig.json") )))}, |moduleFileExtensions: ${ujson.Arr.from(Seq("ts", "tsx", "js", "jsx", "json", "node"))}, - |moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps), + |moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps, {prefix: ""}), | |collectCoverage: true, |collectCoverageFrom: ${ujson.Arr.from(coverageDirs())}, @@ -235,35 +235,27 @@ object TestModule { |} |""".stripMargin - os.checker.withValue(os.Checker.Nop) { - if (!os.exists(customConfig)) os.write.over(config, content) - else os.copy.over(customConfig, config) - } + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) PathRef(config) } def runCoverage: T[TestResult] = Task { - coverageSetupSymlinks() - os.checker.withValue(os.Checker.Nop) { - os.call( - ( - "node", - "node_modules/jest/bin/jest.js", - "--config", - coverageConf().path, - "--coverage", - getPathToTest() - ), - stdout = os.Inherit, - env = forkEnv(), - cwd = Task.workspace / "out" - ) - - // remove symlink - os.remove(Task.workspace / "out/node_modules") - os.remove(Task.workspace / "out/tsconfig.json") - } + val compileDir = compile()._1.path + os.call( + ( + "node", + "node_modules/jest/bin/jest.js", + "--config", + compileDir / "jest.config.coverage.ts", + "--coverage", + getPathToTest() + ), + stdout = os.Inherit, + env = forkEnv(), + cwd = compileDir + ) () } @@ -285,10 +277,18 @@ object TestModule { override def getPathToTest: T[String] = Task { super.getPathToTest() + "/**/**/*.test.ts" } + override def compile: T[(PathRef, PathRef)] = Task { + conf() + istanbulNycrcConfigBuilder() + symLink() + os.copy(super.compile()._1.path, T.dest, mergeFolders = true) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) + } + // test-runner.js: run tests on ts files def conf: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path - val runner = compiled / "test-runner.js" + val runner = T.dest / "test-runner.js" val content = """|require('ts-node/register'); @@ -296,22 +296,22 @@ object TestModule { |require('mocha/bin/_mocha'); |""".stripMargin - os.checker.withValue(os.Checker.Nop) { - os.write.over(runner, content) - } + os.write.over(runner, content) + PathRef(runner) } private def runTest: T[Unit] = Task { + val compileDir = compile()._1.path os.call( ( "node", - conf().path, + compileDir / "test-runner.js", getPathToTest() ), stdout = os.Inherit, env = forkEnv(), - cwd = compile()._1.path + cwd = compileDir ) () } @@ -322,26 +322,18 @@ object TestModule { // with coverage def runCoverage: T[TestResult] = Task { - istanbulNycrcConfigBuilder() - coverageSetupSymlinks() + val compileDir = compile()._1.path os.call( ( "./node_modules/.bin/nyc", "node", - conf().path, + compileDir / "test-runner.js", getPathToTest() ), stdout = os.Inherit, env = forkEnv(), - cwd = Task.workspace / "out" + cwd = compileDir ) - - // remove symlink - os.checker.withValue(os.Checker.Nop) { - os.remove(Task.workspace / "out/node_modules") - os.remove(Task.workspace / "out/tsconfig.json") - os.remove(Task.workspace / "out/.nycrc") - } () } } @@ -360,7 +352,7 @@ object TestModule { override def compilerOptions: T[Map[String, ujson.Value]] = Task { - super.compilerOptions() + ( + Map( "target" -> ujson.Str("ESNext"), "module" -> ujson.Str("ESNext"), "moduleResolution" -> ujson.Str("Node"), @@ -372,45 +364,53 @@ object TestModule { } def conf: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path val fileName = "vitest.config.ts" - val config = compiled / fileName + val config = T.dest / fileName val customConfig = Task.workspace / fileName val content = - """|import { defineConfig } from 'vite'; - |import tsconfigPaths from 'vite-tsconfig-paths'; - | - |export default defineConfig({ - | plugins: [tsconfigPaths()], - | test: { - | globals: true, - | environment: 'node', - | include: ['**/**/*.test.ts'] - | }, - |}); - |""".stripMargin + s"""|import { defineConfig } from 'vite'; + |import tsconfigPaths from 'vite-tsconfig-paths'; + | + |export default defineConfig({ + | plugins: [tsconfigPaths()], + | test: { + | globals: true, + | environment: 'node', + | include: ['typescript/$testDir/**/**/*.test.ts'] + | }, + |}); + |""".stripMargin + + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) - os.checker.withValue(os.Checker.Nop) { - if (!os.exists(customConfig)) os.write.over(config, content) - else os.copy.over(customConfig, config) - } PathRef(config) } + override def compile: T[(PathRef, PathRef)] = Task { + conf() + coverageConf() + symLink() + os.copy(super.compile()._1.path, T.dest, mergeFolders = true) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) + } + private def runTest: T[TestResult] = Task { + val compileDir = compile()._1.path os.call( ( npmInstall().path / "node_modules/.bin/ts-node", npmInstall().path / "node_modules/.bin/vitest", "--run", "--config", - conf().path, + compileDir / "vitest.config.ts", getPathToTest() ), stdout = os.Inherit, env = forkEnv(), - cwd = compile()._1.path + cwd = compileDir ) () } @@ -421,11 +421,9 @@ object TestModule { // coverage def coverageConf: Task[PathRef] = Task.Anon { - val compiled = compile()._1.path - val fileName = "vitest.config.ts" - val config = compiled / fileName + val fileName = "vitest.config.coverage.ts" + val config = T.dest / fileName val customConfig = Task.workspace / fileName - val content = s"""|import { defineConfig } from 'vite'; |import tsconfigPaths from 'vite-tsconfig-paths'; @@ -435,9 +433,7 @@ object TestModule { | test: { | globals: true, | environment: 'node', - | include: [${ujson.Str( - s"${compile()._2.path.subRelativeTo(Task.workspace / "out") / "src"}/**/*.test.ts" - )}], + | include: ['typescript/$testDir/**/**/*.test.ts'], | coverage: { | provider: 'v8', | reporter: ['text', 'json', 'html'], @@ -451,34 +447,28 @@ object TestModule { |}); |""".stripMargin - os.checker.withValue(os.Checker.Nop) { - if (!os.exists(customConfig)) os.write.over(config, content) - else os.copy.over(customConfig, config) - } + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) + PathRef(config) } def runCoverage: T[TestResult] = Task { - coverageSetupSymlinks() + val compileDir = compile()._1.path os.call( ( npmInstall().path / "node_modules/.bin/ts-node", npmInstall().path / "node_modules/.bin/vitest", "--run", "--config", - coverageConf().path, + compileDir / "vitest.config.coverage.ts", "--coverage", getPathToTest() ), stdout = os.Inherit, env = forkEnv(), - cwd = Task.workspace / "out" + cwd = compileDir ) - // remove symlink - os.checker.withValue(os.Checker.Nop) { - os.remove(Task.workspace / "out/node_modules") - os.remove(Task.workspace / "out/tsconfig.json") - } () } @@ -498,7 +488,7 @@ object TestModule { override def compilerOptions: T[Map[String, ujson.Value]] = Task { - super.compilerOptions() + ( + Map( "target" -> ujson.Str("ES5"), "module" -> ujson.Str("commonjs"), "moduleResolution" -> ujson.Str("node"), @@ -507,43 +497,47 @@ object TestModule { } def conf: Task[PathRef] = Task.Anon { - val path = compile()._1.path / "jasmine.json" - os.checker.withValue(os.Checker.Nop) { - os.write.over( - 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) - ) + val path = T.dest / "jasmine.json" + os.write.over( + path, + ujson.write( + ujson.Obj( + "spec_dir" -> ujson.Str("typescript"), + "spec_files" -> ujson.Arr(ujson.Str(s"$testDir/**/*.test.ts")), + "stopSpecOnExpectationFailure" -> ujson.Bool(false), + "random" -> ujson.Bool(false) ) ) + ) - } PathRef(path) } - private def runTest: T[Unit] = Task { + override def compile: T[(PathRef, PathRef)] = Task { conf() + istanbulNycrcConfigBuilder() + symLink() + os.copy(super.compile()._1.path, T.dest, mergeFolders = true) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) + } + + private def runTest: T[Unit] = Task { val jasmine = "node_modules/jasmine/bin/jasmine.js" val tsnode = "node_modules/ts-node/register/transpile-only.js" val tsconfigPath = "node_modules/tsconfig-paths/register.js" - os.checker.withValue(os.Checker.Nop) { - os.call( - ( - "node", - jasmine, - "--config=jasmine.json", - s"--require=$tsnode", - s"--require=$tsconfigPath" - ), - stdout = os.Inherit, - env = forkEnv(), - cwd = compile()._1.path - ) - } + os.call( + ( + "node", + jasmine, + "--config=jasmine.json", + s"--require=$tsnode", + s"--require=$tsconfigPath" + ), + stdout = os.Inherit, + env = forkEnv(), + cwd = compile()._1.path + ) () } @@ -551,54 +545,25 @@ object TestModule { runTest() } - // with coverage - def coverageConf: T[PathRef] = Task { - val path = compile()._1.path / "jasmine.json" - val specDir = compile()._2.path.subRelativeTo(Task.workspace / "out") / "src" - os.checker.withValue(os.Checker.Nop) { - os.write.over( - path, - ujson.write( - ujson.Obj( - "spec_dir" -> ujson.Str(specDir.toString), - "spec_files" -> ujson.Arr(ujson.Str("**/*.test.ts")), - "stopSpecOnExpectationFailure" -> ujson.Bool(false), - "random" -> ujson.Bool(false) - ) - ) - ) - } - PathRef(path) - } - def runCoverage: T[TestResult] = Task { - os.checker.withValue(os.Checker.Nop) { - istanbulNycrcConfigBuilder() - coverageSetupSymlinks() - val jasmine = "node_modules/jasmine/bin/jasmine.js" - val tsnode = "node_modules/ts-node/register/transpile-only.js" - val tsconfigPath = "node_modules/tsconfig-paths/register.js" - val relConfigPath = coverageConf().path.subRelativeTo(Task.workspace / "out") - os.call( - ( - "./node_modules/.bin/nyc", - "node", - jasmine, - s"--config=$relConfigPath", - s"--require=$tsnode", - s"--require=$tsconfigPath" - ), - stdout = os.Inherit, - env = forkEnv(), - cwd = Task.workspace / "out" - ) + val jasmine = "node_modules/jasmine/bin/jasmine.js" + val tsnode = "node_modules/ts-node/register/transpile-only.js" + val tsconfigPath = "node_modules/tsconfig-paths/register.js" - // remove symlink - os.remove(Task.workspace / "out/node_modules") - os.remove(Task.workspace / "out/tsconfig.json") - os.remove(Task.workspace / "out/.nycrc") - () - } + os.call( + ( + "./node_modules/.bin/nyc", + "node", + jasmine, + s"--config=jasmine.json", + s"--require=$tsnode", + s"--require=$tsconfigPath" + ), + stdout = os.Inherit, + env = forkEnv(), + cwd = compile()._1.path + ) + () } } @@ -610,12 +575,12 @@ object TestModule { ) } - def testConfigSource: T[PathRef] = + def configSource: T[PathRef] = Task.Source(Task.workspace / "cypress.config.ts") override def compilerOptions: T[Map[String, ujson.Value]] = Task { - super.compilerOptions() + ( + Map( "target" -> ujson.Str("ES5"), "module" -> ujson.Str("ESNext"), "moduleResolution" -> ujson.Str("Node"), @@ -630,7 +595,7 @@ object TestModule { val tsc = npmInstall().path / "node_modules/.bin/tsc" os.call(( tsc, - testConfigSource().path.toString, + configSource().path.toString, "--outDir", compile()._1.path, "--target", @@ -693,16 +658,22 @@ object TestModule { ) } - def testConfigSource: T[PathRef] = + def configSource: T[PathRef] = Task.Source(Task.workspace / "playwright.config.ts") - private def copyConfig: Task[TestResult] = Task.Anon { - os.checker.withValue(os.Checker.Nop) { - os.copy.over( - testConfigSource().path, - compile()._1.path / "playwright.config.ts" - ) - } + def conf: Task[TestResult] = Task.Anon { + os.copy.over( + configSource().path, + T.dest / configSource().path.last + ) + } + + override def compile: T[(PathRef, PathRef)] = Task { + conf() + symLink() + os.copy(super.compile()._1.path, T.dest, mergeFolders = true) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) } private def runTest: T[TestResult] = Task { @@ -718,7 +689,6 @@ object TestModule { cwd = service.compile()._1.path ) - copyConfig() os.call( ( "node", diff --git a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala index 16efac71e45..f5167667e23 100644 --- a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala @@ -34,7 +34,8 @@ trait TsLintModule extends Module { T.workspace / "eslint.config.mjs", T.workspace / "eslint.config.cjs", T.workspace / "eslint.config.js", - T.workspace / ".prettierrc" + T.workspace / ".prettierrc", + T.workspace / ".prettierrc.json" ) private def resolvedFmtConfig: Task[Lint] = Task.Anon { diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 4f9d7820daf..fd277fe6ca8 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -3,13 +3,39 @@ package mill.javascriptlib import mill.* import os.* +import scala.annotation.tailrec import scala.util.Try trait TypeScriptModule extends Module { outer => + // custom module names + def moduleName: String = super.toString + + override def toString: String = moduleName + def moduleDeps: Seq[TypeScriptModule] = Nil + // recursively retrieve dependecies of all module dependencies + def recModuleDeps: Seq[TypeScriptModule] = { + @tailrec + def recModuleDeps_( + t: Seq[TypeScriptModule], + acc: Seq[TypeScriptModule] + ): Seq[TypeScriptModule] = { + if (t.isEmpty) acc + else { + val currentMod = t.head + val cmModDeps = currentMod.moduleDeps + recModuleDeps_(t.tail ++ cmModDeps, cmModDeps ++ acc) + } + } + + recModuleDeps_(moduleDeps, moduleDeps).distinct + } + def npmDeps: T[Seq[String]] = Task { Seq.empty[String] } + def enableEsm: T[Boolean] = Task { false } + def npmDevDeps: T[Seq[String]] = Task { Seq.empty[String] } def unmanagedDeps: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } @@ -18,13 +44,11 @@ trait TypeScriptModule extends Module { outer => Task.traverse(moduleDeps)(_.npmDeps)().flatten ++ npmDeps() } - def transitiveNpmDevDeps: T[Seq[String]] = Task { + def transitiveNpmDevDeps: T[Seq[String]] = Task.traverse(moduleDeps)(_.npmDevDeps)().flatten ++ npmDevDeps() - } - def transitiveUnmanagedDeps: T[Seq[PathRef]] = Task { + def transitiveUnmanagedDeps: T[Seq[PathRef]] = Task.traverse(moduleDeps)(_.unmanagedDeps)().flatten ++ unmanagedDeps() - } def npmInstall: T[PathRef] = Task { Try(os.copy.over(Task.workspace / ".npmrc", Task.dest / ".npmrc")).getOrElse(()) @@ -43,6 +67,7 @@ trait TypeScriptModule extends Module { outer => "@esbuild-plugins/tsconfig-paths@0.1.2", "esbuild-copy-static-files@0.1.0", "tsconfig-paths@4.2.0", + Seq(if (enableEsm()) Some("@swc/core@1.10.12") else None).flatten, transitiveNpmDeps(), transitiveNpmDevDeps(), transitiveUnmanagedDeps().map(_.path.toString) @@ -50,147 +75,323 @@ trait TypeScriptModule extends Module { outer => PathRef(Task.dest) } - def sources: T[PathRef] = Task.Source("src") + // sources :) + def sources: T[Seq[PathRef]] = Task.Sources(moduleDir / "src") def resources: T[Seq[PathRef]] = Task { Seq(PathRef(moduleDir / "resources")) } def generatedSources: T[Seq[PathRef]] = Task { Seq[PathRef]() } - def allSources: T[IndexedSeq[PathRef]] = - Task { - val fileExt: Path => Boolean = _.ext == "ts" - os.walk(sources().path).filter(fileExt).map(PathRef(_)) - } + private def tscModDepsResources: T[Seq[(PathRef, Seq[PathRef])]] = + Task + .traverse(recModuleDeps)(_.resources)() + .zip(recModuleDeps) + .map { case (r, m) => (PathRef(m.moduleDir), r) } - // Generate coverage directories for TestModule - private[javascriptlib] def coverageDirs: T[Seq[String]] = Task { - Task.traverse(moduleDeps)(mod => { - Task.Anon { - val comp = mod.compile() - val generated = mod.generatedSources() - val combined = Seq(comp._2) ++ generated + private def tscModDepsSources: T[Seq[(PathRef, Seq[PathRef])]] = + Task + .traverse(recModuleDeps)(_.sources)() + .zip(recModuleDeps) + .map { case (s, m) => (PathRef(m.moduleDir), s) } - combined.map(_.path.subRelativeTo(Task.workspace / "out").toString + "/**/**/*.ts") - } - })().flatten - } - - private[javascriptlib] def compiledSources: Task[IndexedSeq[PathRef]] = Task.Anon { - val generated = for { + private def tscCoreGenSources: T[Seq[PathRef]] = Task { + for { pr <- generatedSources() file <- os.walk(pr.path) if file.ext == "ts" - } yield file - - val typescriptOut = Task.dest / "typescript" - val core = for { - file <- allSources() - } yield file.path match { - case coreS if coreS.startsWith(moduleDir) => - // core - regular sources - // expected to exist within boundaries of `millSourcePath` - typescriptOut / coreS.relativeTo(moduleDir) - case otherS => - // sources defined by a modified source task - // mv to compile source - val destinationDir = Task.dest / "typescript/src" - val fileName = otherS.last - val destinationFile = destinationDir / fileName - os.makeDir.all(destinationDir) - os.copy.over(otherS, destinationFile) - destinationFile + } yield PathRef(file) + } + + private def tscModDepsGenSources: T[Seq[(PathRef, Seq[PathRef])]] = + Task + .traverse(recModuleDeps)(_.generatedSources)() + .zip(recModuleDeps) + .map { case (s, m) => + ( + PathRef(m.moduleDir), + s.flatMap { genS => os.walk(genS.path).filter(_.ext == "ts").map(PathRef(_)) } + ) + } + + def tscCopySources: Task[Unit] = Task.Anon { + val dest = T.dest / "typescript" + val coreTarget = dest / "src" + + if (!os.exists(dest)) os.makeDir.all(dest) + + // Copy everything except "build.mill" and the "/out" directory from Task.workspace + os.walk(moduleDir, skip = _.last == "out").filter(_.last != "build.mill").foreach { path => + val relativePath = path.relativeTo(moduleDir) + val destination = dest / relativePath + + if (os.isDir(path)) os.makeDir.all(destination) + else os.copy.over(path, destination) } - // symlink node_modules for generated sources - // remove `node_module/` package import format - generatedSources().foreach(source => - os.call( - ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), - cwd = source.path + object IsSrcDirectory { + def unapply(path: Path): Option[Path] = + if (os.isDir(path) && path.last == "src") Some(path) else None + } + + // handle copy `/out///src` directories + def copySrcDirectory(srcDir: Path, targetDir: Path): Unit = { + os.list(srcDir).foreach { srcFile => + os.copy.over(srcFile, targetDir / srcFile.last, createFolders = true) + } + } + + // handle sources generated in /out (eg: `out//sources.dest`) + def copyOutSources(sources: Seq[PathRef], target: Path): Unit = { + + def copySource(source: PathRef): Unit = { + if (!source.path.startsWith(Task.workspace / "out")) () // Guard clause + else os.list(source.path).foreach { + case IsSrcDirectory(srcDir) => copySrcDirectory(srcDir, target) + case path => os.copy.over(path, target / path.last, createFolders = true) + } + } + + sources.foreach(copySource) + } + + // core + copyOutSources(sources(), coreTarget) + + // mod deps + tscModDepsSources() + .foreach { case (mod, sources_) => + copyOutSources(sources_, dest / mod.path.relativeTo(Task.workspace) / "src") + } + + } + + private def tscCopyModDeps: Task[Unit] = Task.Anon { + val targets = + recModuleDeps.map { _.moduleDir.subRelativeTo(Task.workspace).segments.head }.distinct + + targets.foreach { target => + val destination = T.dest / "typescript" / target + os.makeDir.all(destination / os.up) + os.copy( + Task.workspace / target, + destination, + mergeFolders = true ) - ) + } + } - (core ++ generated).map(PathRef(_)) + // mv generated sources for base mod and its deps + private def tscCopyGenSources: Task[Unit] = Task.Anon { + def copyGeneratedSources(sourcePath: os.Path, destinationPath: os.Path): Unit = { + os.makeDir.all(destinationPath / os.up) + os.copy.over(sourcePath, destinationPath) + } + + tscCoreGenSources().foreach { target => + val destination = T.dest / "typescript" / "generatedSources" / target.path.last + copyGeneratedSources(target.path, destination) + } + + tscModDepsGenSources().foreach { case (mod, source_) => + source_.foreach { target => + val modDir = mod.path.relativeTo(Task.workspace) + val destination = T.dest / "typescript" / modDir / "generatedSources" / target.path.last + copyGeneratedSources(target.path, destination) + } + } } + /** + * Link all external resources eg: `out//resources.dest` + * to `moduleDir / src / resources` + */ + private def tscLinkResources: Task[Unit] = Task.Anon { + val dest = T.dest / "typescript/resources" + if (!os.exists(dest)) os.makeDir.all(dest) + + val externalResource: PathRef => Boolean = p => + p.path.startsWith(Task.workspace / "out") && + os.exists(p.path) && + os.isDir(p.path) + + def linkResource(resources_ : Seq[PathRef], dest: Path): Unit = { + resources_ + .filter(externalResource) + .flatMap(p => os.list(p.path)) // Get all items from valid directories + .foreach(item => os.copy.over(item, dest / item.last, createFolders = true)) + } + + linkResource(resources(), dest) + + tscModDepsResources().foreach { case (mod, r) => + val modDir = mod.path.relativeTo(Task.workspace) + val modDest = T.dest / "typescript" / modDir / "resources" + if (!os.exists(modDest)) os.makeDir.all(modDest) + linkResource(r, modDest) + } + } + + def tscAllSources: T[IndexedSeq[String]] = Task { + val fileExt: Path => Boolean = _.ext == "ts" + + def relativeToTS(base: Path, path: Path, prefix: Option[String] = None): Option[String] = + prefix match { + case Some(value) => Some(s"typescript/$value/${path.relativeTo(base)}") + case None => Some(s"typescript/${path.relativeTo(base)}") + } + + def handleOutTS(base: Path, path: Path, prefix: Option[String] = None): Option[String] = { + val segments = path.relativeTo(base).segments + val externalSourceDir = base / segments.head + prefix match { + case Some(_) => relativeToTS(externalSourceDir, path, prefix) + case None => relativeToTS(externalSourceDir, path) + } + } + + def relativeToTypescript(base: Path, path: Path, prefix: String): Option[String] = + Some(s"typescript/$prefix/${path.relativeTo(base)}") + + def handleOutPath(base: Path, path: Path, prefix: String): Option[String] = { + val segments = path.relativeTo(base).segments + val externalSourceDir = base / segments.head + relativeToTypescript(externalSourceDir, path, prefix) + } + + val cores = sources() + .toIndexedSeq + .flatMap(pr => if (isDir(pr.path)) os.walk(pr.path) else Seq(pr.path)) + .filter(fileExt) + .flatMap { p => + p match { + case _ if p.startsWith(moduleDir) && !p.startsWith(moduleDir / "out") => + relativeToTS(moduleDir, p) + case _ if p.startsWith(Task.workspace / "out" / moduleName) => + handleOutTS(Task.workspace / "out" / moduleName, p) + case _ if p.startsWith(Task.workspace / "out") => + handleOutTS(Task.workspace / "out", p) + case _ => None + } + } + + val modDeps = tscModDepsSources() + .toIndexedSeq + .flatMap { case (mod, source_) => + source_ + .flatMap(pr => if (isDir(pr.path)) os.walk(pr.path) else Seq(pr.path)) + .filter(fileExt) + .flatMap { p => + val modDir = mod.path.relativeTo(Task.workspace) + val modmoduleDir = Task.workspace / modDir + val modOutPath = Task.workspace / "out" / modDir + + p match { + case _ if p.startsWith(modmoduleDir) => + relativeToTypescript(modmoduleDir, p, modDir.toString) + case _ if p.startsWith(modOutPath) => + handleOutPath(modOutPath, p, modDir.toString) + case _ => None + } + + } + } + + val coreGenSources = tscCoreGenSources() + .toIndexedSeq + .map(pr => "typescript/generatedSources/" + pr.path.last) + + val modGenSources = tscModDepsGenSources() + .toIndexedSeq + .flatMap { case (mod, source_) => + val modDir = mod.path.relativeTo(Task.workspace) + source_.map(s"typescript/$modDir/generatedSources/" + _.path.last) + } + + cores ++ modDeps ++ coreGenSources ++ modGenSources + + } + + // sources + + // compile :) + def declarationDir: T[ujson.Value] = Task { ujson.Str("declarations") } + // specify tsconfig.compilerOptions def compilerOptions: T[Map[String, ujson.Value]] = Task { Map( + "skipLibCheck" -> ujson.Bool(true), "esModuleInterop" -> ujson.Bool(true), "declaration" -> ujson.Bool(true), - "emitDeclarationOnly" -> ujson.Bool(true) - ) + "emitDeclarationOnly" -> ujson.Bool(true), + "baseUrl" -> ujson.Str("."), + "rootDir" -> ujson.Str("typescript") + ) ++ Seq( + if (enableEsm()) Some("module" -> ujson.Str("nodenext")) else None, + if (enableEsm()) Some("moduleResolution" -> ujson.Str("nodenext")) else None + ).flatten } // specify tsconfig.compilerOptions.Paths - def compilerOptionsPaths: Task[Map[String, String]] = Task.Anon { Map.empty[String, String] } - - def upstreams: T[(PathRef, PathRef, Seq[PathRef])] = Task { - val comp = compile() - - (comp._1, comp._2, resources()) - } - - def upstreamPathsBuilder: Task[Seq[(String, String)]] = Task.Anon { + def compilerOptionsPaths: T[Map[String, String]] = Task { Map.empty[String, String] } + def upstreamPathsBuilder: T[Seq[(String, String)]] = Task { val upstreams = (for { - ((comp, ts, res), mod) <- Task.traverse(moduleDeps)(_.upstreams)().zip(moduleDeps) + (res, mod) <- Task.traverse(recModuleDeps)(_.resources)().zip(recModuleDeps) } yield { - Seq(( - mod.moduleDir.subRelativeTo(Task.workspace).toString + "/*", - (ts.path / "src").toString + ":" + (comp.path / "declarations").toString - )) ++ - res.map { rp => - val resourceRoot = rp.path.last - ( - "@" + mod.moduleDir.subRelativeTo(Task.workspace).toString + s"/$resourceRoot/*", - resourceRoot match { - case s if s.contains(".dest") => - rp.path.toString - case _ => - (ts.path / resourceRoot).toString - } - ) + val prefix = mod.moduleName.replaceAll("\\.", "/") + val customResource: PathRef => Boolean = pathRef => + pathRef.path.startsWith(Task.workspace / "out" / mod.moduleName) || !pathRef.path.equals( + mod.moduleDir / "src" / "resources" + ) + + val customResources = res + .filter(customResource) + .map { pathRef => + val resourceRoot = pathRef.path.last + s"@$prefix/$resourceRoot/*" -> s"typescript/$prefix/$resourceRoot" } + Seq( + ( + prefix + "/*", + s"typescript/$prefix/src" + ":" + s"declarations/$prefix" + ), + (s"@$prefix/resources/*", s"typescript/$prefix/resources") + ) ++ customResources + }).flatten upstreams } - def modulePaths: Task[Seq[(String, String)]] = Task.Anon { - val module = moduleDir.last - val typescriptOut = Task.dest / "typescript" - val declarationsOut = Task.dest / "declarations" - - Seq((s"$module/*", (typescriptOut / "src").toString + ":" + declarationsOut.toString)) ++ - resources().map { rp => - val resourceRoot = rp.path.last - val result = ( - s"@$module/$resourceRoot/*", - resourceRoot match { - case s if s.contains(".dest") => rp.path.toString - case _ => - (typescriptOut / resourceRoot).toString - } - ) - result + def modulePaths: T[Seq[(String, String)]] = Task { + val customResource: PathRef => Boolean = pathRef => + pathRef.path.startsWith(Task.workspace / "out") || !pathRef.path.equals( + moduleDir / "src" / "resources" + ) + + val customResources = resources() + .filter(customResource) + .map { pathRef => + val resourceRoot = pathRef.path.last + s"@$moduleName/$resourceRoot/*" -> s"typescript/$resourceRoot" } + + Seq( + (s"$moduleName/*", "typescript/src" + ":" + "declarations"), + (s"@$moduleName/resources/*", "typescript/resources") + ) ++ customResources } def typeRoots: Task[ujson.Value] = Task.Anon { ujson.Arr( "node_modules/@types", - (Task.dest / "declarations").toString + "declarations" ) } - def declarationDir: Task[ujson.Value] = Task.Anon { - ujson.Str((Task.dest / "declarations").toString) - } - def generatedSourcesPathsBuilder: T[Seq[(String, String)]] = Task { - generatedSources().map(p => ("@generated/*", p.path.toString)) + Seq(("@generated/*", "typescript/generatedSources")) } def compilerOptionsBuilder: Task[Map[String, ujson.Value]] = Task.Anon { @@ -212,35 +413,55 @@ trait TypeScriptModule extends Module { outer => combinedCompilerOptions } - // create a symlink for node_modules in compile.dest - // removes need for node_modules prefix in import statements `node_modules/` - // import * as somepackage from "" - private def symLink: Task[Unit] = Task.Anon { - os.checker.withValue(os.Checker.Nop) { - os.symlink(Task.dest / "node_modules", npmInstall().path / "node_modules") - os.symlink(Task.dest / "package-lock.json", npmInstall().path / "package-lock.json") - } + /** + * create a symlink for node_modules in compile.dest + * removes need for node_modules prefix in import statements `node_modules/` + * import * as somepackage from "" + */ + private[javascriptlib] def symLink: Task[Unit] = Task.Anon { + if (!os.exists(T.dest / "node_modules")) + os.symlink(T.dest / "node_modules", npmInstall().path / "node_modules") + + if (!os.exists(T.dest / "package-lock.json")) + os.symlink(T.dest / "package-lock.json", npmInstall().path / "package-lock.json") } def compile: T[(PathRef, PathRef)] = Task { symLink() os.write( - Task.dest / "tsconfig.json", + T.dest / "tsconfig.json", ujson.Obj( "compilerOptions" -> ujson.Obj.from( compilerOptionsBuilder().toSeq ++ Seq("typeRoots" -> typeRoots()) ), - "files" -> compiledSources().map(_.path.toString) + "files" -> tscAllSources() ) ) - os.copy(moduleDir, Task.dest / "typescript", mergeFolders = true) - os.call(npmInstall().path / "node_modules/typescript/bin/tsc", cwd = Task.dest) + tscCopySources() + tscCopyModDeps() + tscCopyGenSources() + tscLinkResources() - (PathRef(Task.dest), PathRef(Task.dest / "typescript")) + // Run type check, build declarations + os.call("node_modules/typescript/bin/tsc", cwd = T.dest) + (PathRef(T.dest), PathRef(T.dest / "typescript")) } - def mainFileName: T[String] = Task { s"${moduleDir.last}.ts" } + // compile + + // additional ts-config options + def options: T[Map[String, ujson.Value]] = Task { + Seq( + Some("exclude" -> ujson.Arr.from(Seq("node_modules", "**/node_modules/*"))), + if (enableEsm()) Some("ts-node" -> ujson.Obj("esm" -> ujson.True, "swc" -> ujson.True)) + else None + ).flatten.toMap: Map[String, ujson.Value] + } + + // Execution :) + + def mainFileName: T[String] = Task { s"$moduleName.ts" } def mainFilePath: T[Path] = Task { compile()._2.path / "src" / mainFileName() } @@ -252,32 +473,50 @@ trait TypeScriptModule extends Module { outer => def run(args: mill.define.Args): Command[CommandResult] = Task.Command { val mainFile = mainFilePath() - val tsnode = npmInstall().path / "node_modules/.bin/ts-node" - val tsconfigpaths = npmInstall().path / "node_modules/tsconfig-paths/register" val env = forkEnv() - val execFlags: Seq[String] = executionFlags().map { - case (key, "") => s"--$key" - case (key, value) => s"--$key=$value" - }.toSeq + val tsnode: String = + if (enableEsm()) "ts-node/esm" + else (npmInstall().path / "node_modules/.bin/ts-node").toString + + val tsconfigPaths: Seq[String] = + Seq( + if (enableEsm()) Some("tsconfig-paths/register") + else Some((npmInstall().path / "node_modules/tsconfig-paths/register").toString), + if (enableEsm()) Some("--no-warnings=ExperimentalWarning") else None + ).flatten + + val flags: Seq[String] = + (executionFlags() + .map { + case (key, "") => Some(s"--$key") + case (key, value) => Some(s"--$key=$value") + case _ => None + }.toSeq ++ Seq(if (enableEsm()) Some("--loader") else None)).flatten + + val runnable: Shellable = ( + "node", + flags, + tsnode, + "-r", + tsconfigPaths, + mainFile, + computedArgs(), + args.value + ) os.call( - ( - "node", - execFlags, - tsnode, - "-r", - tsconfigpaths, - mainFile, - computedArgs(), - args.value - ), + runnable, stdout = os.Inherit, env = env, cwd = compile()._1.path ) } + // Execution + + // bundle :) + def bundleExternal: T[Seq[ujson.Value]] = Task { Seq(ujson.Str("fs"), ujson.Str("path")) } def bundleFlags: T[Map[String, ujson.Value]] = Task { @@ -288,8 +527,10 @@ trait TypeScriptModule extends Module { outer => ) } - // configure esbuild with @esbuild-plugins/tsconfig-paths - // include .d.ts files + /** + * configure esbuild with `@esbuild-plugins/tsconfig-paths` + * include .d.ts files + */ def bundleScriptBuilder: Task[String] = Task.Anon { val bundle = (Task.dest / "bundle.js").toString val rps = resources().map { p => p.path }.filter(os.exists) @@ -350,26 +591,140 @@ trait TypeScriptModule extends Module { outer => } def bundle: T[PathRef] = Task { + symLink() val env = forkEnv() val tsnode = npmInstall().path / "node_modules/.bin/ts-node" - val bundleScript = compile()._1.path / "build.ts" val bundle = Task.dest / "bundle.js" + val out = compile()._1.path - os.checker.withValue(os.Checker.Nop) { - os.write.over(bundleScript, bundleScriptBuilder()) + os.walk(out, skip = p => p.last == "node_modules" || p.last == "package-lock.json") + .foreach(p => os.copy.over(p, T.dest / p.relativeTo(out), createFolders = true)) - os.call( - (tsnode, bundleScript), - stdout = os.Inherit, - env = env, - cwd = compile()._1.path - ) - } + os.write( + T.dest / "build.ts", + bundleScriptBuilder() + ) + + os.call( + (tsnode, T.dest / "build.ts"), + stdout = os.Inherit, + env = env, + cwd = T.dest + ) PathRef(bundle) } + // bundle + + // test methods :) + + private[javascriptlib] def coverageDirs: T[Seq[String]] = Task { Seq.empty[String] } + + private[javascriptlib] def outerModuleName: Option[String] = None + trait TypeScriptTests extends TypeScriptModule { override def moduleDeps: Seq[TypeScriptModule] = Seq(outer) ++ outer.moduleDeps + + override def outerModuleName: Option[String] = Some(outer.moduleName) + + override def declarationDir: T[ujson.Value] = Task { + ujson.Str((outer.compile()._1.path / "declarations").toString) + } + + override def sources: T[Seq[PathRef]] = Task.Sources(moduleDir) + + def allSources: T[IndexedSeq[PathRef]] = + Task { + val fileExt: Path => Boolean = _.ext == "ts" + sources() + .toIndexedSeq + .flatMap(pr => os.walk(pr.path)) + .filter(fileExt) + .map(PathRef(_)) + } + + def testResourcesPath: T[Seq[(String, String)]] = Task { + Seq(( + "@test/resources/*", + s"typescript/test/resources" + )) + } + + override def compilerOptionsBuilder: T[Map[String, ujson.Value]] = Task { + val combinedPaths = + outer.upstreamPathsBuilder() ++ + upstreamPathsBuilder() ++ + outer.generatedSourcesPathsBuilder() ++ + outer.modulePaths() ++ + outer.compilerOptionsPaths().toSeq ++ + testResourcesPath() + + val combinedCompilerOptions: Map[String, ujson.Value] = + outer.compilerOptions() ++ compilerOptions() ++ Map( + "declarationDir" -> outer.declarationDir(), + "paths" -> ujson.Obj.from(combinedPaths.map { case (k, v) => + val splitValues = + v.split(":").map(s => s"$s/*") // Split by ":" and append "/*" to each part + (k, ujson.Arr.from(splitValues)) + }) + ) + + combinedCompilerOptions + } + + override def compile: T[(PathRef, PathRef)] = Task { + val out = outer.compile() + + val files: IndexedSeq[String] = + allSources() + .map(x => "typescript/test/" + x.path.relativeTo(moduleDir)) ++ + outer.tscAllSources() + + // mv compile to compile + os.list(out._1.path) + .filter(item => + item.last != "tsconfig.json" && + item.last != "package-lock.json" && + !(item.last == "node_modules" && os.isDir( + item + )) + ) + .foreach(item => os.copy.over(item, T.dest / item.last, createFolders = true)) + + // inject test specific tsconfig into tsconfig + os.write( + Task.dest / "tsconfig.json", + ujson.Obj( + "compilerOptions" -> ujson.Obj.from( + compilerOptionsBuilder().toSeq ++ Seq("typeRoots" -> outer.typeRoots()) + ), + "files" -> files + ) + ) + + (PathRef(T.dest), PathRef(T.dest / "typescript")) + } + + override def npmInstall: T[PathRef] = Task { + os.call( + ( + "npm", + "install", + "--userconfig", + ".npmrc", + "--save-dev", + transitiveNpmDeps(), + transitiveNpmDevDeps(), + transitiveUnmanagedDeps().map(_.path.toString) + ), + cwd = outer.npmInstall().path + ) + outer.npmInstall() + } + + override private[javascriptlib] def coverageDirs: T[Seq[String]] = + Task { outer.tscAllSources() } + } } diff --git a/website/docs/modules/ROOT/pages/extending/example-typescript-support.adoc b/website/docs/modules/ROOT/pages/extending/example-typescript-support.adoc index e38f14ef863..905a22e9e06 100644 --- a/website/docs/modules/ROOT/pages/extending/example-typescript-support.adoc +++ b/website/docs/modules/ROOT/pages/extending/example-typescript-support.adoc @@ -33,7 +33,7 @@ include::partial$example/extending/typescript/4-npm-deps-bundle.adoc[] As mentioned earlier, the `TypeScriptModule` examples on this page are meant for demo purposes: to show what it looks like to add support in Mill for a new programming language toolchain. It would take significantly more work to flesh out -the featureset and performance of `TypeScriptModule` to be usable in a real world +the feature set and performance of `TypeScriptModule` to be usable in a real world build. But this should be enough to get you started working with Mill to add support to any language you need: whether it's TypeScript or some other language, most programming language toolchains have similar concepts of `compile`, `run`, `bundle`, etc. diff --git a/website/docs/modules/ROOT/pages/javascriptlib/module-config.adoc b/website/docs/modules/ROOT/pages/javascriptlib/module-config.adoc index bb4da576b1d..747de78a872 100644 --- a/website/docs/modules/ROOT/pages/javascriptlib/module-config.adoc +++ b/website/docs/modules/ROOT/pages/javascriptlib/module-config.adoc @@ -25,3 +25,7 @@ include::partial$example/javascriptlib/module/5-resources.adoc[] == Bundling Configuration include::partial$example/javascriptlib/module/6-executable-config.adoc[] + +== Root Module + +include::partial$example/javascriptlib/module/7-root-module.adoc[]