init
Some checks failed
CI / checks (push) Has been cancelled

This commit is contained in:
병준 박 2025-05-06 13:52:36 +00:00
commit 18932f4a5f
148 changed files with 15857 additions and 0 deletions

4
.commitlintrc.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {}
}

4
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM mcr.microsoft.com/devcontainers/base:ubuntu
RUN apt update
RUN apt install -y pkg-config

View File

@ -0,0 +1,50 @@
{
"name": "loafle.nx.plugin",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"shutdownAction": "stopCompose",
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/zsh"
}
},
"scss.validate": false,
"css.validate": false,
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore"
},
"extensions": [
"bradlc.vscode-tailwindcss",
"csstools.postcss",
"dbaeumer.vscode-eslint",
"donjayamanne.githistory",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"ms-azuretools.vscode-docker"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "/bin/sh ./.devcontainer/postCreateCommand.sh",
"mounts": [
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,consistency=cached",
"source=${localEnv:HOME}/.netrc,target=/home/vscode/.netrc,type=bind,consistency=cached",
"source=${localEnv:HOME}/.config/pypoetry,target=/home/vscode/.config/pypoetry,type=bind,consistency=cached",
"source=${localEnv:HOME}/.ssh/id_rsa,target=/home/vscode/.ssh/id_rsa,type=bind,consistency=cached"
]
}

View File

@ -0,0 +1,48 @@
version: "3.8"
services:
app:
# Using a Dockerfile is optional, but included for completeness.
build:
context: .
dockerfile: Dockerfile
# [Optional] You can use build args to set options. e.g. 'VARIANT' below affects the image in the Dockerfile
# args:
# VARIANT: buster
# environment:
# PORT: 3000
# ports:
# - 3000:3000
volumes:
# This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json
- ..:/workspace:cached
# Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
# - /var/run/docker.sock:/var/run/docker.sock
# Overrides default command so things don't shut down after the process ends.
# command: /bin/sh -c "while sleep 1000; do :; done"
command: sleep infinity
# Runs app on the same network as the service container, allows "forwardPorts" in devcontainer.json function.
# network_mode: service:another-service
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
# Uncomment the next line to use a non-root user for all processes - See https://aka.ms/vscode-remote/containers/non-root for details.
# user: vscode
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined
# You can include other services not opened by VS Code as well
# another-service:
# image: mongo:latest
# restart: unless-stopped
# As in the "app" service, use "forwardPorts" in **devcontainer.json** to forward an app port locally.

View File

@ -0,0 +1,3 @@
npm install -g npm
npm add --global nx@latest

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

4
.env Normal file
View File

@ -0,0 +1,4 @@
# Nx 18 enables using plugins to infer targets by default
# This is disabled for existing workspaces to maintain compatibility
# For more info, see: https://nx.dev/concepts/inferred-tasks
NX_ADD_PLUGINS=false

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
node_modules

48
.eslintrc.json Normal file
View File

@ -0,0 +1,48 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": "*.json",
"parser": "jsonc-eslint-parser",
"rules": {}
}
]
}

24
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: CI
on:
push:
branches:
- main
pull_request:
env:
node_version: 20
jobs:
checks:
runs-on: ubuntu-latest
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/workflows/setup
with:
node_version: ${{ env.node_version }}
- run: yarn nx run-many --target=build,test,lint --exclude loafle --parallel --max-parallel=3
- run: yarn nx run-many --target=e2e --exclude loafle --parallel
- run: yarn nx-cloud stop-all-agents

27
.github/workflows/setup/action.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Setup
description: Setup tasks
inputs:
node_version: # id of input
description: 'Version of node to use'
required: true
default: '20'
runs:
using: 'composite'
steps:
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version}}
- name: Install
uses: dtolnay/rust-toolchain@1.79.0
- uses: actions/cache@v4
id: workspace-cache
with:
path: node_modules
key: ${{ runner.os }}-${{ inputs.node_version }}-workspace-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ inputs.node_version }}-workspace-
- run: yarn install --frozen-lockfile
shell: bash

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
# Added by cargo
/target
.nx/cache
.nx/workspace-data

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
# Add files here to ignore them from prettier formatting
/dist
/coverage
/.nx/cache
/.nx/workspace-data

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

47
.verdaccio/config.yml Normal file
View File

@ -0,0 +1,47 @@
# path to a directory with all packages
storage: ../tmp/local-registry/storage
auth:
htpasswd:
file: ./htpasswd
algorithm: bcrypt
# a list of other known repositories we can talk to
uplinks:
npmjs:
url: https://registry.npmjs.org/
maxage: 60m
max_fails: 20
fail_timeout: 2m
yarn:
url: https://registry.yarnpkg.com
maxage: 60m
max_fails: 20
fail_timeout: 2
packages:
'@*/*':
# scoped packages
access: $all
publish: $all
unpublish: $all
proxy: npmjs
'**':
# give all users (including non-authenticated users) full access
# because it is a local registry
access: $all
publish: $all
unpublish: $all
# if package is not available locally, proxy requests to npm registry
proxy: npmjs
# log settings
logs:
type: stdout
format: pretty
level: warn
publish:
allow_offline: true # set offline to true to allow publish offline

1
.verdaccio/htpasswd Normal file
View File

@ -0,0 +1 @@
test:$2y$10$lVWrhBqHffH6dnroJWR.0ug.Zgehrsxdh0dRcrFSqdktWqf/sRk9S

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"firsttris.vscode-jest-runner"
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.validate": [
"json"
]
}

14
README.md Normal file
View File

