Skip to content

Commit 8d03edd

Browse files
authored
Merge pull request #1 from sjrd/initial-implementation
Initial implementation.
2 parents 242ce05 + 95b4d9a commit 8d03edd

File tree

13 files changed

+2344
-1
lines changed

13 files changed

+2344
-1
lines changed

.github/workflows/ci.yml

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Node.js test and Build
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
matrix:
13+
os: [ubuntu-latest, windows-latest]
14+
node-version: ['16']
15+
java: ['8']
16+
17+
runs-on: ${{ matrix.os }}
18+
19+
steps:
20+
- uses: actions/checkout@v3
21+
- name: Use Node.js ${{ matrix.node-version }}
22+
uses: actions/setup-node@v3
23+
with:
24+
node-version: ${{ matrix.node-version }}
25+
- name: Set up JDK ${{ matrix.java }}
26+
uses: actions/setup-java@v3
27+
with:
28+
distribution: 'adopt'
29+
java-version: ${{ matrix.java }}
30+
cache: 'sbt'
31+
32+
- name: Cache dependencies
33+
uses: actions/cache@v3
34+
with:
35+
path: |
36+
**/node_modules
37+
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
38+
39+
- name: Install dependencies
40+
run: npm install
41+
- name: Run sbt once in the test project to make sure sbt is downloaded
42+
run: sbt projects
43+
working-directory: ./test/testproject
44+
- name: Perform unit test
45+
run: npm test
46+
- name: Build
47+
run: npm run build

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules/
2+
/dist/
3+
target/

README.md

+115-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,118 @@
22

