Skip to content

Commit

Permalink
Add first class Javascript/Typescript support to the Mill build tool (#…
Browse files Browse the repository at this point in the history
…4127)

This pr implements the examples for jslib/module.

#3927

Key Changes:

- handle usage of resources files in bundled and non-bundled
environments
 - handle custom-resources usage
- Standard resource files in `@<module-name>/resources` or custom
resources files in user defined custom resource path can be imported
globally`@<module-name>/resources-directory`
 - allows for access to resources defined in dependent modules
 - allows for access to resources in bundled code and environment
 - allows for access to resources define in `/test`
 - updated build script to handle multiple resources directories

Implements Task generatedSources
  - imported via `@generated/generated-file-name`

Note:

For `example/javascriptlib/module/3-override-tasks/`, the foo directory
needs to exist, hence the file `foo/readme.md`, code sources are
generated from its .mill file

### Checklist
- [x] **example/jslib/module**
     - [x] common-config/
     - [x] resources/
     - [x] custom-tasks/
     - [x] override-tasks/
     - [x] compilation-execution-flags/
     - [x] executable-config
  • Loading branch information
monyedavid authored Dec 27, 2024
1 parent edc3b54 commit 168ceee
Show file tree
Hide file tree
Showing 39 changed files with 787 additions and 181 deletions.
27 changes: 23 additions & 4 deletions docs/modules/ROOT/pages/javascriptlib/intro.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@

= Building Javascript with Mill

include::partial$gtag-config.adoc[]


:language: Java
:language-small: java
:language: Typescript
:language-small: typescript

== Simple Typescript Module

include::partial$example/javascriptlib/basic/1-simple.adoc[]

== React Module

include::partial$example/javascriptlib/basic/2-react.adoc[]

== Custom Build Logic

include::partial$example/javascriptlib/basic/3-custom-build-logic.adoc[]

== Multi Module Project

include::partial$example/javascriptlib/basic/4-multi-modules.adoc[]

== Simple Client-Server

include::partial$example/javascriptlib/basic/5-client-server-hello.adoc[]

== Realistic Client-Server Example Project

include::partial$example/javascriptlib/basic/6-client-server-realistic.adoc[]

30 changes: 30 additions & 0 deletions docs/modules/ROOT/pages/javascriptlib/module-config.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
= Typescript Module Configuration

include::partial$gtag-config.adoc[]

:language: Typescript
:language-small: typescript

== Common Configuration Overrides

include::partial$example/javascriptlib/module/1-common-config.adoc[]

== Custom Tasks

include::partial$example/javascriptlib/module/2-custom-tasks.adoc[]

== Overriding Tasks

include::partial$example/javascriptlib/module/3-override-tasks.adoc[]

== Compilation & Execution Flags

include::partial$example/javascriptlib/module/4-compilation-execution-flags.adoc[]

== Filesystem Resources

include::partial$example/javascriptlib/module/5-resources.adoc[]

== Bundling Configuration

include::partial$example/javascriptlib/module/6-executable-config.adoc[]
14 changes: 14 additions & 0 deletions docs/modules/ROOT/pages/javascriptlib/testing.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
= Testing Typescript Projects

include::partial$gtag-config.adoc[]

This page will discuss common topics around working with test suites using the Mill build tool

== Defining Unit Test Suites

include::partial$example/javascriptlib/testing/1-test-suite.adoc[]


== Test Dependencies

include::partial$example/javascriptlib/testing/2-test-deps.adoc[]
5 changes: 2 additions & 3 deletions example/javascriptlib/basic/2-react/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package build
import mill._, javascriptlib._

object foo extends RsWithServeModule {
override def mkENV = Task {
super.mkENV() ++ Map("PORT" -> "3000")
}
override def forkEnv = super.forkEnv() + ("PORT" -> "3000")

}

// Documentation for mill.example.javascriptlib
Expand Down
24 changes: 5 additions & 19 deletions example/javascriptlib/basic/3-custom-build-logic/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ object foo extends TypeScriptModule {
allSources().map(f => os.read.lines(f.path).size).sum
}