@ -0,0 +1,14 @@
<p align="center">
<img width="256px" src="./assets/monodon.png" />
</p>
<div align="center">
# Monodon
**A collection of utilities and plugins for the Nx ecosystem**
</div>
[![npm (scoped)](https://img.shields.io/static/v1?label=%20&message=README&logo=markdown&style=for-the-badge)](./packages/rust/README.md)
[![npm (scoped)](https://img.shields.io/npm/v/@loafle/nx-rust?label=rust&logo=Rust&style=for-the-badge)](https://www.npmjs.com/package/@loafle/nx-rust)<br/>
[![npm (scoped)](https://img.shields.io/static/v1?label=%20&message=README&logo=markdown&style=for-the-badge)](./packages/rust/README.md)

BIN
assets/monodon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

View File

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,12 @@
/* eslint-disable */
export default {
displayName: 'rust-e2e',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/e2e/rust-e2e',
globalSetup: '../../tools/scripts/start-local-registry.ts',
globalTeardown: '../../tools/scripts/stop-local-registry.ts',
};

22
e2e/rust-e2e/project.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "rust-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "e2e/rust-e2e/src",
"targets": {
"e2e": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "e2e/rust-e2e/jest.config.ts",
"runInBand": true
},
"dependsOn": ["^build"]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"implicitDependencies": ["rust"]
}

View File

@ -0,0 +1,69 @@
import { execSync } from 'child_process';
import { createTestProject, runNxCommand } from './utils';
import { rmSync } from 'fs';
import { listFiles, readFile, updateFile } from '@nx/plugin/testing';
describe('napi', () => {
let projectDirectory: string;
beforeAll(() => {
projectDirectory = createTestProject('napi');
// The plugin has been built and published to a local registry in the jest globalSetup
// Install the plugin built with the latest source code into the test repo
execSync(`yarn add -D @loafle/nx-rust@e2e`, {
cwd: projectDirectory,
stdio: 'inherit',
env: process.env,
});
});
afterAll(() => {
// Cleanup the test project
rmSync(projectDirectory, {
recursive: true,
force: true,
});
});
it('should create a napi project', () => {
runNxCommand(
`generate @loafle/nx-rust:lib napi-proj --napi`,
projectDirectory
);
const projectConfigPath = `test-project-napi/napi_proj/project.json`;
const projectFile = JSON.parse(readFile(projectConfigPath));
projectFile['targets']['build']['options'] = {
...projectFile['targets']['build']['options'],
jsFile: 'native.js',
dts: 'native.d.ts',
};
updateFile(projectConfigPath, JSON.stringify(projectFile, null, 2));
expect(listFiles(`test-project-napi/napi_proj/npm`).length).toBeGreaterThan(
0
);
expect(() =>
runNxCommand(`build napi_proj`, projectDirectory)
).not.toThrow();
const files = listFiles(`test-project-napi/napi_proj`);
expect(files.some((file) => file.endsWith('native.js'))).toBeTruthy();
expect(files.some((file) => file.endsWith('native.d.ts'))).toBeTruthy();
expect(files.some((file) => file.endsWith('.node'))).toBeTruthy();
expect(() =>
runNxCommand(
`build napi_proj -- --target wasm32-wasip1-threads`,
projectDirectory
)
).not.toThrow();
const files2 = listFiles(`test-project-napi/napi_proj`);
expect(
files2.some((file) => file.endsWith('wasm32-wasi.wasm'))
).toBeTruthy();
expect(files2).toContain('wasi-worker.mjs');
expect(files2).toContain('wasi-worker-browser.mjs');
});
});

View File

@ -0,0 +1,75 @@
import { ProjectGraph } from '@nx/devkit';
import { execSync } from 'child_process';
import { readFileSync, rmSync } from 'fs';
import { join } from 'path';
import { createTestProject, runNxCommand } from './utils';
describe('rust', () => {
let projectDirectory: string;
beforeAll(() => {
projectDirectory = createTestProject();
// The plugin has been built and published to a local registry in the jest globalSetup
// Install the plugin built with the latest source code into the test repo
execSync(`yarn add -D @loafle/nx-rust@e2e`, {
cwd: projectDirectory,
stdio: 'inherit',
env: process.env,
});
});
afterAll(() => {
// Cleanup the test project
rmSync(projectDirectory, {
recursive: true,
force: true,
});
});
it('should be installed', () => {
// npm ls will fail if the package is not installed properly
execSync('npm ls @loafle/nx-rust', {
cwd: projectDirectory,
stdio: 'inherit',
});
});
it('should generate a cargo project and update the project graph', () => {
runNxCommand(`generate @loafle/nx-rust:bin hello-world`, projectDirectory);
runNxCommand(`generate @loafle/nx-rust:lib lib1`, projectDirectory);
execSync('cargo add itertools -p lib1', { cwd: projectDirectory });
execSync(`cargo add lib1 --path ./lib1 -p hello_world`, {
cwd: projectDirectory,
});
expect(() =>
runNxCommand(`build hello_world`, projectDirectory)
).not.toThrow();
const projectGraph: ProjectGraph = JSON.parse(
readFileSync(
join(projectDirectory, '.nx/workspace-data/project-graph.json')
).toString()
);
expect(projectGraph.dependencies['hello_world']).toMatchInlineSnapshot(`
Array [
Object {
"source": "hello_world",
"target": "lib1",
"type": "static",
},
]
`);
expect(projectGraph.dependencies['lib1']).toMatchInlineSnapshot(`
Array [
Object {
"source": "lib1",
"target": "cargo:itertools",
"type": "static",
},
]
`);
});
});

38
e2e/rust-e2e/src/utils.ts Normal file
View File

@ -0,0 +1,38 @@
import { dirname, join } from 'path';
import { mkdirSync, rmSync } from 'fs';
import { execSync } from 'child_process';
import { tmpProjPath } from '@nx/plugin/testing';
/**
* Creates a test project with create-nx-workspace and installs the plugin
* @returns The directory where the test project was created
*/
export function createTestProject(testId = '') {
const projectName = 'test-project-' + testId;
const projectDirectory = tmpProjPath(projectName);
// Ensure projectDirectory is empty
rmSync(projectDirectory, {
recursive: true,
force: true,
});
mkdirSync(dirname(projectDirectory), {
recursive: true,
});
execSync(
`npx --yes create-nx-workspace@latest ${projectName} --preset apps --nxCloud=skip --no-interactive --packageManager yarn`,
{
cwd: dirname(projectDirectory),
stdio: 'inherit',
env: process.env,
}
);
console.log(`Created test project in "${projectDirectory}"`);
return projectDirectory;
}
export function runNxCommand(command: string, projectDir: string) {
execSync(`npx nx ${command}`, { cwd: projectDir, stdio: 'inherit' });
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

5
jest.config.ts Normal file
View File

@ -0,0 +1,5 @@
import { getJestProjectsAsync } from '@nx/jest';
export default async () => ({
projects: await getJestProjectsAsync(),
});

15
jest.preset.js Normal file
View File

@ -0,0 +1,15 @@
const nxPreset = require('@nx/jest/preset').default;
module.exports = {
...nxPreset,
/* TODO: Update to latest Jest snapshotFormat
* By default Nx has kept the older style of Jest Snapshot formats
* to prevent breaking of any existing tests with snapshots.
* It's recommend you update to the latest format.
* You can do this by removing snapshotFormat property
* and running tests with --update-snapshot flag.
* Example: "nx affected --targets=e2e --update-snapshot"
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
*/
snapshotFormat: { escapeString: true, printBasicPrototype: true },
};

27
migrations.json Normal file
View File

@ -0,0 +1,27 @@
{
"migrations": [
{
"version": "20.0.0-beta.7",
"description": "Migration for v20.0.0-beta.7",
"implementation": "./src/migrations/update-20-0-0/move-use-daemon-process",
"package": "nx",
"name": "move-use-daemon-process"
},
{
"version": "20.0.1",
"description": "Set `useLegacyCache` to true for migrating workspaces",
"implementation": "./src/migrations/update-20-0-1/use-legacy-cache",
"x-repair-skip": true,
"package": "nx",
"name": "use-legacy-cache"
},
{
"cli": "nx",
"version": "20.0.0-beta.5",
"description": "replace getJestProjects with getJestProjectsAsync",
"implementation": "./src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync",
"package": "@nx/jest",
"name": "replace-getJestProjects-with-getJestProjectsAsync"
}
]
}

67
nx.json Normal file
View File

@ -0,0 +1,67 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"nxCloudAccessToken": "NzcyM2FlMTMtMjU2MS00NTE0LWFjNzQtNmU0OGU5YjExNzVlfHJlYWQ=",
"workspaceLayout": {
"appsDir": "e2e",
"libsDir": "packages"
},
"release": {
"projects": ["rust"],
"projectsRelationship": "independent",
"releaseTagPattern": "{projectName}-{version}",
"version": {
"conventionalCommits": true,
"generatorOptions": {
"packageRoot": "dist/packages/{projectName}"
}
},
"changelog": {
"projectChangelogs": {
"file": "{projectRoot}/CHANGELOG.md",
"createRelease": "github"
}
}
},
"targetDefaults": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/packages/{projectName}"
}
},
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"@nx/jest:jest": {
"cache": true,
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"options": {
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"@nx/eslint:lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"sharedGlobals": [],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/src/test-setup.[jt]s"
]
},
"useLegacyCache": false
}

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "loafle",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"nx": "nx",
"prepare": "husky install",
"local-registry": "nx run loafle:local-registry",
"build": "nx run rust:build",
"test": "nx run rust:test",
"e2e": "nx run rust-e2e:e2e",
"release": "node tools/scripts/release.js"
},
"private": true,
"packageManager": "yarn@1.22.19",
"dependencies": {
"@ltd/j-toml": "1.38.0",
"@napi-rs/cli": "3.0.0-alpha.63",
"@nx/devkit": "20.0.1",
"@nx/js": "20.0.1",
"@swc/helpers": "0.5.13",
"chalk": "^4.1.2",
"tslib": "^2.0.0"
},
"devDependencies": {
"@commitlint/cli": "17.3.0",
"@commitlint/config-conventional": "17.3.0",
"@nx/eslint": "20.0.1",
"@nx/eslint-plugin": "20.0.1",
"@nx/jest": "20.0.1",
"@nx/node": "20.0.1",
"@nx/plugin": "20.0.1",
"@nx/workspace": "20.0.1",
"@swc-node/register": "1.9.2",
"@swc/cli": "0.3.14",
"@swc/core": "1.5.7",
"@types/jest": "29.5.13",
"@types/node": "18.19.11",
"@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"dotenv": "~10.0.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.0.0",
"husky": "^8.0.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "20.0.1",
"prettier": "2.8.0",
"semver": "7.5.4",
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"typescript": "5.5.4",
"verdaccio": "^5.25.0"
},
"volta": {
"node": "20.11.1",
"yarn": "1.22.19"
},
"nx": {
"includedScripts": []
}
}

View File

@ -0,0 +1,31 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": [
"./package.json",
"./generators.json",
"./executors.json",
"./migrations.json"
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error",
"@nx/dependency-checks": "warn"
}
}
]
}

View File

122
packages/rust/README.md Normal file
View File

