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

feature(create-neon): namespaced libraries #1083

Merged
merged 10 commits into from
Nov 20, 2024
8 changes: 4 additions & 4 deletions package-lock.json

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

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

{
"name": "{{options.name}}",
"name": "{{options.fullName}}",
"version": "{{options.version}}",
"main": "index.node",
"scripts": {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

{
"name": "{{options.name}}",
"name": "{{options.fullName}}",
"version": "{{options.version}}",
{{#eq options.library.module compare="esm"}}
"exports": {
Expand Down Expand Up @@ -28,6 +28,9 @@
{{#if options.library.cache.org}}
"org": "{{options.library.cache.org}}",
{{/if}}
{{#if options.library.cache.prefix}}
"prefix": "{{options.library.cache.prefix}}",
{{/if}}
{{/eq}}
"platforms": {},
"load": "./src/load.cts"
Expand Down
4 changes: 2 additions & 2 deletions pkgs/create-neon/data/versions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"neon": "1",
"neonCLI": "0.1.73",
"neonLoad": "0.1.73",
"neonCLI": "0.1.82",
"neonLoad": "0.1.82",
"typescript": "5.3.3",
"typesNode": "20.11.16",
"tsconfigNode": {
Expand Down
4 changes: 2 additions & 2 deletions pkgs/create-neon/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-neon",
"version": "0.5.2",
"version": "0.6.0",
"description": "Create Neon projects with no build configuration.",
"type": "module",
"exports": "./dist/src/bin/create-neon.js",
Expand Down Expand Up @@ -48,7 +48,7 @@
"typescript": "^5.3.2"
},
"dependencies": {
"@neon-rs/manifest": "^0.1.0",
"@neon-rs/manifest": "^0.2.1",
"chalk": "^5.3.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1",
Expand Down
56 changes: 47 additions & 9 deletions pkgs/create-neon/src/bin/create-neon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,24 @@ try {
}

const [pkg] = opts._unknown;
const { org, basename } = /^((?<org>@[^/]+)\/)?(?<basename>.*)/.exec(pkg)
?.groups as {
org?: string;
basename: string;
};
const fullName = org ? pkg : basename;
const platforms = parsePlatforms(opts.platform);
const cache = parseCache(opts.lib, opts.bins, pkg);
const cache = parseCache(opts.lib, opts.bins, basename, org);
const ci = parseCI(opts.ci);

if (opts.yes) {
process.env["npm_configure_yes"] = "true";
}

createNeon({
name: pkg,
org,
basename,
fullName,
version: "0.1.0",
library: opts.lib
? {
Expand Down Expand Up @@ -112,21 +120,51 @@ function parseCI(ci: string): CI | undefined {
function parseCache(
lib: boolean,
bins: string,
pkg: string
pkg: string,
org: string | undefined
): Cache | undefined {
if (bins === "none") {
return lib ? new NPM(pkg) : undefined;
const defaultPrefix = org ? `${pkg}-` : "";
org ??= `@${pkg}`;

// CASE: npm create neon -- --app logos-r-us
// CASE: npm create neon -- --app @acme/logos-r-us
// - <no binaries cache>
if (bins === "none" && !lib) {
return undefined;
}

if (bins === "npm") {
return new NPM(pkg);
// CASE: npm create neon -- --lib logo-generator
// CASE: npm create neon -- --lib --bins npm logo-generator
// - lib: `logo-generator`
// - bin: `@logo-generator/darwin-arm64`

// CASE: npm create neon -- --lib @acme/logo-generator
// CASE: npm create neon -- --lib --bins npm @acme/logo-generator
// - lib: `@acme/logo-generator`
// - bin: `@acme/logo-generator-darwin-arm64`
if (bins === "none" || bins === "npm") {
return new NPM(org, defaultPrefix);
}

// CASE: npm create neon -- --lib --bins=npm:acme logo-generator
// lib: logo-generator
// bin: @acme/logo-generator-darwin-arm64

// CASE: npm create neon -- --lib --bins=npm:acme/libs-logo-generator- logo-generator
// lib: logo-generator
// bin: @acme/libs-logo-generator-darwin-arm64

// CASE: npm create neon -- --lib --bins=npm:acme-libs @acme/logo-generator
// lib: @acme-libs/logo-generator
// bin: @acme-libs/logo-generator-darwin-arm64
if (bins.startsWith("npm:")) {
return new NPM(pkg, bins.substring(4));
const split = bins.substring(4).split("/", 2);
const org = split[0].replace(/^@?/, "@"); // don't care if they include the @ or not
const prefix = split.length > 1 ? split[1] : defaultPrefix;
return new NPM(org, prefix);
}

throw new Error(
`Unrecognized binaries cache ${bins}, expected 'npm[:org]' or 'none'`
`Unrecognized binaries cache ${bins}, expected 'npm[:org[/prefix]]' or 'none'`
);
}
11 changes: 4 additions & 7 deletions pkgs/create-neon/src/cache/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { Cache } from "../cache.js";

export class NPM implements Cache {
readonly org: string;
readonly prefix: string;

readonly type: string = "npm";

constructor(pkg: string, org?: string) {
this.org = org || NPM.inferOrg(pkg);
}

static inferOrg(pkg: string): string {
const m = pkg.match(/^@([^/]+)\/(.*)/);
return `@${m?.[1] ?? pkg}`;
constructor(org: string, prefix: string) {
this.org = org;
this.prefix = prefix;
}
}
12 changes: 7 additions & 5 deletions pkgs/create-neon/src/create/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export type LibraryOptions = {
};

export type ProjectOptions = {
name: string;
org?: string | undefined;
basename: string;
fullName: string;
version: string;
library: LibraryOptions | null;
app: boolean | null;
Expand Down Expand Up @@ -64,12 +66,12 @@ export abstract class Creator {
async create(cx: Context): Promise<void> {
try {
this._temp = await mktemp();
this._tempPkg = path.join(this._temp, this._options.name);
this._tempPkg = path.join(this._temp, this._options.basename);

await fs.mkdir(this._tempPkg);
} catch (err: any) {
await die(
`Could not create \`${this._options.name}\`: ${err.message}`,
`Could not create \`${this._options.basename}\`: ${err.message}`,
this._temp
);
}
Expand Down Expand Up @@ -122,11 +124,11 @@ export abstract class Creator {
await this.createNeonBoilerplate(cx);

try {
await fs.rename(this._tempPkg, this._options.name);
await fs.rename(this._tempPkg, this._options.basename);
await fs.rmdir(this._temp);
} catch (err: any) {
await die(
`Could not create \`${this._options.name}\`: ${err.message}`,
`Could not create \`${this._options.basename}\`: ${err.message}`,
this._tempPkg
);
}
Expand Down
25 changes: 19 additions & 6 deletions pkgs/create-neon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,23 @@ async function askProjectType(options: ProjectOptions) {

const org =
cache === "npm"
? (
await dialog.ask({
prompt: "cache org",
parse: (v: string): string => v,
default: options.org ?? `@${options.basename}`,
})
).replace(/^@?/, "@") // don't care if they include the @ or not
: null;

const prefix =
cache === "npm" && org === `@${options.basename}`
? ""
: cache === "npm"
? await dialog.ask({
prompt: "cache org",
prompt: "cache prefix",
parse: (v: string): string => v,
default: NPM.inferOrg(options.name),
default: `${options.basename}-`,
})
: null;

Expand All @@ -76,7 +89,7 @@ async function askProjectType(options: ProjectOptions) {
options.library = {
lang: Lang.TS,
module: ModuleType.ESM,
cache: cache === "npm" ? new NPM(options.name, org!) : undefined,
cache: cache === "npm" ? new NPM(org!, prefix!) : undefined,
ci: ci === "github" ? new GitHub() : undefined,
platforms: platforms.length === 1 ? platforms[0] : platforms,
};
Expand All @@ -88,9 +101,9 @@ async function askProjectType(options: ProjectOptions) {

export async function createNeon(options: ProjectOptions): Promise<void> {
try {
await assertCanMkdir(options.name);
await assertCanMkdir(options.basename);
} catch (err: any) {
await die(`Could not create \`${options.name}\`: ${err.message}`);
await die(`Could not create \`${options.basename}\`: ${err.message}`);
}

const cx = new Context(options);
Expand All @@ -113,6 +126,6 @@ export async function createNeon(options: ProjectOptions): Promise<void> {
await creator.create(cx);

console.log(
`✨ Created Neon project \`${options.name}\`. Happy 🦀 hacking! ✨`
`✨ Created Neon project \`${options.fullName}\`. Happy 🦀 hacking! ✨`
);
}
41 changes: 41 additions & 0 deletions pkgs/create-neon/test/create-neon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("Command-line argument validation", () => {
});

const PROJECT = "create-neon-test-project";
const NAMESPACED_PROJECT = "@dherman/create-neon-test-project";

describe("Project creation", () => {
afterEach(async () => {
Expand Down Expand Up @@ -182,6 +183,46 @@ describe("Project creation", () => {

assert.strictEqual(json.neon.type, "library");
assert.strictEqual(json.neon.org, "@create-neon-test-project");
assert.notProperty(json.neon, "prefix");
assert.strictEqual(json.neon.platforms, "common");
assert.strictEqual(json.neon.load, "./src/load.cts");

TOML.parse(
await fs.readFile(path.join(PROJECT, "Cargo.toml"), { encoding: "utf8" })
);
});

it("asks for a prefix when Neon lib is namespaced", async () => {
try {
await expect(spawn(NODE, [CREATE_NEON, NAMESPACED_PROJECT]), {
"neon project type": "lib",
"neon target platforms": "",
"neon binary cache": "",
"neon cache org": "",
"neon cache prefix": "",
"neon ci provider": "",
"package name:": "",
"version:": "",
"description:": "",
"git repository:": "",
"keywords:": "",
"author:": "",
"license:": "",
"Is this OK?": "",
});
} catch (error: any) {
assert.fail("create-neon unexpectedly failed: " + error.message);
}

let json = JSON.parse(
await fs.readFile(path.join(PROJECT, "package.json"), {
encoding: "utf8",
})
);

assert.strictEqual(json.neon.type, "library");
assert.strictEqual(json.neon.org, "@dherman");
assert.strictEqual(json.neon.prefix, "create-neon-test-project-");
assert.strictEqual(json.neon.platforms, "common");
assert.strictEqual(json.neon.load, "./src/load.cts");

Expand Down