/** Generate resources using lineCount of sources */
/** Generate resource using lineCount of sources */
def resources = Task {
os.write(Task.dest / "line-count.txt", "" + lineCount())
PathRef(Task.dest)
}

// define resource path for the node app
override def mkENV = Task {
super.mkENV() ++ Map("RESOURCE_PATH" -> resources().path.toString)
super.resources() ++ Seq(PathRef(Task.dest))
}

object test extends TypeScriptTests with TestModule.Jest
Expand All @@ -28,21 +23,12 @@ object foo extends TypeScriptModule {

/** Usage

> mill foo.test
PASS .../foo.test.ts
...
Test Suites:...1 passed, 1 total...
Tests:...2 passed, 2 total...
...

> mill foo.run
[Reading file:] .../out/foo/resources.dest/line-count.txt
Line Count: ...
Line Count: 21

> mill show foo.bundle
Build succeeded!

> node out/foo/bundle.dest/bundle.js out/foo/resources.dest/
[Reading file:] .../line-count.txt
Line Count: ...
> node out/foo/bundle.dest/bundle.js
Line Count: 21
*/
35 changes: 14 additions & 21 deletions example/javascriptlib/basic/3-custom-build-logic/foo/src/foo.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import * as fs from 'fs';
import * as path from 'path';
import * as fs from 'fs/promises';

const Resources: string = process.env.RESOURCESDEST || "@foo/resources.dest" // `RESOURCES` is generated on bundle
const LineCount = require.resolve(`${Resources}/line-count.txt`);

export default class Foo {
static getLineCount(resourcePath: string): string | null {
try {
const filePath = path.join(resourcePath, 'line-count.txt');
console.log('[Reading file:]', filePath);
return fs.readFileSync(filePath, 'utf-8');
} catch (error) {
console.error('Error reading file:', error);
return null;
}
static async getLineCount(): Promise<string> {
return await fs.readFile(LineCount, 'utf-8');
}
}

if (process.env.NODE_ENV !== "test") {
let resourcePath = process.argv[2];
if (!resourcePath) resourcePath = process.env.RESOURCE_PATH;
// no resource found, exit
if (!resourcePath) {
console.error('Error: No resource path provided.');
process.exit(1);
}

const lineCount = Foo.getLineCount(resourcePath);
console.log('Line Count:', lineCount);
(async () => {
try {
const lineCount = await Foo.getLineCount();
console.log('Line Count:', lineCount);
} catch (err) {
console.error('Error:', err);
}
})()
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,13 @@
import * as fs from 'fs';
import * as path from 'path';
import Foo from 'foo/foo';

// Mock the 'fs' module
jest.mock('fs');

describe('Foo.getLineCount', () => {
const mockResourcePath = path.join(__dirname, '../resources');
const mockFilePath = path.join(mockResourcePath, 'line-count.txt');

let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: jest.SpyInstance;

beforeEach(() => {
process.env.NODE_ENV = "test"; // Set NODE_ENV for all tests
jest.clearAllMocks();
// Mock console.log and console.error
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {
});
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {
});
});

afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
jest.clearAllMocks();
});

it('should return the content of the line-count.txt file', () => {
const mockContent = '42';
jest.spyOn(fs, 'readFileSync').mockReturnValue(mockContent);

const result = Foo.getLineCount(mockResourcePath);
expect(result).toBe(mockContent);
expect(fs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf-8');
it('should return the content of the line-count.txt file', async () => {
const expected: string = await Foo.getLineCount();
expect(expected).toBe("21");
});

it('should return null if the file cannot be read', () => {
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
throw new Error('File not found');
});

const result = Foo.getLineCount(mockResourcePath);
expect(result).toBeNull();
expect(fs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf-8');
});
});
22 changes: 7 additions & 15 deletions example/javascriptlib/basic/5-client-server-hello/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import mill._, javascriptlib._
object client extends ReactScriptsModule

object server extends TypeScriptModule {
override def mkENV = Task {
super.mkENV() ++ Map("PORT" -> "3000")
}

override def resources = Task {
val clientBundle = client.bundle().path
os.copy(clientBundle, Task.dest / "build")
PathRef(Task.dest)
/** Bundle client as resource */
def resources = Task {
os.copy(client.bundle().path, Task.dest / "build")
super.resources() ++ Seq(PathRef(Task.dest))
}

override def forkEnv = super.forkEnv() + ("PORT" -> "3000")

object test extends TypeScriptTests with TestModule.Jest
}

Expand All @@ -31,14 +30,7 @@ PASS .../App.test.tsx
> mill client.bundle # bundle the react app;
...

> mill server.test
...
PASS .../server.test.ts
> mill show server.bundle # bundle the node server
...
Test Suites:...1 passed, 1 total...
Tests:...1 passed, 1 total...
...

> mill show server.bundle # bundle the express server
Build succeeded!
*/
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,50 @@ import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';

const resource = process.env.RESOURCES || ""
const client = resource + "/build"
const Resources: string = (process.env.RESOURCESDEST || "@server/resources.dest") + "/build" // `RESOURCES` is generated on bundle
const Client = require.resolve(`${Resources}/index.html`);

const server = http.createServer((req, res) => {
if (req.url?.startsWith('/api') && req.method === 'GET') {
// Handle API routes
if (req.url === '/api/hello') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from the server!');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('API Not Found');
}
} else {
// Serve static files or fallback to index.html for React routes
const requestedPath = path.join(client, req.url || '');
const buildPath = Client.replace(/index\.html$/, "");
const requestedPath = path.join(buildPath, req.url || '');
const filePath = path.extname(requestedPath)
? requestedPath // Serve the requested file if it has an extension
: path.join(client, 'index.html'); // Fallback to index.html for non-file paths
: Client; // Fallback to index.html for non-file paths

fs.readFile(filePath, (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
// If file not found, fallback to index.html for React routes
fs.readFile(path.join(client, 'index.html'), (error, indexData) => {
fs.readFile(Client, (error, indexData) => {
if (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
console.log("[error], ", error)
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('Internal Server Error');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(indexData);
}
});
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.writeHead(500, {'Content-Type': 'text/plain'});
res.end('Internal Server Error');
}
} else {
// Serve the static file
const ext = path.extname(filePath);
const contentType = getContentType(ext);
res.writeHead(200, { 'Content-Type': contentType });
res.writeHead(200, {'Content-Type': contentType});
res.end(data);
}
});
Expand Down
33 changes: 15 additions & 18 deletions example/javascriptlib/basic/6-client-server-realistic/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ object server extends TypeScriptModule {
def npmDevDeps =
super.npmDevDeps() ++ Seq("@types/supertest@^6.0.2", "supertest@^7.0.0")

override def bundleFlags = Map("external" -> Seq("express", "cors"))
def bundleExternal = super.bundleExternal() ++ Seq(ujson.Str("express"), ujson.Str("cors"))

override def mkENV = Task {
super.mkENV() ++ Map("PORT" -> "3001")
}
def forkEnv = super.forkEnv() + ("PORT" -> "3001")

override def resources = Task {
val clientBundle = client.bundle().path
os.copy(clientBundle, Task.dest / "build")
PathRef(Task.dest)
/** Bundle client as resource */
def resources = Task {
os.copy(client.bundle().path, Task.dest / "build")
super.resources() ++ Seq(PathRef(Task.dest))
}

object test extends TypeScriptTests with TestModule.Jest {
override def testConfigSource = Task.Source(millSourcePath / os.up / "jest.config.ts")
}
object test extends TypeScriptTests with TestModule.Jest
}

// Documentation for mill.example.javascriptlib
Expand All @@ -36,6 +32,13 @@ object server extends TypeScriptModule {

/** Usage

> mill client.test
PASS src/test/App.test.tsx
...

> mill client.bundle # bundle the react app;
...

> mill server.test
PASS .../server.test.ts
...
Expand All @@ -44,12 +47,6 @@ Tests:...3 passed, 3 total...
...

> mill show server.bundle # bundle the express server
Build succeeded!

> mill client.test # bundle the node-js express app
PASS src/test/App.test.tsx
...

> mill client.bundle # bundle the react app;
...
Build succeeded!
*/
Loading

0 comments on commit 168ceee

Please sign in to comment.