@ -0,0 +1,122 @@
# @loafle/nx-rust
A Nx plugin that adds support for Cargo and Rust in your Nx workspace.
## Compatibility Chart
| @loafle/nx-rust | nx |
| --------------- | -------- |
| <=1.2.1 | <=17.1.0 |
| >=1.3.0 | >=17.1.0 |
## Getting Started
### Prerequisites
The following tools need to be installed on your system to take full advantage of `@loafle/nx-rust`
- Node (LTS)
- Rust / Cargo via [https://rustup.rs](https://rustup.rs)
### Install with `npx create-nx-workspace` preset
To bootstrap a new workspace with `@loafle/nx-rust` installed and ready, run:
```shell
npx create-nx-workspace --preset=@loafle/nx-rust
```
### Installation in already set up workspace
Use your favourite package manager to install in your project:
```shell
yarn add -D @loafle/nx-rust
```
```shell
npm install -D @loafle/nx-rust
```
```shell
pnpm add -D @loafle/nx-rust
```
#### Initialization
After installing, you can run any of the project generators (binary, library) to have @loafle/nx-rust set up Cargo in your workspace.
## Generators
Use Nx Console to see the full list of options for each generator.
### `@loafle/nx-rust:binary`
Creates a Rust binary application to be run independently.
> Create a new binary:
>
> ```shell
> nx generate @loafle/nx-rust:binary my-rust-app
> ```
### `@loafle/nx-rust:library`
Creates a Rust library that can be used in binaries, or compiled to be used for napi.
> Create a new library:
>
> ```shell
> nx generate @loafle/nx-rust:library my-rust-lib
> ```
> Create a new library with napi:
>
> ```shell
> nx generate @loafle/nx-rust:library my-rust-node-lib --napi
> ```
#### Napi
Generating a library with the `--napi` flag will set up the project to be built with it.
## Executors
All the executors support these additional properties:
- toolchain: (e.g. `--toolchain='stable' | 'beta' | 'nightly'`);
- Uses `stable` by default
- target (e.g. `--target=aarch64-apple-darwin`);
- profile (e.g. `--profile=dev`)
- [Cargo profiles](https://doc.rust-lang.org/cargo/reference/profiles.html)
- release
- target-dir
- features (e.g. `--features=bmp`)
- [Cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
- all-features
- args
- [Arguments forwarding](https://nx.dev/nx-api/nx/executors/run-commands#args) to the executor.
### `@loafle/nx-rust:build`
Runs cargo to build the project
> Not supported with napi
### `@loafle/nx-rust:lint`
Runs cargo clippy to lint the project
### `@loafle/nx-rust:napi`
Runs the napi cli to build the project
### `@loafle/nx-rust:run`
Runs `cargo run` for the project
> Not supported with napi
### `@loafle/nx-rust:test`
Runs `cargo test` for the project

View File

@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/schema",
"executors": {
"build": {
"implementation": "./src/executors/build/executor",
"schema": "./src/executors/build/schema.json",
"description": "Build a Rust project with Cargo"
},
"check": {
"implementation": "./src/executors/check/executor",
"schema": "./src/executors/check/schema.json",
"description": "Check a Rust project with Cargo"
},
"lint": {
"implementation": "./src/executors/lint/executor",
"schema": "./src/executors/lint/schema.json",
"description": "Lint a Rust project with Cargo clippy"
},
"run": {
"implementation": "./src/executors/run/executor",
"schema": "./src/executors/run/schema.json",
"description": "Run a Rust project with Cargo"
},
"test": {
"implementation": "./src/executors/test/executor",
"schema": "./src/executors/test/schema.json",
"description": "Test a Rust project with Cargo"
},
"wasm-pack": {
"implementation": "./src/executors/wasm-pack/executor",
"schema": "./src/executors/wasm-pack/schema.json",
"description": "Builds a Cargo project using wasm-pack"
},
"napi": {
"implementation": "./src/executors/napi/executor",
"schema": "./src/executors/napi/schema.json",
"description": "Wrapper to run the napi-cli"
},
"release-publish": {
"implementation": "./src/executors/release-publish/executor",
"schema": "./src/executors/release-publish/schema.json",
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
"hidden": true
}
}
}

View File

@ -0,0 +1,65 @@
{
"$schema": "http://json-schema.org/schema",
"name": "rust",
"version": "0.0.1",
"generators": {
"binary": {
"factory": "./src/generators/binary/generator",
"schema": "./src/generators/binary/schema.json",
"description": "Generate a Rust bin with Cargo",
"x-type": "application",
"aliases": [
"bin"
]
},
"init": {
"factory": "./src/generators/init/generator",
"schema": "./src/generators/init/schema.json",
"description": "initializes a Cargo workspace within a Nx workspace",
"hidden": true
},
"library": {
"factory": "./src/generators/library/generator",
"schema": "./src/generators/library/schema.json",
"description": "Generate a Rust library with Cargo",
"x-type": "library",
"aliases": [
"lib"
]
},
"add-wasm": {
"factory": "./src/generators/add-wasm/generator",
"schema": "./src/generators/add-wasm/schema.json",
"description": "Adds wasm support to a Cargo project",
"hidden": true
},
"add-wasm-reference": {
"factory": "./src/generators/add-wasm-reference/generator",
"schema": "./src/generators/add-wasm-reference/schema.json",
"description": "Adds wasm support to an existing JavaScript/TypeScript project",
"hidden": true
},
"preset": {
"factory": "./src/generators/preset/generator",
"schema": "./src/generators/preset/schema.json",
"description": "preset generator",
"hidden": true
},
"add-napi": {
"factory": "./src/generators/add-napi/generator",
"schema": "./src/generators/add-napi/schema.json",
"description": "Generates support for napi-rs"
},
"create-napi-npm-dirs": {
"factory": "./src/generators/create-napi-npm-dirs/generator",
"schema": "./src/generators/create-napi-npm-dirs/schema.json",
"description": "Generates npm package directories for a NAPI project"
},
"release-version": {
"factory": "./src/generators/release-version/release-version#releaseVersionGenerator",
"schema": "./src/generators/release-version/schema.json",
"description": "DO NOT INVOKE DIRECTLY WITH `nx generate`. Use `nx release version` instead.",
"hidden": true
}
}
}

View File

@ -0,0 +1,17 @@
/* eslint-disable */
export default {
displayName: 'rust',
preset: '../../jest.preset.js',
globals: {},
testEnvironment: 'node',
transform: {
'^.+\\.[tj]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/packages/rust',
};

View File

@ -0,0 +1,25 @@
{
"generators": {},
"packageJsonUpdates": {
"2.0.0": {
"version": "2.0.0-beta.1",
"requires": {
"@napi-rs/cli": "<3.0.0"
},
"packages": {
"@napi-rs/cli": {
"version": "3.0.0-alpha.63",
"alwaysAddToPackageJson": false
},
"@napi-rs/wasm-runtime": {
"version": "^0.2.4",
"alwaysAddToPackageJson": true
},
"emnapi": {
"version": "^1.1.0",
"alwaysAddToPackageJson": true
}
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"name": "@loafle/nx-rust",
"version": "0.0.0-updated-during-release",
"main": "src/index.js",
"repository": {
"url": "https://git.loafle.net/loafle/nx",
"type": "git"
},
"license": "MIT",
"private": false,
"generators": "./generators.json",
"executors": "./executors.json",
"dependencies": {
"@nx/devkit": ">= 19 < 21",
"@ltd/j-toml": "1.38.0",
"chalk": "^4.1.2",
"npm-run-path": "^4.0.1",
"semver": "7.5.4",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@napi-rs/cli": "^3.0.0-alpha.55"
},
"nx-migrations": {
"migrations": "./migrations.json"
}
}

View File

@ -0,0 +1,58 @@
{
"name": "rust",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/rust/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/packages/rust"],
"options": {
"jestConfig": "packages/rust/jest.config.ts"
}
},
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/rust",
"tsConfig": "packages/rust/tsconfig.lib.json",
"packageJson": "packages/rust/package.json",
"main": "packages/rust/src/index.ts",
"assets": [
"packages/rust/*.md",
{
"input": "./packages/rust/src",
"glob": "**/!(*.ts)",
"output": "./src"
},
{
"input": "./packages/rust/src",
"glob": "**/*.d.ts",
"output": "./src"
},
{
"input": "./packages/rust",
"glob": "generators.json",
"output": "."
},
{
"input": "./packages/rust",
"glob": "executors.json",
"output": "."
},
{
"input": "./packages/rust",
"glob": "migrations.json",
"output": "."
}
]
}
}
}
}

View File

@ -0,0 +1,15 @@
// import * as cargoUtils from '../../utils/cargo';
// jest.mock('../../utils/cargo', (): Partial<typeof cargoUtils> => {
// return { runCargoSync: jest.fn(() => ({ output: 'output', success: true })) };
// });
import {} from '@nx/devkit/testing';
import { BuildExecutorSchema } from './schema';
const options: BuildExecutorSchema = {};
describe('Build Executor', () => {
it('can run', async () => {
// e2es should cover this
});
});

View File

@ -0,0 +1,16 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { cargoCommand } from '../../utils/cargo';
import { BuildExecutorSchema } from './schema';
export default async function* runExecutor(
options: BuildExecutorSchema,
context: ExecutorContext
): AsyncGenerator<{ success: boolean }> {
const command = buildCommand('build', options, context);
const { success } = await cargoCommand(...command);
yield {
success,
};
}

View File

@ -0,0 +1,4 @@
import { BaseOptions } from '../../models/base-options';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface BuildExecutorSchema extends BaseOptions {}

View File

@ -0,0 +1,138 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema",
"title": "Build executor",
"description": "",
"type": "object",
"properties": {
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"profile": {
"type": "string"
},
"target-dir": {
"type": "string"
},
"toolchain": {
"type": "string",
"enum": [
"stable",
"beta",
"nightly"
],
"default": "stable"
},
"features": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Features of workspace members may be enabled with package-name/feature-name syntax. Array of names is supported"
},
"all-features": {
"type": "boolean",
"default": false,
"description": "Build all binary targets"
},
"lib": {
"type": "boolean",
"description": "Build the package's library",
"default": false
},
"bin": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified binary. Array of names or common Unix glob patterns is supported"
},
"bins": {
"type": "boolean",
"default": false,
"description": "Build all binary targets"
},
"example": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified example. Array of names or common Unix glob patterns is supported"
},
"examples": {
"type": "boolean",
"default": false,
"description": "Build all example targets"
},
"test": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified test. Array of names or common Unix glob patterns is supported"
},
"tests": {
"type": "boolean",
"default": false,
"description": "Build all test targets"
},
"bench": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified bench. Array of names or common Unix glob patterns is supported"
},
"benches": {
"type": "boolean",
"default": false,
"description": "Build all targets in benchmark mode that have the bench = true manifest flag set. By default this includes the library and binaries built as benchmarks, and bench targets. Be aware that this will also build any required dependencies, so the lib target may be built twice (once as a benchmark, and once as a dependency for binaries, benchmarks, etc.). Targets may be enabled or disabled by setting the bench flag in the manifest settings for the target."
},
"all-targets": {
"type": "boolean",
"default": false,
"description": "Build all test targets"
}
},
"required": []
}

