Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
[email protected] authored and [email protected] committed Feb 1, 2022
1 parent a18d1b4 commit 5e91539
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tests/ver.ts
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": false,
}
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,31 @@
# package
This module generates a package (with name and version) file from the latest git tag for you to use in code
# ver
This module generates a ver.ts with as version from the latest git tag for you to use in code.

## What?
In node.js there is een idiomatic way of storing the version of whichever program you're building which is the package.json.

In Deno the versions are managed by git tags, which is better, because you manage the version of your software in a more abstract way: using git, and not npm. npm is more locked-in to a technology. (git is also a technology but if you're not using that then I don't know what to say).

This presents a problem though:

The .git directory (where the tags are stored) is not available in production code (or at least shouldn't be) but it's a pretty common approach in node.js to parse the package.json and use the version found there to display in your app. For example, an api could return it's version in a header, and a cli wants to display the version in it's --help.

There is a Deno module that solves this problem with a rich feature set, including a cli which does the git tagging. Check it out here: https://deno.land/x/version_ts

While the above solution is excellent, I wanted a super simple version of this. Not requiring a cli to be installed but just generating a single .ts file for you to use based on the latest git tag.

## How?

In the startup of your app, put
```typescript
await ensureVersion();
```
This will generate a ver.ts file in the cwd of your project. ensureVersion will make just that file always contains the latest version from your git tag list.

Whenever you need the current version, you can import the ver.ts or use
```typescript
await getVersion();
```
anywhere in your application to get the sematic version.

Note that using getVersion is better because it lazy loads (with await import) the ver.ts
5 changes: 5 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * as log from "https://deno.land/[email protected]/log/mod.ts";
export * as fs from "https://deno.land/[email protected]/fs/mod.ts";
export * as asserts from "https://deno.land/[email protected]/testing/asserts.ts";
export * as path from "https://deno.land/[email protected]/path/mod.ts";
export * as semver from "https://deno.land/x/semver/mod.ts";
132 changes: 132 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { fs, path, semver } from "./deps.ts";

export async function ensureVersion(
root: string = Deno.cwd()
): Promise<semver.SemVer | null> {
let parsedVersion: semver.SemVer | null = null;
let maybeError: Error | null = null;

try {
await del(root);
const output = await mostRecentGitTag();
parsedVersion = semver.parse(output);

if (!parsedVersion) {
throw new Error(
`Failed to parse version: ${output}. (which was the most recent git tag)`
);
}
} catch (err) {
maybeError = err;
}

await save(parsedVersion, root, maybeError);

return await get(root);
}

export async function getVersion(
root: string = Deno.cwd()
): Promise<semver.SemVer | null> {
return await get(root);
}

const fileName = "ver.ts";

async function get(root: string): Promise<semver.SemVer | null> {
try {
const p = path.join(root, fileName);
await Deno.stat(p);

const v = await import(p);
const keys = Object.keys(v);

if (!keys.includes("default")) {
throw new TypeError(`${fileName} does not include a default export`);
}

const val = v["default"];

if (typeof val !== "string" && !(val instanceof String)) {
throw new TypeError(
`${fileName} default export did not contain a string value`
);
}

return semver.parse(val.toString());
} catch (err) {
console.error(err);
return null;
}
}

async function save(
version: semver.SemVer | null,
root = Deno.cwd(),
err: Error | null = null
): Promise<void> {
const dest = path.join(root, fileName);

const versionString = version
? `export default '${version.toString()}';`
: '// "git tag" last line did not return a valid version';

const datetimeString = `// Auto-generated from 'git tag' on ${new Date().toString()}.`;
let errString = "";

if (err) {
errString = `// Error: ${err.message}`;
}

await Deno.writeTextFile(
dest,
`${datetimeString}
${errString}
${versionString}`
);
}

export async function del(root = Deno.cwd()) {
const dest = path.join(root, fileName);
const exists = await Deno.stat(dest)
.then(() => true)
.catch(() => false);

if (exists) {
await Deno.remove(dest);
}
}

const textDecoder = new TextDecoder();
export async function run(args: string[]): Promise<string> {
const process = Deno.run({
cmd: args,
stdout: "piped",
stdin: "piped",
stderr: "piped",
});

const rawOutput = await process.output();
const rawError = await process.stderrOutput();

if (rawError && rawError.length > 0) {
const err = textDecoder.decode(rawError);
throw new Error(err);
}
//await process.status();
return textDecoder.decode(rawOutput);
}

export async function mostRecentGitTag(): Promise<string> {
const output = await run(["git", "tag"]);
const nl = fs.detect(output) || fs.EOL.LF;
const lines = output.split(nl);

const versions = lines.map((l) => l.trim()).filter((l) => l.length > 0);

if (versions.length === 0) {
throw new Error("Could not find any git tags");
}

return versions[versions.length - 1];
}
21 changes: 21 additions & 0 deletions tests/get-version-fail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { asserts } from "../deps.ts";
import { getVersion } from "../mod.ts";
import { deleteTestVer } from "./testHelpers.ts";

Deno.test(
"Get Version (Fail)",
{
permissions: {
read: true,
write: true
},
},
async (ctx: Deno.TestContext) => {
await ctx.step("Delete Existing", async () => await deleteTestVer());
await ctx.step("Get Undefined Version", async () => {
const v = await getVersion();
asserts.assert(v === null);
});
await ctx.step("Delete Existing", async () => await deleteTestVer());
}
);
27 changes: 27 additions & 0 deletions tests/get-version-success.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { asserts } from "../deps.ts";
import { getVersion } from "../mod.ts";
import { deleteTestVer,createTestVer,testPath } from "./testHelpers.ts";

Deno.test(
"Get Version (Success)",
{
permissions: {
read: true,
write: true,
},
},
async (ctx: Deno.TestContext) => {
await ctx.step("Delete Existing", async () => await deleteTestVer());
await ctx.step("Create New", async () => await createTestVer());
await ctx.step("Get Defined Version", async () => {
const v = await getVersion(testPath);

asserts.assert(v !== null);
asserts.assert(v.major === 1);
asserts.assert(v.minor === 2);
asserts.assert(v.patch === 3);
});

await ctx.step("Delete Existing", async () => await deleteTestVer());
}
);
25 changes: 25 additions & 0 deletions tests/git-tag-fail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { asserts } from "../deps.ts";
import { ensureVersion } from "../mod.ts";
import { addBogusGitTag, deleteTestVer, removeBogusGitTag, testPath } from "./testHelpers.ts";

Deno.test(
"Git Tag (Fail)",
{
permissions: {
read: true,
write: true
},
sanitizeResources: false
},
async (ctx: Deno.TestContext) => {
const testTag = "bogus-tag";
await ctx.step("Delete Existing", async () => await deleteTestVer());
await ctx.step("Add Bogus Test Tag", async () => await addBogusGitTag(testTag));
await ctx.step("Ensure Version", async () => {
const v = await ensureVersion(testPath);
asserts.assert(v === null);
});
await ctx.step("Remove Bogus Test Tag", async () => await removeBogusGitTag(testTag));
await ctx.step("Delete Existing", async () => await deleteTestVer());
}
);
28 changes: 28 additions & 0 deletions tests/git-tag-success.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { asserts } from "../deps.ts";
import { ensureVersion } from "../mod.ts";
import { addBogusGitTag, deleteTestVer, removeBogusGitTag, testPath } from "./testHelpers.ts";

Deno.test(
"Git Tag (Success)",
{
permissions: {
read: true,
write: true
},
sanitizeResources: false
},
async (ctx: Deno.TestContext) => {
const testTag = "v45.78.90";
await ctx.step("Delete Existing", async () => await deleteTestVer());
await ctx.step("Add Bogus Test Tag", async () => await addBogusGitTag(testTag));
await ctx.step("Ensure Version", async () => {
const v = await ensureVersion(testPath);
asserts.assert(v !== null);
asserts.assert(v.major === 45);
asserts.assert(v.minor === 78);
asserts.assert(v.patch === 90);
});
await ctx.step("Remove Bogus Test Tag", async () => await removeBogusGitTag(testTag));
await ctx.step("Delete Existing", async () => await deleteTestVer());
}
);
26 changes: 26 additions & 0 deletions tests/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { path } from "../deps.ts";
import { run } from "../mod.ts";

export const testPath = path.join(Deno.cwd(), "tests");

export async function createTestVer(ver = "1.2.3"): Promise<void> {
const p = path.join(testPath, "ver.ts");
await Deno.writeTextFile(p, `export default '${ver}';`);
}

export async function deleteTestVer(): Promise<void> {
const p = path.join(testPath, "ver.ts");
const exists = await Deno.stat(p).then(() => true).catch(() => false);

if (exists) {
await Deno.remove(p);
}
}

export async function addBogusGitTag(tag: string): Promise<void> {
await run(["git", "tag", tag]);
}

export async function removeBogusGitTag(tag: string): Promise<void> {
await run(["git", "tag", "-d", tag]);
}

0 comments on commit 5e91539

Please sign in to comment.