Skip to content
Closed
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
107 changes: 89 additions & 18 deletions .github/scripts/sync-package-readmes.mjs
Original file line number Diff line number Diff line change
@@ -1,40 +1,111 @@
import { copyFileSync } from "node:fs";
/**
* Syncs the SHARED blocks of the root README into each package README.
*
* Each package README is a real, hand-written, committed file — what you read
* in the repo is what publishes to npm. This script only refreshes the regions
* delimited by:
*
* <!-- polycss:shared:<name>:start --> … <!-- polycss:shared:<name>:end -->
*
* Everything between those blocks is package-specific and never touched. A
* package opts in per block simply by containing the matching markers; a
* package with no markers (or a subset) is left alone accordingly.
*
* Runs as `prepack` in every publishable package, so a stale block can never
* reach npm. Run with `--check` in CI to fail on drift instead of writing.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
const source = resolve(repoRoot, "README.md");

const targets = [
"packages/core/README.md",
"packages/polycss/README.md",
"packages/react/README.md",
"packages/vue/README.md",
];
const packageSpecificTargets = [
"packages/fonts/README.md",
"packages/morph/README.md",
];

const invokedFrom = relative(repoRoot, process.cwd());
const invokedFromPackageReadme = invokedFrom.startsWith("packages/")
? `${invokedFrom}/README.md`
: undefined;
const checkOnly = process.argv.includes("--check");

if (
invokedFromPackageReadme !== undefined
&& packageSpecificTargets.includes(invokedFromPackageReadme)
) {
console.log(`[sync-package-readmes] preserved ${invokedFromPackageReadme}`);
process.exit(0);
const blockRe = (name) =>
new RegExp(
`<!-- polycss:shared:${name}:start -->[\\s\\S]*?<!-- polycss:shared:${name}:end -->`,
);

/** Every block name the root README publishes, in document order. */
function sharedBlockNames(text) {
return [...text.matchAll(/<!-- polycss:shared:([a-z-]+):start -->/g)].map(
(m) => m[1],
);
}