View File

@ -0,0 +1,15 @@
// import * as cargoUtils from '../../utils/cargo';
// jest.mock('../../utils/cargo', (): Partial<typeof cargoUtils> => {
// return { runCargoSync: jest.fn(() => ({ output: 'output', success: true })) };
// });
import {} from '@nx/devkit/testing';
import { CheckExecutorSchema } from './schema';
const options: CheckExecutorSchema = {};
describe('Build Executor', () => {
it('can run', async () => {
// e2es should cover this
});
});

View File

@ -0,0 +1,16 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { cargoCommand } from '../../utils/cargo';
import { CheckExecutorSchema } from './schema';
export default async function* runExecutor(
options: CheckExecutorSchema,
context: ExecutorContext
): AsyncGenerator<{ success: boolean }> {
const command = buildCommand('check', options, context);
const { success } = await cargoCommand(...command);
yield {
success,
};
}

View File

@ -0,0 +1,4 @@
import { BaseOptions } from '../../models/base-options';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CheckExecutorSchema extends BaseOptions {}

View File

@ -0,0 +1,138 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema",
"title": "Check executor",
"description": "",
"type": "object",
"properties": {
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"profile": {
"type": "string"
},
"target-dir": {
"type": "string"
},
"toolchain": {
"type": "string",
"enum": [
"stable",
"beta",
"nightly"
],
"default": "stable"
},
"features": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Features of workspace members may be enabled with package-name/feature-name syntax. Array of names is supported"
},
"all-features": {
"type": "boolean",
"default": false,
"description": "Check all binary targets"
},
"lib": {
"type": "boolean",
"description": "Check the package's library",
"default": false
},
"bin": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Check the specified binary. Array of names or common Unix glob patterns is supported"
},
"bins": {
"type": "boolean",
"default": false,
"description": "Check all binary targets"
},
"example": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Check the specified example. Array of names or common Unix glob patterns is supported"
},
"examples": {
"type": "boolean",
"default": false,
"description": "Check all example targets"
},
"test": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Check the specified test. Array of names or common Unix glob patterns is supported"
},
"tests": {
"type": "boolean",
"default": false,
"description": "Check all test targets"
},
"bench": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Check the specified bench. Array of names or common Unix glob patterns is supported"
},
"benches": {
"type": "boolean",
"default": false,
"description": "Check all targets in benchmark mode that have the bench = true manifest flag set. By default this includes the library and binaries built as benchmarks, and bench targets. Be aware that this will also build any required dependencies, so the lib target may be built twice (once as a benchmark, and once as a dependency for binaries, benchmarks, etc.). Targets may be enabled or disabled by setting the bench flag in the manifest settings for the target."
},
"all-targets": {
"type": "boolean",
"default": false,
"description": "Check all test targets"
}
},
"required": []
}

View File

@ -0,0 +1,10 @@
import { LintExecutorSchema } from './schema';
import executor from './executor';
const options: LintExecutorSchema = {};
describe('Lint Executor', () => {
it('can run', async () => {
// e2es should cover this
});
});

View File

@ -0,0 +1,16 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { cargoCommand } from '../../utils/cargo';
import { LintExecutorSchema } from './schema';
export default async function* runExecutor(
options: LintExecutorSchema,
context: ExecutorContext
) {
const command = buildCommand('clippy', options, context);
const { success } = await cargoCommand(...command);
yield {
success,
};
}

View File

@ -0,0 +1,5 @@
import { BaseOptions } from '../../models/base-options';
export interface LintExecutorSchema extends BaseOptions {
fix?: boolean;
} // eslint-disable-line

View File

@ -0,0 +1,37 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema",
"title": "Lint executor",
"description": "",
"type": "object",
"properties": {
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"profile": {
"type": "string"
},
"target-dir": {
"type": "string"
},
"toolchain": {
"type": "string",
"enum": [
"stable",
"beta",
"nightly"
],
"default": "stable"
},
"fix": {
"type": "boolean",
"default": false
}
},
"required": []
}

View File

@ -0,0 +1,54 @@
import { ExecutorContext, joinPathFragments, workspaceRoot } from '@nx/devkit';
import { NapiExecutorSchema } from './schema.js';
import { join } from 'path';
import { fileExists } from 'nx/src/utils/fileutils.js';
import { cargoMetadata } from '../../utils/cargo';
export default async function runExecutor(
options: NapiExecutorSchema,
context: ExecutorContext
) {
const { NapiCli } = await import('@napi-rs/cli');
const projectRoot =
context.projectGraph?.nodes[context.projectName ?? ''].data.root;
const packageJson = join(projectRoot ?? '.', 'package.json');
if (!fileExists(packageJson)) {
throw new Error(`Could not find package.json at ${packageJson}`);
}
const napi = new NapiCli();
const buildOptions: Parameters<typeof napi.build>[0] = {};
buildOptions.platform = true;
buildOptions.jsBinding = options.jsFile;
buildOptions.dts = options.dts;
buildOptions.outputDir = options.dist;
buildOptions.manifestPath = join(projectRoot ?? '.', 'Cargo.toml');
buildOptions.packageJsonPath = packageJson;
if (options.release) {
buildOptions.release = true;
}
if (options.target) {
buildOptions.target = options.target;
}
if (options.zig) {
buildOptions.crossCompile = true;
}
const metadata = cargoMetadata();
buildOptions.targetDir =
metadata?.target_directory ??
joinPathFragments(workspaceRoot, 'dist', 'cargo');
if (process.env.VERCEL) {
// Vercel doesnt have support for cargo atm, so auto success builds
return { success: true };
}
const { task } = await napi.build(buildOptions);
const output = await task;
return { success: true, terminalOutput: output };
}

View File

@ -0,0 +1,8 @@
export interface NapiExecutorSchema {
dist: string;
jsFile: string;
dts?: string;
release?: boolean;
target?: string;
zig?: boolean;
}

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "Napi executor",
"description": "",
"type": "object",
"properties": {
"dist": {
"type": "string"
},
"jsFile": {
"type": "string"
},
"dts": {
"type": "string",
"default": "index.d.ts"
},
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"zig": {
"type": "boolean",
"default": false
}
},
"required": ["dist", "jsFile"]
}

View File

