From d04c15c760b26506370cf94ef3f61d58b724aed2 Mon Sep 17 00:00:00 2001 From: Monye David Date: Fri, 24 Jan 2025 17:54:59 +0100 Subject: [PATCH 01/20] example/javascriptlib/linting --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/javascriptlib/linting.adoc | 23 ++ .../basic/1-simple/jest.config.ts | 31 -- .../basic/3-custom-build-logic/jest.config.ts | 31 -- .../basic/4-multi-modules/jest.config.ts | 31 -- .../5-client-server-hello/jest.config.ts | 31 -- .../6-client-server-realistic/jest.config.ts | 31 -- .../linting/1-linting/.prettierignore | 4 + .../linting/1-linting/.prettierrc | 7 + .../linting/1-linting/build.mill | 55 +++ .../linting/1-linting/eslint.config.mjs | 34 ++ .../linting/1-linting/foo/src/foo.ts | 7 + .../linting/2-autoformatting/.prettierignore | 4 + .../linting/2-autoformatting/.prettierrc | 7 + .../linting/2-autoformatting/build.mill | 76 ++++ .../2-autoformatting/eslint.config.mjs | 34 ++ .../linting/2-autoformatting/foo/src/foo.ts | 7 + .../linting/2-autoformatting/todo.txt | 20 + .../3-code-coverage/bar/src/calculator.ts} | 7 +- .../bar/test/src/foo/calculator.test.ts | 30 ++ .../3-code-coverage/baz/src/calculator.ts} | 7 +- .../baz/test/src/baz/calculator.test.ts} | 8 +- .../linting/3-code-coverage/build.mill | 77 ++++ .../3-code-coverage/foo/src/calculator.ts | 12 + .../foo/test/src/foo/calculator.test.ts | 28 ++ .../3-code-coverage/qux/src/calculator.ts | 12 + .../qux/test/src/calculator.test.ts | 28 ++ .../module/5-resources/jest.config.ts | 31 -- .../testing/1-test-suite/jest.config.ts | 23 -- .../testing/1-test-suite/vite.config.ts | 12 - .../testing/2-test-deps/jest.config.ts | 23 -- example/package.mill | 1 + .../src/mill/javascriptlib/TestModule.scala | 383 +++++++++++++++--- .../src/mill/javascriptlib/TsLintModule.scala | 227 +++++++++++ .../mill/javascriptlib/TypeScriptModule.scala | 8 +- 35 files changed, 1046 insertions(+), 305 deletions(-) create mode 100644 docs/modules/ROOT/pages/javascriptlib/linting.adoc delete mode 100644 example/javascriptlib/basic/1-simple/jest.config.ts delete mode 100644 example/javascriptlib/basic/3-custom-build-logic/jest.config.ts delete mode 100644 example/javascriptlib/basic/4-multi-modules/jest.config.ts delete mode 100644 example/javascriptlib/basic/5-client-server-hello/jest.config.ts delete mode 100644 example/javascriptlib/basic/6-client-server-realistic/jest.config.ts create mode 100644 example/javascriptlib/linting/1-linting/.prettierignore create mode 100644 example/javascriptlib/linting/1-linting/.prettierrc create mode 100644 example/javascriptlib/linting/1-linting/build.mill create mode 100644 example/javascriptlib/linting/1-linting/eslint.config.mjs create mode 100644 example/javascriptlib/linting/1-linting/foo/src/foo.ts create mode 100644 example/javascriptlib/linting/2-autoformatting/.prettierignore create mode 100644 example/javascriptlib/linting/2-autoformatting/.prettierrc create mode 100644 example/javascriptlib/linting/2-autoformatting/build.mill create mode 100644 example/javascriptlib/linting/2-autoformatting/eslint.config.mjs create mode 100644 example/javascriptlib/linting/2-autoformatting/foo/src/foo.ts create mode 100644 example/javascriptlib/linting/2-autoformatting/todo.txt rename example/javascriptlib/{testing/1-test-suite/baz/src/calculator.js => linting/3-code-coverage/bar/src/calculator.ts} (67%) create mode 100644 example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts rename example/javascriptlib/{testing/1-test-suite/qux/src/calculator.js => linting/3-code-coverage/baz/src/calculator.ts} (67%) rename example/javascriptlib/{testing/1-test-suite/baz/test/src/baz/calculator.test.js => linting/3-code-coverage/baz/test/src/baz/calculator.test.ts} (94%) create mode 100644 example/javascriptlib/linting/3-code-coverage/build.mill create mode 100644 example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts create mode 100644 example/javascriptlib/linting/3-code-coverage/foo/test/src/foo/calculator.test.ts create mode 100644 example/javascriptlib/linting/3-code-coverage/qux/src/calculator.ts create mode 100644 example/javascriptlib/linting/3-code-coverage/qux/test/src/calculator.test.ts delete mode 100644 example/javascriptlib/module/5-resources/jest.config.ts delete mode 100644 example/javascriptlib/testing/1-test-suite/jest.config.ts delete mode 100644 example/javascriptlib/testing/1-test-suite/vite.config.ts delete mode 100644 example/javascriptlib/testing/2-test-deps/jest.config.ts create mode 100644 javascriptlib/src/mill/javascriptlib/TsLintModule.scala diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c556a5a5249..dc962def130 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -44,6 +44,7 @@ *** xref:javascriptlib/module-config.adoc[] *** xref:javascriptlib/testing.adoc[] *** xref:javascriptlib/publishing.adoc[] +*** xref:javascriptlib/linting.adoc[] * xref:comparisons/why-mill.adoc[] ** xref:comparisons/maven.adoc[] ** xref:comparisons/gradle.adoc[] diff --git a/docs/modules/ROOT/pages/javascriptlib/linting.adoc b/docs/modules/ROOT/pages/javascriptlib/linting.adoc new file mode 100644 index 00000000000..9bfc48e0536 --- /dev/null +++ b/docs/modules/ROOT/pages/javascriptlib/linting.adoc @@ -0,0 +1,23 @@ += Linting Typescript Projects +:page-aliases: Linting_Typescript_Projects.adoc + +include::partial$gtag-config.adoc[] + +This page will discuss common topics around maintaining the code quality of Typescript +codebases using the Mill build tool + +== Linting and AutoFormatting with Eslint and Prettier + +Eslint and Prettier are tools that analyzes your Scala source code, performing intelligent analyses and +code quality checks, and is often able to automatically fix the issues that it discovers. +It can also perform automated refactoring. + +include::partial$example/javascriptlib/linting/1-linting.adoc[] + +include::partial$example/javascriptlib/linting/2-autoformatting.adoc[] + +== Code Coverage with Jest, Mocha, Vitest and Jasmine + +Mill supports code coverage analysis with multiple Typescript testing frameworks. + +include::partial$example/javascriptlib/linting/3-code-coverage.adoc[] diff --git a/example/javascriptlib/basic/1-simple/jest.config.ts b/example/javascriptlib/basic/1-simple/jest.config.ts deleted file mode 100644 index eaa2b8e671f..00000000000 --- a/example/javascriptlib/basic/1-simple/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/basic/3-custom-build-logic/jest.config.ts b/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts deleted file mode 100644 index eaa2b8e671f..00000000000 --- a/example/javascriptlib/basic/3-custom-build-logic/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/basic/4-multi-modules/jest.config.ts b/example/javascriptlib/basic/4-multi-modules/jest.config.ts deleted file mode 100644 index eaa2b8e671f..00000000000 --- a/example/javascriptlib/basic/4-multi-modules/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/basic/5-client-server-hello/jest.config.ts b/example/javascriptlib/basic/5-client-server-hello/jest.config.ts deleted file mode 100644 index eaa2b8e671f..00000000000 --- a/example/javascriptlib/basic/5-client-server-hello/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/basic/6-client-server-realistic/jest.config.ts b/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts deleted file mode 100644 index eaa2b8e671f..00000000000 --- a/example/javascriptlib/basic/6-client-server-realistic/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/linting/1-linting/.prettierignore b/example/javascriptlib/linting/1-linting/.prettierignore new file mode 100644 index 00000000000..4171269761e --- /dev/null +++ b/example/javascriptlib/linting/1-linting/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +build +.git \ No newline at end of file diff --git a/example/javascriptlib/linting/1-linting/.prettierrc b/example/javascriptlib/linting/1-linting/.prettierrc new file mode 100644 index 00000000000..986beea4f82 --- /dev/null +++ b/example/javascriptlib/linting/1-linting/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} \ No newline at end of file diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill new file mode 100644 index 00000000000..9cb61d08212 --- /dev/null +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -0,0 +1,55 @@ +package build + +import mill._, javascriptlib._ + +object foo extends TypeScriptModule + +// Mill supports code linting via `eslint` https://eslint.org, `typescript-eslint` https://typescript-eslint.io/getting-started +// and `prettier` https://prettier.io/docs/en out of the box. +// You can lint your projects code by providing a configuration for your preferred linter and running `mill _.checkFormatAll`. + +// If both configurations files are present, the command `mill _.checkFormatAll` will default to eslint. +// You can lint via a specificied linter via the commands `mill _.checkFormatEslint` to lint with eslint and +// `mill _.checkFormatPrettier` to lint with prettier. + +// When using prettier you can specify the path to lint via command line argument, `mill _.checkFormatAll "*/**/*.ts"` +// just as you would when running `prettier --check` if no path is provided mill will default to using "*/**/*.ts". +// Also if a `.prettierignore` is not provided, mill will generate one ignoring "node_modules" and ".git". + +// You can define `npmLintDeps` field to add dependencies specific to linting to your module. +// The task `npmLintDeps` works the same way as `npmDeps` or `npmDevDeps`. + +/** Usage +> cat foo/src/foo.ts # initial poorly formatted source code +export class Foo{ +static main( +args: string[ +]) +{console.log("Hello World!") +} +} + +> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are found, mill will opt to use eslint. +... +foo/src/foo.ts + 2:1 error Expected indentation of 2 spaces but found 0 indent + 3:1 error Expected indentation of 4 spaces but found 0 indent + 5:1 error Opening curly brace does not appear on the same line as controlling statement brace-style + 5:1 error Statement inside of curly braces should be on next line brace-style + 5:1 error Requires a space after '{' block-spacing + 5:1 error Expected indentation of 2 spaces but found 0 indent + 5:14 error Strings must use singlequote quotes + 5:29 error Missing semicolon semi + 6:1 error Expected indentation of 2 spaces but found 0 indent + 7:2 error Newline required at end of file but not found eol-last +... +...10 problems (10 errors, 0 warnings) +...10 errors and 0 warnings potentially fixable with running foo.reformatAll. + +> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use the prettier configuration available. + +> mill foo.checkFormatAll # run lint with prettier configuration. +Checking formatting... +[warn] foo/src/foo.ts +[warn] Code style issues found. Run foo.reformatAll to fix. +*/ diff --git a/example/javascriptlib/linting/1-linting/eslint.config.mjs b/example/javascriptlib/linting/1-linting/eslint.config.mjs new file mode 100644 index 00000000000..913514ffdc3 --- /dev/null +++ b/example/javascriptlib/linting/1-linting/eslint.config.mjs @@ -0,0 +1,34 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + files: ['*/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + // styling rules + 'semi': ['error', 'always'], + 'quotes': ['error', 'single'], + 'comma-dangle': ['error', 'always-multiline'], + 'max-len': ['error', {code: 80, ignoreUrls: true}], + 'indent': ['error', 2, {SwitchCase: 1}], + 'brace-style': ['error', '1tbs'], + 'space-before-function-paren': ['error', 'never'], + 'no-multi-spaces': 'error', + 'array-bracket-spacing': ['error', 'never'], + 'object-curly-spacing': ['error', 'always'], + 'arrow-spacing': ['error', {before: true, after: true}], + 'key-spacing': ['error', {beforeColon: false, afterColon: true}], + 'keyword-spacing': ['error', {before: true, after: true}], + 'space-infix-ops': 'error', + 'block-spacing': ['error', 'always'], + 'eol-last': ['error', 'always'], + 'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}], + 'padded-blocks': ['error', 'never'], + }, +}); \ No newline at end of file diff --git a/example/javascriptlib/linting/1-linting/foo/src/foo.ts b/example/javascriptlib/linting/1-linting/foo/src/foo.ts new file mode 100644 index 00000000000..d433279976f --- /dev/null +++ b/example/javascriptlib/linting/1-linting/foo/src/foo.ts @@ -0,0 +1,7 @@ +export class Foo{ +static main( +args: string[ +]) +{console.log("Hello World!") +} +} \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/.prettierignore b/example/javascriptlib/linting/2-autoformatting/.prettierignore new file mode 100644 index 00000000000..4171269761e --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +build +.git \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/.prettierrc b/example/javascriptlib/linting/2-autoformatting/.prettierrc new file mode 100644 index 00000000000..986beea4f82 --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/build.mill b/example/javascriptlib/linting/2-autoformatting/build.mill new file mode 100644 index 00000000000..eae38f047c4 --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/build.mill @@ -0,0 +1,76 @@ +package build + +import mill._, javascriptlib._ + +object foo extends TypeScriptModule + +// Mill supports code formatting via `eslint` https://eslint.org, `typescript-eslint` https://typescript-eslint.io/getting-started +// and `prettier` https://prettier.io/docs/en out of the box. +// You can reformat your projects code by providing a configuration for your preferred linter and running `mill _.reformatAll`. + +// If both configurations files are present, the command `mill _.reformatAll` will default to eslint. +// You can format via a specificied linter via the commands `mill _.reformatEslint` to format with eslint and +// `mill _.reformatPrettier` to format with prettier. + +// When using prettier you can specify the path to reformat via command line argument, `mill _.reformatAll "*/**/*.ts"` +// just as you would when running `prettier --write` if no path is provided mill will default to using "*/**/*.ts". +// Also if a `.prettierignore` is not provided, mill will generate one ignoring "node_modules" and ".git". + +/** Usage +> cat foo/src/foo.ts # initial poorly formatted source code +export class Foo{ +static main( +args: string[ +]) +{console.log("Hello World!") +} +} + +> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are found, mill will opt to use eslint. +... +foo/src/foo.ts + 2:1 error Expected indentation of 2 spaces but found 0 indent + 3:1 error Expected indentation of 4 spaces but found 0 indent + 5:1 error Opening curly brace does not appear on the same line as controlling statement brace-style + 5:1 error Statement inside of curly braces should be on next line brace-style + 5:1 error Requires a space after '{' block-spacing + 5:1 error Expected indentation of 2 spaces but found 0 indent + 5:14 error Strings must use singlequote quotes + 5:29 error Missing semicolon semi + 6:1 error Expected indentation of 2 spaces but found 0 indent + 7:2 error Newline required at end of file but not found eol-last +... +...10 problems (10 errors, 0 warnings) +...10 errors and 0 warnings potentially fixable with running foo.reformatAll. + +> mill foo.reformatAll +... +All matched files have been reformatted! + +> cat foo/src/foo.ts # code formatted with eslint configuration. +export class Foo{ + static main( + args: string[ +]) { + console.log('Hello World!'); + } +} + +> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use the prettier configuration available. + +> mill foo.checkFormatAll # run lint with prettier configuration. +Checking formatting... +[warn] foo/src/foo.ts +[warn] Code style issues found. Run foo.reformatAll to fix. + +> mill foo.reformatAll +... +All matched files have been reformatted! + +> cat foo/src/foo.ts # code formatted with prettier configuration. +export class Foo { + static main(args: string[]) { + console.log('Hello World!'); + } +} +*/ diff --git a/example/javascriptlib/linting/2-autoformatting/eslint.config.mjs b/example/javascriptlib/linting/2-autoformatting/eslint.config.mjs new file mode 100644 index 00000000000..913514ffdc3 --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/eslint.config.mjs @@ -0,0 +1,34 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + files: ['*/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + // styling rules + 'semi': ['error', 'always'], + 'quotes': ['error', 'single'], + 'comma-dangle': ['error', 'always-multiline'], + 'max-len': ['error', {code: 80, ignoreUrls: true}], + 'indent': ['error', 2, {SwitchCase: 1}], + 'brace-style': ['error', '1tbs'], + 'space-before-function-paren': ['error', 'never'], + 'no-multi-spaces': 'error', + 'array-bracket-spacing': ['error', 'never'], + 'object-curly-spacing': ['error', 'always'], + 'arrow-spacing': ['error', {before: true, after: true}], + 'key-spacing': ['error', {beforeColon: false, afterColon: true}], + 'keyword-spacing': ['error', {before: true, after: true}], + 'space-infix-ops': 'error', + 'block-spacing': ['error', 'always'], + 'eol-last': ['error', 'always'], + 'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}], + 'padded-blocks': ['error', 'never'], + }, +}); \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/foo/src/foo.ts b/example/javascriptlib/linting/2-autoformatting/foo/src/foo.ts new file mode 100644 index 00000000000..d433279976f --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/foo/src/foo.ts @@ -0,0 +1,7 @@ +export class Foo{ +static main( +args: string[ +]) +{console.log("Hello World!") +} +} \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/todo.txt b/example/javascriptlib/linting/2-autoformatting/todo.txt new file mode 100644 index 00000000000..7f16632a353 --- /dev/null +++ b/example/javascriptlib/linting/2-autoformatting/todo.txt @@ -0,0 +1,20 @@ +Todo: + - check_format prettier ✅ + - check_format eslint ✅ + - fix format prettier ✅ + - fix format eslint ✅ + - use checkFormatAll / reformatAll + - find config + - use prettier / eslint (based on config) ✅ + - fail if no config provided :( ✅ + - use eslint as default ✅ + - Prettier: + - collect "*/**/*.ts" from arguments? ✅ + - default to if not provided ✅ + - create .prettierignore if does not exist :( + +Todo + Coverage: + - clean coverage out => out/coverage before every coverage run.. + - task to access coverage data. + diff --git a/example/javascriptlib/testing/1-test-suite/baz/src/calculator.js b/example/javascriptlib/linting/3-code-coverage/bar/src/calculator.ts similarity index 67% rename from example/javascriptlib/testing/1-test-suite/baz/src/calculator.js rename to example/javascriptlib/linting/3-code-coverage/bar/src/calculator.ts index 7d632b027a2..2e9b2aa67d5 100644 --- a/example/javascriptlib/testing/1-test-suite/baz/src/calculator.js +++ b/example/javascriptlib/linting/3-code-coverage/bar/src/calculator.ts @@ -1,11 +1,12 @@ export class Calculator { - add(a, b) { + add(a: number, b: number): number { return a + b; } - divide(a, b) { + + divide(a: number, b: number): number { if (b === 0) { throw new Error("Division by zero is not allowed"); } return a / b; } -} +} \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts b/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts new file mode 100644 index 00000000000..33fc3abfeca --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts @@ -0,0 +1,30 @@ +import {expect} from 'chai'; +import {Calculator} from 'bar/calculator'; + +describe('Calculator', () => { + const calculator = new Calculator(); + + describe('Addition', () => { + it('should return the sum of two numbers', () => { + const result = calculator.add(2, 3); + expect(result).to.equal(5); + }); + + it('should return the correct sum for negative numbers', () => { + const result = calculator.add(-2, -3); + expect(result).to.equal(-5); + }); + }); + + describe('Division', () => { + it('should return the quotient of two numbers', () => { + const result = calculator.divide(6, 3); + expect(result).to.equal(2); + }); + + it('should throw an error when dividing by zero', () => { + expect(() => calculator.divide(6, 0)).to.throw("Division by zero is not allowed"); + }); + }); + +}); \ No newline at end of file diff --git a/example/javascriptlib/testing/1-test-suite/qux/src/calculator.js b/example/javascriptlib/linting/3-code-coverage/baz/src/calculator.ts similarity index 67% rename from example/javascriptlib/testing/1-test-suite/qux/src/calculator.js rename to example/javascriptlib/linting/3-code-coverage/baz/src/calculator.ts index 7d632b027a2..2e9b2aa67d5 100644 --- a/example/javascriptlib/testing/1-test-suite/qux/src/calculator.js +++ b/example/javascriptlib/linting/3-code-coverage/baz/src/calculator.ts @@ -1,11 +1,12 @@ export class Calculator { - add(a, b) { + add(a: number, b: number): number { return a + b; } - divide(a, b) { + + divide(a: number, b: number): number { if (b === 0) { throw new Error("Division by zero is not allowed"); } return a / b; } -} +} \ No newline at end of file diff --git a/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.js b/example/javascriptlib/linting/3-code-coverage/baz/test/src/baz/calculator.test.ts similarity index 94% rename from example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.js rename to example/javascriptlib/linting/3-code-coverage/baz/test/src/baz/calculator.test.ts index b0024018ed6..09bc7d16b00 100644 --- a/example/javascriptlib/testing/1-test-suite/baz/test/src/baz/calculator.test.js +++ b/example/javascriptlib/linting/3-code-coverage/baz/test/src/baz/calculator.test.ts @@ -1,24 +1,28 @@ -import { describe, it, expect } from 'vitest'; import { Calculator } from 'baz/calculator'; + describe('Calculator', () => { const calculator = new Calculator(); + describe('Addition', () => { test('should return the sum of two numbers', () => { const result = calculator.add(2, 3); expect(result).toEqual(5); }); + test('should return the correct sum for negative numbers', () => { const result = calculator.add(-2, -3); expect(result).toEqual(-5); }); }); + describe('Division', () => { test('should return the quotient of two numbers', () => { const result = calculator.divide(6, 3); expect(result).toEqual(2); }); + it('should throw an error when dividing by zero', () => { expect(() => calculator.divide(6, 0)).toThrow("Division by zero is not allowed"); }); }); -}); +}); \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill new file mode 100644 index 00000000000..c6610ff4753 --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -0,0 +1,77 @@ +package build + +import mill._, javascriptlib._ + +object foo extends TypeScriptModule { + object test extends TypeScriptTests with TestModule.Jest +} + +object bar extends TypeScriptModule { + object test extends TypeScriptTests with TestModule.Mocha +} + +object baz extends TypeScriptModule { + object test extends TypeScriptTests with TestModule.Vitest +} + +object qux extends TypeScriptModule { + object test extends TypeScriptTests with TestModule.Jasmine +} + +// Mill supports code coverage with `Jest`, `Mocha`, `Vitest` and `Jasmine` out of the box. +// To run a test with coverage, run the command `_.test.coverage`. + +// The path to generated coverage data can be retrieved with `_.test.coverageFiles`, +// the generated coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. + +/** Usage + +> mill foo.test.coverage +...Calculator +... +---------------|---------|----------|---------|---------|------------------- +File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... +---------------|---------|----------|---------|---------|------------------- +...All files...|...100 |...100 |...100 |...100 |... +...calculator.ts...|...100 |...100 |...100 |...100 |... +---------------|---------|----------|---------|---------|------------------- +... +Test Suites:...1 passed, 1 total... +Tests:...4 passed, 4 total... +... + +> mill bar.test.coverage +... +...4 passing... +... +---------------|---------|----------|---------|---------|------------------- +File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... +---------------|---------|----------|---------|---------|------------------- +...All files...|...100 |...100 |...100 |...100 |... +...calculator.ts...|...100 |...100 |...100 |...100 |... +---------------|---------|----------|---------|---------|------------------- + +> mill baz.test.coverage +.../calculator.test.ts... +...Test Files 1 passed... +...Tests 4 passed... +... +...Coverage report from v8 +---------------|---------|----------|---------|---------|------------------- +File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... +---------------|---------|----------|---------|---------|------------------- +...All files...|...100 |...100 |...100 |...100 |... +...calculator.ts...|...100 |...100 |...100 |...100 |... +---------------|---------|----------|---------|---------|------------------- + +> mill qux.test.coverage +... +4 specs, 0 failures +... +---------------|---------|----------|---------|---------|------------------- +File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... +---------------|---------|----------|---------|---------|------------------- +...All files...|...100 |...100 |...100 |...100 |... +...calculator.ts...|...100 |...100 |...100 |...100 |... +---------------|---------|----------|---------|---------|------------------- +*/ diff --git a/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts b/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts new file mode 100644 index 00000000000..2e9b2aa67d5 --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts @@ -0,0 +1,12 @@ +export class Calculator { + add(a: number, b: number): number { + return a + b; + } + + divide(a: number, b: number): number { + if (b === 0) { + throw new Error("Division by zero is not allowed"); + } + return a / b; + } +} \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/foo/test/src/foo/calculator.test.ts b/example/javascriptlib/linting/3-code-coverage/foo/test/src/foo/calculator.test.ts new file mode 100644 index 00000000000..546129e8efb --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/foo/test/src/foo/calculator.test.ts @@ -0,0 +1,28 @@ +import {Calculator} from 'foo/calculator'; + +describe('Calculator', () => { + const calculator = new Calculator(); + + describe('Addition', () => { + test('should return the sum of two numbers', () => { + const result = calculator.add(2, 3); + expect(result).toEqual(5); + }); + + test('should return the correct sum for negative numbers', () => { + const result = calculator.add(-2, -3); + expect(result).toEqual(-5); + }); + }); + + describe('Division', () => { + test('should return the quotient of two numbers', () => { + const result = calculator.divide(6, 3); + expect(result).toEqual(2); + }); + + it('should throw an error when dividing by zero', () => { + expect(() => calculator.divide(6, 0)).toThrow("Division by zero is not allowed"); + }); + }); +}); \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/qux/src/calculator.ts b/example/javascriptlib/linting/3-code-coverage/qux/src/calculator.ts new file mode 100644 index 00000000000..2e9b2aa67d5 --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/qux/src/calculator.ts @@ -0,0 +1,12 @@ +export class Calculator { + add(a: number, b: number): number { + return a + b; + } + + divide(a: number, b: number): number { + if (b === 0) { + throw new Error("Division by zero is not allowed"); + } + return a / b; + } +} \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/qux/test/src/calculator.test.ts b/example/javascriptlib/linting/3-code-coverage/qux/test/src/calculator.test.ts new file mode 100644 index 00000000000..cbaa895121c --- /dev/null +++ b/example/javascriptlib/linting/3-code-coverage/qux/test/src/calculator.test.ts @@ -0,0 +1,28 @@ +import { Calculator } from 'qux/calculator'; + +describe('Calculator', () => { + const calculator = new Calculator(); + + describe('Addition', () => { + it('should return the sum of two numbers', () => { + const result = calculator.add(2, 3); + expect(result).toEqual(5); + }); + + it('should return the correct sum for negative numbers', () => { + const result = calculator.add(-2, -3); + expect(result).toEqual(-5); + }); + }); + + describe('Division', () => { + it('should return the quotient of two numbers', () => { + const result = calculator.divide(6, 3); + expect(result).toEqual(2); + }); + + it('should throw an error when dividing by zero', () => { + expect(() => calculator.divide(6, 0)).toThrowError("Division by zero is not allowed"); + }); + }); +}); \ No newline at end of file diff --git a/example/javascriptlib/module/5-resources/jest.config.ts b/example/javascriptlib/module/5-resources/jest.config.ts deleted file mode 100644 index 416ef18ea8a..00000000000 --- a/example/javascriptlib/module/5-resources/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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/testing/1-test-suite/jest.config.ts b/example/javascriptlib/testing/1-test-suite/jest.config.ts deleted file mode 100644 index f54bc06a50c..00000000000 --- a/example/javascriptlib/testing/1-test-suite/jest.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -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']; - - -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(moduleDeps) // use absolute paths generated in tsconfig. -}; \ No newline at end of file diff --git a/example/javascriptlib/testing/1-test-suite/vite.config.ts b/example/javascriptlib/testing/1-test-suite/vite.config.ts deleted file mode 100644 index a5993128c19..00000000000 --- a/example/javascriptlib/testing/1-test-suite/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; - -export default defineConfig({ - plugins: [tsconfigPaths()], - test: { - globals: true, - environment: 'node', - include: ['**/**/*.test.ts'] - }, -}); \ No newline at end of file diff --git a/example/javascriptlib/testing/2-test-deps/jest.config.ts b/example/javascriptlib/testing/2-test-deps/jest.config.ts deleted file mode 100644 index f54bc06a50c..00000000000 --- a/example/javascriptlib/testing/2-test-deps/jest.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -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']; - - -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(moduleDeps) // use absolute paths generated in tsconfig. -}; \ No newline at end of file diff --git a/example/package.mill b/example/package.mill index 9e2a384d69e..b153c4a9777 100644 --- a/example/package.mill +++ b/example/package.mill @@ -73,6 +73,7 @@ object `package` extends RootModule with Module { object dependencies extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "dependencies")) object publishing extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "publishing")) + object linting extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "linting")) } object pythonlib extends Module { object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic")) diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index c46648c4177..d41badecefa 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -25,6 +25,48 @@ trait TestModule extends TaskModule { object TestModule { type TestResult = Unit + trait Coverage extends TypeScriptModule with TestModule { + def coverage(args: String*): Command[TestResult] = + Task.Command { + coverageTask(Task.Anon { args })() + } + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] + + def coverageDirs: T[Seq[String]] = Task.traverse(moduleDeps)(_.compile)().map { p => + (p._2.path.subRelativeTo(Task.workspace / "out") / "src").toString + "/**/*.ts" + } + + // = '/out'; allow coverage resolve distributed source files. + // & define coverage files relative to . + def link: Task[Unit] = Task.Anon { + 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 nycrcBuilder: Task[Path] = Task.Anon { + val compiled = compile()._1.path + val runner = compiled / ".nycrc" + + val content = + s"""|{ + | "extends": "@istanbuljs/nyc-config-typescript", + | "all": true, + | "include": ${ujson.Arr.from(coverageDirs())}, + | "exclude": ["node_modules"], + | "reporter": ["text", "html"], + | "require": ["ts-node/register", "tsconfig-paths/register"] + |} + |""".stripMargin + + os.write.over(runner, content) + + runner + } + } + trait Shared extends TypeScriptModule { override def upstreamPathsBuilder: T[Seq[(String, String)]] = Task { @@ -47,7 +89,7 @@ object TestModule { def port: T[String] } - trait Jest extends TypeScriptModule with Shared with TestModule { + trait Jest extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { Seq( "@types/jest@^29.5.14", @@ -59,30 +101,54 @@ object TestModule { ) } - def testConfigSource: T[PathRef] = - Task.Source(Task.workspace / "jest.config.ts") - override def compilerOptions: T[Map[String, ujson.Value]] = - Task { super.compilerOptions() + ("resolveJsonModule" -> ujson.Bool(true)) } + Task { + super.compilerOptions() + ("resolveJsonModule" -> ujson.Bool(true)) + } - def getConfigFile: T[String] = - Task { (compile()._1.path / "jest.config.ts").toString } + def conf: Task[Path] = Task.Anon { + val compiled = compile()._1.path + val config = compiled / "jest.config.ts" - private def copyConfig: Task[TestResult] = Task.Anon { - os.copy.over( - testConfigSource().path, - compile()._1.path / "jest.config.ts" - ) + val content = + s"""|import {pathsToModuleNameMapper} from 'ts-jest'; + |import {compilerOptions} from './tsconfig.json'; + | + |const moduleDeps = {...compilerOptions.paths}; + |delete moduleDeps['typeRoots']; + | + |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: ${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(moduleDeps) + |} + |""".stripMargin + + os.write.over(config, content) + + config } private def runTest: T[TestResult] = Task { - copyConfig() os.call( ( "node", npmInstall().path / "node_modules/jest/bin/jest.js", "--config", - getConfigFile(), + conf(), getPathToTest() ), stdout = os.Inherit, @@ -96,15 +162,89 @@ object TestModule { runTest() } + // with coverage + private def coverageConf: Task[Path] = Task.Anon { + val compiled = compile()._1.path + val config = compiled / "jest.config.ts" + + val content = + s"""|import {pathsToModuleNameMapper} from 'ts-jest'; + |import {compilerOptions} from './tsconfig.json'; + | + |const moduleDeps = {...compilerOptions.paths}; + |delete moduleDeps['typeRoots']; + | + |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 { + |rootDir: ${ujson.Str((Task.workspace / "out").toString)}, + |preset: 'ts-jest', + |testEnvironment: 'node', + |testMatch: [${ujson.Str( + s"/${compile()._2.path.subRelativeTo(Task.workspace / "out") / "src"}/**/*.test.ts" + )}], + |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), + | + |collectCoverage: true, + |collectCoverageFrom: ${ujson.Arr.from(coverageDirs())}, + |coverageDirectory: 'coverage', + |coverageReporters: ['text', 'html'], + |} + |""".stripMargin + + os.write.over(config, content) + + config + } + + private def runCoverage: T[TestResult] = Task { + link() + os.call( + ( + "node", + "node_modules/jest/bin/jest.js", + "--config", + coverageConf(), + "--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") + () + } + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { + runCoverage() + } + } - trait Mocha extends TypeScriptModule with Shared with TestModule { + trait Mocha extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { Seq( "@types/chai@4.3.1", "@types/mocha@9.1.1", "chai@4.3.6", - "mocha@10.0.0" + "mocha@10.0.0", + "@istanbuljs/nyc-config-typescript@1.0.2", + "nyc@17.1.0", + "source-map-support@0.5.21" ) } @@ -112,9 +252,9 @@ object TestModule { Task { super.getPathToTest() + "/**/**/*.test.ts" } // test-runner.js: run tests on ts files - private def testRunnerBuilder: Task[Path] = Task.Anon { + def conf: Task[Path] = Task.Anon { val compiled = compile()._1.path - val testRunner = compiled / "test-runner.js" + val runner = compiled / "test-runner.js" val content = """|require('ts-node/register'); @@ -122,16 +262,16 @@ object TestModule { |require('mocha/bin/_mocha'); |""".stripMargin - os.write(testRunner, content) + os.write.over(runner, content) - testRunner + runner } private def runTest: T[Unit] = Task { os.call( ( "node", - testRunnerBuilder(), + conf(), getPathToTest() ), stdout = os.Inherit, @@ -145,22 +285,46 @@ object TestModule { runTest() } + // with coverage + private def runCoverage: T[TestResult] = Task { + nycrcBuilder() + link() + os.call( + ( + "./node_modules/.bin/nyc", + "node", + conf(), + 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") + os.remove(Task.workspace / "out/.nycrc") + () + } + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { + runCoverage() + } } - trait Vitest extends TypeScriptModule with Shared with TestModule { + trait Vitest extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { Seq( - "@vitest/runner@2.1.8", - "vite@5.4.11", + "@vitest/runner@3.0.3", + "vite@6.0.11", + "vitest@3.0.3", "vite-tsconfig-paths@3.6.0", - "vitest@2.1.8" + "@vitest/coverage-v8@3.0.3" ) } - def testConfigSource: T[PathRef] = - Task.Source(Task.workspace / "vite.config.ts") - override def compilerOptions: T[Map[String, ujson.Value]] = Task { super.compilerOptions() + ( @@ -174,25 +338,37 @@ object TestModule { ) } - def getConfigFile: T[String] = - Task { (compile()._1.path / "vite.config.ts").toString } + def conf: Task[Path] = Task.Anon { + val compiled = compile()._1.path + val config = compiled / "vite.config.ts" - private def copyConfig: Task[Unit] = Task.Anon { - os.copy.over( - testConfigSource().path, - compile()._1.path / "vite.config.ts" - ) + 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 + + os.write.over(config, content) + + config } private def runTest: T[TestResult] = Task { - copyConfig() os.call( ( - "node", + npmInstall().path / "node_modules/.bin/ts-node", npmInstall().path / "node_modules/.bin/vitest", "--run", "--config", - getConfigFile(), + conf(), getPathToTest() ), stdout = os.Inherit, @@ -206,17 +382,75 @@ object TestModule { runTest() } + // coverage + def coverageConf: Task[Path] = Task.Anon { + val compiled = compile()._1.path + val config = compiled / "vite.config.ts" + + val content = + s"""|import { defineConfig } from 'vite'; + |import tsconfigPaths from 'vite-tsconfig-paths'; + | + |export default defineConfig({ + | plugins: [tsconfigPaths()], + | test: { + | globals: true, + | environment: 'node', + | include: [${ujson.Str( + s"${compile()._2.path.subRelativeTo(Task.workspace / "out") / "src"}/**/*.test.ts" + )}], + | coverage: { + | provider: 'v8', + | reporter: ['text', 'json', 'html'], + | reportsDirectory: 'coverage', + | include: ${ujson.Arr.from( + coverageDirs() + )}, // Specify files to include for coverage + | }, + | }, + |}); + |""".stripMargin + + os.write.over(config, content) + + config + } + + private def runCoverage: T[TestResult] = Task { + link() + os.call( + ( + npmInstall().path / "node_modules/.bin/ts-node", + npmInstall().path / "node_modules/.bin/vitest", + "--run", + "--config", + coverageConf(), + "--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") + () + } + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task { runCoverage() } + } - trait Jasmine extends TypeScriptModule with Shared with TestModule { + trait Jasmine extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { Seq( "@types/jasmine@5.1.2", "jasmine@5.1.0", - "ts-node@10.9.1", - "tsconfig-paths@4.2.0", - "typescript@5.2.2" + "@istanbuljs/nyc-config-typescript@1.0.2", + "nyc@17.1.0", + "source-map-support@0.5.21" ) } @@ -230,9 +464,9 @@ object TestModule { ) } - def configBuilder: T[PathRef] = Task { + def conf: T[Path] = Task { val path = compile()._1.path / "jasmine.json" - os.write( + os.write.over( path, ujson.write( ujson.Obj( @@ -243,14 +477,15 @@ object TestModule { ) ) ) - PathRef(path) + + path } private def runTest: T[Unit] = Task { - configBuilder() - val jasmine = npmInstall().path / "node_modules/jasmine/bin/jasmine.js" - val tsnode = npmInstall().path / "node_modules/ts-node/register/transpile-only.js" - val tsconfigPath = npmInstall().path / "node_modules/tsconfig-paths/register.js" + conf() + 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.call( ( "node", @@ -270,6 +505,56 @@ object TestModule { runTest() } + // with coverage + def coverageConf: T[Path] = Task { + val path = compile()._1.path / "jasmine.json" + val specDir = compile()._2.path.subRelativeTo(Task.workspace / "out") / "src" + 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) + ) + ) + ) + path + } + + private def runCoverage: T[TestResult] = Task { + nycrcBuilder() + link() + 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().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" + ) + + // remove symlink + os.remove(Task.workspace / "out/node_modules") + os.remove(Task.workspace / "out/tsconfig.json") + os.remove(Task.workspace / "out/.nycrc") + () + } + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { + runCoverage() + } + } trait Cypress extends TypeScriptModule with IntegrationSuite with TestModule { diff --git a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala new file mode 100644 index 00000000000..e3d6332d4ec --- /dev/null +++ b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala @@ -0,0 +1,227 @@ +package mill.javascriptlib + +import mill.* +import mill.api.Result +import scala.util.{Try, Success, Failure} +import os.* + +trait TsLintModule extends Module { + sealed trait Lint + private case object Eslint extends Lint + private case object Prettier extends Lint + + def npmLintDeps: T[Seq[String]] = Task { Seq.empty[String] } + + private def npmInstallLint: T[PathRef] = Task { + Try(os.copy.over(Task.workspace / ".npmrc", Task.dest / ".npmrc")).getOrElse(()) + os.call(( + "npm", + "install", + "--userconfig", + ".npmrc", + "--save-dev", + "prettier@3.4.2", + "eslint@9.18.0", + "typescript-eslint@8.21.0", + "@eslint/js@9.18.0", + npmLintDeps() + )) + PathRef(Task.dest) + } + + // Handle config - prioritize eslint config + private def fmtConfig: T[Seq[PathRef]] = Task.Sources( + T.workspace / "eslint.config.mjs", + T.workspace / "eslint.config.cjs", + T.workspace / "eslint.config.js", + T.workspace / ".prettierrc" + ) + + private[TsLintModule] def resolvedFmtConfig: Task[Lint] = Task.Anon { + val locs = fmtConfig() + + val lintT: Path => Lint = _.last match { + case s if s.contains("eslint.config") => Eslint + case _ => Prettier + } + + locs.find(p => os.exists(p.path)) match { + case None => + Result.Failure(s"Lint couldn't find an eslint.config.(js|mjs|cjs) or a `.pretiierrc` file.") + case Some(c) => Result.Success(lintT(c.path)) + } + } + + // eslint + def checkFormatEslint(args: mill.define.Args): Command[Unit] = Task.Command { + resolvedFmtConfig() match { + case Eslint => + val cwd = Task.workspace + os.symlink(cwd / "node_modules", npmInstallLint().path / "node_modules") + val eslint = npmInstallLint().path / "node_modules/.bin/eslint" + val logPath = npmInstallLint().path / "eslint.log" + val result = + Try { + os.call( + (eslint, "."), + stdout = os.PathRedirect(logPath), + stderr = os.PathRedirect(logPath), + cwd = cwd + ) + } + + val replacements = Seq( + s"$cwd/" -> "", + "potentially fixable with the `--fix` option" -> + s"potentially fixable with running ${millSourcePath.last}.reformatAll" + ) + + os.remove(cwd / "node_modules") + result match { + case Failure(e: os.SubprocessException) if e.result.exitCode == 1 => + val lines = os.read.lines(logPath) + val logMssg = lines.map(line => + replacements.foldLeft(line) { case (currentLine, (target, replacement)) => + currentLine.replace(target, replacement) + } + ) + println(logMssg.mkString("\n")) + case Failure(e: os.SubprocessException) => + println(s"Eslint exited with code: ${e.result.exitCode}") + println(os.read.lines(logPath).mkString("\n")) + case Failure(_) => + println(os.read.lines(logPath).mkString("\n")) + case Success(_) => println("All matched files use Eslint code style!") + } + case _ => + } + } + + def reformatEslint(args: mill.define.Args): Command[Unit] = Task.Command { + resolvedFmtConfig() match { + case Eslint => + val cwd = Task.workspace + os.symlink(cwd / "node_modules", npmInstallLint().path / "node_modules") + val eslint = npmInstallLint().path / "node_modules/.bin/eslint" + val logPath = npmInstallLint().path / "eslint.log" + + val result = + Try { + os.call( + (eslint, ".", "--fix"), + stdout = os.PathRedirect(logPath), + stderr = os.PathRedirect(logPath), + cwd = cwd + ) + } + + val replacements = Seq( + s"$cwd/" -> "", + "potentially fixable with the `--fix` option" -> + s"potentially fixable with running ${millSourcePath.last}.reformatAll" + ) + + os.remove(cwd / "node_modules") + result match { + case Failure(e: os.SubprocessException) if e.result.exitCode == 1 => + val lines = os.read.lines(logPath) + val logMssg = lines.map(line => + replacements.foldLeft(line) { case (currentLine, (target, replacement)) => + currentLine.replace(target, replacement) + } + ) + println(logMssg.mkString("\n")) + case Failure(e: os.SubprocessException) => + println(s"Eslint exited with code: ${e.result.exitCode}") + println(os.read.lines(logPath).mkString("\n")) + case Failure(_) => + println(os.read.lines(logPath).mkString("\n")) + case Success(_) => println("All matched files have been reformatted!") + } + case _ => + } + } + + // prettier + def checkFormatPrettier(args: mill.define.Args): Command[Unit] = Task.Command { + resolvedFmtConfig() match { + case Prettier => + val cwd = Task.workspace + val prettier = npmInstallLint().path / "node_modules/.bin/prettier" + val logPath = npmInstallLint().path / "prettier.log" + val defaultArgs = if (args.value.isEmpty) Seq("*/**/*.ts") else args.value + val result = + Try { + os.call( + (prettier, "--check", defaultArgs), // todo: collect from command line? + stdout = os.Inherit, + stderr = os.PathRedirect(logPath), + cwd = cwd + ) + } + + result match { + case Failure(e: os.SubprocessException) if e.result.exitCode == 1 => + val lines = os.read.lines(logPath) + val logMssg = lines.map(_.replace( + "[warn] Code style issues found in the above file. Run Prettier with --write to fix.", + s"[warn] Code style issues found. Run ${millSourcePath.last}.reformatAll to fix." + )) + println(logMssg.mkString("\n")) + case Failure(e: os.SubprocessException) if e.result.exitCode == 2 => + println(os.read.lines(logPath).mkString("\n")) + case Failure(e: os.SubprocessException) => + println(s"Prettier exited with code: ${e.result.exitCode}") + println(os.read.lines(logPath).mkString("\n")) + case Failure(_) => + println(os.read.lines(logPath).mkString("\n")) + case Success(_) => + } + case _ => + } + + } + + def reformatPrettier(args: mill.define.Args): Command[Unit] = Task.Command { + resolvedFmtConfig() match { + case Prettier => + val cwd = Task.workspace + val prettier = npmInstallLint().path / "node_modules/.bin/prettier" + val logPath = npmInstallLint().path / "prettier.log" + val defaultArgs = if (args.value.isEmpty) Seq("*/**/*.ts") else args.value + val result = + Try { + os.call( + (prettier, "--write", defaultArgs), // todo: collect from command line? + stdout = os.Inherit, + stderr = os.PathRedirect(logPath), + cwd = cwd + ) + } + + result match { + case Failure(e: os.SubprocessException) + if e.result.exitCode == 1 || e.result.exitCode == 2 => + println(os.read.lines(logPath).mkString("\n")) + case Failure(e: os.SubprocessException) => + println(s"Prettier exited with code: ${e.result.exitCode}") + println(os.read.lines(logPath).mkString("\n")) + case Failure(_) => + println(os.read.lines(logPath).mkString("\n")) + case Success(_) => println("All matched files have been reformatted!") + } + case _ => + } + } + + def checkFormatAll(args: mill.define.Args): Command[Unit] = Task.Command { + checkFormatEslint(args)() + checkFormatPrettier(args)() + } + + def reformatAll(args: mill.define.Args): Command[Unit] = Task.Command { + reformatEslint(args)() + reformatPrettier(args)() + } + +} diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index ac4ddb358da..b6dcfdab5c3 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -5,7 +5,7 @@ import os.* import scala.util.Try -trait TypeScriptModule extends Module { outer => +trait TypeScriptModule extends TsLintModule { outer => def moduleDeps: Seq[TypeScriptModule] = Nil def npmDeps: T[Seq[String]] = Task { Seq.empty[String] } @@ -34,11 +34,11 @@ trait TypeScriptModule extends Module { outer => "--userconfig", ".npmrc", "--save-dev", - "@types/node@22.10.2", + "@types/node@22.10.9", "@types/esbuild-copy-static-files@0.1.4", - "typescript@5.7.2", + "typescript@5.7.3", "ts-node@^10.9.2", - "esbuild@0.24.0", + "esbuild@0.24.2", "esbuild-plugin-copy@2.1.1", "@esbuild-plugins/tsconfig-paths@0.1.2", "esbuild-copy-static-files@0.1.0", From 771f4f37b8ff256a0d927a871fb20519a145a065 Mon Sep 17 00:00:00 2001 From: Monye David Date: Fri, 24 Jan 2025 17:57:45 +0100 Subject: [PATCH 02/20] minor edit --- .../linting/2-autoformatting/todo.txt | 20 ------------------- .../linting/3-code-coverage/build.mill | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 example/javascriptlib/linting/2-autoformatting/todo.txt diff --git a/example/javascriptlib/linting/2-autoformatting/todo.txt b/example/javascriptlib/linting/2-autoformatting/todo.txt deleted file mode 100644 index 7f16632a353..00000000000 --- a/example/javascriptlib/linting/2-autoformatting/todo.txt +++ /dev/null @@ -1,20 +0,0 @@ -Todo: - - check_format prettier ✅ - - check_format eslint ✅ - - fix format prettier ✅ - - fix format eslint ✅ - - use checkFormatAll / reformatAll - - find config - - use prettier / eslint (based on config) ✅ - - fail if no config provided :( ✅ - - use eslint as default ✅ - - Prettier: - - collect "*/**/*.ts" from arguments? ✅ - - default to if not provided ✅ - - create .prettierignore if does not exist :( - -Todo - Coverage: - - clean coverage out => out/coverage before every coverage run.. - - task to access coverage data. - diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill index c6610ff4753..4fe616e1e4f 100644 --- a/example/javascriptlib/linting/3-code-coverage/build.mill +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -22,7 +22,7 @@ object qux extends TypeScriptModule { // To run a test with coverage, run the command `_.test.coverage`. // The path to generated coverage data can be retrieved with `_.test.coverageFiles`, -// the generated coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. +// and coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. /** Usage From 8d128d688922610b0d4ecdab5a17a03a4de88dad Mon Sep 17 00:00:00 2001 From: Monye David Date: Fri, 24 Jan 2025 19:33:53 +0100 Subject: [PATCH 03/20] minor edit --- javascriptlib/src/mill/javascriptlib/TestModule.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index d41badecefa..64c1954d3f5 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -133,7 +133,7 @@ object TestModule { ujson.Obj("tsconfig" -> "tsconfig.json") )))}, |moduleFileExtensions: ${ujson.Arr.from(Seq("ts", "tsx", "js", "jsx", "json", "node"))}, - |moduleNameMapper: pathsToModuleNameMapper(moduleDeps) + |moduleNameMapper: pathsToModuleNameMapper(sortedModuleDeps) |} |""".stripMargin From 12a9e45e792ebdc10f8209cf00817da08b844359 Mon Sep 17 00:00:00 2001 From: Monye David Date: Sat, 25 Jan 2025 04:30:16 +0100 Subject: [PATCH 04/20] - generate default prettier ignore. --- .../linting/1-linting/build.mill | 10 +++++-- .../src/mill/javascriptlib/TsLintModule.scala | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 9cb61d08212..c4d4a198fb7 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -46,10 +46,16 @@ foo/src/foo.ts ...10 problems (10 errors, 0 warnings) ...10 errors and 0 warnings potentially fixable with running foo.reformatAll. -> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use the prettier configuration available. +> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use prettier if a `.prettierrc` conf file is available. > mill foo.checkFormatAll # run lint with prettier configuration. Checking formatting... [warn] foo/src/foo.ts [warn] Code style issues found. Run foo.reformatAll to fix. -*/ + +> rm -rf .prettierrc # if no configuration files are available the operation will fail. + +> mill foo.checkFormatAll # attempt to run lint will fail. +... +...Lint couldn't find an eslint.config.(js|mjs|cjs) or a `.pretiierrc` file. + */ diff --git a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala index e3d6332d4ec..1406c62ed56 100644 --- a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala @@ -13,7 +13,7 @@ trait TsLintModule extends Module { def npmLintDeps: T[Seq[String]] = Task { Seq.empty[String] } private def npmInstallLint: T[PathRef] = Task { - Try(os.copy.over(Task.workspace / ".npmrc", Task.dest / ".npmrc")).getOrElse(()) + Try(os.copy.over(T.workspace / ".npmrc", Task.dest / ".npmrc")).getOrElse(()) os.call(( "npm", "install", @@ -37,7 +37,7 @@ trait TsLintModule extends Module { T.workspace / ".prettierrc" ) - private[TsLintModule] def resolvedFmtConfig: Task[Lint] = Task.Anon { + private def resolvedFmtConfig: Task[Lint] = Task.Anon { val locs = fmtConfig() val lintT: Path => Lint = _.last match { @@ -56,7 +56,7 @@ trait TsLintModule extends Module { def checkFormatEslint(args: mill.define.Args): Command[Unit] = Task.Command { resolvedFmtConfig() match { case Eslint => - val cwd = Task.workspace + val cwd = T.workspace os.symlink(cwd / "node_modules", npmInstallLint().path / "node_modules") val eslint = npmInstallLint().path / "node_modules/.bin/eslint" val logPath = npmInstallLint().path / "eslint.log" @@ -100,7 +100,7 @@ trait TsLintModule extends Module { def reformatEslint(args: mill.define.Args): Command[Unit] = Task.Command { resolvedFmtConfig() match { case Eslint => - val cwd = Task.workspace + val cwd = T.workspace os.symlink(cwd / "node_modules", npmInstallLint().path / "node_modules") val eslint = npmInstallLint().path / "node_modules/.bin/eslint" val logPath = npmInstallLint().path / "eslint.log" @@ -146,10 +146,12 @@ trait TsLintModule extends Module { def checkFormatPrettier(args: mill.define.Args): Command[Unit] = Task.Command { resolvedFmtConfig() match { case Prettier => - val cwd = Task.workspace + val cwd = T.workspace val prettier = npmInstallLint().path / "node_modules/.bin/prettier" val logPath = npmInstallLint().path / "prettier.log" val defaultArgs = if (args.value.isEmpty) Seq("*/**/*.ts") else args.value + val userPrettierIgnore = os.exists(cwd / ".prettierignore") + if (!userPrettierIgnore) os.symlink(cwd / ".prettierignore", prettierIgnore().path) val result = Try { os.call( @@ -160,6 +162,7 @@ trait TsLintModule extends Module { ) } + if (!userPrettierIgnore) os.remove(cwd / ".prettierignore") result match { case Failure(e: os.SubprocessException) if e.result.exitCode == 1 => val lines = os.read.lines(logPath) @@ -185,10 +188,12 @@ trait TsLintModule extends Module { def reformatPrettier(args: mill.define.Args): Command[Unit] = Task.Command { resolvedFmtConfig() match { case Prettier => - val cwd = Task.workspace + val cwd = T.workspace val prettier = npmInstallLint().path / "node_modules/.bin/prettier" val logPath = npmInstallLint().path / "prettier.log" val defaultArgs = if (args.value.isEmpty) Seq("*/**/*.ts") else args.value + val userPrettierIgnore = os.exists(cwd / ".prettierignore") + if (!userPrettierIgnore) os.symlink(cwd / ".prettierignore", prettierIgnore().path) val result = Try { os.call( @@ -199,6 +204,7 @@ trait TsLintModule extends Module { ) } + if (!userPrettierIgnore) os.remove(cwd / ".prettierignore") result match { case Failure(e: os.SubprocessException) if e.result.exitCode == 1 || e.result.exitCode == 2 => @@ -214,6 +220,17 @@ trait TsLintModule extends Module { } } + private def prettierIgnore: T[PathRef] = Task { + val config = T.dest / ".prettierignore" + val content = + s"""|node_modules + |.git + |""".stripMargin + os.write.over(config, content) + + PathRef(config) + } + def checkFormatAll(args: mill.define.Args): Command[Unit] = Task.Command { checkFormatEslint(args)() checkFormatPrettier(args)() From f1e9f2ac1af9fb6283ed339cd291ff30f5ad4b59 Mon Sep 17 00:00:00 2001 From: Monye David Date: Sat, 25 Jan 2025 04:46:05 +0100 Subject: [PATCH 05/20] minor edit --- example/javascriptlib/linting/1-linting/build.mill | 11 +++-------- .../src/mill/javascriptlib/TsLintModule.scala | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index c4d4a198fb7..42a570f686c 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -8,7 +8,8 @@ object foo extends TypeScriptModule // and `prettier` https://prettier.io/docs/en out of the box. // You can lint your projects code by providing a configuration for your preferred linter and running `mill _.checkFormatAll`. -// If both configurations files are present, the command `mill _.checkFormatAll` will default to eslint. +// If both configuration files are present, the command `mill _.checkFormatAll` will default to eslint. +// If neither files are present, the command will exit with an error, you must include at least one configuration file. // You can lint via a specificied linter via the commands `mill _.checkFormatEslint` to lint with eslint and // `mill _.checkFormatPrettier` to lint with prettier. @@ -52,10 +53,4 @@ foo/src/foo.ts Checking formatting... [warn] foo/src/foo.ts [warn] Code style issues found. Run foo.reformatAll to fix. - -> rm -rf .prettierrc # if no configuration files are available the operation will fail. - -> mill foo.checkFormatAll # attempt to run lint will fail. -... -...Lint couldn't find an eslint.config.(js|mjs|cjs) or a `.pretiierrc` file. - */ +*/ diff --git a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala index 1406c62ed56..1f5b5a152ad 100644 --- a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala @@ -47,7 +47,7 @@ trait TsLintModule extends Module { locs.find(p => os.exists(p.path)) match { case None => - Result.Failure(s"Lint couldn't find an eslint.config.(js|mjs|cjs) or a `.pretiierrc` file.") + Result.Failure(s"Lint couldn't find an eslint.config.(js|mjs|cjs) or a `.pretiierrc` file") case Some(c) => Result.Success(lintT(c.path)) } } From 957df4d21d640aac5bfa3eb51df7dfe19009b65a Mon Sep 17 00:00:00 2001 From: Monye David Date: Sat, 25 Jan 2025 10:15:01 +0100 Subject: [PATCH 06/20] - module name coverage directories - htmlReports for coverage --- .../linting/3-code-coverage/build.mill | 2 +- .../src/mill/javascriptlib/TestModule.scala | 71 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill index 4fe616e1e4f..b54bf2e910e 100644 --- a/example/javascriptlib/linting/3-code-coverage/build.mill +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -22,7 +22,7 @@ object qux extends TypeScriptModule { // To run a test with coverage, run the command `_.test.coverage`. // The path to generated coverage data can be retrieved with `_.test.coverageFiles`, -// and coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. +// coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. /** Usage diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index 64c1954d3f5..d8e0fe35eee 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -26,13 +26,19 @@ object TestModule { type TestResult = Unit trait Coverage extends TypeScriptModule with TestModule { + override def npmDevDeps: T[Seq[String]] = Task { + super.npmDevDeps() ++ Seq("serve@14.2.4") + } + + protected def runCoverage: T[TestResult] + + protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task { runCoverage() } + def coverage(args: String*): Command[TestResult] = Task.Command { coverageTask(Task.Anon { args })() } - protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] - def coverageDirs: T[Seq[String]] = Task.traverse(moduleDeps)(_.compile)().map { p => (p._2.path.subRelativeTo(Task.workspace / "out") / "src").toString + "/**/*.ts" } @@ -52,6 +58,7 @@ object TestModule { val content = s"""|{ + | "report-dir": ${ujson.Str(s"${moduleDeps.head}_coverage")}, | "extends": "@istanbuljs/nyc-config-typescript", | "all": true, | "include": ${ujson.Arr.from(coverageDirs())}, @@ -65,6 +72,32 @@ object TestModule { runner } + + // web browser - serve coverage report + def htmlReport: T[Unit] = Task { + runCoverage() + val server = npmInstall().path / "node_modules/.bin/serve" + val env = forkEnv() + os.call( + ( + server, + "-s", + coverageFiles().path.toString, + "-l", + env.get("COVERAGE_REPORT_PORT").orElse(Option("4332")) + ), + stdout = os.Inherit, + env = env + ) + () + } + + // coverage files - returnn coverage files directory + def coverageFiles: T[PathRef] = Task { + val dir = Task.workspace / "out" / s"${moduleDeps.head}_coverage" + println(s"coverage files: $dir") + PathRef(dir) + } } trait Shared extends TypeScriptModule { @@ -91,7 +124,7 @@ object TestModule { trait Jest extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { - Seq( + super.npmDevDeps() ++ Seq( "@types/jest@^29.5.14", "@babel/core@^7.26.0", "@babel/preset-env@^7.26.0", @@ -197,7 +230,7 @@ object TestModule { | |collectCoverage: true, |collectCoverageFrom: ${ujson.Arr.from(coverageDirs())}, - |coverageDirectory: 'coverage', + |coverageDirectory: '${moduleDeps.head}_coverage', |coverageReporters: ['text', 'html'], |} |""".stripMargin @@ -207,7 +240,7 @@ object TestModule { config } - private def runCoverage: T[TestResult] = Task { + protected def runCoverage: T[TestResult] = Task { link() os.call( ( @@ -229,15 +262,11 @@ object TestModule { () } - protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { - runCoverage() - } - } trait Mocha extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { - Seq( + super.npmDevDeps() ++ Seq( "@types/chai@4.3.1", "@types/mocha@9.1.1", "chai@4.3.6", @@ -286,7 +315,7 @@ object TestModule { } // with coverage - private def runCoverage: T[TestResult] = Task { + protected def runCoverage: T[TestResult] = Task { nycrcBuilder() link() os.call( @@ -307,16 +336,12 @@ object TestModule { os.remove(Task.workspace / "out/.nycrc") () } - - protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { - runCoverage() - } } trait Vitest extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { - Seq( + super.npmDevDeps() ++ Seq( "@vitest/runner@3.0.3", "vite@6.0.11", "vitest@3.0.3", @@ -402,7 +427,7 @@ object TestModule { | coverage: { | provider: 'v8', | reporter: ['text', 'json', 'html'], - | reportsDirectory: 'coverage', + | reportsDirectory: '${moduleDeps.head}_coverage', | include: ${ujson.Arr.from( coverageDirs() )}, // Specify files to include for coverage @@ -416,7 +441,7 @@ object TestModule { config } - private def runCoverage: T[TestResult] = Task { + protected def runCoverage: T[TestResult] = Task { link() os.call( ( @@ -438,14 +463,12 @@ object TestModule { () } - protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task { runCoverage() } - } trait Jasmine extends Coverage with Shared { override def npmDevDeps: T[Seq[String]] = Task { - Seq( + super.npmDevDeps() ++ Seq( "@types/jasmine@5.1.2", "jasmine@5.1.0", "@istanbuljs/nyc-config-typescript@1.0.2", @@ -523,7 +546,7 @@ object TestModule { path } - private def runCoverage: T[TestResult] = Task { + protected def runCoverage: T[TestResult] = Task { nycrcBuilder() link() val jasmine = "node_modules/jasmine/bin/jasmine.js" @@ -551,10 +574,6 @@ object TestModule { () } - protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task.Anon { - runCoverage() - } - } trait Cypress extends TypeScriptModule with IntegrationSuite with TestModule { From b5a1bccd5daf63ee9d8e3db0de534074aed8b256 Mon Sep 17 00:00:00 2001 From: Monye David Date: Sat, 25 Jan 2025 15:14:11 +0100 Subject: [PATCH 07/20] - include custom sources and generated sources in coverage runs --- .../linting/1-linting/build.mill | 4 +-- .../linting/2-autoformatting/build.mill | 4 +-- .../publishing/2-realistic/jest.config.ts | 32 ------------------- .../src/mill/javascriptlib/TestModule.scala | 22 ++++++------- .../mill/javascriptlib/TypeScriptModule.scala | 15 ++++++++- 5 files changed, 28 insertions(+), 49 deletions(-) delete mode 100644 example/javascriptlib/publishing/2-realistic/jest.config.ts diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 42a570f686c..b6c5d3714dc 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -30,7 +30,7 @@ args: string[ } } -> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are found, mill will opt to use eslint. +> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are present, mill will opt to use eslint. ... foo/src/foo.ts 2:1 error Expected indentation of 2 spaces but found 0 indent @@ -47,7 +47,7 @@ foo/src/foo.ts ...10 problems (10 errors, 0 warnings) ...10 errors and 0 warnings potentially fixable with running foo.reformatAll. -> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use prettier if a `.prettierrc` conf file is available. +> rm -rf eslint.config.mjs # since there is no eslint config file `eslint.config.(js|mjs|cjs)`, mill will use prettier if a `.prettierrc` conf file is available. > mill foo.checkFormatAll # run lint with prettier configuration. Checking formatting... diff --git a/example/javascriptlib/linting/2-autoformatting/build.mill b/example/javascriptlib/linting/2-autoformatting/build.mill index eae38f047c4..2d492a51c92 100644 --- a/example/javascriptlib/linting/2-autoformatting/build.mill +++ b/example/javascriptlib/linting/2-autoformatting/build.mill @@ -26,7 +26,7 @@ args: string[ } } -> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are found, mill will opt to use eslint. +> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are present, mill will opt to use eslint. ... foo/src/foo.ts 2:1 error Expected indentation of 2 spaces but found 0 indent @@ -56,7 +56,7 @@ export class Foo{ } } -> rm -rf eslint.config.mjs # since there is no an eslint config file `eslint.config.(js|mjs|cjs)` present, mill will use the prettier configuration available. +> rm -rf eslint.config.mjs # since there is no eslint config file `eslint.config.(js|mjs|cjs)`, mill will use the prettier configuration available. > mill foo.checkFormatAll # run lint with prettier configuration. Checking formatting... diff --git a/example/javascriptlib/publishing/2-realistic/jest.config.ts b/example/javascriptlib/publishing/2-realistic/jest.config.ts deleted file mode 100644 index 528b0886da9..00000000000 --- a/example/javascriptlib/publishing/2-realistic/jest.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -// @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/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index d8e0fe35eee..a464ea0d1a5 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -30,7 +30,7 @@ object TestModule { super.npmDevDeps() ++ Seq("serve@14.2.4") } - protected def runCoverage: T[TestResult] + private[TestModule] def runCoverage: T[TestResult] protected def coverageTask(args: Task[Seq[String]]): Task[TestResult] = Task { runCoverage() } @@ -39,13 +39,9 @@ object TestModule { coverageTask(Task.Anon { args })() } - def coverageDirs: T[Seq[String]] = Task.traverse(moduleDeps)(_.compile)().map { p => - (p._2.path.subRelativeTo(Task.workspace / "out") / "src").toString + "/**/*.ts" - } - // = '/out'; allow coverage resolve distributed source files. // & define coverage files relative to . - def link: Task[Unit] = Task.Anon { + private[TestModule] def link: Task[Unit] = Task.Anon { 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")) @@ -62,7 +58,7 @@ object TestModule { | "extends": "@istanbuljs/nyc-config-typescript", | "all": true, | "include": ${ujson.Arr.from(coverageDirs())}, - | "exclude": ["node_modules"], + | "exclude": ["node_modules", "*/**/*.test.ts"], | "reporter": ["text", "html"], | "require": ["ts-node/register", "tsconfig-paths/register"] |} @@ -196,7 +192,7 @@ object TestModule { } // with coverage - private def coverageConf: Task[Path] = Task.Anon { + def coverageConf: Task[Path] = Task.Anon { val compiled = compile()._1.path val config = compiled / "jest.config.ts" @@ -230,6 +226,7 @@ object TestModule { | |collectCoverage: true, |collectCoverageFrom: ${ujson.Arr.from(coverageDirs())}, + |coveragePathIgnorePatterns: [${ujson.Str(".*\\.test\\.ts$")}], |coverageDirectory: '${moduleDeps.head}_coverage', |coverageReporters: ['text', 'html'], |} @@ -240,7 +237,7 @@ object TestModule { config } - protected def runCoverage: T[TestResult] = Task { + def runCoverage: T[TestResult] = Task { link() os.call( ( @@ -315,7 +312,7 @@ object TestModule { } // with coverage - protected def runCoverage: T[TestResult] = Task { + def runCoverage: T[TestResult] = Task { nycrcBuilder() link() os.call( @@ -431,6 +428,7 @@ object TestModule { | include: ${ujson.Arr.from( coverageDirs() )}, // Specify files to include for coverage + | exclude: ['*/**/*.test.ts'], // Specify files to exclude from coverage | }, | }, |}); @@ -441,7 +439,7 @@ object TestModule { config } - protected def runCoverage: T[TestResult] = Task { + def runCoverage: T[TestResult] = Task { link() os.call( ( @@ -546,7 +544,7 @@ object TestModule { path } - protected def runCoverage: T[TestResult] = Task { + def runCoverage: T[TestResult] = Task { nycrcBuilder() link() val jasmine = "node_modules/jasmine/bin/jasmine.js" diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index b6dcfdab5c3..53d853aa25e 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -62,7 +62,20 @@ trait TypeScriptModule extends TsLintModule { outer => os.walk(sources().path).filter(fileExt).map(PathRef(_)) } - private def compiledSources: Task[IndexedSeq[PathRef]] = Task.Anon { + // 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 + + combined.map(_.path.subRelativeTo(Task.workspace / "out").toString + "/**/**/*.ts") + } + })().flatten + } + + private[javascriptlib] def compiledSources: Task[IndexedSeq[PathRef]] = Task.Anon { val generated = for { pr <- generatedSources() file <- os.walk(pr.path) From b51f0c8012ca16b3f54fc78a31888531aaa34e1a Mon Sep 17 00:00:00 2001 From: Monye David Date: Sat, 25 Jan 2025 17:56:30 +0100 Subject: [PATCH 08/20] minor edit --- .../src/mill/javascriptlib/TsLintModule.scala | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala index 1f5b5a152ad..caca06365e5 100644 --- a/javascriptlib/src/mill/javascriptlib/TsLintModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TsLintModule.scala @@ -115,22 +115,8 @@ trait TsLintModule extends Module { ) } - val replacements = Seq( - s"$cwd/" -> "", - "potentially fixable with the `--fix` option" -> - s"potentially fixable with running ${millSourcePath.last}.reformatAll" - ) - os.remove(cwd / "node_modules") result match { - case Failure(e: os.SubprocessException) if e.result.exitCode == 1 => - val lines = os.read.lines(logPath) - val logMssg = lines.map(line => - replacements.foldLeft(line) { case (currentLine, (target, replacement)) => - currentLine.replace(target, replacement) - } - ) - println(logMssg.mkString("\n")) case Failure(e: os.SubprocessException) => println(s"Eslint exited with code: ${e.result.exitCode}") println(os.read.lines(logPath).mkString("\n")) @@ -206,9 +192,6 @@ trait TsLintModule extends Module { if (!userPrettierIgnore) os.remove(cwd / ".prettierignore") result match { - case Failure(e: os.SubprocessException) - if e.result.exitCode == 1 || e.result.exitCode == 2 => - println(os.read.lines(logPath).mkString("\n")) case Failure(e: os.SubprocessException) => println(s"Prettier exited with code: ${e.result.exitCode}") println(os.read.lines(logPath).mkString("\n")) From d8a3ba05b074316b64617c9ff957dee33f06363f Mon Sep 17 00:00:00 2001 From: Monye David Date: Sun, 26 Jan 2025 06:45:55 +0100 Subject: [PATCH 09/20] minor edit --- example/javascriptlib/linting/3-code-coverage/build.mill | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill index b54bf2e910e..d75efbfb396 100644 --- a/example/javascriptlib/linting/3-code-coverage/build.mill +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -19,10 +19,10 @@ object qux extends TypeScriptModule { } // Mill supports code coverage with `Jest`, `Mocha`, `Vitest` and `Jasmine` out of the box. -// To run a test with coverage, run the command `_.test.coverage`. +// To run a test with coverage, run the command `mill _.test.coverage`. -// The path to generated coverage data can be retrieved with `_.test.coverageFiles`, -// coverage data can also be displayed in a web brower via the task `_.test.htmlReport`. +// The path to generated coverage data can be retrieved with `mill _.test.coverageFiles`, +// coverage data can also be displayed in a web brower via the task `mill _.test.htmlReport`. /** Usage From a9a8ff0fb432ce9bc8c48cb51e9a3c8420b3d571 Mon Sep 17 00:00:00 2001 From: Monye David Onoh Date: Sun, 26 Jan 2025 22:05:43 +0100 Subject: [PATCH 10/20] minor edit --- docs/modules/ROOT/pages/javascriptlib/linting.adoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/javascriptlib/linting.adoc b/docs/modules/ROOT/pages/javascriptlib/linting.adoc index 9bfc48e0536..cb089ce6af4 100644 --- a/docs/modules/ROOT/pages/javascriptlib/linting.adoc +++ b/docs/modules/ROOT/pages/javascriptlib/linting.adoc @@ -8,9 +8,7 @@ codebases using the Mill build tool == Linting and AutoFormatting with Eslint and Prettier -Eslint and Prettier are tools that analyzes your Scala source code, performing intelligent analyses and -code quality checks, and is often able to automatically fix the issues that it discovers. -It can also perform automated refactoring. +Eslint and Prettier are tools that analyzes your Typescript source code, performing intelligent analyses and code quality checks, and is often able to automatically fix the issues that it discovers. It can also perform automated refactoring. include::partial$example/javascriptlib/linting/1-linting.adoc[] From 1c770bd5601914e5911b41dcd6645c29adefc492 Mon Sep 17 00:00:00 2001 From: Monye David Date: Mon, 27 Jan 2025 09:12:15 +0100 Subject: [PATCH 11/20] - updated docs - allow use of custom test suite configurations --- .../linting/2-autoformatting/build.mill | 26 +---------- .../linting/3-code-coverage/build.mill | 32 ++++++++++++++ .../testing/1-test-suite/build.mill | 23 +++++++++- .../src/mill/javascriptlib/TestModule.scala | 43 +++++++++++++------ 4 files changed, 85 insertions(+), 39 deletions(-) diff --git a/example/javascriptlib/linting/2-autoformatting/build.mill b/example/javascriptlib/linting/2-autoformatting/build.mill index 2d492a51c92..a9dc93b64da 100644 --- a/example/javascriptlib/linting/2-autoformatting/build.mill +++ b/example/javascriptlib/linting/2-autoformatting/build.mill @@ -4,8 +4,7 @@ import mill._, javascriptlib._ object foo extends TypeScriptModule -// Mill supports code formatting via `eslint` https://eslint.org, `typescript-eslint` https://typescript-eslint.io/getting-started -// and `prettier` https://prettier.io/docs/en out of the box. +// Mill supports code formatting via `eslint` and `prettier`. // You can reformat your projects code by providing a configuration for your preferred linter and running `mill _.reformatAll`. // If both configurations files are present, the command `mill _.reformatAll` will default to eslint. @@ -14,7 +13,6 @@ object foo extends TypeScriptModule // When using prettier you can specify the path to reformat via command line argument, `mill _.reformatAll "*/**/*.ts"` // just as you would when running `prettier --write` if no path is provided mill will default to using "*/**/*.ts". -// Also if a `.prettierignore` is not provided, mill will generate one ignoring "node_modules" and ".git". /** Usage > cat foo/src/foo.ts # initial poorly formatted source code @@ -26,23 +24,6 @@ args: string[ } } -> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are present, mill will opt to use eslint. -... -foo/src/foo.ts - 2:1 error Expected indentation of 2 spaces but found 0 indent - 3:1 error Expected indentation of 4 spaces but found 0 indent - 5:1 error Opening curly brace does not appear on the same line as controlling statement brace-style - 5:1 error Statement inside of curly braces should be on next line brace-style - 5:1 error Requires a space after '{' block-spacing - 5:1 error Expected indentation of 2 spaces but found 0 indent - 5:14 error Strings must use singlequote quotes - 5:29 error Missing semicolon semi - 6:1 error Expected indentation of 2 spaces but found 0 indent - 7:2 error Newline required at end of file but not found eol-last -... -...10 problems (10 errors, 0 warnings) -...10 errors and 0 warnings potentially fixable with running foo.reformatAll. - > mill foo.reformatAll ... All matched files have been reformatted! @@ -58,11 +39,6 @@ export class Foo{ > rm -rf eslint.config.mjs # since there is no eslint config file `eslint.config.(js|mjs|cjs)`, mill will use the prettier configuration available. -> mill foo.checkFormatAll # run lint with prettier configuration. -Checking formatting... -[warn] foo/src/foo.ts -[warn] Code style issues found. Run foo.reformatAll to fix. - > mill foo.reformatAll ... All matched files have been reformatted! diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill index d75efbfb396..e55ed03e677 100644 --- a/example/javascriptlib/linting/3-code-coverage/build.mill +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -24,6 +24,38 @@ object qux extends TypeScriptModule { // The path to generated coverage data can be retrieved with `mill _.test.coverageFiles`, // coverage data can also be displayed in a web brower via the task `mill _.test.htmlReport`. +// To use custom configuations for test suites, you can simply include matching test suite config file in your project root. + +// For custom configurations: + +// Jest suite expects a `jest.config.ts` file. + +// Jasmine suite expects a `jasmine.json` file. + +// Mocha suite expects a `test-runner.js` file. + +// Vitest suite expects a `vitest.config.ts` file. + +// Mocha & Jasmine both rely on `nyc` https://www.npmjs.com/package/nyc and `@istanbuljs/nyc-config-typescript` https://www.npmjs.com/package/@istanbuljs/nyc-config-typescript +// for coverage, when using either, to use custom coverage configurations you must include a `.nycrc` file in your project root. + +// Example `.nycrc` configuration + +//// SNIPPET:BUILD +// [source,json] +// ---- +// { +// "extends": "@istanbuljs/nyc-config-typescript", +// "require": ["ts-node/register", "tsconfig-paths/register"], +// "exclude": ["node_modules", "*/**/*.test.ts"], +// "reporter": ["text", "html"], +// ... +//} +// ---- +//// SNIPPET:END + +// As always for most use cases you will never need to define a custom test configurtion file. + /** Usage > mill foo.test.coverage diff --git a/example/javascriptlib/testing/1-test-suite/build.mill b/example/javascriptlib/testing/1-test-suite/build.mill index 1782edfa304..eb84bc1d9d4 100644 --- a/example/javascriptlib/testing/1-test-suite/build.mill +++ b/example/javascriptlib/testing/1-test-suite/build.mill @@ -18,9 +18,28 @@ object qux extends TypeScriptModule { object test extends TypeScriptTests with TestModule.Jasmine } -// Documentation for mill.example.javascriptlib // This build defines 4 modules bar, baz, foo & qux with test suites configured to use -// Jest, Vitest, Mocaha & Jasmine respectively +// Jest, Vitest, Mocaha & Jasmine respectively. + +// Mill will auto-magically generate test configurations for each respective test suite. +// Custom test configurations can be used by simply including a matching test suite config file in your project root, +// (same directory as your `build.mill` file). +// +// You can view the generated config file by looking in the `compile` destination for your modules test. +// For example, the module `bar` will have its j`est.config.ts` file live in `out/bar/test/compile.dest/`. +// +// It is important to note, that for most use cases you will never need +// to define a custom test configurtion file. + +// For custom configurations: + +// Jest suite expects a `jest.config.ts` file. + +// Jasmine suite expects a `jasmine.json` file. + +// Mocha suite expects a `test-runner.js` file. + +// Vitest suite expects a `vitest.config.ts` file. /** Usage diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index a464ea0d1a5..69f0908efac 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -6,6 +6,8 @@ import os.* trait TestModule extends TaskModule { import TestModule.TestResult + def conf: Task[Path] + def test(args: String*): Command[TestResult] = Task.Command { testTask(Task.Anon { args })() @@ -50,7 +52,9 @@ object TestModule { def nycrcBuilder: Task[Path] = Task.Anon { val compiled = compile()._1.path - val runner = compiled / ".nycrc" + val fileName = ".nycrc" + val config = compiled / fileName + val customConfig = Task.workspace / fileName val content = s"""|{ @@ -64,9 +68,12 @@ object TestModule { |} |""".stripMargin - os.write.over(runner, content) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) - runner + os.write.over(config, content) + + config } // web browser - serve coverage report @@ -137,7 +144,9 @@ object TestModule { def conf: Task[Path] = Task.Anon { val compiled = compile()._1.path - val config = compiled / "jest.config.ts" + val fileName = "jest.config.ts" + val config = compiled / fileName + val customConfig = Task.workspace / fileName val content = s"""|import {pathsToModuleNameMapper} from 'ts-jest'; @@ -166,7 +175,8 @@ object TestModule { |} |""".stripMargin - os.write.over(config, content) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) config } @@ -194,7 +204,9 @@ object TestModule { // with coverage def coverageConf: Task[Path] = Task.Anon { val compiled = compile()._1.path - val config = compiled / "jest.config.ts" + val fileName = "jest.config.ts" + val config = compiled / fileName + val customConfig = Task.workspace / fileName val content = s"""|import {pathsToModuleNameMapper} from 'ts-jest'; @@ -232,7 +244,8 @@ object TestModule { |} |""".stripMargin - os.write.over(config, content) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) config } @@ -362,7 +375,9 @@ object TestModule { def conf: Task[Path] = Task.Anon { val compiled = compile()._1.path - val config = compiled / "vite.config.ts" + val fileName = "vitest.config.ts" + val config = compiled / fileName + val customConfig = Task.workspace / fileName val content = """|import { defineConfig } from 'vite'; @@ -378,7 +393,8 @@ object TestModule { |}); |""".stripMargin - os.write.over(config, content) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) config } @@ -407,7 +423,9 @@ object TestModule { // coverage def coverageConf: Task[Path] = Task.Anon { val compiled = compile()._1.path - val config = compiled / "vite.config.ts" + val fileName = "vitest.config.ts" + val config = compiled / fileName + val customConfig = Task.workspace / fileName val content = s"""|import { defineConfig } from 'vite'; @@ -434,7 +452,8 @@ object TestModule { |}); |""".stripMargin - os.write.over(config, content) + if (!os.exists(customConfig)) os.write.over(config, content) + else os.copy.over(customConfig, config) config } @@ -485,7 +504,7 @@ object TestModule { ) } - def conf: T[Path] = Task { + def conf: Task[Path] = Task.Anon { val path = compile()._1.path / "jasmine.json" os.write.over( path, From 5ae07843c348a3e60ddc3aceeb82058b3e7cca8c Mon Sep 17 00:00:00 2001 From: Monye David Date: Mon, 27 Jan 2025 11:03:11 +0100 Subject: [PATCH 12/20] minor fix --- javascriptlib/src/mill/javascriptlib/TestModule.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index 69f0908efac..84c79395ba1 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -6,8 +6,6 @@ import os.* trait TestModule extends TaskModule { import TestModule.TestResult - def conf: Task[Path] - def test(args: String*): Command[TestResult] = Task.Command { testTask(Task.Anon { args })() From 705bd9ffdc6d3eedf9d7a9df47c7a9a417828cc1 Mon Sep 17 00:00:00 2001 From: Monye David Onoh Date: Mon, 27 Jan 2025 16:03:12 +0100 Subject: [PATCH 13/20] minor edit --- example/javascriptlib/testing/1-test-suite/build.mill | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/javascriptlib/testing/1-test-suite/build.mill b/example/javascriptlib/testing/1-test-suite/build.mill index eb84bc1d9d4..c7a80ca53e6 100644 --- a/example/javascriptlib/testing/1-test-suite/build.mill +++ b/example/javascriptlib/testing/1-test-suite/build.mill @@ -19,14 +19,14 @@ object qux extends TypeScriptModule { } // This build defines 4 modules bar, baz, foo & qux with test suites configured to use -// Jest, Vitest, Mocaha & Jasmine respectively. +// Jest, Vitest, Mocha & Jasmine respectively. // Mill will auto-magically generate test configurations for each respective test suite. // Custom test configurations can be used by simply including a matching test suite config file in your project root, -// (same directory as your `build.mill` file). +// same directory as your `build.mill` file. // // You can view the generated config file by looking in the `compile` destination for your modules test. -// For example, the module `bar` will have its j`est.config.ts` file live in `out/bar/test/compile.dest/`. +// For example, the module `bar` will have its `jest.config.ts` file live in `out/bar/test/compile.dest/`. // // It is important to note, that for most use cases you will never need // to define a custom test configurtion file. From 0336720266f0deca66cfad7493834a5194b8eb32 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 08:54:35 +0100 Subject: [PATCH 14/20] requested changes --- .../linting/1-linting/.prettierignore | 4 - .../linting/1-linting/.prettierrc | 7 -- .../linting/1-linting/build.mill | 52 ++++-------- .../linting/1-linting/eslint.config.mjs | 1 - .../linting/1-linting/foo/src/foo.ts | 10 +-- .../linting/2-autoformatting/build.mill | 44 +++++++++-- .../bar/test/src/foo/calculator.test.ts | 11 --- .../linting/3-code-coverage/build.mill | 12 +-- .../3-code-coverage/foo/src/calculator.ts | 10 ++- .../src/mill/javascriptlib/TestModule.scala | 79 ++++++++----------- .../mill/javascriptlib/TypeScriptModule.scala | 2 +- 11 files changed, 106 insertions(+), 126 deletions(-) delete mode 100644 example/javascriptlib/linting/1-linting/.prettierignore delete mode 100644 example/javascriptlib/linting/1-linting/.prettierrc diff --git a/example/javascriptlib/linting/1-linting/.prettierignore b/example/javascriptlib/linting/1-linting/.prettierignore deleted file mode 100644 index 4171269761e..00000000000 --- a/example/javascriptlib/linting/1-linting/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -build -.git \ No newline at end of file diff --git a/example/javascriptlib/linting/1-linting/.prettierrc b/example/javascriptlib/linting/1-linting/.prettierrc deleted file mode 100644 index 986beea4f82..00000000000 --- a/example/javascriptlib/linting/1-linting/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "trailingComma": "all", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2 -} \ No newline at end of file diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index b6c5d3714dc..0bf280fc799 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -2,55 +2,33 @@ package build import mill._, javascriptlib._ -object foo extends TypeScriptModule +object foo extends TypeScriptModule with TsLintModule -// Mill supports code linting via `eslint` https://eslint.org, `typescript-eslint` https://typescript-eslint.io/getting-started -// and `prettier` https://prettier.io/docs/en out of the box. -// You can lint your projects code by providing a configuration for your preferred linter and running `mill _.checkFormatAll`. - -// If both configuration files are present, the command `mill _.checkFormatAll` will default to eslint. -// If neither files are present, the command will exit with an error, you must include at least one configuration file. -// You can lint via a specificied linter via the commands `mill _.checkFormatEslint` to lint with eslint and -// `mill _.checkFormatPrettier` to lint with prettier. - -// When using prettier you can specify the path to lint via command line argument, `mill _.checkFormatAll "*/**/*.ts"` -// just as you would when running `prettier --check` if no path is provided mill will default to using "*/**/*.ts". -// Also if a `.prettierignore` is not provided, mill will generate one ignoring "node_modules" and ".git". +// Mill supports code linting via `eslint` https://eslint.org out of the box. +// You can lint your projects code by providing a configuration for eslint and running `mill _.checkFormatAll`. // You can define `npmLintDeps` field to add dependencies specific to linting to your module. // The task `npmLintDeps` works the same way as `npmDeps` or `npmDevDeps`. /** Usage -> cat foo/src/foo.ts # initial poorly formatted source code +> cat foo/src/foo.ts # initial code with lint errors. export class Foo{ -static main( -args: string[ -]) -{console.log("Hello World!") -} + static main( + args: string[ +]) { + console.log('Hello World!'); + } } -> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are present, mill will opt to use eslint. +> mill foo.checkFormatAll # run eslint ... foo/src/foo.ts - 2:1 error Expected indentation of 2 spaces but found 0 indent - 3:1 error Expected indentation of 4 spaces but found 0 indent - 5:1 error Opening curly brace does not appear on the same line as controlling statement brace-style - 5:1 error Statement inside of curly braces should be on next line brace-style - 5:1 error Requires a space after '{' block-spacing - 5:1 error Expected indentation of 2 spaces but found 0 indent - 5:14 error Strings must use singlequote quotes - 5:29 error Missing semicolon semi - 6:1 error Expected indentation of 2 spaces but found 0 indent - 7:2 error Newline required at end of file but not found eol-last + 3:5 error 'args' is defined but never used @typescript-eslint/no-unused-vars ... -...10 problems (10 errors, 0 warnings) -...10 errors and 0 warnings potentially fixable with running foo.reformatAll. +...1 problem (1 error, 0 warnings) -> rm -rf eslint.config.mjs # since there is no eslint config file `eslint.config.(js|mjs|cjs)`, mill will use prettier if a `.prettierrc` conf file is available. +> sed -i '' "s|console.log('Hello World!')|console.log(\`Hello World! \${args.join(' ')}\`)|" foo/src/foo.ts # fix lint error. -> mill foo.checkFormatAll # run lint with prettier configuration. -Checking formatting... -[warn] foo/src/foo.ts -[warn] Code style issues found. Run foo.reformatAll to fix. +> mill foo.checkFormatAll # run eslint +All matched files use Eslint code style! */ diff --git a/example/javascriptlib/linting/1-linting/eslint.config.mjs b/example/javascriptlib/linting/1-linting/eslint.config.mjs index 913514ffdc3..f6181eaffaa 100644 --- a/example/javascriptlib/linting/1-linting/eslint.config.mjs +++ b/example/javascriptlib/linting/1-linting/eslint.config.mjs @@ -10,7 +10,6 @@ export default tseslint.config({ ...tseslint.configs.recommended, ], rules: { - '@typescript-eslint/no-unused-vars': 'off', // styling rules 'semi': ['error', 'always'], 'quotes': ['error', 'single'], diff --git a/example/javascriptlib/linting/1-linting/foo/src/foo.ts b/example/javascriptlib/linting/1-linting/foo/src/foo.ts index d433279976f..e129979b8fd 100644 --- a/example/javascriptlib/linting/1-linting/foo/src/foo.ts +++ b/example/javascriptlib/linting/1-linting/foo/src/foo.ts @@ -1,7 +1,7 @@ export class Foo{ -static main( -args: string[ -]) -{console.log("Hello World!") + static main( + args: string[ +]) { + console.log('Hello World!'); + } } -} \ No newline at end of file diff --git a/example/javascriptlib/linting/2-autoformatting/build.mill b/example/javascriptlib/linting/2-autoformatting/build.mill index a9dc93b64da..eacc2dc9e17 100644 --- a/example/javascriptlib/linting/2-autoformatting/build.mill +++ b/example/javascriptlib/linting/2-autoformatting/build.mill @@ -2,17 +2,23 @@ package build import mill._, javascriptlib._ -object foo extends TypeScriptModule +object foo extends TypeScriptModule with TsLintModule -// Mill supports code formatting via `eslint` and `prettier`. -// You can reformat your projects code by providing a configuration for your preferred linter and running `mill _.reformatAll`. +// Mill supports code auto formatting via `eslint` https://eslint.org and `prettier` https://prettier.io/docs/en. +// You can reformat your projects code by providing a configuration for your preferred formtter then running +// `mill _.checkFormatAll` to check for formatting and linting (when using eslint) errors or +// `mill _.reformatAll` to reformat your code. -// If both configurations files are present, the command `mill _.reformatAll` will default to eslint. -// You can format via a specificied linter via the commands `mill _.reformatEslint` to format with eslint and -// `mill _.reformatPrettier` to format with prettier. +// If both configuration files are present, the command `mill _.checkFormatAll` and +// `mill _.reformatAll` will default to eslint. If neither files are present, +// the command will exit with an error, you must include at least one configuration file. -// When using prettier you can specify the path to reformat via command line argument, `mill _.reformatAll "*/**/*.ts"` -// just as you would when running `prettier --write` if no path is provided mill will default to using "*/**/*.ts". +// When using prettier you can specify the path to check format or reformat via command line argument, +// `mill _.checkFormatAll "*/**/*.ts"` just as you would when running `prettier --check` or +// `mill _.reformatAll "*/**/*.ts"` just as you would when running `prettier --write`, +// if no path is provided mill will default to using "*/**/*.ts". +// +// Also if a `.prettierignore` is not provided, mill will generate one ignoring "node_modules" and ".git". /** Usage > cat foo/src/foo.ts # initial poorly formatted source code @@ -24,6 +30,23 @@ args: string[ } } +> mill foo.checkFormatAll # run linter - since both eslint and prettier configurations are present, mill will opt to use eslint. +... +foo/src/foo.ts + 2:1 error Expected indentation of 2 spaces but found 0 indent + 3:1 error Expected indentation of 4 spaces but found 0 indent + 5:1 error Opening curly brace does not appear on the same line as controlling statement brace-style + 5:1 error Statement inside of curly braces should be on next line brace-style + 5:1 error Requires a space after '{' block-spacing + 5:1 error Expected indentation of 2 spaces but found 0 indent + 5:14 error Strings must use singlequote quotes + 5:29 error Missing semicolon semi + 6:1 error Expected indentation of 2 spaces but found 0 indent + 7:2 error Newline required at end of file but not found eol-last +... +...10 problems (10 errors, 0 warnings) +...10 errors and 0 warnings potentially fixable with running foo.reformatAll. + > mill foo.reformatAll ... All matched files have been reformatted! @@ -39,6 +62,11 @@ export class Foo{ > rm -rf eslint.config.mjs # since there is no eslint config file `eslint.config.(js|mjs|cjs)`, mill will use the prettier configuration available. +> mill foo.checkFormatAll # run lint with prettier configuration. +Checking formatting... +[warn] foo/src/foo.ts +[warn] Code style issues found. Run foo.reformatAll to fix. + > mill foo.reformatAll ... All matched files have been reformatted! diff --git a/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts b/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts index 33fc3abfeca..502ce62a4b5 100644 --- a/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts +++ b/example/javascriptlib/linting/3-code-coverage/bar/test/src/foo/calculator.test.ts @@ -16,15 +16,4 @@ describe('Calculator', () => { }); }); - describe('Division', () => { - it('should return the quotient of two numbers', () => { - const result = calculator.divide(6, 3); - expect(result).to.equal(2); - }); - - it('should throw an error when dividing by zero', () => { - expect(() => calculator.divide(6, 0)).to.throw("Division by zero is not allowed"); - }); - }); - }); \ No newline at end of file diff --git a/example/javascriptlib/linting/3-code-coverage/build.mill b/example/javascriptlib/linting/3-code-coverage/build.mill index e55ed03e677..d5f5a7322f8 100644 --- a/example/javascriptlib/linting/3-code-coverage/build.mill +++ b/example/javascriptlib/linting/3-code-coverage/build.mill @@ -22,7 +22,7 @@ object qux extends TypeScriptModule { // To run a test with coverage, run the command `mill _.test.coverage`. // The path to generated coverage data can be retrieved with `mill _.test.coverageFiles`, -// coverage data can also be displayed in a web brower via the task `mill _.test.htmlReport`. +// The path to generated html file can be retrieved with `mill _.test.htmlReport`. // To use custom configuations for test suites, you can simply include matching test suite config file in your project root. @@ -64,8 +64,8 @@ object qux extends TypeScriptModule { ---------------|---------|----------|---------|---------|------------------- File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... ---------------|---------|----------|---------|---------|------------------- -...All files...|...100 |...100 |...100 |...100 |... -...calculator.ts...|...100 |...100 |...100 |...100 |... +...All files...|...62.5 |...50 |...66.66 |...62.5 |... +...calculator.ts...|...62.5 |...50 |...66.66 |...62.5 | 14-17... ---------------|---------|----------|---------|---------|------------------- ... Test Suites:...1 passed, 1 total... @@ -74,13 +74,13 @@ Tests:...4 passed, 4 total... > mill bar.test.coverage ... -...4 passing... +...2 passing... ... ---------------|---------|----------|---------|---------|------------------- File...| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s... ---------------|---------|----------|---------|---------|------------------- -...All files...|...100 |...100 |...100 |...100 |... -...calculator.ts...|...100 |...100 |...100 |...100 |... +...All files...|...66.66 |...0 |...66.66 |...62.5 |... +...calculator.ts...|...66.66 |...0 |...66.66 |...62.5 | 7-10... ---------------|---------|----------|---------|---------|------------------- > mill baz.test.coverage diff --git a/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts b/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts index 2e9b2aa67d5..937831b5a68 100644 --- a/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts +++ b/example/javascriptlib/linting/3-code-coverage/foo/src/calculator.ts @@ -9,4 +9,12 @@ export class Calculator { } return a / b; } -} \ No newline at end of file + + multiply(a: number, b: number): number { + if (b === 0) { + throw new Error("Division by zero is not allowed"); + } + return a / b; + } +} + diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index 84c79395ba1..1a56c03628c 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -41,14 +41,14 @@ object TestModule { // = '/out'; allow coverage resolve distributed source files. // & define coverage files relative to . - private[TestModule] def link: Task[Unit] = Task.Anon { + private[TestModule] def coverageSetupSymlinks: Task[Unit] = Task.Anon { 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 nycrcBuilder: Task[Path] = Task.Anon { + def istanbulNycrcConfigBuilder: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val fileName = ".nycrc" val config = compiled / fileName @@ -71,26 +71,15 @@ object TestModule { os.write.over(config, content) - config + PathRef(config) } // web browser - serve coverage report - def htmlReport: T[Unit] = Task { + def htmlReport: T[PathRef] = Task { runCoverage() - val server = npmInstall().path / "node_modules/.bin/serve" - val env = forkEnv() - os.call( - ( - server, - "-s", - coverageFiles().path.toString, - "-l", - env.get("COVERAGE_REPORT_PORT").orElse(Option("4332")) - ), - stdout = os.Inherit, - env = env - ) - () + val htmlPath = coverageFiles().path / "index.html" + println(s"HTML Report: $htmlPath") + PathRef(htmlPath) } // coverage files - returnn coverage files directory @@ -140,7 +129,7 @@ object TestModule { super.compilerOptions() + ("resolveJsonModule" -> ujson.Bool(true)) } - def conf: Task[Path] = Task.Anon { + def conf: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val fileName = "jest.config.ts" val config = compiled / fileName @@ -176,7 +165,7 @@ object TestModule { if (!os.exists(customConfig)) os.write.over(config, content) else os.copy.over(customConfig, config) - config + PathRef(config) } private def runTest: T[TestResult] = Task { @@ -185,7 +174,7 @@ object TestModule { "node", npmInstall().path / "node_modules/jest/bin/jest.js", "--config", - conf(), + conf().path, getPathToTest() ), stdout = os.Inherit, @@ -200,7 +189,7 @@ object TestModule { } // with coverage - def coverageConf: Task[Path] = Task.Anon { + def coverageConf: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val fileName = "jest.config.ts" val config = compiled / fileName @@ -245,17 +234,17 @@ object TestModule { if (!os.exists(customConfig)) os.write.over(config, content) else os.copy.over(customConfig, config) - config + PathRef(config) } def runCoverage: T[TestResult] = Task { - link() + coverageSetupSymlinks() os.call( ( "node", "node_modules/jest/bin/jest.js", "--config", - coverageConf(), + coverageConf().path, "--coverage", getPathToTest() ), @@ -289,7 +278,7 @@ object TestModule { Task { super.getPathToTest() + "/**/**/*.test.ts" } // test-runner.js: run tests on ts files - def conf: Task[Path] = Task.Anon { + def conf: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val runner = compiled / "test-runner.js" @@ -301,14 +290,14 @@ object TestModule { os.write.over(runner, content) - runner + PathRef(runner) } private def runTest: T[Unit] = Task { os.call( ( "node", - conf(), + conf().path, getPathToTest() ), stdout = os.Inherit, @@ -324,13 +313,13 @@ object TestModule { // with coverage def runCoverage: T[TestResult] = Task { - nycrcBuilder() - link() + istanbulNycrcConfigBuilder() + coverageSetupSymlinks() os.call( ( "./node_modules/.bin/nyc", "node", - conf(), + conf().path, getPathToTest() ), stdout = os.Inherit, @@ -371,7 +360,7 @@ object TestModule { ) } - def conf: Task[Path] = Task.Anon { + def conf: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val fileName = "vitest.config.ts" val config = compiled / fileName @@ -394,7 +383,7 @@ object TestModule { if (!os.exists(customConfig)) os.write.over(config, content) else os.copy.over(customConfig, config) - config + PathRef(config) } private def runTest: T[TestResult] = Task { @@ -404,7 +393,7 @@ object TestModule { npmInstall().path / "node_modules/.bin/vitest", "--run", "--config", - conf(), + conf().path, getPathToTest() ), stdout = os.Inherit, @@ -419,7 +408,7 @@ object TestModule { } // coverage - def coverageConf: Task[Path] = Task.Anon { + def coverageConf: Task[PathRef] = Task.Anon { val compiled = compile()._1.path val fileName = "vitest.config.ts" val config = compiled / fileName @@ -453,18 +442,18 @@ object TestModule { if (!os.exists(customConfig)) os.write.over(config, content) else os.copy.over(customConfig, config) - config + PathRef(config) } def runCoverage: T[TestResult] = Task { - link() + coverageSetupSymlinks() os.call( ( npmInstall().path / "node_modules/.bin/ts-node", npmInstall().path / "node_modules/.bin/vitest", "--run", "--config", - coverageConf(), + coverageConf().path, "--coverage", getPathToTest() ), @@ -502,7 +491,7 @@ object TestModule { ) } - def conf: Task[Path] = Task.Anon { + def conf: Task[PathRef] = Task.Anon { val path = compile()._1.path / "jasmine.json" os.write.over( path, @@ -516,7 +505,7 @@ object TestModule { ) ) - path + PathRef(path) } private def runTest: T[Unit] = Task { @@ -544,7 +533,7 @@ object TestModule { } // with coverage - def coverageConf: T[Path] = Task { + def coverageConf: T[PathRef] = Task { val path = compile()._1.path / "jasmine.json" val specDir = compile()._2.path.subRelativeTo(Task.workspace / "out") / "src" os.write.over( @@ -558,16 +547,16 @@ object TestModule { ) ) ) - path + PathRef(path) } def runCoverage: T[TestResult] = Task { - nycrcBuilder() - link() + 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().subRelativeTo(Task.workspace / "out") + val relConfigPath = coverageConf().path.subRelativeTo(Task.workspace / "out") os.call( ( "./node_modules/.bin/nyc", diff --git a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala index 53d853aa25e..01830c0fcd5 100644 --- a/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TypeScriptModule.scala @@ -5,7 +5,7 @@ import os.* import scala.util.Try -trait TypeScriptModule extends TsLintModule { outer => +trait TypeScriptModule extends Module { outer => def moduleDeps: Seq[TypeScriptModule] = Nil def npmDeps: T[Seq[String]] = Task { Seq.empty[String] } From 1e4404c782ad0722fc42c5d71a9776909ad12952 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 08:03:17 +0000 Subject: [PATCH 15/20] [autofix.ci] apply automated fixes --- javascriptlib/src/mill/javascriptlib/TestModule.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/javascriptlib/src/mill/javascriptlib/TestModule.scala b/javascriptlib/src/mill/javascriptlib/TestModule.scala index 1a56c03628c..5e2d0d4008b 100644 --- a/javascriptlib/src/mill/javascriptlib/TestModule.scala +++ b/javascriptlib/src/mill/javascriptlib/TestModule.scala @@ -1,7 +1,6 @@ package mill.javascriptlib import mill.* -import os.* trait TestModule extends TaskModule { import TestModule.TestResult From dac944966dbd5e0bd82b931a31c9865f592ff48c Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 11:46:14 +0100 Subject: [PATCH 16/20] requested changes --- example/javascriptlib/linting/1-linting/build.mill | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 0bf280fc799..5fa370f5ddc 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -27,7 +27,16 @@ foo/src/foo.ts ... ...1 problem (1 error, 0 warnings) -> sed -i '' "s|console.log('Hello World!')|console.log(\`Hello World! \${args.join(' ')}\`)|" foo/src/foo.ts # fix lint error. +> sed -i '' "s/'Hello World!'/\`Hello World! \${args.join(' ')}\`/g" foo/src/foo.ts # fix lint error. + +> cat foo/src/foo.ts # initial code with lint errors. +export class Foo{ + static main( + args: string[ +]) { + console.log(`Hello World! ${args.join(' ')}`); + } +} > mill foo.checkFormatAll # run eslint All matched files use Eslint code style! From 317d0717d0753dd7564f6749ea33a6b9e0e304d9 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 11:48:24 +0100 Subject: [PATCH 17/20] minor edit --- example/javascriptlib/linting/2-autoformatting/build.mill | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/javascriptlib/linting/2-autoformatting/build.mill b/example/javascriptlib/linting/2-autoformatting/build.mill index eacc2dc9e17..ee893e39609 100644 --- a/example/javascriptlib/linting/2-autoformatting/build.mill +++ b/example/javascriptlib/linting/2-autoformatting/build.mill @@ -6,8 +6,8 @@ object foo extends TypeScriptModule with TsLintModule // Mill supports code auto formatting via `eslint` https://eslint.org and `prettier` https://prettier.io/docs/en. // You can reformat your projects code by providing a configuration for your preferred formtter then running -// `mill _.checkFormatAll` to check for formatting and linting (when using eslint) errors or -// `mill _.reformatAll` to reformat your code. +// `mill _.reformatAll` to reformat your code. You can also check for formatting errors by running +// `mill _.checkFormatAll`. // If both configuration files are present, the command `mill _.checkFormatAll` and // `mill _.reformatAll` will default to eslint. If neither files are present, From 8dec87afcf98c4e422ea028ae4679a6a5001b374 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 12:20:20 +0100 Subject: [PATCH 18/20] fix failing --- example/javascriptlib/linting/1-linting/build.mill | 6 +++--- example/javascriptlib/linting/1-linting/foo/src/foo.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 5fa370f5ddc..4fab6dc52d5 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -16,7 +16,7 @@ export class Foo{ static main( args: string[ ]) { - console.log('Hello World!'); + console.log('Hello World'); } } @@ -27,14 +27,14 @@ foo/src/foo.ts ... ...1 problem (1 error, 0 warnings) -> sed -i '' "s/'Hello World!'/\`Hello World! \${args.join(' ')}\`/g" foo/src/foo.ts # fix lint error. +> sed -i '' "s/'Hello World'/\`Hello World \${args.join(' ')}\`/g" foo/src/foo.ts # fix lint error. > cat foo/src/foo.ts # initial code with lint errors. export class Foo{ static main( args: string[ ]) { - console.log(`Hello World! ${args.join(' ')}`); + console.log(`Hello World ${args.join(' ')}`); } } diff --git a/example/javascriptlib/linting/1-linting/foo/src/foo.ts b/example/javascriptlib/linting/1-linting/foo/src/foo.ts index e129979b8fd..915bf3f6c28 100644 --- a/example/javascriptlib/linting/1-linting/foo/src/foo.ts +++ b/example/javascriptlib/linting/1-linting/foo/src/foo.ts @@ -2,6 +2,6 @@ export class Foo{ static main( args: string[ ]) { - console.log('Hello World!'); + console.log('Hello World'); } } From 38561e65b9548c9f3a606cb537852e6db9ed9758 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 13:46:32 +0100 Subject: [PATCH 19/20] Fix --- example/javascriptlib/linting/1-linting/build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 4fab6dc52d5..40f2bd2b958 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -27,7 +27,7 @@ foo/src/foo.ts ... ...1 problem (1 error, 0 warnings) -> sed -i '' "s/'Hello World'/\`Hello World \${args.join(' ')}\`/g" foo/src/foo.ts # fix lint error. +> sed -i '' "s|console.log('Hello World')|console.log(\`Hello World \${args.join(' ')}\`)|" foo/src/foo.ts # fix lint error. > cat foo/src/foo.ts # initial code with lint errors. export class Foo{ From ef21ccc941ca3a76f7acedd2074804314fc2ff47 Mon Sep 17 00:00:00 2001 From: Monye David Date: Tue, 28 Jan 2025 15:50:22 +0100 Subject: [PATCH 20/20] Fix --- example/javascriptlib/linting/1-linting/build.mill | 4 ++-- example/javascriptlib/linting/1-linting/eslint.config.mjs | 1 - example/javascriptlib/linting/1-linting/foo/src/foo.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/example/javascriptlib/linting/1-linting/build.mill b/example/javascriptlib/linting/1-linting/build.mill index 40f2bd2b958..162413f8bcb 100644 --- a/example/javascriptlib/linting/1-linting/build.mill +++ b/example/javascriptlib/linting/1-linting/build.mill @@ -16,7 +16,7 @@ export class Foo{ static main( args: string[ ]) { - console.log('Hello World'); + console.log(`Hello World`); } } @@ -27,7 +27,7 @@ foo/src/foo.ts ... ...1 problem (1 error, 0 warnings) -> sed -i '' "s|console.log('Hello World')|console.log(\`Hello World \${args.join(' ')}\`)|" foo/src/foo.ts # fix lint error. +> sed -i.bak 's/Hello World/Hello World ${args.join('\'' '\'')}/' foo/src/foo.ts # fix lint error. > cat foo/src/foo.ts # initial code with lint errors. export class Foo{ diff --git a/example/javascriptlib/linting/1-linting/eslint.config.mjs b/example/javascriptlib/linting/1-linting/eslint.config.mjs index f6181eaffaa..4a84a25b023 100644 --- a/example/javascriptlib/linting/1-linting/eslint.config.mjs +++ b/example/javascriptlib/linting/1-linting/eslint.config.mjs @@ -12,7 +12,6 @@ export default tseslint.config({ rules: { // styling rules 'semi': ['error', 'always'], - 'quotes': ['error', 'single'], 'comma-dangle': ['error', 'always-multiline'], 'max-len': ['error', {code: 80, ignoreUrls: true}], 'indent': ['error', 2, {SwitchCase: 1}], diff --git a/example/javascriptlib/linting/1-linting/foo/src/foo.ts b/example/javascriptlib/linting/1-linting/foo/src/foo.ts index 915bf3f6c28..5da7ade49ad 100644 --- a/example/javascriptlib/linting/1-linting/foo/src/foo.ts +++ b/example/javascriptlib/linting/1-linting/foo/src/foo.ts @@ -2,6 +2,6 @@ export class Foo{ static main( args: string[ ]) { - console.log('Hello World'); + console.log(`Hello World`); } }