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.
|
inline, and arrays and dates skip it, so it costs one `Symbol.toStringTag` read per object.
|
||||||
Validation is unchanged.
|
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
|
## [0.2.0] - 2026-08-04
|
||||||
|
|
||||||
> The project stays on 0.x while nothing has been published: under semver that signals the
|
> 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
|
user.greet(); // your methods are still there
|
||||||
```
|
```
|
||||||
|
|
||||||
`docs/index.html` is a self-contained page with an interactive playground that runs this
|
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
|
||||||
library in the browser. Build its assets with `npm run build:docs` and open the file — it
|
interactive playground that runs this library in your browser, the full decorator reference,
|
||||||
loads nothing from the network.
|
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
|
## Where it fits
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,20 @@
|
|||||||
<title>cereale — validated domain objects, not validated data</title>
|
<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="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">
|
<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 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>
|
<style>
|
||||||
/* ---------------------------------------------------------------- tokens */
|
/* ---------------------------------------------------------------- tokens */
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Vendored
+2
@@ -3,3 +3,5 @@
|
|||||||
Generated by `npm run build:docs`. Do not edit by hand.
|
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.
|
- `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"
|
"./dist/cjs/metadata.js"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"src",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"!src/**/*.test.ts",
|
||||||
|
"!src/example.ts"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Avalon-Vanguard/cereale.git"
|
"url": "git+https://github.com/avalon-vanguard/cereale.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"json",
|
"json",
|
||||||
@@ -62,9 +66,9 @@
|
|||||||
"author": "Avalon Vanguard",
|
"author": "Avalon Vanguard",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/standalone": "^8.0.4",
|
"@babel/standalone": "^8.0.4",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -78,5 +82,8 @@
|
|||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.2",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vitest": "^4.1.4"
|
"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}, ` +
|
`- \`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. ` +
|
`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, ` +
|
`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/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/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'))}`);
|
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. */
|
/** Subresource references — the things a browser fetches without being clicked. */
|
||||||
const SUBRESOURCES = [
|
const SUBRESOURCES = [
|
||||||
[/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'script src'],
|
[/<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'],
|
[/<(?:img|iframe|video|audio|source|embed)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi, 'media src'],
|
||||||
[/@import\s+(?:url\()?["']([^"']+)["']/gi, 'css @import'],
|
[/@import\s+(?:url\()?["']([^"']+)["']/gi, 'css @import'],
|
||||||
[/url\(\s*["']?(https?:\/\/[^)"']+)/gi, 'css url()'],
|
[/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 isRemote = (url) => /^(?:https?:)?\/\//i.test(url);
|
||||||
|
|
||||||
const html = (await readdir(docs)).filter((name) => name.endsWith('.html'));
|
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]}`);
|
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.
|
// 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)) {
|
for (const match of source.matchAll(/\b(?:fetch|importScripts)\(\s*["'`](https?:\/\/[^"'`]+)/gi)) {
|
||||||
failures.push(`docs/${name}: remote fetch — ${match[1]}`);
|
failures.push(`docs/${name}: remote fetch — ${match[1]}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user