@ -0,0 +1,87 @@
import { ExecutorContext, joinPathFragments, output } from '@nx/devkit';
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { relative } from 'node:path';
import { env as appendLocalEnv } from 'npm-run-path';
import { parseCargoToml } from '../../utils/toml';
import { PublishExecutorSchema } from './schema';
import chalk = require('chalk');
const LARGE_BUFFER = 1024 * 1000000;
function processEnv(color: boolean) {
const env = {
...process.env,
...appendLocalEnv(),
};
if (color) {
env.FORCE_COLOR = `${color}`;
}
return env;
}
export default async function runExecutor(
options: PublishExecutorSchema,
context: ExecutorContext
) {
/**
* We need to check both the env var and the option because the executor may have been triggered
* indirectly via dependsOn, in which case the env var will be set, but the option will not.
*/
const isDryRun = process.env.NX_DRY_RUN === 'true' || options.dryRun || false;
const projectConfig =
context.projectsConfigurations!.projects[context.projectName!]!;
const packageRoot = joinPathFragments(
context.root,
options.packageRoot ?? projectConfig.root
);
const workspaceRelativePackageRoot = relative(context.root, packageRoot);
const cargoTomlPath = joinPathFragments(packageRoot, 'Cargo.toml');
const cargoTomlContents = readFileSync(cargoTomlPath, 'utf-8');
const cargoToml = parseCargoToml(cargoTomlContents);
const crateName = cargoToml.package.name;
const cargoPublishCommandSegments = [
`cargo publish --allow-dirty -p ${crateName}`,
];
if (isDryRun) {
cargoPublishCommandSegments.push(`--dry-run`);
}
try {
const command = cargoPublishCommandSegments.join(' ');
output.logSingleLine(`Running "${command}"...`);
execSync(command, {
maxBuffer: LARGE_BUFFER,
env: processEnv(true),
cwd: packageRoot,
stdio: 'inherit',
});
console.log('');
if (isDryRun) {
console.log(
`Would publish to https://crates.io, but ${chalk.keyword('orange')(
'[dry-run]'
)} was set`
);
} else {
console.log(`Published to https://crates.io`);
}
return {
success: true,
};
} catch (err: any) {
return {
success: false,
};
}
}

View File

@ -0,0 +1,4 @@
export interface PublishExecutorSchema {
packageRoot?: string;
dryRun?: boolean;
}

View File

@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/schema",
"version": 2,
"title": "Implementation details of `nx release publish`",
"description": "DO NOT INVOKE DIRECTLY WITH `nx run`. Use `nx release publish` instead.",
"type": "object",
"properties": {
"packageRoot": {
"type": "string",
"description": "The root directory of the directory (containing a manifest file at its root) to publish. Defaults to the project root."
},
"dryRun": {
"type": "boolean",
"description": "Whether to run the command without actually publishing the package to the registry."
}
},
"required": []
}

View File

@ -0,0 +1,10 @@
import { RunExecutorSchema } from './schema';
import executor from './executor';
const options: RunExecutorSchema = {};
describe('Run Executor', () => {
it('can run', async () => {
// e2es should cover this
});
});

View File

@ -0,0 +1,16 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { cargoRunCommand } from '../../utils/cargo';
import { RunExecutorSchema } from './schema';
export default async function* runExecutor(
options: RunExecutorSchema,
context: ExecutorContext
) {
const command = buildCommand('run', options, context);
const { success } = await cargoRunCommand(...command);
yield {
success,
};
}

View File

@ -0,0 +1,3 @@
import { BaseOptions } from '../../models/base-options';
export interface RunExecutorSchema extends BaseOptions {} // eslint-disable-line

View File

@ -0,0 +1,77 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/schema",
"title": "Run executor",
"description": "",
"type": "object",
"properties": {
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"profile": {
"type": "string"
},
"target-dir": {
"type": "string"
},
"cwd": {
"type": "string"
},
"toolchain": {
"type": "string",
"enum": [
"stable",
"beta",
"nightly"
],
"default": "stable"
},
"features": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Features of workspace members may be enabled with package-name/feature-name syntax. Array of names is supported"
},
"all-features": {
"type": "boolean",
"default": false,
"description": "Build all binary targets"
},
"bin": {
"type": "string",
"description": "Run the specified binary"
},
"example": {
"type": "string",
"description": "Run the specified example"
},
"args": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Extra arguments. You can pass them as follows: nx run project:run --args='--wait=100'."
}
},
"required": []
}

View File

@ -0,0 +1,10 @@
import { TestExecutorSchema } from './schema';
import executor from './executor';
const options: TestExecutorSchema = {};
describe('Test Executor', () => {
it('can run', async () => {
// e2es should cover this
});
});

View File

@ -0,0 +1,16 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { cargoCommand } from '../../utils/cargo';
import { TestExecutorSchema } from './schema';
export default async function* runExecutor(
options: TestExecutorSchema,
context: ExecutorContext
) {
const command = buildCommand('test', options, context);
const { success } = await cargoCommand(...command);
yield {
success,
};
}

View File

@ -0,0 +1,7 @@
import { BaseOptions } from '../../models/base-options';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TestExecutorSchema extends BaseOptions {
'no-run'?: boolean;
'no-fail-fast'?: boolean;
}

View File

@ -0,0 +1,160 @@
{
"version": 2,
"outputCapture": "pipe",
"$schema": "http://json-schema.org/schema",
"title": "Test executor",
"description": "",
"type": "object",
"properties": {
"no-run": {
"type": "boolean",
"default": false
},
"no-fail-fast": {
"type": "boolean",
"default": false
},
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string"
},
"profile": {
"type": "string"
},
"target-dir": {
"type": "string"
},
"toolchain": {
"type": "string",
"enum": [
"stable",
"beta",
"nightly"
],
"default": "stable"
},
"features": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Features of workspace members may be enabled with package-name/feature-name syntax. Array of names is supported"
},
"all-features": {
"type": "boolean",
"default": false,
"description": "Build all binary targets"
},
"lib": {
"type": "boolean",
"description": "Build the package's library",
"default": false
},
"bin": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified binary. Array of names or common Unix glob patterns is supported"
},
"bins": {
"type": "boolean",
"default": false,
"description": "Build all binary targets"
},
"example": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified example. Array of names or common Unix glob patterns is supported"
},
"examples": {
"type": "boolean",
"default": false,
"description": "Build all example targets"
},
"test": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified test. Array of names or common Unix glob patterns is supported"
},
"tests": {
"type": "boolean",
"default": false,
"description": "Build all test targets"
},
"bench": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Build the specified bench. Array of names or common Unix glob patterns is supported"
},
"benches": {
"type": "boolean",
"default": false,
"description": "Build all targets in benchmark mode that have the bench = true manifest flag set. By default this includes the library and binaries built as benchmarks, and bench targets. Be aware that this will also build any required dependencies, so the lib target may be built twice (once as a benchmark, and once as a dependency for binaries, benchmarks, etc.). Targets may be enabled or disabled by setting the bench flag in the manifest settings for the target."
},
"all-targets": {
"type": "boolean",
"default": false,
"description": "Build all test targets"
},
"args": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Extra arguments. You can pass them as follows: nx run project:run --args='--wait=100'."
}
},
"required": []
}

View File

@ -0,0 +1,11 @@
import { WasmPackExecutorSchema } from './schema';
import executor from './executor';
xdescribe('Wasm Executor', () => {
it('can run', async () => {
// const output = await executor(options);
// expect(output.success).toBe(true);
});
});

View File

@ -0,0 +1,29 @@
import { ExecutorContext } from '@nx/devkit';
import { buildCommand } from '../../utils/build-command';
import { runProcess } from '../../utils/run-process';
import { WasmPackExecutorSchema } from './schema';
interface WasmPackOptions extends Omit<WasmPackExecutorSchema, 'target-dir'> {
'out-dir': string;
}
export default async function runExecutor(
options: WasmPackExecutorSchema,
context: ExecutorContext
) {
const wasmPackOptions = wasmPackArgs(options);
const args = buildCommand('build', wasmPackOptions, context);
return runWasmPack(...args);
}
function runWasmPack(...args: string[]) {
return runProcess('wasm-pack', ...args);
}
function wasmPackArgs(options: WasmPackExecutorSchema): WasmPackOptions {
return {
release: options.release,
target: options.target,
'out-dir': options['target-dir'],
};
}

View File

@ -0,0 +1,5 @@
export interface WasmPackExecutorSchema {
['target-dir']: string;
target: 'bundler' | 'nodejs' | 'web' | 'no-module';
release: boolean;
}

View File

@ -0,0 +1,27 @@
{
"$schema": "http://json-schema.org/schema",
"title": "Wasm Pack executor",
"description": "",
"type": "object",
"properties": {
"release": {
"type": "boolean",
"default": false
},
"target": {
"type": "string",
"enum": [
"bundler",
"nodejs",
"web",
"no-module"
],
"default": "bundler"
},
"target-dir": {
"type": "string",
"default": "pkg"
}
},
"required": []
}

