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
160 changes: 160 additions & 0 deletions .claude/skills/test-assemble-lite/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
---
name: test-assemble-lite
description: >-
Verify local (uncommitted) assemble-lite changes by running a real Living
Styleguide (LSG) build against a consumer project. Use when testing or verifying
assemble-lite dependency bumps or changes to its render/glob/data/helper code
before publishing. assemble-lite has no bin and is consumed transitively via
@pro-vision/pv-stylemark, so the test injects the local package into the
consumer's node_modules and runs `pv-stylemark prod`. The skill ALWAYS asks which
consumer project to build — no project is hardcoded.
---

# Test assemble-lite against a real consumer project

Verifies the **local, possibly uncommitted** `packages/assemble-lite` by running an
actual LSG build that renders Handlebars through it. There is no unit-test suite in
this repo, so this is the documented way to validate assemble-lite changes.

`assemble-lite` is **not a CLI** — it is a library required by `@pro-vision/pv-stylemark`
(which renders clickdummies + the Stylemark LSG with it). So unlike `pv-scripts`
(see the `test-pv-scripts` skill, which runs a local bin via the direct-bin method),
assemble-lite can only be exercised by making a consumer's `pv-stylemark` use the
local copy. The reliable way to do that is to **inject** the local package into the
consumer's `node_modules`, build, then **restore** the original.

> The same inject-and-restore method works for any monorepo library consumed
> transitively (e.g. the external-vs-local distinction aside, `handlebars-helpers`),
> not just assemble-lite.

## Step 0 — Make sure the local package's deps are installed

The injection copies the local package **including its own `node_modules`** (see
why in Step 2). So those nested deps must exist and be current first:

```sh
cd "$(git rev-parse --show-toplevel)"
npm run bootstrap # lerna bootstrap — installs each package's deps + symlinks local cross-deps
# (or, narrower: cd packages/assemble-lite && npm install)
```

Sanity-check the new versions actually landed in the package's nested tree:

```sh
REPO="$(git rev-parse --show-toplevel)"
for d in glob js-yaml fs-extra handlebars yaml-front-matter; do
echo "$d: $(node -e "console.log(require('$REPO/packages/assemble-lite/node_modules/$d/package.json').version)")"
done
```

## Step 1 — Determine the consumer project (ALWAYS ask)

Never assume a specific project. If the user did not pass a project path, ask, e.g.:

> "Gegen welches Consumer-Projekt soll ich assemble-lite testen? (absoluter Pfad zum
> Frontend-Ordner, der `@pro-vision/pv-stylemark` nutzt und eine `pv.config.js` mit
> LSG-Konfiguration enthält)"

Accept an absolute path to the project's frontend directory. The project must:
- be **installed** already (`node_modules` present),
- depend on `@pro-vision/pv-stylemark`,
- have a `pv.config.js` with LSG config (`cdTemplatesSrc`, `cdPagesSrc`, `hbsHelperSrc`,
`lsgConfigPath`, `lsgIndex`, `destPath`).

Validate and locate the consumer's assemble-lite copy (usually a single hoisted copy
at the top level):

```sh
PROJ="<absolute path the user gave>"
test -f "$PROJ/pv.config.js" || echo "WARN: no pv.config.js"
node -e "console.log('pv-stylemark:', require('$PROJ/node_modules/@pro-vision/pv-stylemark/package.json').version)"
find "$PROJ/node_modules" -type d -name assemble-lite # note: should be ONE hoisted dir
```

Note the `destPath` from `pv.config.js` (default `target`) — that is where assembled
HTML lands.

## Step 2 — Inject the local assemble-lite (with backup)

Replace the consumer's installed assemble-lite with the **whole local package
directory, including its nested `node_modules`**, and keep a backup to restore later.

```sh
REPO="$(git rev-parse --show-toplevel)"
DEST="$PROJ/node_modules/@pro-vision/assemble-lite"
BAK="$PROJ/node_modules/@pro-vision/.assemble-lite.bak-claude"

if [ -e "$BAK" ]; then echo "ABORT: stale backup at $BAK — investigate"; exit 1; fi
mv "$DEST" "$BAK"
cp -R "$REPO/packages/assemble-lite" "$DEST"

# verify the injected copy carries the NEW code + NEW nested deps
node -e "console.log('nested glob:', require('$DEST/node_modules/glob/package.json').version, '| js-yaml:', require('$DEST/node_modules/js-yaml/package.json').version)"
```

**Why copy the whole dir incl. `node_modules`, not just the source files:** the
consumer hoists assemble-lite's *old* deps (e.g. `glob@7`) to its top-level
`node_modules`. New code (e.g. the `glob@9+` promise API) needs the *new* deps.
Copying the self-contained local package makes its nested `node_modules/glob@13`
**shadow** the hoisted old one during Node resolution. Copying only `helper/*.js`
would run new code against the old hoisted `glob@7` and break.

