Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/actions/report-pr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ runs:
badge: true
fail_below_min: false
format: markdown
hide_branch_rate: false
hide_branch_rate: true
hide_complexity: true
indicators: true
output: both
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: Test PR
"on": pull_request

permissions:
contents: read
pull-requests: write

env:
# kcov/kcov:latest, pinned for reproducible builds
KCOV_IMAGE: kcov/kcov@sha256:481289ae32e55e5b733019515acd10948a4f76dfed381765577db909664fc603

jobs:
install-and-test:
name: 🛠️ Install & Test
Expand Down Expand Up @@ -30,6 +38,27 @@ jobs:
- name: ✅ Validate
run: yarn validate

- name: 🛠️ Build coverage tests
run: yarn coverage:build

- name: 🧪 Generate coverage
run: |
docker run --rm \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
--volume "$PWD:$PWD" \
--workdir "$PWD" \
--user "$(id -u):$(id -g)" \
--entrypoint /bin/sh \
"$KCOV_IMAGE" \
"$PWD/scripts/coverage.sh"

- name: 📊 Report coverage
if: github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/actions/report-pr
with:
coverage-file: coverage/merged/kcov-merged/cobertura.xml

- name: 🛠️ Setup package
run: yarn script:package-npm

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Thumbs.db

# Temp files
version-info.ts
coverage
dist
tmp
src/package.json
30 changes: 9 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ yarn add package-version-info --dev
### 2. Generate the file

```bash
npx package-version-info generate --output src/version-info.ts
npx package-version-info --output src/version-info.ts
```

```text
Expand Down Expand Up @@ -91,18 +91,12 @@ The `author` property is omitted when `package.json` has no object-style author

## 🧭 CLI reference

Running the CLI without arguments displays help:
Running the CLI without arguments generates `version-info.ts` from `package.json`:

```bash
npx package-version-info
```

### Commands

| Command | Description |
| --- | --- |
| `generate` | Generate the TypeScript version information file. |

### Options

| Option | Alias | Default | Description |
Expand All @@ -118,30 +112,24 @@ npx package-version-info

```bash
# Generate with default paths
npx package-version-info generate
npx package-version-info

# Show detailed progress
npx package-version-info generate --verbose
npx package-version-info --verbose

# Use custom paths
npx package-version-info generate \
npx package-version-info \
--input package.json \
--output src/generated/version-info.ts \
--git .git

# Short aliases
npx package-version-info generate \
npx package-version-info \
-i package.json \
-o src/generated/version-info.ts \
-g .git
```

Flag-only generation remains supported for existing scripts:

```bash
npx package-version-info --output src/version-info.ts
```

## 🎨 Compact and verbose logging

Compact mode is designed for normal builds:
Expand All @@ -153,7 +141,7 @@ Compact mode is designed for normal builds:
Use verbose mode when diagnosing package, author, timestamp, Git, or output-path behavior:

```bash
npx package-version-info generate --verbose
npx package-version-info --verbose
```

## 🛠️ Build-tool integration
Expand All @@ -163,7 +151,7 @@ Generate version information automatically before your application build:
```json
{
"scripts": {
"version-info": "package-version-info generate --output src/version-info.ts",
"version-info": "package-version-info --output src/version-info.ts",
"prebuild": "npm run version-info",
"build": "tsc"
}
Expand All @@ -186,7 +174,7 @@ Requirements:
zig build

# Run the CLI
zig build run -- generate
zig build run

# Run all formatting, build, and test checks
yarn validate
Expand Down
21 changes: 21 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,25 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run tests");
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
test_step.dependOn(&b.addRunArtifact(cli_tests).step);

const mod_coverage_tests = b.addTest(.{
.name = "root-coverage",
.root_module = mod,
.use_llvm = true,
});
const cli_coverage_tests = b.addTest(.{
.name = "cli-coverage",
.root_module = cli_mod,
.use_llvm = true,
});
const install_mod_coverage_tests = b.addInstallArtifact(mod_coverage_tests, .{
.dest_dir = .{ .override = .{ .custom = "coverage-tests" } },
});
const install_cli_coverage_tests = b.addInstallArtifact(cli_coverage_tests, .{
.dest_dir = .{ .override = .{ .custom = "coverage-tests" } },
});

const coverage_step = b.step("coverage", "Build test executables for code coverage");
coverage_step.dependOn(&install_mod_coverage_tests.step);
coverage_step.dependOn(&install_cli_coverage_tests.step);
}
Binary file modified docs/assets/terminal-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/terminal-demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Sleep 3s
Ctrl+L
Sleep 500ms

Type "package-version-info generate --output generated/version-info.ts"
Type "package-version-info --output generated/version-info.ts"
Enter
Sleep 2s

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"validate": "yarn format:check && yarn build && yarn test",
"format:check": "zig fmt --check src/root.zig src/main.zig src/cli.zig build.zig",
"build": "zig build",
"start": "zig build run -- generate",
"start": "zig build run",
"test": "zig build test",
"coverage:build": "rimraf coverage zig-out/coverage-tests && zig build coverage",
"clean": "rimraf zig-out dist",
"prepare": "husky",
"copy-files": "copyfiles -u 2 zig-out/bin/version_info dist/bin && copyfiles README.md LICENSE dist",
Expand Down
30 changes: 30 additions & 0 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh

set -eu

workspace=$(pwd)
coverage_dir="$workspace/coverage"
tests_dir="$workspace/zig-out/coverage-tests"

rm -rf "$coverage_dir"
mkdir -p "$coverage_dir/root" "$coverage_dir/cli" "$coverage_dir/merged"