View File

@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

View File

@ -0,0 +1,6 @@
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export function sum(a: number, b: number): number

View File

@ -0,0 +1,251 @@
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim();
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, '<%= projectName %>.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.android-arm64.node')
} else {
nativeBinding = require('<%= packageName %>-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, '<%= projectName %>.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.android-arm-eabi.node')
} else {
nativeBinding = require('<%= packageName %>-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.win32-x64-msvc.node')
} else {
nativeBinding = require('<%= packageName %>-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.win32-ia32-msvc.node')
} else {
nativeBinding = require('<%= packageName %>-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.win32-arm64-msvc.node')
} else {
nativeBinding = require('<%= packageName %>-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, '<%= projectName %>.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.darwin-universal.node')
} else {
nativeBinding = require('<%= packageName %>-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, '<%= projectName %>.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.darwin-x64.node')
} else {
nativeBinding = require('<%= packageName %>-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.darwin-arm64.node')
} else {
nativeBinding = require('<%= packageName %>-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, '<%= projectName %>.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.freebsd-x64.node')
} else {
nativeBinding = require('<%= packageName %>-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.linux-x64-musl.node')
} else {
nativeBinding = require('<%= packageName %>-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.linux-x64-gnu.node')
} else {
nativeBinding = require('<%= packageName %>-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.linux-arm64-musl.node')
} else {
nativeBinding = require('<%= packageName %>-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.linux-arm64-gnu.node')
} else {
nativeBinding = require('<%= packageName %>-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, '<%= projectName %>.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./<%= projectName %>.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('<%= packageName %>-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(`Failed to load native binding`)
}
const { sum } = nativeBinding
module.exports.sum = sum

View File

@ -0,0 +1,25 @@
{
"name": "<%= packageName %>",
"version": "0.0.0",
"main": "index.js",
"types": "index.d.ts",
"napi": {
"binaryName": "<%= projectName %>",
"packageName": "<%= packageName %>",
"targets": [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-pc-windows-msvc",
"armv7-unknown-linux-gnueabihf",
"x86_64-unknown-linux-musl",
"x86_64-unknown-freebsd",
"x86_64-apple-darwin",
"i686-pc-windows-msvc",
"wasm32-wasip1-threads"
]
},
"engines": {
"node": ">= 10"
}
}

View File

@ -0,0 +1,7 @@
#[macro_use]
extern crate napi_derive;
#[napi]
pub fn sum(a: i32, b: i32) -> i32 {
a + b
}

View File

@ -0,0 +1,91 @@
import { Tree, readProjectConfiguration } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import libraryGenerator from '../library/generator';
import generator from './generator';
import { AddNapiGeneratorSchema } from './schema';
jest.mock('@nx/devkit', (): typeof import('@nx/devkit') => {
const originalModule = jest.requireActual('@nx/devkit');
return {
...originalModule,
ensurePackage: jest.fn(),
};
});
describe('add-napi generator', () => {
let appTree: Tree;
const options: AddNapiGeneratorSchema = { project: 'test' };
beforeEach(async () => {
appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, { name: 'test' });
});
it('should update the Cargo.toml file', async () => {
await generator(appTree, options);
const cargoString = appTree.read('./test/Cargo.toml')?.toString() ?? '';
expect(cargoString).toMatchInlineSnapshot(`
"
[package]
name = 'test'
version = '0.1.0'
edition = '2021'
[dependencies]
napi = { version = '2.10.2', default-features = false, features = [
'napi4',
] }
napi-derive = '2.9.3'
[lib]
crate-type = [
'cdylib',
]
[build-dependencies]
napi-build = '2.0.1'
"
`);
});
it('should update the base tsconfig file', async () => {
await generator(appTree, options);
expect(JSON.parse(appTree.read('tsconfig.base.json')?.toString() ?? ''))
.toMatchInlineSnapshot(`
Object {
"compilerOptions": Object {
"paths": Object {
"@proj/test": Array [
"test/index.d.ts",
],
},
},
}
`);
});
it('should update a project', async () => {
await generator(appTree, options);
const project = readProjectConfiguration(appTree, 'test');
expect(project.targets?.build).toMatchInlineSnapshot(`
Object {
"cache": true,
"configurations": Object {
"production": Object {
"dist": "dist/test",
"release": true,
},
},
"executor": "@loafle/nx-rust:napi",
"options": Object {
"dist": "test",
"jsFile": "index.js",
},
"outputs": Array [
"{workspaceRoot}/test",
],
}
`);
});
});

View File

@ -0,0 +1,205 @@
import {
addDependenciesToPackageJson,
formatFiles,
generateFiles,
getProjects,
joinPathFragments,
names,
offsetFromRoot,
ProjectConfiguration,
readJson,
Tree,
updateJson,
updateProjectConfiguration,
} from '@nx/devkit';
import * as path from 'path';
import {
modifyCargoTable,
parseCargoTomlWithTree,
stringifyCargoToml,
} from '../../utils/toml';
import {
NAPI_EMNAPI,
NAPI_VERSION,
NAPI_WASM_RUNTIME,
} from '../../utils/versions';
import { AddNapiGeneratorSchema } from './schema';
interface NormalizedSchema extends AddNapiGeneratorSchema {
projectName: string;
projectRoot: string;
packageName: string;
offsetFromRoot: string;
dryRun?: boolean;
}
export default async function (tree: Tree, options: AddNapiGeneratorSchema) {
const project = getProjects(tree).get(options.project);
if (!project) {
throw 'Project not found';
}
const normalizedOptions = normalizeOptions(tree, options, project);
addFiles(tree, normalizedOptions);
updateCargo(tree, normalizedOptions);
const addPackage = addDependenciesToPackageJson(
tree,
{},
{
'@napi-rs/cli': NAPI_VERSION,
'@napi-rs/wasm-runtime': NAPI_WASM_RUNTIME,
emnapi: NAPI_EMNAPI,
}
);
updateGitIgnore(tree);
updateTsConfig(tree, normalizedOptions);
updateProjectConfiguration(tree, normalizedOptions.projectName, {
...project,
targets: {
...project.targets,
build: {
cache: true,
outputs: [`{workspaceRoot}/${normalizedOptions.projectRoot}`],
executor: '@loafle/nx-rust:napi',
options: {
dist: normalizedOptions.projectRoot,
jsFile: 'index.js',
},
configurations: {
production: {
dist: `dist/${normalizedOptions.projectName}`,
release: true,
},
},
},
},
});
await formatFiles(tree);
return async () => {
await addPackage();
const { NapiCli } = await import('@napi-rs/cli');
const napi = new NapiCli();
await napi.createNpmDirs({
npmDir: `${normalizedOptions.projectRoot}/npm`,
packageJsonPath: `${normalizedOptions.projectRoot}/package.json`,
dryRun: normalizedOptions.dryRun,
});
};
}
function normalizeOptions(
tree: Tree,
options: AddNapiGeneratorSchema,
project: ProjectConfiguration
): NormalizedSchema {
const npmScope = getNpmScope(tree);
const projectName = project.name ?? options.project;
const packageName = npmScope
? `@${npmScope}/${names(projectName).fileName}`
: projectName;
return {
...options,
projectName,
projectRoot: project.root,
packageName,
offsetFromRoot: offsetFromRoot(project.root),
};
}
/**
* Read the npm scope that a workspace should use by default
*/
function getNpmScope(tree: Tree) {
const { name } = tree.exists('package.json')
? readJson(tree, 'package.json')
: { name: null };
if (name?.startsWith('@')) {
return name.split('/')[0].substring(1);
}
}
function getRootTsConfigPathInTree(tree: Tree) {
for (const path of ['tsconfig.base.json', 'tsconfig.json']) {
if (tree.exists(path)) {
return path;
}
}
return 'tsconfig.base.json';
}
function addFiles(tree: Tree, options: NormalizedSchema) {
const templateOptions = {
...options,
...names(options.project),
template: '',
};
generateFiles(
tree,
path.join(__dirname, 'files'),
options.projectRoot,
templateOptions
);
}
function updateCargo(tree: Tree, options: NormalizedSchema) {
const cargoToml = parseCargoTomlWithTree(
tree,
options.projectRoot,
options.projectName
);
modifyCargoTable(cargoToml, 'lib', 'crate-type', ['cdylib']);
modifyCargoTable(cargoToml, 'dependencies', 'napi', {
version: '2.10.2',
'default-features': false,
features: ['napi4'],
});
modifyCargoTable(cargoToml, 'dependencies', 'napi-derive', '2.9.3');
modifyCargoTable(cargoToml, 'build-dependencies', 'napi-build', '2.0.1');
tree.write(
options.projectRoot + '/Cargo.toml',
stringifyCargoToml(cargoToml)
);
}
function updateGitIgnore(tree: Tree) {
if (!tree.exists('.gitignore')) {
return;
}
let gitIgnore = tree.read('.gitignore')?.toString() ?? '';
gitIgnore += '\n*.node';
tree.write('.gitignore', gitIgnore);
}
function updateTsConfig(tree: Tree, options: NormalizedSchema) {
if (!tree.exists('tsconfig.base.json')) {
return;
}
const tsConfig = getRootTsConfigPathInTree(tree);
if (!tsConfig) {
return;
}
updateJson(tree, tsConfig, (json) => {
const c = json.compilerOptions;
c.paths = c.paths || {};
if (c.paths[options.packageName]) {
throw new Error(
`You already have a library using the import path "${options.packageName}". Make sure to specify a unique one.`
);
}
c.paths[options.packageName] = [
joinPathFragments(options.projectRoot, 'index.d.ts'),
];
return json;
});
}

View File

@ -0,0 +1,3 @@
export interface AddNapiGeneratorSchema {
project: string;
}

View File

@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "AddNapi",
"title": "",
"type": "object",
"properties": {
"project": {
"description": "The name of the project",
"$default": {
"$source": "projectName",
"index": 0
}
}
},
"required": []
}

View File

@ -0,0 +1 @@
const variable = "<%= projectName %>";

View File

@ -0,0 +1,20 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import generator from './generator';
import { AddWasmReferenceGeneratorSchema } from './schema';
describe('add-wasm-reference generator', () => {
let appTree: Tree;
const options: AddWasmReferenceGeneratorSchema = { name: 'test' };
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
});
it('should run successfully', async () => {
await generator(appTree, options);
const config = readProjectConfiguration(appTree, 'test');
expect(config).toBeDefined();
});
});

View File

@ -0,0 +1,76 @@
import {
addProjectConfiguration,
formatFiles,
generateFiles,
getWorkspaceLayout,
names,
offsetFromRoot,
Tree,
} from '@nx/devkit';
import * as path from 'path';
import { AddWasmReferenceGeneratorSchema } from './schema';
interface NormalizedSchema extends AddWasmReferenceGeneratorSchema {
projectName: string;
projectRoot: string;
projectDirectory: string;
parsedTags: string[];
}
function normalizeOptions(
tree: Tree,
options: AddWasmReferenceGeneratorSchema
): NormalizedSchema {
const name = names(options.name).fileName;
const projectDirectory = options.directory
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const projectRoot = `${getWorkspaceLayout(tree).libsDir}/${projectDirectory}`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
return {
...options,
projectName,
projectRoot,
projectDirectory,
parsedTags,
};
}
function addFiles(tree: Tree, options: NormalizedSchema) {
const templateOptions = {
...options,
...names(options.name),
offsetFromRoot: offsetFromRoot(options.projectRoot),
template: '',
};
generateFiles(
tree,
path.join(__dirname, 'files'),
options.projectRoot,
templateOptions
);
}
export default async function (
tree: Tree,
options: AddWasmReferenceGeneratorSchema
) {
const normalizedOptions = normalizeOptions(tree, options);
addProjectConfiguration(tree, normalizedOptions.projectName, {
root: normalizedOptions.projectRoot,
projectType: 'library',
sourceRoot: `${normalizedOptions.projectRoot}/src`,
targets: {
build: {
executor: '@loafle/nx-rust:build',
},
},
tags: normalizedOptions.parsedTags,
});
addFiles(tree, normalizedOptions);
await formatFiles(tree);
}

View File

@ -0,0 +1,5 @@
export interface AddWasmReferenceGeneratorSchema {
name: string;
tags?: string;
directory?: string;
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "AddWasmReference",
"title": "",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"tags": {
"type": "string",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed",
"alias": "d"
}
},
"required": [
"name"
]
}

View File

@ -0,0 +1,32 @@
mod utils;
use wasm_bindgen::prelude::*;
<%_ if (useWebSys) { _%>
use web_sys::window;
<%_ } _%>
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
<%_ if (!useWebSys) { _%>
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, <%= projectName %>!");
}
<%_ } _%>
<%_ if (useWebSys) { _%>
#[wasm_bindgen]
pub fn greet() -> Result<(), JsValue> {
window()
.ok_or("no window")?
.alert_with_message("Hello, world!")
}
<%_ } _%>