if (invokedFromPackageReadme !== undefined && !targets.includes(invokedFromPackageReadme)) {
console.log(`[sync-package-readmes] skipped for ${invokedFromPackageReadme}`);
process.exit(0);
const rootText = readFileSync(source, "utf8");
const names = sharedBlockNames(rootText);

if (names.length === 0) {
console.error(
"[sync-package-readmes] no shared blocks found in the root README — refusing to run",
);
process.exit(1);
}

const blocks = new Map();
for (const name of names) {
const match = rootText.match(blockRe(name));
if (!match) {
console.error(
`[sync-package-readmes] block "${name}" has a start marker but no end marker`,
);
process.exit(1);
}
blocks.set(name, match[0]);
}

const drifted = [];
let updated = 0;

for (const target of targets) {
copyFileSync(source, resolve(repoRoot, target));
const path = resolve(repoRoot, target);
let text;
try {
text = readFileSync(path, "utf8");
} catch {
continue;
}

let next = text;
for (const [name, block] of blocks) {
const re = blockRe(name);
if (re.test(next)) next = next.replace(re, block);
}

if (next === text) continue;
if (checkOnly) {
drifted.push(target);
continue;
}
writeFileSync(path, next);
updated += 1;
console.log(`[sync-package-readmes] updated ${relative(repoRoot, path)}`);
}

if (checkOnly) {
if (drifted.length > 0) {
console.error(
`[sync-package-readmes] shared blocks are stale in:\n ${drifted.join("\n ")}\n` +
"Edit the block in the root README, then run `pnpm sync:readmes`.",
);
process.exit(1);
}
console.log("[sync-package-readmes] shared blocks are up to date");
process.exit(0);
}

console.log(`[sync-package-readmes] copied README.md to ${targets.length} package READMEs`);
console.log(
`[sync-package-readmes] ${updated} README${updated === 1 ? "" : "s"} updated, ${names.length} shared block${names.length === 1 ? "" : "s"}`,
);
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check README shared blocks
run: pnpm check:readmes

- name: Run tests
run: pnpm test

Expand Down
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Before opening a PR:
- [ ] If I touched the canvas atlas pipeline (`rasterise.ts` / `buildAtlasPages.ts`), browser-feature detection, or direct voxel renderer in ONE renderer, the same fix lands in the other two renderers (`polycss` + react + vue) in this PR.
- [ ] If I touched any of the three `styles.ts` (`packages/polycss/src/styles/styles.ts`, `packages/react/src/styles/styles.ts`, `packages/vue/src/styles/styles.ts`), the other two are consistent — CSS rules cover every emitted tag for both lighting modes, and shared properties like `will-change: transform` on `.polycss-scene` exist in all three.
- [ ] Website docs (`website/src/content/docs/**`) and READMEs reflect any user-visible change.
- [ ] If I edited a `<!-- polycss:shared:* -->` block, I edited it in the ROOT `README.md` and ran `pnpm sync:readmes` (see "Package READMEs" below).
- [ ] If I changed a render strategy, lighting mode, naming convention, or the JS-in-render-loop rules, `AGENTS.md` reflects the new state in this same PR.

## Iterating on the system
Expand All @@ -200,6 +201,29 @@ The rendering model, tag table, lighting modes, and naming conventions described
- **Same-PR sync.** Any PR that adds, removes, or materially changes a render strategy, lighting mode, naming rule, or cross-package contract must update `AGENTS.md` in the same PR. An API change that lands without an AGENTS.md update is an incomplete change.
- **Don't append-only.** Prune content that no longer reflects the codebase. If a strategy is dropped, remove its row from the tag table — don't leave a "deprecated" note. If a hook is renamed, update the naming section in place — don't list the old name "for reference".

## Package READMEs

Each `packages/*/README.md` is a real, hand-written, committed file that is
published to npm **as-is**. What you read in the repo is what ships — there is
no generated README.

Regions wrapped in `<!-- polycss:shared:<name>:start -->` /
`<!-- polycss:shared:<name>:end -->` are the exception: they are owned by the
root `README.md` and mirrored into every package README that contains the
matching markers. Current blocks are `links`, `packages`, `showcase`, and
`license`.

- **Edit a shared block in the root `README.md`, never in a package README.**
Then run `pnpm sync:readmes`.
- Everything outside the markers is package-specific. Write it in the root
README's voice, but say what that package actually does — `core` documents
core, `vue` shows Vue code.
- `.github/scripts/sync-package-readmes.mjs` runs as `prepack` in every
publishable package, so a stale shared block cannot reach npm.
- CI runs `pnpm check:readmes`, which fails on drift instead of writing.
- A package opts in per block simply by containing the markers. `fonts` and
`morph` carry none today and are left entirely alone.

## Backward compatibility

- **No BC shims.** Clean breaks only. No re-export aliases for renamed symbols. No `@deprecated` wrappers. If the API changes, callers update.
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

A CSS polygon mesh library. A 3D engine for the DOM. Renders OBJ/MTL, STL, glTF/GLB, and VOX as real HTML elements transformed with CSS `matrix3d(...)`. Supports colors, textures, lighting, shadows, shapes and animations. Works with React, Vue or plain JavaScript.

<!-- polycss:shared:links:start -->
Visit [polycss.com](https://polycss.com) for docs and model examples.

Join [chat.polycss.com](https://chat,polycss.com) for support and community discussions.
Join [chat.polycss.com](https://chat.polycss.com) for support and community discussions.

<img width="1600" height="300" alt="PolyCSS primitives banner" src="https://github.com/user-attachments/assets/b05e2204-9323-4f83-8d1b-01ea0dd000db" />
<!-- polycss:shared:links:end -->

## Installation

Expand Down Expand Up @@ -233,6 +235,7 @@ Each visible polygon is emitted as one leaf element; the renderer chooses the le
- `<i>` clips solid polygons with `border-shape: polygon(...)` when the browser supports it.
- `<s>` maps a packed texture-atlas slice with `background-image`, and is the fallback for textured or unsupported shapes.

<!-- polycss:shared:packages:start -->
## Packages

| Package | Description |
Expand All @@ -241,8 +244,11 @@ Each visible polygon is emitted as one leaf element; the renderer chooses the le
| `@layoutit/polycss` | Vanilla custom elements and imperative `createPolyScene` API. |
| `@layoutit/polycss-react` | React components, hooks, controls, and core re-exports. |
| `@layoutit/polycss-vue` | Vue 3 components, composables, controls, and core re-exports. |
| `@layoutit/polycss-fonts` | Fonts + text → extruded 3D `Polygon[]`. Framework-agnostic. |
| `@layoutit/polycss-morph` | Prepared-model loading, retained DOM animation, morph targets, skinning, and playback. |
<!-- polycss:shared:packages:end -->

<!-- polycss:shared:showcase:start -->
## Made with PolyCSS

[cssQuake](https://cssquake.com)
Expand All @@ -255,7 +261,10 @@ Each visible polygon is emitted as one leaf element; the renderer chooses the le
-> A CSS Terrain Generator

<img width="1000" height="601" alt="layoutit-terra" src="https://polycss.com/layoutit-terra.png" />
<!-- polycss:shared:showcase:end -->

<!-- polycss:shared:license:start -->
## License

MIT.
<!-- polycss:shared:license:end -->
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test": "pnpm --filter './packages/*' -r --if-present test",
"test:coverage": "pnpm --filter './packages/*' -r --if-present test:coverage",
"sync:readmes": "node .github/scripts/sync-package-readmes.mjs",
"check:readmes": "node .github/scripts/sync-package-readmes.mjs --check",
"publish:all": "pnpm sync:readmes && pnpm --filter './packages/*' -r publish --access public",
"dev:website": "pnpm --filter @layoutit/polycss-website dev",
"build:website": "pnpm --filter @layoutit/polycss-website build",
Expand Down
Loading
Loading