kcov \
--include-path="$workspace/src" \
"$coverage_dir/root" \
"$tests_dir/root-coverage"

kcov \
--include-path="$workspace/src" \
"$coverage_dir/cli" \
"$tests_dir/cli-coverage"

kcov \
--merge \
"$coverage_dir/merged" \
"$coverage_dir/root" \
"$coverage_dir/cli"

sed -i \
's/ branch-rate="[^"]*" branches-covered="[^"]*" branches-rate="[^"]*"/ branch-rate="0" branches-covered="0" branches-valid="0"/' \
"$coverage_dir/merged/kcov-merged/cobertura.xml"
55 changes: 21 additions & 34 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,50 +29,39 @@ pub const Action = union(enum) {

pub fn parse(args: anytype) Action {
var options: GenerateOptions = .{};
var generate = false;

while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
return .help;
} else if (std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v")) {
return .version;
} else if (std.mem.eql(u8, arg, "generate")) {
generate = true;
} else if (std.mem.eql(u8, arg, "--verbose")) {
options.verbose = true;
generate = true;
} else if (std.mem.eql(u8, arg, "--input") or std.mem.eql(u8, arg, "-i")) {
const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } };
if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } };
options.input_path = value;
generate = true;
} else if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) {
const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } };
if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } };
options.output_path = value;
generate = true;
} else if (std.mem.eql(u8, arg, "--git") or std.mem.eql(u8, arg, "-g")) {
const value = args.next() orelse return .{ .invalid = .{ .missing_value = arg } };
if (std.mem.startsWith(u8, value, "-")) return .{ .invalid = .{ .missing_value = arg } };
options.git_path = value;
generate = true;
} else {
return .{ .invalid = .{ .unknown = arg } };
}
}

return if (generate) .{ .generate = options } else .help;
return .{ .generate = options };
}

pub fn writeHelp(writer: anytype) !void {
try writer.writeAll(
color.bright_cyan ++ "Package Version Info" ++ color.reset ++
"\n\n" ++ color.yellow ++ "Usage:" ++ color.reset ++
"\n package-version-info generate [options]" ++
"\n package-version-info [generation options]" ++
"\n\n" ++ color.yellow ++ "Commands:" ++ color.reset ++
"\n " ++ color.green ++ "generate" ++ color.reset ++
" Generate TypeScript version information." ++
"\n package-version-info [options]" ++
"\n\n" ++ color.yellow ++ "Options:" ++ color.reset ++
"\n " ++ color.green ++ "-h, --help" ++ color.reset ++
" Display usage information." ++
Expand Down Expand Up @@ -119,28 +108,25 @@ fn parseTest(args: []const []const u8) Action {
return parse(&iterator);
}

test "no arguments and help flags show help" {
try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{})));
test "no arguments generate with default paths" {
const options = parseTest(&.{}).generate;
try std.testing.expectEqualStrings("package.json", options.input_path);
try std.testing.expectEqualStrings("version-info.ts", options.output_path);
try std.testing.expectEqualStrings(".git", options.git_path);
try std.testing.expect(!options.verbose);
}

test "help flags show help" {
try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{"-h"})));
try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{"--help"})));
try std.testing.expectEqual(.help, std.meta.activeTag(parseTest(&.{ "generate", "--help" })));
}

test "version flags show version" {
try std.testing.expectEqual(.version, std.meta.activeTag(parseTest(&.{"-v"})));
try std.testing.expectEqual(.version, std.meta.activeTag(parseTest(&.{"--version"})));
}

test "generate command uses default paths" {
const action = parseTest(&.{"generate"});
const options = action.generate;
try std.testing.expectEqualStrings("package.json", options.input_path);
try std.testing.expectEqualStrings("version-info.ts", options.output_path);
try std.testing.expectEqualStrings(".git", options.git_path);
try std.testing.expect(!options.verbose);
}

test "legacy generation options remain supported" {
test "generation options are parsed" {
const action = parseTest(&.{
"--verbose",
"--input",
Expand All @@ -157,18 +143,18 @@ test "legacy generation options remain supported" {
try std.testing.expect(options.verbose);
}

test "verbose triggers generation with or without command" {
const legacy = parseTest(&.{"--verbose"}).generate;
try std.testing.expect(legacy.verbose);
try std.testing.expectEqualStrings("package.json", legacy.input_path);

const command = parseTest(&.{ "generate", "--verbose" }).generate;
try std.testing.expect(command.verbose);
test "verbose enables detailed generation progress" {
const options = parseTest(&.{"--verbose"}).generate;
try std.testing.expect(options.verbose);
try std.testing.expectEqualStrings("package.json", options.input_path);
}

test "unknown argument is invalid" {
const action = parseTest(&.{"--unknown"});
try std.testing.expectEqualStrings("--unknown", action.invalid.unknown);

const removed_command = parseTest(&.{"generate"});
try std.testing.expectEqualStrings("generate", removed_command.invalid.unknown);
}

test "options require values" {
Expand All @@ -188,8 +174,9 @@ test "help contains usage commands and options" {
try writeHelp(&output.writer);
const help = output.writer.buffered();
try std.testing.expect(std.mem.indexOf(u8, help, "Usage:") != null);
try std.testing.expect(std.mem.indexOf(u8, help, "Commands:") != null);
try std.testing.expect(std.mem.indexOf(u8, help, "Options:") != null);
try std.testing.expect(std.mem.indexOf(u8, help, "Commands:") == null);
try std.testing.expect(std.mem.indexOf(u8, help, " generate") == null);
try std.testing.expect(std.mem.indexOf(u8, help, "--help") != null);
try std.testing.expect(std.mem.indexOf(u8, help, "--verbose") != null);
}
Loading