View File

@ -0,0 +1,10 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}

View File

@ -0,0 +1,141 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import libraryGenerator from '../library/generator';
import generator from './generator';
import { AddWasmGeneratorSchema } from './schema';
describe('add-wasm generator', () => {
let appTree: Tree;
const options: AddWasmGeneratorSchema = {
project: 'test_lib',
generateDefaultLib: true,
useWebSys: false,
};
beforeEach(async () => {
appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
name: 'test_lib',
});
});
it('should add wasm to an existing library', async () => {
await generator(appTree, options);
const lib = appTree.read('./test_lib/src/lib.rs')?.toString();
expect(lib).toMatchInlineSnapshot(`
"mod utils;
use wasm_bindgen::prelude::*;
// When the \`wee_alloc\` feature is enabled, use \`wee_alloc\` as the global
// allocator.
#[cfg(feature = \\"wee_alloc\\")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert(\\"Hello, test_lib!\\");
}
"
`);
const cargoString = appTree.read('./test_lib/Cargo.toml')?.toString() ?? '';
expect(cargoString).toMatchInlineSnapshot(`
"
[package]
name = 'test_lib'
version = '0.1.0'
edition = '2021'
[dependencies]
wasm-bindgen = '0.2'
console_error_panic_hook = { version = '0.1.6', optional = true }
wee_alloc = { version = '0.4', optional = true }
[lib]
crate-type = [
'cdylib',
'rlib',
]
[feature]
default = [
'console_error_panic_hook',
]
[dev-dependencies]
wasm-bindgen-test = '0.3'
[profile.release]
opt-level = 's' #Tell \`rustc\` to optimize for small code size.
"
`);
});
it('should add wasm to an existing library with webSys', async () => {
await generator(appTree, { ...options, useWebSys: true });
const lib = appTree.read('./test_lib/src/lib.rs')?.toString();
expect(lib).toMatchInlineSnapshot(`
"mod utils;
use wasm_bindgen::prelude::*;
use web_sys::window;
// When the \`wee_alloc\` feature is enabled, use \`wee_alloc\` as the global
// allocator.
#[cfg(feature = \\"wee_alloc\\")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn greet() -> Result<(), JsValue> {
window()
.ok_or(\\"no window\\")?
.alert_with_message(\\"Hello, world!\\")
}
"
`);
const cargoString = appTree.read('./test_lib/Cargo.toml')?.toString() ?? '';
expect(cargoString).toMatchInlineSnapshot(`
"
[package]
name = 'test_lib'
version = '0.1.0'
edition = '2021'
[dependencies]
wasm-bindgen = '0.2'
js-sys = '0.3'
web-sys = { version = '0.3', features = [
'Window',
] }
console_error_panic_hook = { version = '0.1.6', optional = true }
wee_alloc = { version = '0.4', optional = true }
[lib]
crate-type = [
'cdylib',
'rlib',
]
[feature]
default = [
'console_error_panic_hook',
]
[dev-dependencies]
wasm-bindgen-test = '0.3'
[profile.release]
opt-level = 's' #Tell \`rustc\` to optimize for small code size.
"
`);
});
});

View File

