Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83375f1ae4 | ||
|
|
fd42675d3d | ||
|
|
e3873dcdd3 | ||
|
|
e626a1003a | ||
|
|
364f44f4bf | ||
|
|
2dcc8d74d0 | ||
|
|
8bd770e343 | ||
|
|
b5b5576439 | ||
|
|
9796d599dc | ||
|
|
7aaef4d388 | ||
|
|
551d53912f |
@@ -0,0 +1,93 @@
|
||||
name: Publish to npm
|
||||
|
||||
# Deliberately manual. Pushing a tag does NOT publish — a tag is a decision to cut a release,
|
||||
# not a decision to make it public and immutable, and npm's 72-hour unpublish window makes the
|
||||
# second one hard to take back. Run this workflow from the Actions tab when you mean it.
|
||||
#
|
||||
# Before the first real run:
|
||||
# 1. Create an npm automation token and add it as the NPM_TOKEN repository secret.
|
||||
# 2. Run once with dry_run left as `true` and read the file list it prints.
|
||||
# 3. Run again with dry_run set to `false`.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Resolve and pack everything, but do not publish'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # required for npm provenance
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: ${{ inputs.dry_run && 'Dry run' || 'Publish' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Tags are not fetched at the default depth, and the version gate below
|
||||
# resolves one — without this it fails on every release, tag or no tag.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# The tag and the manifest disagreeing is the classic way to publish 0.3.0 as 0.2.0.
|
||||
- name: Tag and package.json version must agree
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "package.json version: $VERSION"
|
||||
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
|
||||
echo "tag v$VERSION exists"
|
||||
else
|
||||
echo "::error::No tag v$VERSION. Tag the release commit before publishing."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(git rev-parse HEAD)" != "$(git rev-parse "v$VERSION^{commit}")" ]; then
|
||||
echo "::error::v$VERSION does not point at the commit being published."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Refuse to republish a version already on the registry
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
NAME=$(node -p "require('./package.json').name")
|
||||
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
|
||||
echo "::error::$NAME@$VERSION is already published. Bump the version."
|
||||
exit 1
|
||||
fi
|
||||
echo "$NAME@$VERSION is not on the registry yet."
|
||||
|
||||
# The same gate that guards every push: type-check, lint, 259 tests, build, and the
|
||||
# checks that the published types stand alone and the landing page has no CDN deps.
|
||||
- name: Verify
|
||||
run: npm run verify
|
||||
|
||||
- name: Show exactly what would ship
|
||||
run: npm publish --dry-run
|
||||
|
||||
- name: Publish
|
||||
if: ${{ inputs.dry_run == false }}
|
||||
run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
{
|
||||
echo "### cereale@$VERSION"
|
||||
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||
echo "Dry run — nothing was published."
|
||||
else
|
||||
echo "Published to https://www.npmjs.com/package/cereale/v/$VERSION"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -166,6 +166,25 @@ Serialization is a few percent slower for the representability check. Primitives
|
||||
inline, and arrays and dates skip it, so it costs one `Symbol.toStringTag` read per object.
|
||||
Validation is unchanged.
|
||||
|
||||
### Packaging
|
||||
|
||||
`files` was `["dist"]`, but `dist` carries 256 KB of `.js.map` and `.d.ts.map` files whose
|
||||
`sources` point at `../../src/*.ts` — which was not published. Every shipped sourcemap
|
||||
resolved to nothing: 44% of the tarball, dead. The source is only 116 KB and its comments are
|
||||
the most detailed explanation of why the engine does what it does, so it is now published
|
||||
(tests and the demo excluded) and the maps resolve. Stepping into cereale in a debugger, and
|
||||
"go to definition" from a decorator, both land in the real TypeScript.
|
||||
|
||||
`CHANGELOG.md` ships too. The `repository`, `homepage` and `bugs` URLs said `Avalon-Vanguard`
|
||||
and only worked through GitHub's redirect; they now use the org's actual lowercase name.
|
||||
`publishConfig.access` is set explicitly so a future move to a scoped name cannot quietly
|
||||
attempt a private publish.
|
||||
|
||||
A `Publish to npm` workflow is in place but deliberately manual — pushing a tag does not
|
||||
publish. It checks that the tag exists and points at the commit being published, refuses a
|
||||
version already on the registry, runs the full `verify` gate, prints the file list, and
|
||||
defaults to a dry run. Publishing needs an `NPM_TOKEN` secret and someone choosing to run it.
|
||||
|
||||
## [0.2.0] - 2026-08-04
|
||||
|
||||
> The project stays on 0.x while nothing has been published: under semver that signals the
|
||||
|
||||
@@ -25,9 +25,10 @@ const user = fromJsonSync(User, body); // a real User
|
||||
user.greet(); // your methods are still there
|
||||
```
|
||||
|
||||
`docs/index.html` is a self-contained page with an interactive playground that runs this
|
||||
library in the browser. Build its assets with `npm run build:docs` and open the file — it
|
||||
loads nothing from the network.
|
||||
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
|
||||
interactive playground that runs this library in your browser, the full decorator reference,
|
||||
and the toolchain matrix. The page is self-contained and loads nothing from the network; it is
|
||||
served from `docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
|
||||
|
||||
## Where it fits
|
||||
|
||||
|
||||
@@ -6,8 +6,20 @@
|
||||
<title>cereale — validated domain objects, not validated data</title>
|
||||
<meta name="description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Maps JSON onto your own classes and checks the validation rules against the fields they are attached to, at compile time.">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<link rel="canonical" href="https://avalon-vanguard.github.io/cereale/">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌾</text></svg>">
|
||||
|
||||
<!-- Link previews. No og:image: a preview card with a broken image is worse than one
|
||||
without, and there is no artwork to point at yet. -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://avalon-vanguard.github.io/cereale/">
|
||||
<meta property="og:site_name" content="cereale">
|
||||
<meta property="og:title" content="cereale — validated domain objects, not validated data">
|
||||
<meta property="og:description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Maps JSON onto your own classes and checks every rule against the field it is attached to, at compile time.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="cereale — validated domain objects, not validated data">
|
||||
<meta name="twitter:description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Rules are checked against fields at compile time.">
|
||||
|
||||
<style>
|
||||
/* ---------------------------------------------------------------- tokens */
|
||||
:root {
|
||||
|
||||
Vendored
+2
@@ -3,3 +3,5 @@
|
||||
Generated by `npm run build:docs`. Do not edit by hand.
|
||||
|
||||
- `babel.min.js` — @babel/standalone 8.0.4, used by the playground to compile TypeScript with standard decorators in the browser. Vendored rather than loaded from a CDN: the previous page referenced an unpinned unpkg URL, which silently began serving Babel 8 and broke the playground.
|
||||
|
||||
- `../.nojekyll` — opts the directory out of Jekyll, so GitHub Pages serves it verbatim. Jekyll ignores paths beginning with an underscore and carries default `vendor/` exclusions, and the failure mode is an asset that silently does not publish — for this page, the playground's compiler 404ing while everything else looks fine.
|
||||
|
||||
+11
-4
@@ -23,7 +23,11 @@
|
||||
"./dist/cjs/metadata.js"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"src",
|
||||
"CHANGELOG.md",
|
||||
"!src/**/*.test.ts",
|
||||
"!src/example.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
||||
@@ -45,7 +49,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Avalon-Vanguard/cereale.git"
|
||||
"url": "git+https://github.com/avalon-vanguard/cereale.git"
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
@@ -62,9 +66,9 @@
|
||||
"author": "Avalon Vanguard",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Avalon-Vanguard/cereale/issues"
|
||||
"url": "https://github.com/avalon-vanguard/cereale/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Avalon-Vanguard/cereale#readme",
|
||||
"homepage": "https://avalon-vanguard.github.io/cereale/",
|
||||
"devDependencies": {
|
||||
"@babel/standalone": "^8.0.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -78,5 +82,8 @@
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,16 @@ await writeFile(
|
||||
`- \`babel.min.js\` — @babel/standalone ${JSON.parse(await readFile(path.join(root, 'node_modules/@babel/standalone/package.json'), 'utf8')).version}, ` +
|
||||
`used by the playground to compile TypeScript with standard decorators in the browser. ` +
|
||||
`Vendored rather than loaded from a CDN: the previous page referenced an unpinned unpkg URL, ` +
|
||||
`which silently began serving Babel 8 and broke the playground.\n`
|
||||
`which silently began serving Babel 8 and broke the playground.\n\n` +
|
||||
`- \`../.nojekyll\` — opts the directory out of Jekyll, so GitHub Pages serves it verbatim. ` +
|
||||
`Jekyll ignores paths beginning with an underscore and carries default \`vendor/\` exclusions, ` +
|
||||
`and the failure mode is an asset that silently does not publish — for this page, the ` +
|
||||
`playground's compiler 404ing while everything else looks fine.\n`
|
||||
);
|
||||
|
||||
// 5. Written rather than committed by hand so it cannot be lost in a docs/ rewrite.
|
||||
await writeFile(path.join(docs, '.nojekyll'), '');
|
||||
|
||||
console.log(`docs/cereale.js ${await size(path.join(docs, 'cereale.js'))}`);
|
||||
console.log(`docs/meta.js ${await size(path.join(docs, 'meta.js'))} (v${pkg.version})`);
|
||||
console.log(`docs/vendor/babel.min.js ${await size(path.join(vendor, 'babel.min.js'))}`);
|
||||
|
||||
+21
-1
@@ -22,12 +22,23 @@ const failures = [];
|
||||
/** Subresource references — the things a browser fetches without being clicked. */
|
||||
const SUBRESOURCES = [
|
||||
[/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'script src'],
|
||||
[/<link\b[^>]*\bhref\s*=\s*["']([^"']+)["']/gi, 'link href'],
|
||||
[/<(?:img|iframe|video|audio|source|embed)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'media src'],
|
||||
[/@import\s+(?:url\()?["']([^"']+)["']/gi, 'css @import'],
|
||||
[/url\(\s*["']?(https?:\/\/[^)"']+)/gi, 'css url()'],
|
||||
];
|
||||
|
||||
/**
|
||||
* `<link>` relations the browser actually fetches or connects to.
|
||||
*
|
||||
* Checked against `rel` rather than flagging every `<link href>`, because the metadata
|
||||
* relations — `canonical` above all — are declarations about the document, not requests. A
|
||||
* check that cannot tell the difference gets switched off the first time it is wrong.
|
||||
*/
|
||||
const FETCHING_REL = new Set([
|
||||
'stylesheet', 'icon', 'shortcut icon', 'apple-touch-icon', 'apple-touch-icon-precomposed',
|
||||
'manifest', 'preload', 'modulepreload', 'prefetch', 'prerender', 'preconnect', 'dns-prefetch',
|
||||
]);
|
||||
|
||||
const isRemote = (url) => /^(?:https?:)?\/\//i.test(url);
|
||||
|
||||
const html = (await readdir(docs)).filter((name) => name.endsWith('.html'));
|
||||
@@ -40,6 +51,15 @@ for (const name of html) {
|
||||
if (isRemote(match[1])) failures.push(`docs/${name}: remote ${kind} — ${match[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(/<link\b([^>]*)>/gi)) {
|
||||
const attrs = match[1];
|
||||
const rel = (/\brel\s*=\s*["']([^"']+)["']/i.exec(attrs)?.[1] ?? '').trim().toLowerCase();
|
||||
const href = /\bhref\s*=\s*["']([^"']+)["']/i.exec(attrs)?.[1];
|
||||
if (href && isRemote(href) && FETCHING_REL.has(rel)) {
|
||||
failures.push(`docs/${name}: remote link rel="${rel}" — ${href}`);
|
||||
}
|
||||
}
|
||||
// A fetch to a CDN would not be caught by the markup scan.
|
||||
for (const match of source.matchAll(/\b(?:fetch|importScripts)\(\s*["'`](https?:\/\/[^"'`]+)/gi)) {
|
||||
failures.push(`docs/${name}: remote fetch — ${match[1]}`);
|
||||
|
||||
Reference in New Issue
Block a user