**Why not the direct-bin method (as in test-pv-scripts):** assemble-lite is a library,
not a bin — there is no `cwd`-resolved entry point to run. `npm link` is also
unreliable here (splits the dep tree, phantom-dep failures).

## Step 3 — Run the LSG build

`pv-stylemark prod` is **standalone**: it assembles clickdummy components + pages and
builds the DDS/LSG, all through assemble-lite. It does **not** need the webpack /
`pv-scripts` asset build to run, so it isolates the assemble-lite change.

```sh
LOG="$(mktemp -t altest).log"
BIN="$PROJ/node_modules/@pro-vision/pv-stylemark/bin/pv-stylemark.js"
cd "$PROJ"
node "$BIN" prod > "$LOG" 2>&1
echo "BUILD EXIT: $?"
tail -30 "$LOG"
```

Run on **Node ≥ 22** (glob 13 `engines`: `18 || 20 || >=22`; Node 24 is fine — a
`punycode` DeprecationWarning is benign).

## Step 4 — Evaluate the result

A pass = **exit 0**, no error markers, a non-trivial count of assembled HTML, and no
unresolved templates leaking into the output.

```sh
grep -iE "error|cannot find|is not a function|MODULE_NOT_FOUND|TypeError|failed" "$LOG" \
| grep -viE "punycode|DeprecationWarning" || echo " (no error markers)"

find "$PROJ/target" -type f -name "*.html" | wc -l # expect hundreds in a real project
# leftover Handlebars / missing-helper leaks (expect 0):
grep -rlE "Missing helper|\{\{[#/]?pv-|\{\{> " "$PROJ/target" 2>/dev/null | wc -l
```

Distinguish failure causes — report which it is:
- **assemble-lite regression** (what we care about): a `require` error / `is not a
function` from `glob`/`js-yaml`/`fs-extra`, `MODULE_NOT_FOUND` for one of
assemble-lite's own deps, or templates left unrendered (`{{…}}` / `Missing helper`
in the output) where the published version rendered them.
- **Consumer-content issue** (NOT an assemble-lite regression): broken Handlebars in
the project's *own* templates/data — these fail with the published assemble-lite
too. If unsure, restore (Step 5) and rebuild with the project's installed copy to
get a baseline, then compare.

## Step 5 — Restore (always)

```sh
DEST="$PROJ/node_modules/@pro-vision/assemble-lite"
BAK="$PROJ/node_modules/@pro-vision/.assemble-lite.bak-claude"
[ -e "$BAK" ] || { echo "ABORT: backup missing"; exit 1; }
rm -rf "$DEST" && mv "$BAK" "$DEST"
node -e "console.log('restored deps glob:', require('$DEST/package.json').dependencies.glob)" # expect the published version
```

The build leaves the consumer's `destPath` (`target/`) populated with test artifacts;
the project's own build scripts regenerate it (`rimraf target`), so leaving it is
fine — just mention it. Do not commit anything in the consumer project.
103 changes: 103 additions & 0 deletions .claude/skills/test-pv-scripts/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
name: test-pv-scripts
description: >-
Verify local (uncommitted) pv-scripts changes by running a real build against a
consumer project that uses pv-scripts. Use when testing or verifying pv-scripts
dependency bumps, webpack-config/task changes, or any change under
packages/pv-scripts before publishing. The skill ALWAYS asks which consumer
project to build — no project is hardcoded.
---

# Test pv-scripts against a real consumer project

Verifies the **local, possibly uncommitted** `packages/pv-scripts` against a real
consumer project by running an actual webpack build with it. This is the documented
way to validate pv-scripts changes (there is no unit-test suite in this repo).

## Step 1 — Determine the consumer project (ALWAYS ask)

Never assume a specific project. If the user did not pass a project path as an
argument, ask them for it, e.g. with `AskUserQuestion` (header `Projekt`) or a
plain question:

> "Gegen welches Consumer-Projekt soll ich pv-scripts testen? (absoluter Pfad zum
> Frontend-Ordner, der eine `pv.config.js` enthält)"

Accept an absolute path to the project's frontend directory (the folder containing
`pv.config.js` and a `package.json` that depends on `@pro-vision/pv-scripts`).

Then validate the target:

```sh
PROJ="<absolute path the user gave>"
test -f "$PROJ/pv.config.js" || echo "WARN: no pv.config.js — is this a pv-scripts project?"
node -e "const p=require('$PROJ/package.json'); console.log('pv-scripts:', (p.devDependencies||{})['@pro-vision/pv-scripts'] || (p.dependencies||{})['@pro-vision/pv-scripts'])"
```