@ -0,0 +1,126 @@
import TOML from '@ltd/j-toml';
import {
Tree,
formatFiles,
generateFiles,
offsetFromRoot,
readProjectConfiguration,
updateProjectConfiguration,
} from '@nx/devkit';
import * as path from 'path';
import { addWasmPackExecutor } from '../../utils/add-executors';
import {
modifyCargoNestedTable,
modifyCargoTable,
parseCargoTomlWithTree,
stringifyCargoToml,
} from '../../utils/toml';
import { AddWasmGeneratorSchema } from './schema';
interface NormalizedSchema extends AddWasmGeneratorSchema {
projectName: string;
projectRoot: string;
}
function normalizeOptions(
tree: Tree,
options: AddWasmGeneratorSchema
): NormalizedSchema {
const project = readProjectConfiguration(tree, options.project);
const projectName = project.name ?? options.project ?? '';
const projectRoot = project.root;
return {
...options,
projectName,
projectRoot,
};
}
function addFiles(tree: Tree, options: NormalizedSchema) {
if (!options.generateDefaultLib) {
return;
}
const templateOptions = {
...options,
offsetFromRoot: offsetFromRoot(options.projectRoot),
template: '',
};
generateFiles(
tree,
path.join(__dirname, 'files'),
options.projectRoot,
templateOptions
);
}
function updateCargo(tree: Tree, options: NormalizedSchema) {
const cargoToml = parseCargoTomlWithTree(
tree,
options.projectRoot,
options.projectName
);
modifyCargoTable(cargoToml, 'lib', 'crate-type', ['cdylib', 'rlib']);
modifyCargoTable(cargoToml, 'feature', 'default', [
'console_error_panic_hook',
]);
modifyCargoTable(cargoToml, 'dependencies', 'wasm-bindgen', '0.2');
if (options.useWebSys) {
modifyCargoTable(cargoToml, 'dependencies', 'js-sys', '0.3');
modifyCargoTable(cargoToml, 'dependencies', 'web-sys', {
version: '0.3',
features: ['Window'],
});
}
modifyCargoTable(cargoToml, 'dependencies', 'console_error_panic_hook', {
version: '0.1.6',
optional: true,
});
modifyCargoTable(cargoToml, 'dependencies', 'wee_alloc', {
version: '0.4',
optional: true,
});
modifyCargoTable(cargoToml, 'dev-dependencies', 'wasm-bindgen-test', '0.3');
modifyCargoNestedTable(cargoToml, 'profile', 'release', {
[TOML.commentFor('opt-level')]:
'Tell `rustc` to optimize for small code size.',
'opt-level': 's',
});
tree.write(
options.projectRoot + '/Cargo.toml',
stringifyCargoToml(cargoToml)
);
}
function updateBuildTarget(tree: Tree, options: NormalizedSchema) {
const configuration = readProjectConfiguration(tree, options.projectName);
configuration.targets ??= {};
configuration.targets.build = addWasmPackExecutor({
'target-dir': `dist/target/wasm/${options.projectName}`,
release: false,
target: 'bundler',
});
updateProjectConfiguration(tree, options.projectName, configuration);
}
export default async function wasmGenerator(
tree: Tree,
options: AddWasmGeneratorSchema
) {
const normalizedOptions = normalizeOptions(tree, options);
addFiles(tree, normalizedOptions);
updateCargo(tree, normalizedOptions);
updateBuildTarget(tree, normalizedOptions);
await formatFiles(tree);
}

View File

@ -0,0 +1,5 @@
export interface AddWasmGeneratorSchema {
project: string;
useWebSys: boolean;
generateDefaultLib: boolean;
}

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "AddWasm",
"title": "",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project",
"$default": {
"$source": "projectName",
"index": 0
}
},
"useWebSys": {
"type": "boolean",
"default": true,
"description": "Use the web sys package"
},
"generateDefaultLib": {
"type": "boolean",
"default": false,
"description": "Generates a default lib that contains wasm code. This will over write the existing lib.rs file."
}
},
"required": [
"project"
]
}

View File

@ -0,0 +1,10 @@
[package]
name = "<%= projectName %>"
version = "0.1.0"
edition = "<%= edition %>"
[dependencies]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -0,0 +1,4 @@
fn main() {
println!("Hello, world!");
}

View File

@ -0,0 +1,87 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import TOML from '@ltd/j-toml';
import generator from './generator';
import { RustBinaryGeneratorSchema } from './schema';
describe('rust generator', () => {
let appTree: Tree;
const options: RustBinaryGeneratorSchema = { name: 'test-name' };
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
});
it('should run successfully', async () => {
await generator(appTree, options);
const config = readProjectConfiguration(appTree, 'test_name');
expect(config).toBeDefined();
});
it('should create a Cargo.toml project', async () => {
await generator(appTree, { ...options });
const cargoToml = appTree.read('./test_name/Cargo.toml')?.toString() ?? '';
expect(cargoToml.length).toBeGreaterThan(0);
expect(TOML.parse(cargoToml)).toMatchInlineSnapshot(`
Object {
"dependencies": Object {},
"package": Object {
"edition": "2021",
"name": "test_name",
"version": "0.1.0",
},
}
`);
});
it('should create a project with a specified edition', async () => {
await generator(appTree, { ...options, edition: '2018' });
const cargoToml = appTree.read('./test_name/Cargo.toml')?.toString() ?? '';
expect(TOML.parse(cargoToml)).toMatchInlineSnapshot(`
Object {
"dependencies": Object {},
"package": Object {
"edition": "2018",
"name": "test_name",
"version": "0.1.0",
},
}
`);
});
it('should add a project to the main Cargo.toml workspace members', async () => {
await generator(appTree, options);
const cargoToml = appTree.read('Cargo.toml')?.toString() ?? '';
expect(TOML.parse(cargoToml)).toMatchInlineSnapshot(`
Object {
"profile": Object {
"release": Object {
"lto": true,
},
},
"workspace": Object {
"members": Array [
"test_name",
],
"resolver": "2",
},
}
`);
});
it('should generate into a directory', async () => {
await generator(appTree, { ...options, directory: 'test-dir' });
const cargoToml =
appTree.read('./test_dir/test_name/Cargo.toml')?.toString() ?? '';
expect(TOML.parse(cargoToml)).toMatchInlineSnapshot(`
Object {
"dependencies": Object {},
"package": Object {
"edition": "2021",
"name": "test_dir_test_name",
"version": "0.1.0",
},
}
`);
});
});

View File

@ -0,0 +1,63 @@
import {
addProjectConfiguration,
formatFiles,
generateFiles,
names,
offsetFromRoot,
Tree,
} from '@nx/devkit';
import * as path from 'path';
import {
addBuildExecutor,
addTestExecutor,
addLintExecutor,
addRunExecutor,
} from '../../utils/add-executors';
import { addToCargoWorkspace } from '../../utils/add-to-workspace';
import {
NormalizedSchema,
normalizeOptions,
} from '../../utils/normalize-options';
import init from '../init/generator';
import { RustBinaryGeneratorSchema } from './schema';
function addFiles(
tree: Tree,
options: NormalizedSchema & RustBinaryGeneratorSchema
) {
const templateOptions = {
...options,
...names(options.name),
offsetFromRoot: offsetFromRoot(options.projectRoot),
template: '',
};
generateFiles(
tree,
path.join(__dirname, 'files'),
options.projectRoot,
templateOptions
);
}
export default async function binaryGenerator(
tree: Tree,
options: RustBinaryGeneratorSchema
) {
await init(tree);
const normalizedOptions = normalizeOptions(tree, 'app', options);
addProjectConfiguration(tree, normalizedOptions.projectName, {
root: normalizedOptions.projectRoot,
projectType: 'application',
sourceRoot: `${normalizedOptions.projectRoot}/src`,
targets: {
build: addBuildExecutor({ 'target-dir': normalizedOptions.targetDir }),
test: addTestExecutor({ 'target-dir': normalizedOptions.targetDir }),
lint: addLintExecutor({ 'target-dir': normalizedOptions.targetDir }),
run: addRunExecutor({ 'target-dir': normalizedOptions.targetDir }),
},
tags: normalizedOptions.parsedTags,
});
addFiles(tree, normalizedOptions);
addToCargoWorkspace(tree, normalizedOptions.projectRoot);
await formatFiles(tree);
}

View File

@ -0,0 +1,6 @@
export interface RustBinaryGeneratorSchema {
name: string;
edition?: '2015' | '2018' | '2021';
tags?: string;
directory?: string;
}

View File

@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "Rust",
"title": "",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"edition": {
"type": "string",
"description": "The edition of Rust to use for this binary (default is 2021)",
"default": "2021",
"enum": [
"2015",
"2018",
"2021"
]
},
"tags": {
"type": "string",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed",
"alias": "d"
}
},
"required": [
"name"
]
}

View File

@ -0,0 +1,36 @@
import {
ProjectConfiguration,
Tree,
formatFiles,
getPackageManagerCommand,
getProjects,
joinPathFragments,
} from '@nx/devkit';
import { CreateNapiNpmDirsGeneratorSchema } from './schema';
import { runProcess } from '../../utils/run-process';
export default async function (
tree: Tree,
options: CreateNapiNpmDirsGeneratorSchema
) {
const project = getProjects(tree).get(options.project);
if (!project) {
throw 'Project not found';
}
addNpmFiles(project);
await formatFiles(tree);
}
function addNpmFiles(project: ProjectConfiguration) {
const { exec } = getPackageManagerCommand();
const command = `${exec} napi create-npm-dir`;
runProcess(
command,
'-c',
joinPathFragments(project.root, 'package.json'),
'-t',
project.root
);
}

Some files were not shown because too many files have changed in this diff Show More