Skip to content
Open
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: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs`
7337a36f999567a0c4843ad1a7c5afc9704b82f3 # prettier, 7 files
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
node-version: "22"
- run: npm ci --ignore-scripts
# build + the full test suite, identical to what runs locally
- run: npm run verify
8 changes: 4 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ name: Publish
on:
push:
branches: [main]
tags: ['v*']
tags: ["v*"]
# The reconciler, and the reason this is reliable. A merge made by auto-merge
# uses GITHUB_TOKEN, and a push with that token starts no workflow — so the
# push trigger above silently does not fire for exactly the merges that matter.
# The schedule asks the registry the same idempotent question on a timer: is
# package.json's version published? If a release was missed by any means, it
# goes out within the hour without anyone noticing it was missed.
schedule:
- cron: '29 * * * *'
- cron: "29 * * * *"
workflow_dispatch:

jobs:
Expand All @@ -37,8 +37,8 @@ jobs:
- uses: actions/setup-node@v4
with:
# npm >= 11.5.1 is required for trusted publishing (OIDC); Node 24 ships it.
node-version: '24'
registry-url: 'https://registry.npmjs.org'
node-version: "24"
registry-url: "https://registry.npmjs.org"

- name: Is this version already on the registry?
id: check
Expand Down
21 changes: 21 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Build output and vendored trees — formatting these is noise.
node_modules
.next
dist
build
out
coverage
.turbo
.vercel
*.min.js
*.min.css

# Lockfiles are generated; prettier would rewrite them wholesale.
package-lock.json
pnpm-lock.yaml
yarn.lock

# Markdown is deliberately out of scope for now. Prettier rewraps prose, which
# is where it is most opinionated and least useful, and it would bury the real
# diff. Remove this line when you want docs formatted too.
*.md
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "always",
"endOfLine": "lf"
}
14 changes: 7 additions & 7 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@
// library code, and a bespoke rule set would be a second opinion to maintain
// for no benefit. The floor is "lint runs and can fail", not "lint encodes
// taste".
import js from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";

export default tseslint.config(
{
// dist/ is generated by `tsc`; templates/ is copy-paste starter code that
// lives in the consumer's app, not in this package's build.
ignores: ['dist/**', 'node_modules/**', 'templates/**'],
ignores: ["dist/**", "node_modules/**", "templates/**"],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts'],
files: ["**/*.ts"],
languageOptions: { globals: globals.node },
},
{
// Tests are plain Node running under `node --test`.
files: ['test/**/*.js'],
files: ["test/**/*.js"],
languageOptions: { globals: { ...globals.node, ...globals.nodeBuiltin } },
},
)
);
19 changes: 18 additions & 1 deletion package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,17 @@
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test test/*.test.js",
"verify": "npm run lint && npm run typecheck && npm run build && npm test",
"prepare": "npm run build"
"verify": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm test",
"prepare": "npm run build",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"devDependencies": {
"@eslint/js": "^9.39.5",
"@types/node": "^22.10.2",
"eslint": "^9.39.5",
"globals": "^15.15.0",
"prettier": "^3.3.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.67.0"
}
Expand Down
4 changes: 1 addition & 3 deletions src/video-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ const YOUTUBE =
/^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,})/;
const VIMEO = /^(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/;

export function parseVideoEmbed(
url: string,
): { provider: "youtube" | "vimeo"; id: string } | null {
export function parseVideoEmbed(url: string): { provider: "youtube" | "vimeo"; id: string } | null {
const trimmed = url.trim();
const yt = YOUTUBE.exec(trimmed);
if (yt) return { provider: "youtube", id: yt[1] };
Expand Down
73 changes: 34 additions & 39 deletions test/package.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { test, before } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test, before } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

/**
* Consume the package the way npm does.
Expand All @@ -19,84 +19,79 @@ import { join } from 'node:path';
* it by bare specifier from outside the repo.
*/

const PUBLIC_API = [
'parseContentBlocks',
'parseFrontmatter',
'parseVideoEmbed',
'videoEmbedSrc',
];
const PUBLIC_API = ["parseContentBlocks", "parseFrontmatter", "parseVideoEmbed", "videoEmbedSrc"];

let workspace;
let installed;
let probe;

before(() => {
workspace = mkdtempSync(join(tmpdir(), 'bip-kit-pack-'));
installed = join(workspace, 'node_modules', 'bip-kit');
workspace = mkdtempSync(join(tmpdir(), "bip-kit-pack-"));
installed = join(workspace, "node_modules", "bip-kit");
mkdirSync(installed, { recursive: true });

execFileSync('npm', ['pack', '--silent', '--pack-destination', workspace], {
execFileSync("npm", ["pack", "--silent", "--pack-destination", workspace], {
cwd: process.cwd(),
stdio: ['ignore', 'ignore', 'inherit'],
stdio: ["ignore", "ignore", "inherit"],
});
const tarball = readdirSync(workspace).find(f => f.endsWith('.tgz'));
assert.ok(tarball, 'npm pack produced no tarball');
const tarball = readdirSync(workspace).find((f) => f.endsWith(".tgz"));
assert.ok(tarball, "npm pack produced no tarball");

execFileSync('tar', ['-xzf', join(workspace, tarball), '-C', installed, '--strip-components=1']);
execFileSync("tar", ["-xzf", join(workspace, tarball), "-C", installed, "--strip-components=1"]);

// Each probe records failure as a *value*, never a throw. A broken exports map
// that crashes this hook would fail every assertion in the file at once and
// bury which entry point actually broke.
writeFileSync(
join(workspace, 'probe.mjs'),
join(workspace, "probe.mjs"),
[
'const out = {};',
"const out = {};",
'try { out.resolved = import.meta.resolve("bip-kit"); } catch { out.resolved = null; }',
'try { out.exports = Object.keys(await import("bip-kit")).sort(); } catch { out.exports = null; }',
'console.log(JSON.stringify(out));',
].join('\n')
"console.log(JSON.stringify(out));",
].join("\n"),
);

probe = JSON.parse(
execFileSync('node', [join(workspace, 'probe.mjs')], {
execFileSync("node", [join(workspace, "probe.mjs")], {
cwd: workspace,
encoding: 'utf8',
})
encoding: "utf8",
}),
);
});

test('the package resolves from a consumer install', () => {
test("the package resolves from a consumer install", () => {
assert.ok(probe.resolved, '"bip-kit" did not resolve through its own exports map');
});

test('the entry point exposes its whole public API', () => {
test("the entry point exposes its whole public API", () => {
assert.ok(probe.exports, 'importing "bip-kit" from a consumer install threw');
for (const name of PUBLIC_API) {
assert.ok(probe.exports.includes(name), `"${name}" is missing from the published entry point`);
}
});

test('the type declarations it advertises are actually in the tarball', () => {
test("the type declarations it advertises are actually in the tarball", () => {
const pkg = JSON.parse(
execFileSync('node', ['-p', 'JSON.stringify(require("./package.json"))'], {
execFileSync("node", ["-p", 'JSON.stringify(require("./package.json"))'], {
cwd: installed,
encoding: 'utf8',
})
encoding: "utf8",
}),
);
const types = pkg.exports['.'].types;
const types = pkg.exports["."].types;
assert.ok(
existsSync(join(installed, types)),
`the package advertises types at ${types}, which is not in the tarball`
`the package advertises types at ${types}, which is not in the tarball`,
);
});

test('the tarball carries the documentation npm will render', () => {
for (const file of ['README.md', 'LICENSE']) {
test("the tarball carries the documentation npm will render", () => {
for (const file of ["README.md", "LICENSE"]) {
assert.ok(existsSync(join(installed, file)), `${file} is missing from the tarball`);
}
});

test('the tarball ships built output, not raw TypeScript sources', () => {
assert.ok(existsSync(join(installed, 'dist', 'index.js')), 'dist/index.js missing');
assert.ok(!existsSync(join(installed, 'src')), 'src/ leaked into the tarball');
test("the tarball ships built output, not raw TypeScript sources", () => {
assert.ok(existsSync(join(installed, "dist", "index.js")), "dist/index.js missing");
assert.ok(!existsSync(join(installed, "src")), "src/ leaked into the tarball");
});
Loading
Loading