33
A [Vite](https://vitejs.dev/) plugin for [Scala.js](https://www.scala-js.org/).
44

5-
Work in progress.
5+
## Usage
6+
7+
We assume that you have an existing Vite and Scala.js sbt project.
8+
If not, [follow the accompanying tutorial](https://github.com/scala-js/scala-js-website/pull/590).
9+
10+
Install the plugin as a development dependency:
11+
12+
```shell
13+
$ npm install -D @scala-js/vite-plugin-scalajs
14+
```
15+
16+
Tell Vite to use the plugin in `vite.config.js`:
17+
18+
```javascript
19+
import { defineConfig } from "vite";
20+
import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";
21+
22+
export default defineConfig({
23+
plugins: [scalaJSPlugin()],
24+
});
25+
```
26+
27+
Finally, import the Scala.js output from a `.js` or `.ts` file with
28+
29+
```javascript
30+
import 'scalajs:main.js';
31+
```
32+
33+
which will execute the main method of the Scala.js application.
34+
35+
The sbt project must at least be configured to use ES modules.
36+
For the best feedback loop with Vite, we recommend to emit small modules for application code.
37+
If your application lives in the `my.app` package, configure the sbt project with the following settings:
38+
39+
```scala
40+
scalaJSLinkerConfig ~= {
41+
_.withModuleKind(ModuleKind.ESModule)
42+
.withModuleSplitStyle(
43+
ModuleSplitStyle.SmallModulesFor(List("my.app")))
44+
},
45+
```
46+
47+
## Configuration
48+
49+
The plugin supports the following configuration options:
50+
51+
```javascript
52+
export default defineConfig({
53+
plugins: [
54+
scalaJSPlugin({
55+
// path to the directory containing the sbt build
56+
// default: '.'
57+
cwd: '.',
58+
59+
// sbt project ID from within the sbt build to get fast/fullLinkJS from
60+
// default: the root project of the sbt build
61+
projectID: 'client',
62+
63+
// URI prefix of imports that this plugin catches (without the trailing ':')
64+
// default: 'scalajs' (so the plugin recognizes URIs starting with 'scalajs:')
65+
uriPrefix: 'scalajs',
66+
}),
67+
],
68+
});
69+
```
70+
71+
## Importing `@JSExportTopLevel` Scala.js members
72+
73+
`@JSExportTopLevel("foo")` members in the Scala.js code are exported from the modules that Scala.js generates.
74+
They can be imported in `.js` and `.ts` files with the usual JavaScript `import` syntax.
75+
76+
For example, given the following Scala.js definition:
77+
78+
```scala
79+
import scala.scalajs.js
80+
import scala.scalajs.js.annotation._
81+
82+
@JSExportTopLevel("ScalaJSLib")
83+
class ScalaJSLib extends js.Object {
84+
def square(x: Double): Double = x * x
85+
}
86+
```
87+
88+
we can import and use it as
89+
90+
```javascript
91+
import { ScalaJSLib } from 'scalajs:main.js';
92+
93+
const lib = new ScalaJSLib();
94+
console.log(lib.square(5)); // 25
95+
```
96+
97+
### Exports in other modules
98+
99+
By default, `@JSExportTopLevel("Foo")` exports `Foo` from the `main` module, which is why we import from `scalajs:main.js`.
100+
We can also split the Scala.js exports into several modules.
101+
For example,
102+
103+
```scala
104+
import scala.scalajs.js
105+
import scala.scalajs.js.annotation._
106+
107+
@JSExportTopLevel("ScalaJSLib", "library")
108+
class ScalaJSLib extends js.Object {
109+
def square(x: Double): Double = x * x
110+
}
111+
```
112+
113+
can be imported with
114+
115+
```javascript
116+
import { ScalaJSLib } from 'scalajs:library.js';
117+
```
118+
119+
The Scala.js documentation contains [more information about module splitting](https://www.scala-js.org/doc/project/module.html).

index.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { spawn, SpawnOptions } from "child_process";
2+
import type { Plugin as VitePlugin } from "vite";
3+
4+
// Utility to invoke a given sbt task and fetch its output
5+
function printSbtTask(task: string, cwd?: string): Promise<string> {
6+
const args = ["--batch", "-no-colors", "-Dsbt.supershell=false", `print ${task}`];
7+
const options: SpawnOptions = {
8+
cwd: cwd,
9+
stdio: ['ignore', 'pipe', 'inherit'],
10+
};
11+
const child = process.platform === 'win32'
12+
? spawn("sbt.bat", args.map(x => `"${x}"`), {shell: true, ...options})
13+
: spawn("sbt", args, options);
14+
15+
let fullOutput: string = '';
16+
17+
child.stdout!.setEncoding('utf-8');
18+
child.stdout!.on('data', data => {
19+
fullOutput += data;
20+
process.stdout.write(data); // tee on my own stdout
21+
});
22+
23+
return new Promise((resolve, reject) => {
24+
child.on('error', err => {
25+
reject(new Error(`sbt invocation for Scala.js compilation could not start. Is it installed?\n${err}`));
26+
});
27+
child.on('close', code => {
28+
if (code !== 0)
29+
reject(new Error(`sbt invocation for Scala.js compilation failed with exit code ${code}.`));
30+
else
31+
resolve(fullOutput.trimEnd().split('\n').at(-1)!);
32+
});
33+
});
34+
}
35+
36+
export interface ScalaJSPluginOptions {
37+
cwd?: string,
38+
projectID?: string,
39+
uriPrefix?: string,
40+
}
41+
42+
export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): VitePlugin {
43+
const { cwd, projectID, uriPrefix } = options;
44+
45+
const fullURIPrefix = uriPrefix ? (uriPrefix + ':') : 'scalajs:';
46+
47+
let isDev: boolean | undefined = undefined;
48+
let scalaJSOutputDir: string | undefined = undefined;
49+
50+
return {
51+
name: "scalajs:sbt-scalajs-plugin",
52+
53+
// Vite-specific
54+
configResolved(resolvedConfig) {
55+
isDev = resolvedConfig.mode === 'development';
56+
},
57+
58+
// standard Rollup
59+
async buildStart(options) {
60+
if (isDev === undefined)
61+
throw new Error("configResolved must be called before buildStart");
62+
63+
const task = isDev ? "fastLinkJSOutput" : "fullLinkJSOutput";
64+
const projectTask = projectID ? `${projectID}/${task}` : task;
65+
scalaJSOutputDir = await printSbtTask(projectTask, cwd);
66+
},
67+
68+
// standard Rollup
69+
resolveId(source, importer, options) {
70+
if (scalaJSOutputDir === undefined)
71+
throw new Error("buildStart must be called before resolveId");
72+
73+
if (!source.startsWith(fullURIPrefix))
74+
return null;
75+
const path = source.substring(fullURIPrefix.length);
76+
77+
return `${scalaJSOutputDir}/${path}`;
78+
},
79+
};
80+
}

0 commit comments

Comments
 (0)