The project must be **installed** already (`node_modules` present). Read its
`pv.config.js` `destPath` (default `target`) and note any custom build scripts in
its `package.json` (`build:prod`, `build:clientlibs`, …).

## Step 2 — Run the build with the LOCAL pv-scripts (direct-bin method)

Run the local pv-scripts bin directly with the consumer project as the working
directory. pv-scripts resolves the app path from `process.cwd()`, so this runs the
**local/updated** pv-scripts code while leaving the consumer's `node_modules`
untouched:

```sh
REPO="$(git rev-parse --show-toplevel)" # run from inside the fe-tools repo
BIN="$REPO/packages/pv-scripts/bin/pv-scripts.js"
LOG="$(mktemp -t pvtest).log"

cd "$PROJ"
rm -rf target # use the project's destPath if customized
node "$BIN" prod > "$LOG" 2>&1
echo "BUILD EXIT: $?"
```

Notes:
- Use `prod` for a clean one-shot build. For AEM clientlib parity, mirror the
project's own script env, e.g. `AEM_BUILD='true' PUBLIC_PATH='/etc.clientlibs/.../' node "$BIN" prod`.
- Run on **Node ≥ 22.11** (pv-scripts' current `engines` floor; some deps require it).
- First-run baseline: if unsure whether a failure is pre-existing, also build once
with the project's *installed* pv-scripts (`cd "$PROJ" && npm run build:prod`) and
compare.

## Step 3 — Evaluate the result

```sh
grep -iE "error|failed to compile|ERROR in|Module not found|Cannot find module|TS[0-9]{3,}" "$LOG" | head -20
tail -25 "$LOG"
find target -type f | wc -l # expect a non-trivial artifact count
```

A pass = **exit code 0**, no error markers, and artifacts produced in `destPath`.

Distinguish failure causes — report which it is:
- **pv-scripts regression** (what we care about): a `Module not found` for a loader/
plugin pv-scripts owns, a webpack-config crash, a broken loader option, etc.
- **Project-code issue** (NOT a pv-scripts regression): SASS deprecation warnings
(e.g. `darken()`, `percentage()`), TypeScript errors in the project's own source,
missing ambient declarations (`TS2882` for `import "./x.scss"`), etc. These are
the consumer's to fix.

## Why direct-bin and NOT `npm link`

`npm link` is unreliable for this: it symlinks pv-scripts to a path **outside** the
consumer's tree, so npm no longer hoists pv-scripts' transitive deps into the
consumer's `node_modules`. Any module the consumer's own `webpack.config.js`
requires but does not declare itself (a **phantom dependency**, commonly
`mini-css-extract-plugin`, `webpack`) then fails with `MODULE_NOT_FOUND` — a
test-setup artifact, not a real bug. It also splits loaders (from the pv-scripts
tree) and plugins (from the consumer tree) across two module copies. Direct-bin
keeps the consumer install intact and avoids all of this.

If you must use `npm link` anyway, first ensure the consumer declares every module
its own webpack config `require()`s as a real `devDependency`.

## Cleanup

The build writes to the consumer's `destPath` (`target/`), which the project's own
build scripts regenerate — leaving it built is fine; mention it. Do not modify the
consumer's `package.json` or `node_modules`.
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [22.x, 24.x]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.19.0
v22.23.1
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

`pv-fe-tools` is a **Lerna monorepo** (independent versioning) of front-end tooling packages published to npm under the `@pro-vision/*` scope. Most packages are zero-config CLIs that consumer projects install as dev dependencies. The toolchain is built around **webpack** (bundling) and **Handlebars** (templating / living styleguides).

Use Node `v22.11.0` (see `.nvmrc`). CI lints against Node 22/24. Note: `pv-scripts` and `pv-stylemark` require Node `>=22.11.0`; other packages may still run on older Node.

## Commands

Run from the repo root unless noted:

```sh
npm run bootstrap # lerna bootstrap — installs + symlinks local packages (run this first after clone)
npm run lint # eslint packages (CI runs this; no test suite exists)
npm run lint:fix # eslint --fix packages
npm run build # lerna run build (effectively only builds pv-stylemark's UI)
npm run commit # git-cz — interactive Conventional Commit message
npm run publish # lerna publish --conventional-commits (maintainers only)
```

There are **no real tests** — `npm test` runs `lerna run test`, but every package's `test` script is a placeholder (`echo`). Do not assume a test harness exists; verify behavior manually or via the `examples/` consumer project.

### Building pv-stylemark's UI

```sh
cd packages/pv-stylemark && npm run build # tsc && vite build → outputs to lib/
```

### Commit conventions (enforced)

A Husky `commit-msg` hook (`scripts/verify-commit-msg.js`) **rejects commits** that don't match the Conventional Commits format: `type(scope): subject` (≤50 char subject). Allowed types: `feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build`. A `pre-commit` hook runs `lint-staged` (eslint on `*.js`). Package versions and changelogs are derived from these commit messages at publish time.

## Packages

| Package | npm name | Binary | Purpose |
| --- | --- | --- | --- |
| `pv-scripts` | `@pro-vision/pv-scripts` | `pv-scripts` | Zero-config webpack frontend toolchain (`dev`/`prod`) |
| `pv-stylemark` | `@pro-vision/pv-stylemark` | `pv-stylemark` | Living Styleguide (LSG) toolchain + webpack plugin |
| `assemble-lite` | `@pro-vision/assemble-lite` | — | Library to render Handlebars files via Node |
| `handlebars-helpers` | `@pro-vision/handlebars-helpers` | — | Shared collection of `pv-*` Handlebars helpers |
| `custom-elements-data-extractor` | `@pro-vision/custom-elements-data-extractor` | `pv-custom-data` | Extracts custom-element attrs → VSCode HTML custom data |
| `pv-create-component` | `@pro-vision/pv-create-component` | `pv-create-component` | Interactive component-boilerplate scaffolder |
| `vscode-pv-handlebars-language-server` | (unpublished) | — | VSCode LSP extension for Handlebars in Assemble projects |

Packages are mostly plain CommonJS Node (`.js`). The exceptions are the `ui/` of `pv-stylemark` and the `vscode-*` extension, which are TypeScript.

## Key architectural patterns

### Shared `pv.config.js` convention

Both `pv-scripts` and `pv-stylemark` are configured by a single `pv.config.js` file in the **consumer project root**. Each package independently loads it and shallow-merges it over its own defaults:

- `pv-scripts`: `config/default.config.js` + `helpers/buildConfigHelpers.js` (`getBuildConfig`)
- `pv-stylemark`: `config/default.config.js` + `helper/paths.js`

When changing config behavior, update both the default config and the corresponding README config table.

### pv-scripts: composable webpack config

This is the most involved package. Flow:

1. `bin/pv-scripts.js` parses `dev`/`prod` + flags (`--stats`/`--statsJson` set `PV_WEBPACK_STATS`), then spawns `scripts/dev.js` or `scripts/prod.js`.
2. `helpers/prepareWebpackConfig.js` builds the final config by merging (via `webpack-merge`) the default config from `webpack/getConfig.js` with the consumer's optional `webpack.config.js` + `webpack.config.{dev,prod}.js`.
3. The default config is assembled **compositionally** in `webpack/base/combinedConfig.js`: each concern is an isolated module under `webpack/base/settings/*` (entry, output, resolve, …) and `webpack/base/tasks/*` (compileJS, compileCSS, loadHandlebars, …). Tasks are conditionally merged based on config flags (`useTS`, `copyStaticFiles`, etc.). `dev/` and `prod/` then overlay mode-specific settings.

To add a build capability, add a module under `webpack/base/tasks/` and merge it (conditionally if needed) in `combinedConfig.js` — don't inline logic into existing tasks.

### pv-stylemark: two ways to build the LSG

The styleguide build is driven by discrete tasks under `tasks/clickdummy/*` (assemble component/page clickdummies) and `tasks/lsg/*` (`buildDDS`, build the Stylemark styleguide). These are orchestrated two ways:

- **CLI** (`scripts/dev.js` watch mode / `scripts/prod.js` one-shot) — see `scripts/buildStylemarkLsg.js`.
- **Webpack plugin** (`webpack-plugin/index.js`, `PvStylemarkPlugin`) — hooks `compiler.hooks.emit`, registers files via `getFilesToWatch.js`, and re-runs only the affected tasks (assemble vs. copy) on change.

Internally it renders Handlebars via `assemble-lite` and registers `handlebars-helpers`. The `ui/` directory holds the styleguide's own TS web components (`dds-*`) and styles, built by Vite into `lib/` (committed/published, not built on install).

### assemble-lite + handlebars-helpers

`assemble-lite` is a thin Handlebars renderer (`assemble-lite.js` → `Assemble.js`/`Visitor.js`) that takes globs for partials/pages/templates/data/helpers. The `handlebars-helpers` collection (`pv-choose`, `pv-colors`, `pv-icons`, `pv-path`, `pv-concat`) is bundled into it automatically. These are the shared templating substrate beneath `pv-stylemark`.

## Conventions

- **ESLint** extends `prettier`; `.ts` files are ignored by lint (`ignorePatterns`), and `pv-stylemark/` is entirely excluded via `.eslintignore`. Prettier violations are eslint errors.
- Each package vendors its own `node_modules` and `package-lock.json` (Lerna bootstrap symlinks the local cross-dependencies).
- `examples/react-tsx/` is a runnable consumer project for `pv-scripts` (its own `pv.config.js`) — use it to manually verify `pv-scripts` changes.
Loading
Loading