From 6679218317d68e734c93d5e4535a79d6dfe5ff99 Mon Sep 17 00:00:00 2001 From: Monye David Date: Sun, 12 Jan 2025 02:44:07 +0100 Subject: [PATCH 01/11] example/javascriptlib/publishing --- build.mill | 1 + .../ROOT/pages/javascriptlib/publishing.adoc | 14 + .../basic/1-simple/foo/src/foo.ts | 2 +- .../1-simple/foo/test/src/foo/foo.test.ts | 2 +- .../basic/1-simple/jest.config.ts | 3 +- .../basic/3-custom-build-logic/jest.config.ts | 3 +- .../basic/4-multi-modules/foo/bar/src/bar.ts | 2 +- .../basic/4-multi-modules/jest.config.ts | 3 +- .../5-client-server-hello/jest.config.ts | 3 +- .../6-client-server-realistic/jest.config.ts | 3 +- .../dependencies/1-npm-deps/foo/src/foo.ts | 2 +- .../2-unmanaged-packages/foo/src/foo.ts | 2 +- .../foo/src/foo.ts | 2 +- .../4-repository-config/foo/src/foo.ts | 4 +- .../module/2-custom-tasks/build.mill | 2 +- .../module/5-resources/jest.config.ts | 3 +- .../javascriptlib/publishing/1-publish/.npmrc | 2 + .../publishing/1-publish/README.md | 1 + .../publishing/1-publish/build.mill | 57 ++ .../publishing/1-publish/foo/src/foo.ts | 11 + .../publishing/2-realistic/.npmrc | 2 + .../publishing/2-realistic/Readme.md | 1 + .../publishing/2-realistic/build.mill | 128 +++++ .../foo/bar/resources/file-foo-bar.text | 1 + .../publishing/2-realistic/foo/bar/src/bar.ts | 4 + .../2-realistic/foo/resources/file-foo.text | 1 + .../publishing/2-realistic/foo/src/foo.ts | 5 + .../publishing/2-realistic/jest.config.ts | 32 ++ .../2-realistic/qux/resources/file-qux.text | 1 + .../2-realistic/qux/src/generate_user.ts | 15 + .../publishing/2-realistic/qux/src/qux.ts | 16 + .../2-realistic/qux/test/src/qux/qux.test.ts | 57 ++ .../testing/1-test-suite/jest.config.ts | 3 +- .../testing/2-test-deps/bar/src/bar.ts | 2 +- .../2-test-deps/bar/test/src/bar/bar.test.ts | 5 - .../testing/2-test-deps/build.mill | 2 +- .../testing/2-test-deps/jest.config.ts | 3 +- .../cypress.config.ts | 2 +- .../playwright.config.ts | 2 +- .../test/src/server/playwright/app.test.ts | 2 +- example/package.mill | 1 + javascriptlib/package.mill | 2 +- .../mill/javascriptlib/PublishModule.scala | 539 ++++++++++++++++++ .../src/mill/javascriptlib/TestModule.scala | 8 +- .../mill/javascriptlib/TypeScriptModule.scala | 105 ++-- .../src/mill/javascriptlib/package.scala | 18 + main/package.mill | 1 + 47 files changed, 1004 insertions(+), 76 deletions(-) create mode 100644 docs/modules/ROOT/pages/javascriptlib/publishing.adoc create mode 100644 example/javascriptlib/publishing/1-publish/.npmrc create mode 100644 example/javascriptlib/publishing/1-publish/README.md create mode 100644 example/javascriptlib/publishing/1-publish/build.mill create mode 100644 example/javascriptlib/publishing/1-publish/foo/src/foo.ts create mode 100644 example/javascriptlib/publishing/2-realistic/.npmrc create mode 100644 example/javascriptlib/publishing/2-realistic/Readme.md create mode 100644 example/javascriptlib/publishing/2-realistic/build.mill create mode 100644 example/javascriptlib/publishing/2-realistic/foo/bar/resources/file-foo-bar.text create mode 100644 example/javascriptlib/publishing/2-realistic/foo/bar/src/bar.ts create mode 100644 example/javascriptlib/publishing/2-realistic/foo/resources/file-foo.text create mode 100644 example/javascriptlib/publishing/2-realistic/foo/src/foo.ts create mode 100644 example/javascriptlib/publishing/2-realistic/jest.config.ts create mode 100644 example/javascriptlib/publishing/2-realistic/qux/resources/file-qux.text create mode 100644 example/javascriptlib/publishing/2-realistic/qux/src/generate_user.ts create mode 100644 example/javascriptlib/publishing/2-realistic/qux/src/qux.ts create mode 100644 example/javascriptlib/publishing/2-realistic/qux/test/src/qux/qux.test.ts create mode 100644 javascriptlib/src/mill/javascriptlib/PublishModule.scala create mode 100644 javascriptlib/src/mill/javascriptlib/package.scala diff --git a/build.mill b/build.mill index 924c437c527..7fae8823dbf 100644 --- a/build.mill +++ b/build.mill @@ -150,6 +150,7 @@ object Deps { val commonsIo = ivy"commons-io:commons-io:2.18.0" val log4j2Core = ivy"org.apache.logging.log4j:log4j-core:2.24.3" val osLib = ivy"com.lihaoyi::os-lib:0.11.4-M4" + val sbtIo = ivy"org.scala-sbt::io:1.9.0" val pprint = ivy"com.lihaoyi::pprint:0.9.0" val mainargs = ivy"com.lihaoyi::mainargs:0.7.6" val millModuledefsVersion = "0.11.2" diff --git a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc new file mode 100644 index 00000000000..39b49fbcd88 --- /dev/null +++ b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc @@ -0,0 +1,14 @@ += Python Packaging & Publishing +:page-aliases: Publishing_Typescript_Projects.adoc + +include::partial$gtag-config.adoc[] + +This page will discuss common topics around publishing your Typescript projects for others to use. + +== Simple publish + +include::partial$example/javascriptlib/publishing/1-publish-module.adoc[] + +== Realistic publish + +include::partial$example/javascriptlib/publishing/2-publish-module-advanced.adoc[] diff --git a/example/javascriptlib/basic/1-simple/foo/src/foo.ts b/example/javascriptlib/basic/1-simple/foo/src/foo.ts index 82bef21f248..cda766f1e8a 100644 --- a/example/javascriptlib/basic/1-simple/foo/src/foo.ts +++ b/example/javascriptlib/basic/1-simple/foo/src/foo.ts @@ -1,4 +1,4 @@ -import {Map} from 'node_modules/immutable'; +import {Map} from 'immutable'; interface User { firstName: string diff --git a/example/javascriptlib/basic/1-simple/foo/test/src/foo/foo.test.ts b/example/javascriptlib/basic/1-simple/foo/test/src/foo/foo.test.ts index 850b0ff5790..0a359977e7c 100644 --- a/example/javascriptlib/basic/1-simple/foo/test/src/foo/foo.test.ts +++ b/example/javascriptlib/basic/1-simple/foo/test/src/foo/foo.test.ts @@ -1,5 +1,5 @@ import {generateUser, defaultRoles} from "foo/foo"; -import {Map} from 'node_modules/immutable'; +import {Map} from 'immutable'; // Define the type roles object type RoleKeys = "admin" | "user"; diff --git a/example/javascriptlib/basic/1-simple/jest.config.ts b/example/javascriptlib/basic/1-simple/jest.config.ts index 8d03baaa2fc..eaa2b8e671f 100644 --- a/example/javascriptlib/basic/1-simple/jest.config.ts +++ b/example/javascriptlib/basic/1-simple/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts b/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts index 8d03baaa2fc..eaa2b8e671f 100644 --- a/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts +++ b/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/basic/4-multi-modules/foo/bar/src/bar.ts b/example/javascriptlib/basic/4-multi-modules/foo/bar/src/bar.ts index f2b19f11277..c27f8049dd5 100644 --- a/example/javascriptlib/basic/4-multi-modules/foo/bar/src/bar.ts +++ b/example/javascriptlib/basic/4-multi-modules/foo/bar/src/bar.ts @@ -1,4 +1,4 @@ -import {Map} from 'node_modules/immutable'; +import {Map} from 'immutable'; const defaultRoles: Map = Map({ prof: "Professor" }); export default defaultRoles \ No newline at end of file diff --git a/example/javascriptlib/basic/4-multi-modules/jest.config.ts b/example/javascriptlib/basic/4-multi-modules/jest.config.ts index 8d03baaa2fc..eaa2b8e671f 100644 --- a/example/javascriptlib/basic/4-multi-modules/jest.config.ts +++ b/example/javascriptlib/basic/4-multi-modules/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/basic/5-client-server-hello/jest.config.ts b/example/javascriptlib/basic/5-client-server-hello/jest.config.ts index 8d03baaa2fc..eaa2b8e671f 100644 --- a/example/javascriptlib/basic/5-client-server-hello/jest.config.ts +++ b/example/javascriptlib/basic/5-client-server-hello/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts b/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts index 8d03baaa2fc..eaa2b8e671f 100644 --- a/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts +++ b/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/dependencies/1-npm-deps/foo/src/foo.ts b/example/javascriptlib/dependencies/1-npm-deps/foo/src/foo.ts index a1476a95194..c3072e78991 100644 --- a/example/javascriptlib/dependencies/1-npm-deps/foo/src/foo.ts +++ b/example/javascriptlib/dependencies/1-npm-deps/foo/src/foo.ts @@ -1,4 +1,4 @@ -import {sortBy} from 'node_modules/lodash' +import {sortBy} from 'lodash' const args = process.argv.slice(2); console.log(`Sorted with lodash: [${sortBy(args).join(",")}]`); diff --git a/example/javascriptlib/dependencies/2-unmanaged-packages/foo/src/foo.ts b/example/javascriptlib/dependencies/2-unmanaged-packages/foo/src/foo.ts index 003724fca64..4f7b44705b9 100644 --- a/example/javascriptlib/dependencies/2-unmanaged-packages/foo/src/foo.ts +++ b/example/javascriptlib/dependencies/2-unmanaged-packages/foo/src/foo.ts @@ -1,4 +1,4 @@ -import {sortBy} from 'node_modules/lodash' +import {sortBy} from 'lodash' const args = process.argv.slice(2); console.log(`Sorted with lodash: [${sortBy(args).join(",")}]`); \ No newline at end of file diff --git a/example/javascriptlib/dependencies/3-downloading-unmanaged-packages/foo/src/foo.ts b/example/javascriptlib/dependencies/3-downloading-unmanaged-packages/foo/src/foo.ts index 003724fca64..4f7b44705b9 100644 --- a/example/javascriptlib/dependencies/3-downloading-unmanaged-packages/foo/src/foo.ts +++ b/example/javascriptlib/dependencies/3-downloading-unmanaged-packages/foo/src/foo.ts @@ -1,4 +1,4 @@ -import {sortBy} from 'node_modules/lodash' +import {sortBy} from 'lodash' const args = process.argv.slice(2); console.log(`Sorted with lodash: [${sortBy(args).join(",")}]`); \ No newline at end of file diff --git a/example/javascriptlib/dependencies/4-repository-config/foo/src/foo.ts b/example/javascriptlib/dependencies/4-repository-config/foo/src/foo.ts index 72f74a80901..aef906e005d 100644 --- a/example/javascriptlib/dependencies/4-repository-config/foo/src/foo.ts +++ b/example/javascriptlib/dependencies/4-repository-config/foo/src/foo.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; -import {sortBy} from 'node_modules/lodash'; -const PackageLock = require.resolve(`package-lock.json`); +import {sortBy} from 'lodash'; +const PackageLock = require.resolve(`../../package-lock.json`); const args = process.argv.slice(2); console.log(`Sorted with lodash: [${sortBy(args).join(",")}]`); diff --git a/example/javascriptlib/module/2-custom-tasks/build.mill b/example/javascriptlib/module/2-custom-tasks/build.mill index 45489a3cb68..23a4628afcf 100644 --- a/example/javascriptlib/module/2-custom-tasks/build.mill +++ b/example/javascriptlib/module/2-custom-tasks/build.mill @@ -66,5 +66,5 @@ object foo extends TypeScriptModule { > mill foo.run "Hello World!" Bar.value: 123 text: Hello World! -Line count: 13 +Line count: 9 */ diff --git a/example/javascriptlib/module/5-resources/jest.config.ts b/example/javascriptlib/module/5-resources/jest.config.ts index c08e297619d..416ef18ea8a 100644 --- a/example/javascriptlib/module/5-resources/jest.config.ts +++ b/example/javascriptlib/module/5-resources/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/publishing/1-publish/.npmrc b/example/javascriptlib/publishing/1-publish/.npmrc new file mode 100644 index 00000000000..0623701f212 --- /dev/null +++ b/example/javascriptlib/publishing/1-publish/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org +//registry.npmjs.org/:_authToken=... \ No newline at end of file diff --git a/example/javascriptlib/publishing/1-publish/README.md b/example/javascriptlib/publishing/1-publish/README.md new file mode 100644 index 00000000000..85c6ea52808 --- /dev/null +++ b/example/javascriptlib/publishing/1-publish/README.md @@ -0,0 +1 @@ +Greet! \ No newline at end of file diff --git a/example/javascriptlib/publishing/1-publish/build.mill b/example/javascriptlib/publishing/1-publish/build.mill new file mode 100644 index 00000000000..30062f775f2 --- /dev/null +++ b/example/javascriptlib/publishing/1-publish/build.mill @@ -0,0 +1,57 @@ +package build + +import mill._, javascriptlib._ + +object foo extends PublishModule { + def publishMeta = Task { + PublishMeta( + name = "mill-simple", + version = "1.0.0", + description = "A simple Node.js command-line tool", + files = Seq("README.md"), + bin = Map( + "greet" -> "src/foo.js" + ) + ) + } +} + +// You'll need to define some metadata in the `publishMeta` tasks. +// This metadata is roughly equivalent to what you'd define in a +// https://docs.npmjs.com/cli/v11/configuring-npm/package-json[`package.json` file]. + +// Important `package.json` info required for publishing are auto-magically generated. +// `main` file is by default the file defined in the `mainFileName` task, it can be modified if needed. + +// The package.json generated for this simple publish: + +//// SNIPPET:BUILD +//{ +// "name": "mill-simple", +// "version": "1.0.0", +// "description": "A simple Node.js command-line tool", +// "license": "MIT", +// "main": "dist/src/foo.js", +// "types": "declarations/src/foo.d.ts", +// "files": ["README.md", "dist", "declarations"], +// "bin": { +// "greet": "./dist/src/foo.js" +// }, +// "exports": { +// ".": "./dist/src/foo.js" +// }, +// "typesVersions": { +// "*": { +// "./dist/src/foo": ["declarations/src/foo.d.ts"] +// } +// } +//} +//// SNIPPET:END + +/** Usage +> npm i -g mill-simple # install the executable file globally +... + +> greet "James Bond" +Hello James Bond! +*/ diff --git a/example/javascriptlib/publishing/1-publish/foo/src/foo.ts b/example/javascriptlib/publishing/1-publish/foo/src/foo.ts new file mode 100644 index 00000000000..dfb1a60c9b7 --- /dev/null +++ b/example/javascriptlib/publishing/1-publish/foo/src/foo.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +export class Foo { + static hello() { + const args = process.argv.slice(2); + const name = args[0] || 'unknown'; + return `Hello ${name}!` + } +} + +console.log(Foo.hello()); \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/.npmrc b/example/javascriptlib/publishing/2-realistic/.npmrc new file mode 100644 index 00000000000..0623701f212 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org +//registry.npmjs.org/:_authToken=... \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/Readme.md b/example/javascriptlib/publishing/2-realistic/Readme.md new file mode 100644 index 00000000000..fa01d1544fe --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/Readme.md @@ -0,0 +1 @@ +# Mill - advance publish module \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill new file mode 100644 index 00000000000..3100ad9b544 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -0,0 +1,128 @@ +package build + +import mill._, javascriptlib._ + +object foo extends TypeScriptModule { + def generatedSources = Task { + os.write( + Task.dest / "foo.generated.ts", + s"""export default class FooGen { + | static value: number = 123 + |} + """.stripMargin + ) + + Seq(PathRef(Task.dest)) + } + + object bar extends TypeScriptModule { + def generatedSources = Task { + os.write( + Task.dest / "bar.generated.ts", + s"""export default class Bar { + | static value: number = 123 + |} + """.stripMargin + ) + + Seq(PathRef(Task.dest)) + } + + def npmDeps = Seq("immutable@4.3.7") + } + +} + +class qux extends TypeScriptModule { + def moduleDeps = Seq(foo, foo.bar) + + def generatedSources = Task { + os.write( + Task.dest / "qux.generated.ts", + s"""export default class QuxGen { + | static value: number = 123 + |} + """.stripMargin + ) + + Seq(PathRef(Task.dest)) + } + + object test extends TypeScriptTests with TestModule.Jest +} + +object quxmod extends qux + +object quxpub extends qux with PublishModule { + def exports = Map( + "./qux/generate_user" -> "src/generate_user.js", + "./foo" -> "foo/src/foo.js", + "./foo/bar" -> "foo/bar/src/bar.js" + ) + + def publishMeta = PublishMeta( + name = "mill-realistic", + version = "1.0.3", + description = "A simple Node.js command-line tool", + files = Seq("README.md"), + bin = Map( + "qux" -> "src/qux.js" + ) + ) +} + +// In this example, we define multiple exports for our application with the `export` task +// The package.json generated for this simple publish: + +//// SNIPPET:BUILD +// { +// "name": "mill-realistic", +// "version": "1.0.3", +// "description": "A simple Node.js command-line tool", +// "license": "MIT", +// "main": "dist/src/qux.js", +// "types": "declarations/src/qux.d.ts", +// "files": [ +// "README.md", +// "dist", +// "declarations" +// ], +// "bin": { +// "qux": "./dist/src/qux.js" +// }, +// "dependencies": { +// "immutable": "4.3.7" +// }, +// "devDependencies": { +// "immutable": "4.3.7" +// }, +// "exports": { +// ".": "./dist/src/qux.js", +// "./qux/generate_user": "./dist/src/generate_user.js", +// "./foo": "./dist/foo/src/foo.js", +// "./foo/bar": "./dist/foo/bar/src/bar.js" +// }, +// "typesVersions": { +// "*": { +// "./dist/src/qux": [ +// "declarations/src/qux.d.ts" +// ], ... +// } +// } +//} +//// SNIPPET:END + +/** Usage +> mill quxmod.test +PASS .../qux.test.ts +... + +> npm i -g mill-realistic +... + +> qux James Bond prof +{ prof: 'Professor' } +prof +Professor +Hello James Bond Professor +*/ diff --git a/example/javascriptlib/publishing/2-realistic/foo/bar/resources/file-foo-bar.text b/example/javascriptlib/publishing/2-realistic/foo/bar/resources/file-foo-bar.text new file mode 100644 index 00000000000..6e5d8179407 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/foo/bar/resources/file-foo-bar.text @@ -0,0 +1 @@ +From Foo/Bar \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/foo/bar/src/bar.ts b/example/javascriptlib/publishing/2-realistic/foo/bar/src/bar.ts new file mode 100644 index 00000000000..c27f8049dd5 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/foo/bar/src/bar.ts @@ -0,0 +1,4 @@ +import {Map} from 'immutable'; + +const defaultRoles: Map = Map({ prof: "Professor" }); +export default defaultRoles \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/foo/resources/file-foo.text b/example/javascriptlib/publishing/2-realistic/foo/resources/file-foo.text new file mode 100644 index 00000000000..73ae3fb0f2a --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/foo/resources/file-foo.text @@ -0,0 +1 @@ +From Foo \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/foo/src/foo.ts b/example/javascriptlib/publishing/2-realistic/foo/src/foo.ts new file mode 100644 index 00000000000..7414dfd9739 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/foo/src/foo.ts @@ -0,0 +1,5 @@ +export default interface User { + firstName: string + lastName: string + role: string +} diff --git a/example/javascriptlib/publishing/2-realistic/jest.config.ts b/example/javascriptlib/publishing/2-realistic/jest.config.ts new file mode 100644 index 00000000000..528b0886da9 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/jest.config.ts @@ -0,0 +1,32 @@ +// @ts-nocheck +import {pathsToModuleNameMapper} from 'ts-jest'; +import {compilerOptions} from './tsconfig.json'; // this is a generated file. + +// Remove unwanted keys +const moduleDeps = {...compilerOptions.paths}; +delete moduleDeps['*']; +delete moduleDeps['typeRoots']; + +// moduleNameMapper evaluates in order they appear, +// sortedModuleDeps makes sure more specific path mappings always appear first +const sortedModuleDeps = Object.keys(moduleDeps) + .sort((a, b) => b.length - a.length) // Sort by descending length + .reduce((acc, key) => { + acc[key] = moduleDeps[key]; + return acc; + }, {}); + +export default { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: [ + '/**/**/**/*.test.ts', + '/**/**/**/*.test.js', + ], + transform: { + '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }], + '^.+\\.(js|jsx)$': 'babel-jest', // Use babel-jest for JS/JSX files + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps) // use absolute paths generated in tsconfig. +}; \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/qux/resources/file-qux.text b/example/javascriptlib/publishing/2-realistic/qux/resources/file-qux.text new file mode 100644 index 00000000000..a8cc392d0b6 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/qux/resources/file-qux.text @@ -0,0 +1 @@ +From Qux \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/qux/src/generate_user.ts b/example/javascriptlib/publishing/2-realistic/qux/src/generate_user.ts new file mode 100644 index 00000000000..97c5a0ca29e --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/qux/src/generate_user.ts @@ -0,0 +1,15 @@ +import User from "foo/foo"; +import DefaultRoles from "foo/bar/bar"; + +/** + * 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", // Default to "unknown" if first-name not found + lastName: args[1] || "unknown", // Default to "unknown" if last-name not found + role: DefaultRoles.get(args[2], ""), // Default to empty string if role not found + }; +} \ No newline at end of file diff --git a/example/javascriptlib/publishing/2-realistic/qux/src/qux.ts b/example/javascriptlib/publishing/2-realistic/qux/src/qux.ts new file mode 100644 index 00000000000..7ce964aab63 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/qux/src/qux.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +import User from "foo/foo"; +import DefaultRoles from "foo/bar/bar"; +import {generateUser} from "./generate_user"; + +// Main CLI logic +if (require.main === module) { + const args = process.argv.slice(2); // Skip 'node' and script name + const user: 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/publishing/2-realistic/qux/test/src/qux/qux.test.ts b/example/javascriptlib/publishing/2-realistic/qux/test/src/qux/qux.test.ts new file mode 100644 index 00000000000..0c4909b69a1 --- /dev/null +++ b/example/javascriptlib/publishing/2-realistic/qux/test/src/qux/qux.test.ts @@ -0,0 +1,57 @@ +import { generateUser } from "qux/generate_user"; + +// Define the type roles object +type RoleKeys = "admin" | "user"; +type Roles = { + [key in RoleKeys]: string; +}; + +// Mock the defaultRoles.get method +jest.mock("foo/bar/bar", () => ({ + __esModule: true, // This ensures compatibility with ES module imports + default: { + get: jest.fn((role: string, defaultValue: string) => { + const roles: Roles = { admin: "Administrator", user: "User" }; + return roles[role as RoleKeys] || defaultValue; + }), + }, +})); + +describe("generateUser function", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should generate a user with all specified fields", () => { + 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", () => { + const args = ["Jane"]; + const user = generateUser(args); + + expect(user).toEqual({ + firstName: "Jane", + lastName: "unknown", + role: "", + }); + }); + + test("should default all fields when args is empty", () => { + 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/jest.config.ts b/example/javascriptlib/testing/1-test-suite/jest.config.ts index 97e37c4df11..f54bc06a50c 100644 --- a/example/javascriptlib/testing/1-test-suite/jest.config.ts +++ b/example/javascriptlib/testing/1-test-suite/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/testing/2-test-deps/bar/src/bar.ts b/example/javascriptlib/testing/2-test-deps/bar/src/bar.ts index 799b95098ef..fefd951dc57 100644 --- a/example/javascriptlib/testing/2-test-deps/bar/src/bar.ts +++ b/example/javascriptlib/testing/2-test-deps/bar/src/bar.ts @@ -1,4 +1,4 @@ -import {Map} from 'node_modules/immutable'; +import {Map} from 'immutable'; export default interface User { firstName: string diff --git a/example/javascriptlib/testing/2-test-deps/bar/test/src/bar/bar.test.ts b/example/javascriptlib/testing/2-test-deps/bar/test/src/bar/bar.test.ts index ba9340d803d..0b23783cecd 100644 --- a/example/javascriptlib/testing/2-test-deps/bar/test/src/bar/bar.test.ts +++ b/example/javascriptlib/testing/2-test-deps/bar/test/src/bar/bar.test.ts @@ -1,5 +1,4 @@ import {defaultRoles} from 'bar/bar'; -import {Map} from 'node_modules/immutable'; import {compare} from 'bar/test/utils/bar.tests.utils'; test('defaultRoles map should have correct values', () => { @@ -7,8 +6,4 @@ test('defaultRoles map should have correct values', () => { expect(compare(defaultRoles.get('prof'), 'Professor')).toBeTruthy() expect(compare(defaultRoles.get('student'), 'Student')).toBeTruthy() expect(compare(defaultRoles.has('admin'), false)).toBeTruthy() -}); - -test('defaultRoles map should be an instance of Immutable Map', () => { - expect(compare((defaultRoles instanceof Map), true)).toBeTruthy() }); \ No newline at end of file diff --git a/example/javascriptlib/testing/2-test-deps/build.mill b/example/javascriptlib/testing/2-test-deps/build.mill index fb3c7e6edfb..3e6de2af90f 100644 --- a/example/javascriptlib/testing/2-test-deps/build.mill +++ b/example/javascriptlib/testing/2-test-deps/build.mill @@ -32,6 +32,6 @@ Tests:...3 passed, 3 total... PASS .../bar.test.ts ... Test Suites:...1 passed, 1 total... -Tests:...2 passed, 2 total... +Tests:...1 passed, 1 total... ... */ diff --git a/example/javascriptlib/testing/2-test-deps/jest.config.ts b/example/javascriptlib/testing/2-test-deps/jest.config.ts index 97e37c4df11..f54bc06a50c 100644 --- a/example/javascriptlib/testing/2-test-deps/jest.config.ts +++ b/example/javascriptlib/testing/2-test-deps/jest.config.ts @@ -1,5 +1,4 @@ -// @ts-nocheck -import {pathsToModuleNameMapper} from 'node_modules/ts-jest'; +import {pathsToModuleNameMapper} from 'ts-jest'; import {compilerOptions} from './tsconfig.json'; // this is a generated file. // Remove unwanted keys diff --git a/example/javascriptlib/testing/3-integration-suite-cypress/cypress.config.ts b/example/javascriptlib/testing/3-integration-suite-cypress/cypress.config.ts index 5dfc7c5f894..639e608ff8a 100644 --- a/example/javascriptlib/testing/3-integration-suite-cypress/cypress.config.ts +++ b/example/javascriptlib/testing/3-integration-suite-cypress/cypress.config.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { defineConfig } from 'node_modules/cypress'; +import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { diff --git a/example/javascriptlib/testing/3-integration-suite-playwright/playwright.config.ts b/example/javascriptlib/testing/3-integration-suite-playwright/playwright.config.ts index 65dd628d2a9..0266fc92332 100644 --- a/example/javascriptlib/testing/3-integration-suite-playwright/playwright.config.ts +++ b/example/javascriptlib/testing/3-integration-suite-playwright/playwright.config.ts @@ -1,5 +1,5 @@ import {defineConfig} from '@playwright/test'; -import * as glob from 'node_modules/glob'; +import * as glob from 'glob'; import * as path from 'path'; const testFiles = glob.sync('**/playwright/*.test.ts', {absolute: true}); diff --git a/example/javascriptlib/testing/3-integration-suite-playwright/server/test/src/server/playwright/app.test.ts b/example/javascriptlib/testing/3-integration-suite-playwright/server/test/src/server/playwright/app.test.ts index 3f2e3144fe9..cf10cba7591 100644 --- a/example/javascriptlib/testing/3-integration-suite-playwright/server/test/src/server/playwright/app.test.ts +++ b/example/javascriptlib/testing/3-integration-suite-playwright/server/test/src/server/playwright/app.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from 'node_modules/@playwright/test'; +import { test, expect } from '@playwright/test'; test.describe('React App', () => { test('displays the heading', async ({ page }) => { diff --git a/example/package.mill b/example/package.mill index dcbf2d0af15..27e288cefa2 100644 --- a/example/package.mill +++ b/example/package.mill @@ -66,6 +66,7 @@ object `package` extends RootModule with Module { object testing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "testing")) object module extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "module")) object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies")) + object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing")) } object pythonlib extends Module { object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic")) diff --git a/javascriptlib/package.mill b/javascriptlib/package.mill index 41d40772b00..45ff38ab1ec 100644 --- a/javascriptlib/package.mill +++ b/javascriptlib/package.mill @@ -6,5 +6,5 @@ import mill._ // TODO change MillPublishScalaModule to MillStableScalaModule after mill version with pythonlib is released, // because currently there is no previous artifact version object `package` extends RootModule with build.MillPublishScalaModule { - def moduleDeps = Seq(build.main) + def moduleDeps = Seq(build.main, build.scalalib) } \ No newline at end of file diff --git a/javascriptlib/src/mill/javascriptlib/PublishModule.scala b/javascriptlib/src/mill/javascriptlib/PublishModule.scala new file mode 100644 index 00000000000..14b01fc0b0e --- /dev/null +++ b/javascriptlib/src/mill/javascriptlib/PublishModule.scala @@ -0,0 +1,539 @@ +package mill.javascriptlib + +import mill.* +import os.* + +trait PublishModule extends TypeScriptModule { + + /** + * Metadata about your project, required to publish. + * + * This is an equivalent of a `package.json` + */ + def publishMeta: T[PublishModule.PublishMeta] + + override def npmDevDeps: T[Seq[String]] = + Task { Seq("glob@^10.4.5", "ts-patch@3.3.0", "typescript-transform-paths@3.5.3") } + + def bundledOut: T[String] = Task { "dist" } + + private def declarationOut: T[String] = Task { "declarations" } + + override def mainFileName: T[String] = Task { s"${millSourcePath.last}.js" } + + // main file; defined with mainFileName + def main: T[String] = + Task { bundledOut() + "/src/" + mainFileName() } + + private def mainType: T[String] = Task { + main().replaceFirst(bundledOut(), declarationOut()).replaceAll("\\.js", ".d.ts") + } + + // Define exports for the package + // by default: mainFile is exported + // use this to define other exports + def exports: T[Map[String, String]] = Task { Map.empty[String, String] } + + private def mkExports: T[Map[String, PublishModule.ExportEntry]] = Task { + exports().map { case (key, value) => + key -> PublishModule.Export("./" + bundledOut() + "/" + value) + } + } + + private def mkTypesVersion: T[Map[String, Seq[String]]] = Task { + pubAllSources().map { source => + val dist = source.replaceFirst("typescript", bundledOut()) + val declarations = source.replaceFirst("typescript", declarationOut()) + ("./" + dist).replaceAll("\\.ts", "") -> Seq(declarations.replaceAll("\\.ts", ".d.ts")) + }.toMap + } + + // build package.json from publishMeta + // mv to publishDir.dest + def packageJson: T[Unit] = 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) + } + } + + val json = publishMeta() + val updatedJson = json.copy( + files = json.files ++ Seq(bundledOut(), declarationOut()), + main = main(), + types = mainType(), + exports = Map("." -> PublishModule.Export("./" + main())) ++ mkExports(), + bin = json.bin.map { case (k, v) => (k, "./" + bundledOut() + "/" + v) }, + typesVersions = mkTypesVersion(), + dependencies = transitiveNpmDeps().map { deps => splitDeps(deps) }.toMap, + devDependencies = transitiveNpmDeps().map { deps => splitDeps(deps) }.toMap + ).toJsonClean + + os.write.over(publishDir().path / "package.json", updatedJson) + } + + // Package.Json construction + + // Compilation Options + override def modulePaths: Task[Seq[(String, String)]] = Task.Anon { + val module = millSourcePath.last + + Seq((s"$module/*", "typescript/src" + ":" + s"${declarationOut()}")) ++ + 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 { x => + x.millSourcePath.subRelativeTo(Task.workspace).toString.split("/").head + }.distinct + } + + private def pubModDepsSources: T[Seq[PathRef]] = Task { + for { + modSource <- Task.traverse(moduleDeps)(_.sources) + } yield modSource + } + + private def pubModDepsGenSources: T[Seq[PathRef]] = Task { + (for { + modSource <- Task.traverse(moduleDeps)(_.generatedSources)() + } yield { + val fileExt: Path => Boolean = _.ext == "ts" + for { + pr <- modSource + file <- os.walk(pr.path) + if fileExt(file) + } yield PathRef(file) + }).flatten + } + + // mv generated sources for base mod and its deps + private def pubMvGenSources: T[Unit] = Task { + val fileExt: Path => Boolean = _.ext == "ts" + val baseModGenS = for { + pr <- generatedSources() + file <- os.walk(pr.path) + if fileExt(file) + } yield PathRef(file) + + val allGeneratedSources = baseModGenS ++ pubModDepsGenSources() + allGeneratedSources.foreach { target => + val destination = publishDir().path / "typescript" / "generatedSources" / target.path.last + os.makeDir.all(destination / os.up) + os.copy.over( + target.path, + destination + ) + } + } + + private def pubCpModDeps: T[Unit] = Task { + val cpTargets = pubModDeps() + + cpTargets.foreach { target => + val destination = publishDir().path / "typescript" / target + 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.millSourcePath / "resources") } + Seq(PathRef(millSourcePath / "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(millSourcePath.toString, "typescript") + .replaceFirst(project, "typescript")) ++ 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 { + Seq(( + mod.millSourcePath.subRelativeTo(Task.workspace).toString + "/*", + s"typescript/${mod.millSourcePath.subRelativeTo(Task.workspace).toString}/src" + ":" + s"${declarationOut()}" + )) ++ + res.map { rp => + val resourceRoot = rp.path.last + val modName = mod.millSourcePath.subRelativeTo(Task.workspace).toString + // nb: resources are be moved in bundled stage + ( + "@" + modName + s"/$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.Anon { + ujson.Arr( + "node_modules/@types", + "declarations" + ) + } + + override def declarationDir: T[ujson.Value] = Task.Anon { + 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), + "esModuleInterop" -> ujson.Bool(true), + "baseUrl" -> ujson.Str("."), + "rootDir" -> ujson.Str("typescript"), + "declaration" -> ujson.Bool(true), + "outDir" -> ujson.Str(bundledOut()), + "plugins" -> ujson.Arr( + ujson.Obj("transform" -> "typescript-transform-paths"), + ujson.Obj( + "transform" -> "typescript-transform-paths", + "afterDeclarations" -> true + ) + ), + "moduleResolution" -> ujson.Str("node"), + "module" -> ujson.Str("CommonJS"), + "target" -> ujson.Str("ES2020") + ) + } + + // patch typescript + private def tsPatchInstall: T[Unit] = Task { + os.call( + ("node", npmInstall().path / "node_modules/ts-patch/bin/ts-patch", "install"), + cwd = npmInstall().path + ) + () + } + + override def symlinkNodeModules: Task[Unit] = Task { + tsPatchInstall() // patch typescript compiler => use custom transformers + os.call( + ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), + cwd = publishDir().path + ) + if (os.exists(npmInstall().path / ".npmrc")) os.call( + ("ln", "-s", npmInstall().path.toString + "/.npmrc", ".npmrc"), + cwd = publishDir().path + ) + () + } + + override def compile: T[(PathRef, PathRef)] = Task { + symlinkNodeModules() + os.write( + publishDir().path / "tsconfig.json", + ujson.Obj( + "compilerOptions" -> ujson.Obj.from( + compilerOptionsBuilder().toSeq ++ Seq("typeRoots" -> typeRoots()) + ), + "files" -> pubAllSources() + ) + ) + os.copy(millSourcePath, publishDir().path / "typescript", mergeFolders = true) + pubCpModDeps() + pubMvGenSources() + // Run type check, build declarations + os.call( + ("node", npmInstall().path / "node_modules/typescript/bin/tsc"), + cwd = publishDir().path + ) + (publishDir(), PathRef(publishDir().path / "typescript")) + } + + // Compilation Options + + // EsBuild - Copying Resources + // we use tsc to compile to js & ts-patch to transform ts-paths + // esbuild script serves to copy resources to dist/ + override def bundleScriptBuilder: Task[String] = Task.Anon { + def envName(input: String): String = { + val cleaned = input.replaceAll("[^a-zA-Z0-9]", "") // remove special characters + cleaned.toUpperCase + } + + val copyPluginCode = + s""" + | plugins: [ + | ${resources().map { p => p.path }.filter(os.exists).map { rp => + s""" copyStaticFiles({ + | src: ${ujson.Str(rp.toString)}, + | dest: ${ujson.Str( + publishDir().path.toString + "/" + bundledOut() + "/" + rp.last + )}, + | dereference: true, + | preserveTimestamps: true, + | recursive: true, + | }), + """.stripMargin + }.mkString("\n")} + | TsconfigPathsPlugin({ tsconfig: 'tsconfig.json' }), + | ], + |""".stripMargin + + val defineSection = resources().map { rp => + val resourceRoot = rp.path.last + val envVarName = envName(resourceRoot) + s""" "process.env.$envVarName": JSON.stringify(${ujson.Str("./" + resourceRoot)})""" + }.distinct.mkString(",\n") + + s"""|import * as esbuild from 'esbuild'; + |import * as glob from 'glob'; + |import TsconfigPathsPlugin from '@esbuild-plugins/tsconfig-paths' + |import copyStaticFiles from 'esbuild-copy-static-files'; + | + |esbuild.build({ + | entryPoints: [], + | outdir: ${ujson.Str("./" + bundledOut())}, + | write: ${ujson.Bool(false)}, // Prevent esbuild from generating new files + | $copyPluginCode + | define: { + | $defineSection + | }, + |}).then(() => { + | console.log('Build succeeded!'); + |}).catch((e) => { + | console.error('Build failed!'); + | console.error(e) + | process.exit(1); + |}); + |""".stripMargin + + } + + 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[CommandResult] = Task.Command { + // build package.json + packageJson() + + // bundle code for publishing + bundle() + + // run npm publish + os.call(("npm", "publish"), cwd = publishDir().path) + } + +} + +object PublishModule { + case class PublishMeta( + name: String, + version: String, + description: String, + main: String = "", + types: String = "", + author: String = "", + license: mill.scalalib.publish.License = mill.scalalib.publish.License.MIT, + homepage: String = "", + bin: Map[String, String] = Map.empty[String, String], + files: Seq[String] = Seq.empty[String], + scripts: Map[String, String] = Map.empty[String, String], + engines: Map[String, String] = Map.empty[String, String], + keywords: Seq[String] = Seq.empty[String], + repository: Repository = EmptyRepository, + bugs: Bugs = EmptyBugs, + dependencies: Map[String, String] = Map.empty[String, String], + devDependencies: Map[String, String] = Map.empty[String, String], + publishConfig: PublishConfig = EmptyPubConfig, + exports: Map[String, ExportEntry] = Map.empty[String, ExportEntry], + typesVersions: Map[String, Seq[String]] = Map.empty[String, Seq[String]] + ) { + def toJson: ujson.Value = ujson.Obj( + "name" -> name, + "version" -> version, + "description" -> description, + "main" -> main, + "types" -> types, + "files" -> ujson.Arr.from(files), + "scripts" -> ujson.Obj.from(scripts.map { case (k, v) => k -> ujson.Str(v) }), + "bin" -> ujson.Obj.from(bin.map { case (k, v) => k -> ujson.Str(v) }), + "engines" -> ujson.Obj.from(engines.map { case (k, v) => k -> ujson.Str(v) }), + "keywords" -> ujson.Arr.from(keywords), + "author" -> author, + "license" -> license.id, + "repository" -> repository.toJson, + "bugs" -> bugs.toJson, + "homepage" -> homepage, + "dependencies" -> ujson.Obj.from(dependencies.map { case (k, v) => k -> ujson.Str(v) }), + "devDependencies" -> ujson.Obj.from(devDependencies.map { case (k, v) => k -> ujson.Str(v) }), + "publishConfig" -> publishConfig.toJson, + "exports" -> ujson.Obj.from(exports.map { case (key, value) => key -> value.toJson }), + "typesVersions" -> ujson.Obj(( + "*", + ujson.Obj.from(typesVersions.map { case (k, v) => k -> ujson.Arr.from(v) }) + )) + ) + + def toJsonClean: ujson.Value = removeEmptyValues(toJson) + } + + object PublishMeta { + implicit val rw: upickle.default.ReadWriter[PublishMeta] = upickle.default.macroRW + } + + case class Repository(`type`: String, url: String) { + def toJson: ujson.Value = ujson.Obj( + "type" -> `type`, + "url" -> url + ) + } + + object Repository { + implicit val rw: upickle.default.ReadWriter[Repository] = upickle.default.macroRW + } + + object EmptyRepository extends Repository("", "") { + override def toJson: ujson.Value = ujson.Obj() + } + + case class Bugs(url: String, email: Option[String]) { + def toJson: ujson.Value = { + val base = ujson.Obj("url" -> url) + email.foreach(e => base("email") = e) + base + } + } + + object EmptyBugs extends Bugs("", None) { + override def toJson: ujson.Value = ujson.Obj() + } + + object Bugs { + implicit val rw: upickle.default.ReadWriter[Bugs] = upickle.default.macroRW + } + + case class PublishConfig(registry: String, access: String) { + def toJson: ujson.Value = ujson.Obj( + "registry" -> registry, + "access" -> access + ) + } + + object EmptyPubConfig extends PublishConfig("", "") { + override def toJson: ujson.Value = ujson.Obj() + } + + object PublishConfig { + implicit val rw: upickle.default.ReadWriter[PublishConfig] = upickle.default.macroRW + } + + sealed trait ExportEntry { + def toJson: ujson.Value + } + + object ExportEntry { + implicit val rw: upickle.default.ReadWriter[ExportEntry] = upickle.default.macroRW + } + + case class Export(path: String) extends ExportEntry { + def toJson: ujson.Value = ujson.Str(path) + } + + object Export { + implicit val rw: upickle.default.ReadWriter[Export] = upickle.default.macroRW + } + + case class ExportConditions(conditions: Map[String, ExportEntry]) extends ExportEntry { + def toJson: ujson.Value = ujson.Obj.from(conditions.map { case (key, value) => + key -> value.toJson + }) + } + + object ExportConditions { + implicit val rw: upickle.default.ReadWriter[ExportConditions] = upickle.default.macroRW + } + + private def removeEmptyValues(json: ujson.Value): ujson.Value = { + json match { + case obj: ujson.Obj => + val filtered = obj.value.filterNot { case (_, v) => isEmptyValue(v) } + val transformed = filtered.map { case (k, v) => k -> removeEmptyValues(v) } + if (transformed.isEmpty) ujson.Null else ujson.Obj.from(transformed) + case arr: ujson.Arr => + val filtered = arr.value.filterNot(isEmptyValue) + val transformed = filtered.map(removeEmptyValues) + if (transformed.isEmpty) ujson.Null else ujson.Arr(transformed) + case str: ujson.Str if str.value.isEmpty => ujson.Null // Added to remove empty strings + case other => other + } + } + + private def isEmptyValue(json: ujson.Value): Boolean = { + json match { + case ujson.Str("") | ujson.Null => true + case _: ujson.Obj | _: ujson.Arr => removeEmptyValues(json) == ujson.Null // crucial check + case _ => false + } + } + +} diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index 1ba98ad2174..c46648c4177 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -62,10 +62,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)) } @@ -121,9 +117,9 @@ object TestModule { val testRunner = compiled / "test-runner.js" val content = - """|require('node_modules/ts-node/register'); + """|require('ts-node/register'); |require('tsconfig-paths/register'); - |require('node_modules/mocha/bin/_mocha'); + |require('mocha/bin/_mocha'); |""".stripMargin os.write(testRunner, content) diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 6275ea0860a..7a38cb250e8 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -59,14 +59,33 @@ trait TypeScriptModule extends Module { outer => def allSources: T[IndexedSeq[PathRef]] = Task { val fileExt: Path => Boolean = _.ext == "ts" - val generated = for { - pr <- generatedSources() - file <- os.walk(pr.path) - if fileExt(file) - } yield PathRef(file) - os.walk(sources().path).filter(fileExt).map(PathRef(_)) ++ generated + os.walk(sources().path).filter(fileExt).map(PathRef(_)) } + private def compiledSources: Task[IndexedSeq[PathRef]] = Task.Anon { + val generated = 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 typescriptOut / file.path.relativeTo(millSourcePath) + + // 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 + ) + ) + + (core ++ generated).map(PathRef(_)) + } + // specify tsconfig.compilerOptions def compilerOptions: T[Map[String, ujson.Value]] = Task { Map( @@ -77,10 +96,9 @@ trait TypeScriptModule extends Module { outer => } // specify tsconfig.compilerOptions.Paths - def compilerOptionsPaths: Task[Map[String, String]] = - Task.Anon { Map("*" -> npmInstall().path.toString()) } + def compilerOptionsPaths: Task[Map[String, String]] = Task.Anon { Map.empty[String, String] } - private def upstreams: T[(PathRef, PathRef, Seq[PathRef])] = Task { + def upstreams: T[(PathRef, PathRef, Seq[PathRef])] = Task { val comp = compile() (comp._1, comp._2, resources()) @@ -133,21 +151,30 @@ trait TypeScriptModule extends Module { outer => } } - def compilerOptionsBuilder: Task[Map[String, ujson.Value]] = Task.Anon { - val declarationsOut = Task.dest / "declarations" + def typeRoots: Task[ujson.Value] = Task.Anon { + ujson.Arr( + "node_modules/@types", + (Task.dest / "declarations").toString + ) + } + + 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)) + } + + def compilerOptionsBuilder: Task[Map[String, ujson.Value]] = Task.Anon { val combinedPaths = upstreamPathsBuilder() ++ - generatedSources().map(p => ("@generated/*", p.path.toString)) ++ + generatedSourcesPathsBuilder() ++ modulePaths() ++ compilerOptionsPaths().toSeq val combinedCompilerOptions: Map[String, ujson.Value] = compilerOptions() ++ Map( - "declarationDir" -> ujson.Str(declarationsOut.toString), - "typeRoots" -> ujson.Arr( - (npmInstall().path / "node_modules/@types").toString, - declarationsOut.toString - ), + "declarationDir" -> 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 @@ -158,17 +185,35 @@ 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 "" + def symlinkNodeModules: Task[Unit] = Task.Anon { + os.call( + ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), + cwd = Task.dest + ) + os.call( + ("ln", "-s", npmInstall().path.toString + "/package-lock.json", "package-lock.json"), + cwd = Task.dest + ) + () + } + def compile: T[(PathRef, PathRef)] = Task { + symlinkNodeModules() os.write( Task.dest / "tsconfig.json", ujson.Obj( - "compilerOptions" -> ujson.Obj.from(compilerOptionsBuilder().toSeq), - "files" -> allSources().map(_.path.toString) + "compilerOptions" -> ujson.Obj.from( + compilerOptionsBuilder().toSeq ++ Seq("typeRoots" -> typeRoots()) + ), + "files" -> compiledSources().map(_.path.toString) ) ) os.copy(millSourcePath, Task.dest / "typescript", mergeFolders = true) - os.call(npmInstall().path / "node_modules/typescript/bin/tsc") + os.call(npmInstall().path / "node_modules/typescript/bin/tsc", cwd = Task.dest) (PathRef(Task.dest), PathRef(Task.dest / "typescript")) } @@ -177,16 +222,7 @@ trait TypeScriptModule extends Module { outer => def mainFilePath: T[Path] = Task { compile()._2.path / "src" / mainFileName() } - def forkEnv: T[Map[String, String]] = - Task { - Map("NODE_PATH" -> Seq( - ".", - compile()._1.path, - compile()._2.path, - npmInstall().path, - npmInstall().path / "node_modules" - ).mkString(":")) - } + def forkEnv: T[Map[String, String]] = Task { Map.empty[String, String] } def computedArgs: T[Seq[String]] = Task { Seq.empty[String] } @@ -232,6 +268,7 @@ trait TypeScriptModule extends Module { outer => } // 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) @@ -251,7 +288,7 @@ trait TypeScriptModule extends Module { outer => | ${rps.map { rp => s""" copyStaticFiles({ | src: ${ujson.Str(rp.toString)}, - | dest: ${ujson.Str(Task.dest.toString + "/" + rp.last.toString)}, + | dest: ${ujson.Str(Task.dest.toString + "/" + rp.last)}, | dereference: true, | preserveTimestamps: true, | recursive: true, @@ -268,9 +305,9 @@ trait TypeScriptModule extends Module { outer => s""" "process.env.$envVarName": JSON.stringify(${ujson.Str("./" + resourceRoot)})""" }.mkString(",\n") - s"""|import * as esbuild from 'node_modules/esbuild'; - |import TsconfigPathsPlugin from 'node_modules/@esbuild-plugins/tsconfig-paths' - |import copyStaticFiles from 'node_modules/esbuild-copy-static-files'; + s"""|import * as esbuild from 'esbuild'; + |import TsconfigPathsPlugin from '@esbuild-plugins/tsconfig-paths' + |import copyStaticFiles from 'esbuild-copy-static-files'; | |esbuild.build({ | $flags diff --git a/javascriptlib/src/mill/javascriptlib/package.scala b/javascriptlib/src/mill/javascriptlib/package.scala new file mode 100644 index 00000000000..c70da0e6f3f --- /dev/null +++ b/javascriptlib/src/mill/javascriptlib/package.scala @@ -0,0 +1,18 @@ +package mill + +package object javascriptlib { + // These types are commonly used in javascript modules. Export them to make using + // them possible without an import. + + type License = mill.scalalib.publish.License + val License = mill.scalalib.publish.License + + type PublishMeta = PublishModule.PublishMeta + val PublishMeta = PublishModule.PublishMeta + + type Export = PublishModule.Export + val Export = PublishModule.Export + + type ExportConditions = PublishModule.ExportConditions + val ExportConditions = PublishModule.ExportConditions +} diff --git a/main/package.mill b/main/package.mill index a55ee3d5882..4a474765360 100644 --- a/main/package.mill +++ b/main/package.mill @@ -90,6 +90,7 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI def ivyDeps = Agg( build.Deps.osLib, + build.Deps.sbtIo, build.Deps.mainargs, build.Deps.upickle, build.Deps.pprint, From 14ad8afe2545c80449ed5dc37c15ec4d614a3ad7 Mon Sep 17 00:00:00 2001 From: Monye David Date: Sun, 12 Jan 2025 08:01:40 +0100 Subject: [PATCH 02/11] fix failing errors --- .../ROOT/pages/javascriptlib/publishing.adoc | 4 ++-- .../module/3-override-tasks/build.mill | 1 - .../publishing/2-realistic/build.mill | 8 ++----- .../mill/javascriptlib/TypeScriptModule.scala | 21 ++++++++++++++++++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc index 39b49fbcd88..c0c4e77cda4 100644 --- a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc +++ b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc @@ -7,8 +7,8 @@ This page will discuss common topics around publishing your Typescript projects == Simple publish -include::partial$example/javascriptlib/publishing/1-publish-module.adoc[] +include::partial$example/javascriptlib/publishing/1-publishx.adoc[] == Realistic publish -include::partial$example/javascriptlib/publishing/2-publish-module-advanced.adoc[] +include::partial$example/javascriptlib/publishing/2-realistic.adoc[] diff --git a/example/javascriptlib/module/3-override-tasks/build.mill b/example/javascriptlib/module/3-override-tasks/build.mill index a0968ade041..963f80f310e 100644 --- a/example/javascriptlib/module/3-override-tasks/build.mill +++ b/example/javascriptlib/module/3-override-tasks/build.mill @@ -23,7 +23,6 @@ object foo extends TypeScriptModule { def compile = Task { println("Compiling...") - os.copy(sources().path, super.compile()._2.path, mergeFolders = true) super.compile() } diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index 3100ad9b544..7572cb1f300 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -51,7 +51,7 @@ class qux extends TypeScriptModule { object test extends TypeScriptTests with TestModule.Jest } -object quxmod extends qux +object quxdev extends qux object quxpub extends qux with PublishModule { def exports = Map( @@ -103,11 +103,7 @@ object quxpub extends qux with PublishModule { // "./foo/bar": "./dist/foo/bar/src/bar.js" // }, // "typesVersions": { -// "*": { -// "./dist/src/qux": [ -// "declarations/src/qux.d.ts" -// ], ... -// } +// "*": { ... } // } //} //// SNIPPET:END diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 7a38cb250e8..beb4fc6f40a 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -54,6 +54,9 @@ trait TypeScriptModule extends Module { outer => def resources: T[Seq[PathRef]] = Task { Seq(PathRef(millSourcePath / "resources")) } + def nuts: T[Unit] = + Task { println(millSourcePath); println(sources()); println(compiledSources()) } + def generatedSources: T[Seq[PathRef]] = Task { Seq[PathRef]() } def allSources: T[IndexedSeq[PathRef]] = @@ -69,10 +72,26 @@ trait TypeScriptModule extends Module { outer => if file.ext == "ts" } yield file + + val typescriptOut = Task.dest / "typescript" val core = for { file <- allSources() - } yield typescriptOut / file.path.relativeTo(millSourcePath) + } yield file.path match { + case coreS if coreS.startsWith(millSourcePath) => + // core - regular sources + // expected to exist within boundaries of `millSourcePath` + typescriptOut / coreS.relativeTo(millSourcePath) + 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 + } // symlink node_modules for generated sources // remove `node_module/` package import format From 8b52091b9709bde41b782a085ea9a4ac16a1798c Mon Sep 17 00:00:00 2001 From: Monye David Date: Sun, 12 Jan 2025 08:51:58 +0100 Subject: [PATCH 03/11] fix failing --- example/javascriptlib/publishing/2-realistic/build.mill | 2 +- javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index 7572cb1f300..40a2734d753 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -109,7 +109,7 @@ object quxpub extends qux with PublishModule { //// SNIPPET:END /** Usage -> mill quxmod.test +> mill quxdev.test PASS .../qux.test.ts ... diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index beb4fc6f40a..f83acbdd662 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -72,8 +72,6 @@ trait TypeScriptModule extends Module { outer => if file.ext == "ts" } yield file - - val typescriptOut = Task.dest / "typescript" val core = for { file <- allSources() From ac9a445dadcba698dc99b7b50eceab3331db714a Mon Sep 17 00:00:00 2001 From: Monye David Date: Mon, 13 Jan 2025 08:04:47 +0100 Subject: [PATCH 04/11] requested changes --- build.mill | 1 - .../publishing/2-realistic/build.mill | 38 +----- .../mill/javascriptlib/PublishModule.scala | 116 +++++++++--------- .../mill/javascriptlib/TypeScriptModule.scala | 4 +- main/package.mill | 1 - 5 files changed, 67 insertions(+), 93 deletions(-) diff --git a/build.mill b/build.mill index 7fae8823dbf..924c437c527 100644 --- a/build.mill +++ b/build.mill @@ -150,7 +150,6 @@ object Deps { val commonsIo = ivy"commons-io:commons-io:2.18.0" val log4j2Core = ivy"org.apache.logging.log4j:log4j-core:2.24.3" val osLib = ivy"com.lihaoyi::os-lib:0.11.4-M4" - val sbtIo = ivy"org.scala-sbt::io:1.9.0" val pprint = ivy"com.lihaoyi::pprint:0.9.0" val mainargs = ivy"com.lihaoyi::mainargs:0.7.6" val millModuledefsVersion = "0.11.2" diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index 40a2734d753..cb373d613ae 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -1,39 +1,15 @@ -package build + package build import mill._, javascriptlib._ object foo extends TypeScriptModule { - def generatedSources = Task { - os.write( - Task.dest / "foo.generated.ts", - s"""export default class FooGen { - | static value: number = 123 - |} - """.stripMargin - ) - - Seq(PathRef(Task.dest)) - } - object bar extends TypeScriptModule { - def generatedSources = Task { - os.write( - Task.dest / "bar.generated.ts", - s"""export default class Bar { - | static value: number = 123 - |} - """.stripMargin - ) - - Seq(PathRef(Task.dest)) - } - def npmDeps = Seq("immutable@4.3.7") } } -class qux extends TypeScriptModule { +object qux extends PublishModule { def moduleDeps = Seq(foo, foo.bar) def generatedSources = Task { @@ -48,12 +24,6 @@ class qux extends TypeScriptModule { Seq(PathRef(Task.dest)) } - object test extends TypeScriptTests with TestModule.Jest -} - -object quxdev extends qux - -object quxpub extends qux with PublishModule { def exports = Map( "./qux/generate_user" -> "src/generate_user.js", "./foo" -> "foo/src/foo.js", @@ -69,6 +39,8 @@ object quxpub extends qux with PublishModule { "qux" -> "src/qux.js" ) ) + + object test extends TypeScriptTests with TestModule.Jest } // In this example, we define multiple exports for our application with the `export` task @@ -109,7 +81,7 @@ object quxpub extends qux with PublishModule { //// SNIPPET:END /** Usage -> mill quxdev.test +> mill qux.test PASS .../qux.test.ts ... diff --git a/javascriptlib/src/mill/javascriptlib/PublishModule.scala b/javascriptlib/src/mill/javascriptlib/PublishModule.scala index 14b01fc0b0e..6af3172cb13 100644 --- a/javascriptlib/src/mill/javascriptlib/PublishModule.scala +++ b/javascriptlib/src/mill/javascriptlib/PublishModule.scala @@ -15,42 +15,42 @@ trait PublishModule extends TypeScriptModule { override def npmDevDeps: T[Seq[String]] = Task { Seq("glob@^10.4.5", "ts-patch@3.3.0", "typescript-transform-paths@3.5.3") } - def bundledOut: T[String] = Task { "dist" } + def pubBundledOut: T[String] = Task { "dist" } - private def declarationOut: T[String] = Task { "declarations" } + private def pubDeclarationOut: T[String] = Task { "declarations" } override def mainFileName: T[String] = Task { s"${millSourcePath.last}.js" } // main file; defined with mainFileName - def main: T[String] = - Task { bundledOut() + "/src/" + mainFileName() } + def pubMain: T[String] = + Task { pubBundledOut() + "/src/" + mainFileName() } - private def mainType: T[String] = Task { - main().replaceFirst(bundledOut(), declarationOut()).replaceAll("\\.js", ".d.ts") + private def pubMainType: T[String] = Task { + pubMain().replaceFirst(pubBundledOut(), pubDeclarationOut()).replaceAll("\\.js", ".d.ts") } // Define exports for the package // by default: mainFile is exported // use this to define other exports - def exports: T[Map[String, String]] = Task { Map.empty[String, String] } + def pubExports: T[Map[String, String]] = Task { Map.empty[String, String] } - private def mkExports: T[Map[String, PublishModule.ExportEntry]] = Task { - exports().map { case (key, value) => - key -> PublishModule.Export("./" + bundledOut() + "/" + value) + private def pubBuildExports: T[Map[String, PublishModule.ExportEntry]] = Task { + pubExports().map { case (key, value) => + key -> PublishModule.Export("./" + pubBundledOut() + "/" + value) } } - private def mkTypesVersion: T[Map[String, Seq[String]]] = Task { + private def pubTypesVersion: T[Map[String, Seq[String]]] = Task { pubAllSources().map { source => - val dist = source.replaceFirst("typescript", bundledOut()) - val declarations = source.replaceFirst("typescript", declarationOut()) + val dist = source.replaceFirst("typescript", pubBundledOut()) + val declarations = source.replaceFirst("typescript", pubDeclarationOut()) ("./" + dist).replaceAll("\\.ts", "") -> Seq(declarations.replaceAll("\\.ts", ".d.ts")) }.toMap } // build package.json from publishMeta // mv to publishDir.dest - def packageJson: T[Unit] = Task { // PathRef + def pubbPackageJson: T[PathRef] = Task { // PathRef def splitDeps(input: String): (String, String) = { input.split("@", 3).toList match { case first :: second :: tail if input.startsWith("@") => @@ -62,17 +62,19 @@ trait PublishModule extends TypeScriptModule { val json = publishMeta() val updatedJson = json.copy( - files = json.files ++ Seq(bundledOut(), declarationOut()), - main = main(), - types = mainType(), - exports = Map("." -> PublishModule.Export("./" + main())) ++ mkExports(), - bin = json.bin.map { case (k, v) => (k, "./" + bundledOut() + "/" + v) }, - typesVersions = mkTypesVersion(), + files = json.files ++ Seq(pubBundledOut(), pubDeclarationOut()), + main = pubMain(), + types = pubMainType(), + exports = Map("." -> PublishModule.Export("./" + pubMain())) ++ pubBuildExports(), + bin = json.bin.map { case (k, v) => (k, "./" + pubBundledOut() + "/" + v) }, + typesVersions = pubTypesVersion(), dependencies = transitiveNpmDeps().map { deps => splitDeps(deps) }.toMap, devDependencies = transitiveNpmDeps().map { deps => splitDeps(deps) }.toMap ).toJsonClean - os.write.over(publishDir().path / "package.json", updatedJson) + os.write.over(Task.dest / "package.json", updatedJson) + + PathRef(Task.dest) } // Package.Json construction @@ -81,7 +83,7 @@ trait PublishModule extends TypeScriptModule { override def modulePaths: Task[Seq[(String, String)]] = Task.Anon { val module = millSourcePath.last - Seq((s"$module/*", "typescript/src" + ":" + s"${declarationOut()}")) ++ + Seq((s"$module/*", "typescript/src" + ":" + s"${pubDeclarationOut()}")) ++ resources().map { rp => val resourceRoot = rp.path.last val result = ( @@ -96,9 +98,7 @@ trait PublishModule extends TypeScriptModule { } private def pubModDeps: T[Seq[String]] = Task { - moduleDeps.map { x => - x.millSourcePath.subRelativeTo(Task.workspace).toString.split("/").head - }.distinct + moduleDeps.map { _.millSourcePath.subRelativeTo(Task.workspace).segments.head }.distinct } private def pubModDepsSources: T[Seq[PathRef]] = Task { @@ -107,29 +107,28 @@ trait PublishModule extends TypeScriptModule { } yield modSource } + 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 { - (for { - modSource <- Task.traverse(moduleDeps)(_.generatedSources)() - } yield { + 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) - }).flatten + } } // mv generated sources for base mod and its deps - private def pubMvGenSources: T[Unit] = Task { - val fileExt: Path => Boolean = _.ext == "ts" - val baseModGenS = for { - pr <- generatedSources() - file <- os.walk(pr.path) - if fileExt(file) - } yield PathRef(file) - - val allGeneratedSources = baseModGenS ++ pubModDepsGenSources() + private def pubGenSources: T[Unit] = Task { + val allGeneratedSources = pubBaseModeGenSources() ++ pubModDepsGenSources() allGeneratedSources.foreach { target => val destination = publishDir().path / "typescript" / "generatedSources" / target.path.last os.makeDir.all(destination / os.up) @@ -140,10 +139,10 @@ trait PublishModule extends TypeScriptModule { } } - private def pubCpModDeps: T[Unit] = Task { - val cpTargets = pubModDeps() + private def pubCopyModDeps: T[Unit] = Task { + val targets = pubModDeps() - cpTargets.foreach { target => + targets.foreach { target => val destination = publishDir().path / "typescript" / target os.makeDir.all(destination / os.up) os.copy( @@ -172,7 +171,10 @@ trait PublishModule extends TypeScriptModule { ).filter(fileExt) } yield source.toString .replaceFirst(millSourcePath.toString, "typescript") - .replaceFirst(project, "typescript")) ++ pubModDepsGenSources().map(pr => + .replaceFirst( + project, + "typescript" + )) ++ (pubBaseModeGenSources() ++ pubModDepsGenSources()).map(pr => "typescript/generatedSources/" + pr.path.last ) @@ -187,16 +189,17 @@ trait PublishModule extends TypeScriptModule { val upstreams = (for { (res, mod) <- Task.traverse(moduleDeps)(_.resources)().zip(moduleDeps) } yield { + val relative = mod.millSourcePath.subRelativeTo(Task.workspace) Seq(( mod.millSourcePath.subRelativeTo(Task.workspace).toString + "/*", - s"typescript/${mod.millSourcePath.subRelativeTo(Task.workspace).toString}/src" + ":" + s"${declarationOut()}" + s"typescript/$relative/src:${pubDeclarationOut()}" )) ++ res.map { rp => val resourceRoot = rp.path.last val modName = mod.millSourcePath.subRelativeTo(Task.workspace).toString // nb: resources are be moved in bundled stage ( - "@" + modName + s"/$resourceRoot/*", + s"@$modName/$resourceRoot/*", resourceRoot match { case s if s.contains(".dest") => rp.path.toString @@ -231,7 +234,7 @@ trait PublishModule extends TypeScriptModule { "baseUrl" -> ujson.Str("."), "rootDir" -> ujson.Str("typescript"), "declaration" -> ujson.Bool(true), - "outDir" -> ujson.Str(bundledOut()), + "outDir" -> ujson.Str(pubBundledOut()), "plugins" -> ujson.Arr( ujson.Obj("transform" -> "typescript-transform-paths"), ujson.Obj( @@ -246,7 +249,7 @@ trait PublishModule extends TypeScriptModule { } // patch typescript - private def tsPatchInstall: T[Unit] = Task { + private def pubTsPatchInstall: T[Unit] = Task { os.call( ("node", npmInstall().path / "node_modules/ts-patch/bin/ts-patch", "install"), cwd = npmInstall().path @@ -254,8 +257,8 @@ trait PublishModule extends TypeScriptModule { () } - override def symlinkNodeModules: Task[Unit] = Task { - tsPatchInstall() // patch typescript compiler => use custom transformers + private def pubSymLink: Task[Unit] = Task { + pubTsPatchInstall() // patch typescript compiler => use custom transformers os.call( ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), cwd = publishDir().path @@ -268,7 +271,7 @@ trait PublishModule extends TypeScriptModule { } override def compile: T[(PathRef, PathRef)] = Task { - symlinkNodeModules() + pubSymLink() os.write( publishDir().path / "tsconfig.json", ujson.Obj( @@ -279,8 +282,8 @@ trait PublishModule extends TypeScriptModule { ) ) os.copy(millSourcePath, publishDir().path / "typescript", mergeFolders = true) - pubCpModDeps() - pubMvGenSources() + pubCopyModDeps() + pubGenSources() // Run type check, build declarations os.call( ("node", npmInstall().path / "node_modules/typescript/bin/tsc"), @@ -307,7 +310,7 @@ trait PublishModule extends TypeScriptModule { s""" copyStaticFiles({ | src: ${ujson.Str(rp.toString)}, | dest: ${ujson.Str( - publishDir().path.toString + "/" + bundledOut() + "/" + rp.last + publishDir().path.toString + "/" + pubBundledOut() + "/" + rp.last )}, | dereference: true, | preserveTimestamps: true, @@ -332,7 +335,7 @@ trait PublishModule extends TypeScriptModule { | |esbuild.build({ | entryPoints: [], - | outdir: ${ujson.Str("./" + bundledOut())}, + | outdir: ${ujson.Str("./" + pubBundledOut())}, | write: ${ujson.Bool(false)}, // Prevent esbuild from generating new files | $copyPluginCode | define: { @@ -372,15 +375,16 @@ trait PublishModule extends TypeScriptModule { // publishDir; is used to process and compile files for publishing def publishDir: T[PathRef] = Task { PathRef(Task.dest) } - def publish(): Command[CommandResult] = Task.Command { + def publish(): Command[Unit] = Task.Command { // build package.json - packageJson() + os.move(pubbPackageJson().path / "package.json", publishDir().path / "package.json") // bundle code for publishing bundle() // run npm publish - os.call(("npm", "publish"), cwd = publishDir().path) + os.call(("npm", "publish"), stdout = os.Inherit, cwd = publishDir().path) + () } } diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index f83acbdd662..43ade9b874f 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -205,7 +205,7 @@ trait TypeScriptModule extends Module { outer => // create a symlink for node_modules in compile.dest // removes need for node_modules prefix in import statements `node_modules/` // import * as somepackage from "" - def symlinkNodeModules: Task[Unit] = Task.Anon { + private def symLink: Task[Unit] = Task.Anon { os.call( ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), cwd = Task.dest @@ -218,7 +218,7 @@ trait TypeScriptModule extends Module { outer => } def compile: T[(PathRef, PathRef)] = Task { - symlinkNodeModules() + symLink() os.write( Task.dest / "tsconfig.json", ujson.Obj( diff --git a/main/package.mill b/main/package.mill index 4a474765360..a55ee3d5882 100644 --- a/main/package.mill +++ b/main/package.mill @@ -90,7 +90,6 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI def ivyDeps = Agg( build.Deps.osLib, - build.Deps.sbtIo, build.Deps.mainargs, build.Deps.upickle, build.Deps.pprint, From 9195edc8f5d43290cdd68a5bbb2048ae7d20de5d Mon Sep 17 00:00:00 2001 From: Monye David Date: Mon, 13 Jan 2025 10:04:41 +0100 Subject: [PATCH 05/11] fix lint --- example/javascriptlib/publishing/2-realistic/build.mill | 2 +- javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index cb373d613ae..1a6ac99920d 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -1,4 +1,4 @@ - package build +package build import mill._, javascriptlib._ diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 43ade9b874f..2283cff1506 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -54,9 +54,6 @@ trait TypeScriptModule extends Module { outer => def resources: T[Seq[PathRef]] = Task { Seq(PathRef(millSourcePath / "resources")) } - def nuts: T[Unit] = - Task { println(millSourcePath); println(sources()); println(compiledSources()) } - def generatedSources: T[Seq[PathRef]] = Task { Seq[PathRef]() } def allSources: T[IndexedSeq[PathRef]] = From 3f7e0effdfbbfe08ab1c8ad8574404ef380e9604 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 14 Jan 2025 17:39:01 +0100 Subject: [PATCH 06/11] requested changes: - use os.symlink --- .../src/mill/javascriptlib/PublishModule.scala | 13 ++++--------- .../src/mill/javascriptlib/TypeScriptModule.scala | 11 ++--------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/javascriptlib/src/mill/javascriptlib/PublishModule.scala b/javascriptlib/src/mill/javascriptlib/PublishModule.scala index 6af3172cb13..074dfce9dda 100644 --- a/javascriptlib/src/mill/javascriptlib/PublishModule.scala +++ b/javascriptlib/src/mill/javascriptlib/PublishModule.scala @@ -259,15 +259,10 @@ trait PublishModule extends TypeScriptModule { private def pubSymLink: Task[Unit] = Task { pubTsPatchInstall() // patch typescript compiler => use custom transformers - os.call( - ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), - cwd = publishDir().path - ) - if (os.exists(npmInstall().path / ".npmrc")) os.call( - ("ln", "-s", npmInstall().path.toString + "/.npmrc", ".npmrc"), - cwd = publishDir().path - ) - () + os.symlink(publishDir().path / "node_modules", npmInstall().path / "node_modules") + + if (os.exists(npmInstall().path / ".npmrc")) + os.symlink(publishDir().path / ".npmrc", npmInstall().path / ".npmrc") } override def compile: T[(PathRef, PathRef)] = Task { diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 2283cff1506..ac4ddb358da 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -203,15 +203,8 @@ trait TypeScriptModule extends Module { outer => // removes need for node_modules prefix in import statements `node_modules/` // import * as somepackage from "" private def symLink: Task[Unit] = Task.Anon { - os.call( - ("ln", "-s", npmInstall().path.toString + "/node_modules/", "node_modules"), - cwd = Task.dest - ) - os.call( - ("ln", "-s", npmInstall().path.toString + "/package-lock.json", "package-lock.json"), - cwd = Task.dest - ) - () + os.symlink(Task.dest / "node_modules", npmInstall().path / "node_modules") + os.symlink(Task.dest / "package-lock.json", npmInstall().path / "package-lock.json") } def compile: T[(PathRef, PathRef)] = Task { From c362af9c75c22e42383545e3738090e1c240ac03 Mon Sep 17 00:00:00 2001 From: Monye David Date: Wed, 15 Jan 2025 05:38:35 +0100 Subject: [PATCH 07/11] fix doc --- docs/modules/ROOT/pages/javascriptlib/publishing.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc index c0c4e77cda4..f7e70c4b150 100644 --- a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc +++ b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc @@ -7,7 +7,7 @@ This page will discuss common topics around publishing your Typescript projects == Simple publish -include::partial$example/javascriptlib/publishing/1-publishx.adoc[] +include::partial$example/javascriptlib/publishing/1-publish.adoc[] == Realistic publish From 72b3857fdb4244d38ee4b0c0b84a4e06eb2fa86f Mon Sep 17 00:00:00 2001 From: Monye David Date: Thu, 16 Jan 2025 07:57:00 +0100 Subject: [PATCH 08/11] minor edit --- example/javascriptlib/publishing/2-realistic/build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index 1a6ac99920d..379032507c1 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -44,7 +44,7 @@ object qux extends PublishModule { } // In this example, we define multiple exports for our application with the `export` task -// The package.json generated for this simple publish: +// The package.json generated for this lib publish: //// SNIPPET:BUILD // { From a4bd5db96fce761ba3de2c723ed165a42d8dcf06 Mon Sep 17 00:00:00 2001 From: Monye David Date: Thu, 16 Jan 2025 08:00:06 +0100 Subject: [PATCH 09/11] minor edit --- docs/modules/ROOT/pages/javascriptlib/publishing.adoc | 2 +- example/javascriptlib/publishing/1-publish/build.mill | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc index f7e70c4b150..d2964358423 100644 --- a/docs/modules/ROOT/pages/javascriptlib/publishing.adoc +++ b/docs/modules/ROOT/pages/javascriptlib/publishing.adoc @@ -1,4 +1,4 @@ -= Python Packaging & Publishing += Typescript Packaging & Publishing :page-aliases: Publishing_Typescript_Projects.adoc include::partial$gtag-config.adoc[] diff --git a/example/javascriptlib/publishing/1-publish/build.mill b/example/javascriptlib/publishing/1-publish/build.mill index 30062f775f2..3bb6ca6bae2 100644 --- a/example/javascriptlib/publishing/1-publish/build.mill +++ b/example/javascriptlib/publishing/1-publish/build.mill @@ -23,6 +23,9 @@ object foo extends PublishModule { // Important `package.json` info required for publishing are auto-magically generated. // `main` file is by default the file defined in the `mainFileName` task, it can be modified if needed. +// Use the `.npmrc` file to include authentication tokens for publishing to npm or +// change the regsitry to publish to a private registry. + // The package.json generated for this simple publish: //// SNIPPET:BUILD From 0e6d1d16bbb451440e9f4da28c26b8dedb1a713c Mon Sep 17 00:00:00 2001 From: Monye David Date: Thu, 16 Jan 2025 11:38:12 +0100 Subject: [PATCH 10/11] add pub link to navbar --- docs/modules/ROOT/nav.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d9e659bbd4a..c556a5a5249 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -43,6 +43,7 @@ *** xref:javascriptlib/dependencies.adoc[] *** xref:javascriptlib/module-config.adoc[] *** xref:javascriptlib/testing.adoc[] +*** xref:javascriptlib/publishing.adoc[] * xref:comparisons/why-mill.adoc[] ** xref:comparisons/maven.adoc[] ** xref:comparisons/gradle.adoc[] From fa54cc2a59678ec0efdc629140c4d91a66f0751a Mon Sep 17 00:00:00 2001 From: Monye David Date: Thu, 16 Jan 2025 14:09:37 +0100 Subject: [PATCH 11/11] fix doc --- example/javascriptlib/publishing/1-publish/build.mill | 3 +++ example/javascriptlib/publishing/2-realistic/build.mill | 3 +++ 2 files changed, 6 insertions(+) diff --git a/example/javascriptlib/publishing/1-publish/build.mill b/example/javascriptlib/publishing/1-publish/build.mill index 3bb6ca6bae2..7e9107211d6 100644 --- a/example/javascriptlib/publishing/1-publish/build.mill +++ b/example/javascriptlib/publishing/1-publish/build.mill @@ -29,6 +29,8 @@ object foo extends PublishModule { // The package.json generated for this simple publish: //// SNIPPET:BUILD +// [source,json] +// ---- //{ // "name": "mill-simple", // "version": "1.0.0", @@ -49,6 +51,7 @@ object foo extends PublishModule { // } // } //} +// ---- //// SNIPPET:END /** Usage diff --git a/example/javascriptlib/publishing/2-realistic/build.mill b/example/javascriptlib/publishing/2-realistic/build.mill index 379032507c1..5a6e85ac8ec 100644 --- a/example/javascriptlib/publishing/2-realistic/build.mill +++ b/example/javascriptlib/publishing/2-realistic/build.mill @@ -47,6 +47,8 @@ object qux extends PublishModule { // The package.json generated for this lib publish: //// SNIPPET:BUILD +// [source,json] +// ---- // { // "name": "mill-realistic", // "version": "1.0.3", @@ -78,6 +80,7 @@ object qux extends PublishModule { // "*": { ... } // } //} +// ---- //// SNIPPET:END /** Usage