Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create-neon #690

Merged
merged 25 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e40c612
WIP
dherman Mar 1, 2021
e548f23
Add shelling to `npm init` and reading from package.json.
dherman Mar 3, 2021
5cffd02
First working version, with a simple manual test. Next up we need aut…
dherman Mar 4, 2021
fe9e588
Address @kjvalencik's review, plus some general cleanup and refactoring:
dherman Mar 5, 2021
96f3f86
Abstract out `die` helper.
dherman Mar 5, 2021
01c0526
Just call `npm` directly since `npm init create-neon` doesn't pass th…
dherman Mar 5, 2021
d7f1c7f
- remove dead import
dherman Mar 5, 2021
73bbe42
Add shebang
dherman Mar 5, 2021
f7ee6b6
Updates the generated package.json with Neon-specific default configu…
dherman Mar 5, 2021
70fe6e4
Avoids asking the "main" or "test" package.json entries by creating t…
dherman Mar 6, 2021
d79f9ee
Simplify the datatypes for the template expansion.
dherman Mar 6, 2021
7840161
Initial test suite.
dherman Mar 8, 2021
e8803c6
- Move test helpers into dev/ directory
dherman Mar 9, 2021
ddcbcbc
Add create-neon tests to CI.
dherman Mar 9, 2021
7c3c97b
Ensure we run npm install before running the tests, for CI.
dherman Mar 9, 2021
7b13678
Remove `fs/promises` since some supported Node versions don't support…
dherman Mar 9, 2021
9d97541
Add 3 retries to `rmdir` after each test.
dherman Mar 9, 2021
6b21eb1
Longer retry delay for rmdir?
dherman Mar 9, 2021
8400efa
Add rimraf shim since `{ recursive: true }` isn't supported in all ou…
dherman Mar 9, 2021
20727be
Debugging CI: print output on failure of child process
dherman Mar 10, 2021
993d106
More CI debugging: print stderr on failure
dherman Mar 10, 2021
208346f
Call npm.cmd on Windows
dherman Mar 10, 2021
af4a264
Tweaking diagnostic output...
dherman Mar 10, 2021
e9a82cb
Relax test timeout from 2s to 5s, since GH Actions test runners are f…
dherman Mar 10, 2021
6165246
Shell to `npm` using `shell: true` instead of tacking on the extensio…
dherman Mar 10, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Cargo.lock
**/index.node
**/artifacts.json
cli/lib
create-neon/dist
test/cli/lib
npm-debug.log
rls*.log
4 changes: 4 additions & 0 deletions create-neon/data/templates/.gitignore.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
index.node
**/node_modules
**/.DS_Store
22 changes: 22 additions & 0 deletions create-neon/data/templates/Cargo.toml.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "{{project.name}}"
version = "{{project.version}}"
{{#if project.description}}
description = {{project.description.quoted}}
{{/if}}
{{#if project.author}}
authors = [{{project.author.quoted}}]
{{/if}}
{{#if project.license}}
license = "{{project.license}}"
{{/if}}
edition = "2018"
exclude = ["index.node"]

[lib]
crate-type = ["cdylib"]

[dependencies.neon]
version = "{{versions.neon}}"
default-features = false
features = ["napi-{{versions.napi}}"]
5 changes: 5 additions & 0 deletions create-neon/data/templates/README.md.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# {{project.name}}

{{#if project.description}}
{{project.description.raw}}
{{/if}}
11 changes: 11 additions & 0 deletions create-neon/data/templates/lib.rs.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use neon::prelude::*;

fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
Ok(cx.string("hello node"))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("hello", hello)?;
Ok(())
}
5 changes: 5 additions & 0 deletions create-neon/data/versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"neon": "0.7",
"napi": "6",
"cargo-cp-artifact": "0.1"
}
58 changes: 58 additions & 0 deletions create-neon/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions create-neon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "create-neon",
"version": "0.1.0",
"description": "Create Neon projects with no build configuration.",
"author": "Dave Herman <david.herman@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/neon-bindings/neon/issues"
},
"homepage": "https://github.com/neon-bindings/neon#readme",
"bin": {
"create-neon": "dist/src/bin/create-neon.js"
},
"files": [
"dist/**/*"
],
"scripts": {
"build": "tsc && cp -r data/templates dist/data",
"prepublishOnly": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1",
"manual-test": "npm run build && rm -rf throwaway-test && mkdir throwaway-test && cd throwaway-test && node ../dist/src/bin/create-neon.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/neon-bindings/neon.git"
},
"keywords": [
"neon"
],
"devDependencies": {
"@types/node": "^14.14.31",
"typescript": "^4.2.2"
},
"dependencies": {
"handlebars": "^4.7.7"
}
}
62 changes: 62 additions & 0 deletions create-neon/src/bin/create-neon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env node

import { mkdir } from 'fs/promises';
import npmInit from '../npm-init';
import versions from '../../data/versions.json';
import { Project, Metadata } from '../metadata';
import * as path from 'path';
import Template from '../template';
import die from '../die';

async function main() {
await npmInit();

let project: Project;

try {
project = await Project.load('package.json', versions['cargo-cp-artifact']);
} catch (err) {
die("Could not read `package.json`: " + err.message);
}

try {
project.save('package.json');
} catch (err) {
die("Could not update `package.json`: " + err.message);
}

// Select the N-API version associated with the current
// running Node process.
let inferred = process.versions.napi;

let napi = inferred
? Math.min(Number(versions.napi), Number(inferred))
: Number(versions.napi);

let metadata: Metadata = {
project,
versions: {
neon: versions.neon,
napi: napi
}
};

await mkdir('src');

let gitignore = new Template('.gitignore.hbs', '.gitignore');
let manifest = new Template('Cargo.toml.hbs', 'Cargo.toml');
let readme = new Template('README.md.hbs', 'README.md');
let lib = new Template('lib.rs.hbs', path.join('src', 'lib.rs'));

for (let template of [gitignore, manifest, readme, lib]) {
try {
await template.expand(metadata);
} catch (err) {
die(`Could not save ${template.target}: ${err.message}`);
}
}

console.log(`✨ Initialized Neon project \`${metadata.project.name}\`. Happy 🦀 hacking! ✨`);
}

main();
4 changes: 4 additions & 0 deletions create-neon/src/die.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function die(message: string): never {
console.error(`❌ ${message}`);
process.exit(1);
}
85 changes: 85 additions & 0 deletions create-neon/src/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { readFile, writeFile } from "fs/promises";

export interface Metadata {
project: Project,
versions: Versions
}

export class Project {
private json: any;

name: string;
version: string;
author: FreeText | undefined;
license: string;
description: FreeText | undefined;

static async load(source: string, cargoCpArtifact: string): Promise<Project> {
return new Project(JSON.parse(await readFile(source, 'utf8')), cargoCpArtifact);
}

constructor(json: any, cargoCpArtifact: string) {
this.json = json;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have two copies of the data? Overall, I find the class base approach a little complicated. Alternatively,

interface PackageJson {
    name: string,
    author: string,
    [k: string]: any,
}

There might be a package.json types file we could import.

this.name = json.name || "";
this.version = json.version || "";
this.author = quote(json.author);
this.license = json.license || "";
this.description = quote(json.description);

json.name = this.name;
json.version = this.version;
json.author = this.author?.raw;
json.license = this.license;
json.description = this.description?.raw;

json.main = "index.node";

let test = "cargo test";

// If the user specifies a non-default test command, use theirs instead.
// Ideally there would be better extensibility hooks in `npm init` for
// this but there aren't any environment variables or command-line flags
// we can use, so we have to guess based on the default value. This also
// unfortunately leaks to the user when `npm init` shows the values for
// the package.json it's going to use in the final user confirmation.
if (!/\s*echo \".*\" && exit 1\s*/.test(json.scripts.test)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we write a package.json before executing npm init that contains { "scripts": { "test": "cargo test" } }, it won't prompt for a test command. I think this might be a cleaner workaround than matching on the default text because the user won't even be prompted.

test = json.scripts.test;
}

json.scripts = {
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics",
"install": "npm run build",
"test": test
};

json.devDependencies = {
"cargo-cp-artifact": `^${cargoCpArtifact}`
};
}

async save(target: string): Promise<null> {
await writeFile(target, JSON.stringify(this.json, undefined, 2))
return null;
}
}

export interface FreeText {
raw: string;
quoted: string;
}

function quote(text: string): FreeText | undefined {
if (!text) {
return undefined;
}

return {
raw: text,
quoted: JSON.stringify(text)
};
}

export interface Versions {
neon: string,
napi: number
}
15 changes: 15 additions & 0 deletions create-neon/src/npm-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import shell from './shell';

export default async function npmInit(): Promise<number> {
let code = await shell('npm', ['init']);

if (code == null) {
process.exit(1);
}

if (code !== 0) {
process.exit(code);
}

return 0;
}
27 changes: 27 additions & 0 deletions create-neon/src/shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { spawn } from 'child_process';

/**
* Transparently shell out to an executable with a list of arguments.
* All stdio is inherited directly from the current process.
*/
export default function shell(cmd: string, args: string[]): Promise<number | null> {
let child = spawn(cmd, args, { stdio: 'inherit' });

let resolve: (result: number | null) => void;
let reject: (error: Error) => void;

let result: Promise<number | null> = new Promise((res, rej) => {
resolve = res;
reject = rej;
});

child.on('exit', (code) => {
resolve(code);
});

child.on('error', (error) => {
reject(error);
});

return result;
}
27 changes: 27 additions & 0 deletions create-neon/src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { readFile, writeFile } from 'fs/promises';
import handlebars, { TemplateDelegate } from 'handlebars';
import * as path from 'path';
import { Metadata } from './metadata';

const TEMPLATES_DIR = path.join(__dirname, '..', 'data', 'templates');

export default class Template {
source: string;
target: string;
private compiled: Promise<TemplateDelegate<Metadata>>;

constructor(source: string, target: string) {
this.source = source;
this.target = target;
this.compiled = readFile(path.join(TEMPLATES_DIR, source), {
encoding: 'utf8'
}).then(source => handlebars.compile(source, { noEscape: true }));
}

async expand(ctx: Metadata): Promise<null> {
let expanded = (await this.compiled)(ctx);
// The 'wx' flag creates the file but fails if it already exists.
await writeFile(this.target, expanded, { flag: 'wx' });
return null;
}
}
Loading