Skip to content

Commit

Permalink
Merge pull request #1 from sjrd/initial-implementation
Browse files Browse the repository at this point in the history
Initial implementation.
  • Loading branch information
gzm0 authored Mar 11, 2023
2 parents 242ce05 + 95b4d9a commit 8d03edd
Show file tree
Hide file tree
Showing 13 changed files with 2,344 additions and 1 deletion.
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Node.js test and Build

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: ['16']
java: ['8']

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: ${{ matrix.java }}
cache: 'sbt'

- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

- name: Install dependencies
run: npm install
- name: Run sbt once in the test project to make sure sbt is downloaded
run: sbt projects
working-directory: ./test/testproject
- name: Perform unit test
run: npm test
- name: Build
run: npm run build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
/dist/
target/
116 changes: 115 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,118 @@

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

Work in progress.
## Usage

We assume that you have an existing Vite and Scala.js sbt project.
If not, [follow the accompanying tutorial](https://github.com/scala-js/scala-js-website/pull/590).

Install the plugin as a development dependency:

```shell
$ npm install -D @scala-js/vite-plugin-scalajs
```

Tell Vite to use the plugin in `vite.config.js`:

```javascript
import { defineConfig } from "vite";
import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";

export default defineConfig({
plugins: [scalaJSPlugin()],
});
```

Finally, import the Scala.js output from a `.js` or `.ts` file with

```javascript
import 'scalajs:main.js';
```

which will execute the main method of the Scala.js application.

The sbt project must at least be configured to use ES modules.
For the best feedback loop with Vite, we recommend to emit small modules for application code.
If your application lives in the `my.app` package, configure the sbt project with the following settings:

```scala
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.ESModule)
.withModuleSplitStyle(
ModuleSplitStyle.SmallModulesFor(List("my.app")))
},
```

## Configuration

The plugin supports the following configuration options:

```javascript
export default defineConfig({
plugins: [
scalaJSPlugin({
// path to the directory containing the sbt build
// default: '.'
cwd: '.',

// sbt project ID from within the sbt build to get fast/fullLinkJS from
// default: the root project of the sbt build
projectID: 'client',

// URI prefix of imports that this plugin catches (without the trailing ':')
// default: 'scalajs' (so the plugin recognizes URIs starting with 'scalajs:')
uriPrefix: 'scalajs',
}),
],
});
```

## Importing `@JSExportTopLevel` Scala.js members

`@JSExportTopLevel("foo")` members in the Scala.js code are exported from the modules that Scala.js generates.
They can be imported in `.js` and `.ts` files with the usual JavaScript `import` syntax.

For example, given the following Scala.js definition:

```scala
import scala.scalajs.js
import scala.scalajs.js.annotation._

@JSExportTopLevel("ScalaJSLib")
class ScalaJSLib extends js.Object {
def square(x: Double): Double = x * x
}
```

we can import and use it as

```javascript
import { ScalaJSLib } from 'scalajs:main.js';

const lib = new ScalaJSLib();
console.log(lib.square(5)); // 25
```

### Exports in other modules

By default, `@JSExportTopLevel("Foo")` exports `Foo` from the `main` module, which is why we import from `scalajs:main.js`.
We can also split the Scala.js exports into several modules.
For example,

```scala
import scala.scalajs.js
import scala.scalajs.js.annotation._

@JSExportTopLevel("ScalaJSLib", "library")
class ScalaJSLib extends js.Object {
def square(x: Double): Double = x * x
}
```

can be imported with

```javascript
import { ScalaJSLib } from 'scalajs:library.js';
```

The Scala.js documentation contains [more information about module splitting](https://www.scala-js.org/doc/project/module.html).
80 changes: 80 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { spawn, SpawnOptions } from "child_process";
import type { Plugin as VitePlugin } from "vite";

// Utility to invoke a given sbt task and fetch its output
function printSbtTask(task: string, cwd?: string): Promise<string> {
const args = ["--batch", "-no-colors", "-Dsbt.supershell=false", `print ${task}`];
const options: SpawnOptions = {
cwd: cwd,
stdio: ['ignore', 'pipe', 'inherit'],
};
const child = process.platform === 'win32'
? spawn("sbt.bat", args.map(x => `"${x}"`), {shell: true, ...options})
: spawn("sbt", args, options);

let fullOutput: string = '';

child.stdout!.setEncoding('utf-8');
child.stdout!.on('data', data => {
fullOutput += data;
process.stdout.write(data); // tee on my own stdout
});

return new Promise((resolve, reject) => {
child.on('error', err => {
reject(new Error(`sbt invocation for Scala.js compilation could not start. Is it installed?\n${err}`));
});
child.on('close', code => {
if (code !== 0)
reject(new Error(`sbt invocation for Scala.js compilation failed with exit code ${code}.`));
else
resolve(fullOutput.trimEnd().split('\n').at(-1)!);
});
});
}

export interface ScalaJSPluginOptions {
cwd?: string,
projectID?: string,
uriPrefix?: string,
}

export default function scalaJSPlugin(options: ScalaJSPluginOptions = {}): VitePlugin {
const { cwd, projectID, uriPrefix } = options;

const fullURIPrefix = uriPrefix ? (uriPrefix + ':') : 'scalajs:';

let isDev: boolean | undefined = undefined;
let scalaJSOutputDir: string | undefined = undefined;

return {
name: "scalajs:sbt-scalajs-plugin",

// Vite-specific
configResolved(resolvedConfig) {
isDev = resolvedConfig.mode === 'development';
},

// standard Rollup
async buildStart(options) {
if (isDev === undefined)
throw new Error("configResolved must be called before buildStart");

const task = isDev ? "fastLinkJSOutput" : "fullLinkJSOutput";
const projectTask = projectID ? `${projectID}/${task}` : task;
scalaJSOutputDir = await printSbtTask(projectTask, cwd);
},

// standard Rollup
resolveId(source, importer, options) {
if (scalaJSOutputDir === undefined)
throw new Error("buildStart must be called before resolveId");

if (!source.startsWith(fullURIPrefix))
return null;
const path = source.substring(fullURIPrefix.length);

return `${scalaJSOutputDir}/${path}`;
},
};
}
Loading

0 comments on commit 8d03edd

Please sign in to comment.