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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@

# Node artifacts
node_modules
**/node_modules
npm-debug.log
yarn-error.log

# Build artifacts
dist
**/dist
**/dist-*
**/npm-dist
build
*.tgz
cli/src/server/playground-form-generated.ts
cli/src/server/samples/generated.ts
.facet
**/.facet
.gavel
**/.gavel
.task
*-cache/
compiled*
Expand Down Expand Up @@ -57,6 +64,8 @@ data/
out.*

# Temporary files
.tmp
**/.tmp
tmp
temp
*.tmp
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,6 @@ cli/src/server/samples/generated.ts
.grite/
.playwright-cli/
cli/src/server/playground-form-generated.ts
cli/dist-playground/
examples/kitchen-sink/
cli/npm-dist/
18 changes: 8 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,14 @@ LABEL org.opencontainers.image.title="Facet" \
org.opencontainers.image.vendor="Flanksource" \
org.opencontainers.image.version="${VERSION}"

# Warm the pnpm store by rendering a sample that imports @flanksource/facet,
# which also verifies the tarball above is complete (a dist-less package fails
# this build). HTML (not PDF) so the build never launches Chromium: buildkit
# RUN steps lack the dbus/userns environment a browser needs, while the pnpm
# store warming only requires the vite build. FACET_PACKAGE_PATH is scoped to
# this step because the image's own version may not be published to the
# registry at build time.
RUN cd /app/examples && \
FACET_PACKAGE_PATH=/app/facet.tgz facet html FacetReport.tsx --data simple-data.json --output /tmp/warmup.html && \
rm -f /tmp/warmup.html
# Populate the immutable shared module entry from the exact-version tarball and
# verify that the package can render. The cache rejects a tarball whose Facet
# version differs from the CLI version.
WORKDIR /app/examples
RUN mkdir -p /app/.tmp
RUN FACET_PACKAGE_PATH=/app/facet.tgz facet --skip-modules html FacetReport.tsx --data simple-data.json --output /app/.tmp/warmup.html
RUN rm -f /app/.tmp/warmup.html
WORKDIR /app
Comment on lines +138 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore /workspace after the warmup.

Line 142 makes /app the final runtime working directory, overriding the declared /workspace default. Relative template and output paths in container invocations will resolve under /app instead of the intended writable workspace.

Proposed fix
-WORKDIR /app
+WORKDIR /workspace
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WORKDIR /app/examples
RUN mkdir -p /app/.tmp
RUN FACET_PACKAGE_PATH=/app/facet.tgz facet --skip-modules html FacetReport.tsx --data simple-data.json --output /app/.tmp/warmup.html
RUN rm -f /app/.tmp/warmup.html
WORKDIR /app
WORKDIR /app/examples
RUN mkdir -p /app/.tmp
RUN FACET_PACKAGE_PATH=/app/facet.tgz facet --skip-modules html FacetReport.tsx --data simple-data.json --output /app/.tmp/warmup.html
RUN rm -f /app/.tmp/warmup.html
WORKDIR /workspace
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 138 - 142, Restore /workspace as the final WORKDIR
after the warmup commands; update the WORKDIR directive currently setting /app
so the image retains the declared writable workspace for relative template and
output paths.


HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3010/healthz || exit 1
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
.PHONY: build install test serve
.PHONY: build install lint test serve

build:
task build

install:
task install

lint:
task lint

test:
task test
Expand Down
62 changes: 57 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ npm install -g @flanksource/facet-cli
# or: pnpm add -g @flanksource/facet-cli
```

This installs the `facet` command, which runs on your Node.js (>=18). Rendering
additionally needs `pnpm` on PATH and, for PDF output, a system Chrome/Chromium —
run `facet doctor` to check. For a Node-free environment, use the standalone
binary below.
This installs the `facet` command, which runs on your Node.js (>=20.19). Rendering
uses `pnpm` to populate the shared module cache and reconcile project-specific
dependencies. PDF output also needs a system Chrome/Chromium. Run `facet doctor`
to check the environment. For a Node-free environment, use the standalone binary
below.

### Option 2: Standalone binary

Expand Down Expand Up @@ -255,7 +256,7 @@ Keep the fixture, iteration count, worker settings, and environment unchanged.

### Build Process

**CLI mode** — `facet html` and `facet pdf` run the full pipeline locally:
**CLI mode** — without a Facet server URL, `facet html` and `facet pdf` run the full pipeline locally:

1. **Setup `.facet/`** — Creates an isolated build directory with symlinks to your sources
2. **Generate configs** — Auto-generates `vite.config.ts`, `tsconfig.json`, `entry.tsx`
Expand All @@ -267,6 +268,8 @@ Keep the fixture, iteration count, worker settings, and environment unchanged.

**Server mode** — `facet serve` wraps the same pipeline behind an HTTP API with a worker pool, LRU cache, optional S3 upload, and an interactive playground at `localhost:3010`.

When `FACET_URL` or `--facet-url` is set, the CLI loads and validates data locally, uploads the template project to `/render`, downloads the result, and writes it to the normal local output path. The upload excludes Git metadata, dependencies, Facet caches, temporary files, and build output.



## Page Component API
Expand Down Expand Up @@ -401,6 +404,43 @@ dropdown to try it.

## CLI Commands

### Remote rendering

Set `FACET_URL` or pass the global `--facet-url` option to submit `html` and `pdf` jobs to a Facet server. The explicit flag takes precedence over the environment variable.

```bash
FACET_URL=https://facet.example.com facet pdf MyDatasheet.tsx -d data.json -o report.pdf
facet --facet-url https://facet.example.com html MyDatasheet.tsx -o ./dist/
```

Remote mode requires `tar` locally but does not require local Chromium or pnpm. Data loaders and schema validation still run locally. The server controls sandboxing, module mode, and cache lifecycle, so `--sandbox`, `--skip-modules`, and `--clear-cache` fail when combined with a Facet URL. A server or network error stops the command without falling back to local rendering.

### Shared modules

`--skip-modules` is a global option and may appear before or after any subcommand:

```bash
facet --skip-modules html MyDatasheet.tsx -d data.json
facet pdf MyDatasheet.tsx --skip-modules -o out.pdf
facet serve --skip-modules --templates-dir ./templates
```

The first use installs a Facet-only module set pinned to the CLI version under
`${FACET_CACHE_DIR:-~/.facet/cache}/modules/<facet-version>/<platform>-<arch>-node<abi>`.
Later invocations link `.facet/node_modules` directly to that immutable entry, so
they do not read consumer or nested `package.json` files, package-manager pins,
lockfiles, overrides, `.npmrc`, or directory-based `FACET_PACKAGE_PATH` overrides.
Templates that import additional packages must run without `--skip-modules`.
Server requests containing `dependencies` receive HTTP 400 while the server uses
this mode. `facet doctor --skip-modules --fix` verifies or rebuilds the exact
shared entry selected by the current Facet and Node versions.

Without `--skip-modules`, a new `.facet` install is seeded by cloning the shared
`node_modules` directory on APFS, then reconciled against the generated project
manifest with `pnpm install`. Facet logs and uses a fresh install when cloning is
unavailable, including on non-macOS filesystems. `--clear-cache` clears only the
project `.facet` scaffold; it does not remove the versioned shared module cache.

### `facet html <template>`

Generate HTML from a React template.
Expand All @@ -419,6 +459,8 @@ Options:
-v, --verbose Enable verbose logging
```

Facet automatically starts render-related child processes at below-normal scheduling priority so Vite, pnpm, data loaders, archive extraction, and Chromium yield resources to interactive OS workloads. It uses background task policy with niceness `+10` on macOS, niceness `+10` on Linux, and below-normal process priority on Windows.

**Example:**
```bash
facet html MyDatasheet.tsx -o ./dist --verbose
Expand Down Expand Up @@ -484,6 +526,9 @@ facet serve --api-key my-secret-key

# With S3 upload
facet serve --s3-endpoint https://s3.amazonaws.com --s3-bucket my-bucket

# Reuse the immutable Facet-only modules for every request
facet serve --skip-modules --templates-dir ./templates
```

The playground is available at `http://localhost:3010/` with a Monaco editor, live preview, and render logs. Use the **Example** dropdown to load a starting point:
Expand Down Expand Up @@ -627,6 +672,13 @@ pnpm run storybook
pnpm run build:cli
```

To build the npm CLI package from the current checkout and replace any existing
global `facet` command managed through npm's prefix:

```bash
task install
```

### Publishing

```bash
Expand Down
117 changes: 111 additions & 6 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ vars:
# Directories
DIST_DIR: ./dist
PDF_OUTPUT_DIR: ../../../static/pdfs
CLI_NPM_PACKAGE_DIR: "{{.ROOT_DIR}}/.tmp/facet-cli"
CLI_VERSION:
sh: node -p "require('./cli/package.json').version"
BINARY_EXT: '{{if eq OS "windows"}}.exe{{end}}'
FACET_BINARY: "{{.DIST_DIR}}/facet{{.BINARY_EXT}}"

# Lab deployment
APP_NAME: facet
CHART_DIR: chart
LAB_CONTEXT: k3s
LAB_NAMESPACE: facet
LAB_REGISTRY: docker.c0a801af.sslip.io
LAB_COMMIT:
sh: git rev-parse --short=8 HEAD
LAB_VERSION:
sh: printf '%s-%s\n' "$(git describe --tags --always --dirty --abbrev=8)" "$(date -u +%Y%m%d%H%M%S)" | tr '/' '-'

# Source patterns for watching and dependency tracking
SRC_PATTERN: src/**/*.{tsx,ts,jsx,js,css,mdx}
Expand All @@ -29,18 +45,63 @@ tasks:

build:
sources:
- src/**/*.{tsx,ts,jsx,js,css,mdx}
- cli/src/**/*.{tsx,ts,jsx,js,css,mdx}
- Taskfile.yml
- package.json
- pnpm-lock.yaml
- openapi.yaml
- postcss.config.js
- tailwind.config.js
- tsconfig.json
- tsconfig.lib.json
- vite.lib.config.ts
- vite.playground.config.ts
- src/**/*
- cli/package.json
- cli/pnpm-lock.yaml
- cli/scripts/**/*
- cli/src/**/*
- cli/tsconfig.json
- cli/tsdown.config.ts
- cli/tsdown.sea.config.ts
generates:
- dist/facet
- "{{.FACET_BINARY}}"
cmds:
- pnpm run build

build:npm-cli:
desc: "Build the local @flanksource/facet-cli npm package"
cmds:
- pnpm --dir cli run build:bundle
- node cli/scripts/pack-npm-cli.cjs "{{.CLI_VERSION}}" "{{.CLI_NPM_PACKAGE_DIR}}"

install:
desc: "Build and globally install the local @flanksource/facet-cli package, replacing the active facet executable"
deps:
- build
- build:npm-cli
cmds:
- npm install --global --force --install-links "{{.CLI_NPM_PACKAGE_DIR}}"
- ln -sf "$(npm root --global)/@flanksource/facet-cli/facet.cjs" "$(which facet)"

install:symlink:
desc: "Globally link the local Facet CLI and component library to this checkout"
deps:
- build:npm-cli
cmds:
- task: install:symlink:library
- task: install:symlink:cli
- ln -sf "$(npm root --global)/@flanksource/facet-cli/facet.cjs" "$(which facet)"

install:symlink:library:
internal: true
dir: "{{.ROOT_DIR}}"
cmds:
- cp dist/facet /usr/local/bin
- npm link

install:symlink:cli:
internal: true
dir: "{{.CLI_NPM_PACKAGE_DIR}}"
cmds:
- npm link

storybook:
desc: "Run Storybook development server on port 6006"
Expand All @@ -57,6 +118,12 @@ tasks:
- test:unit
# - test:storybook

lint:
desc: "Typecheck the CLI"
dir: cli
cmds:
- pnpm run lint

gen-version:
desc: "Generate cli/src/version-generated.ts"
dir: cli
Expand Down Expand Up @@ -85,7 +152,45 @@ tasks:
deps:
- build
cmds:
- ./dist/facet serve --templates-dir examples
- "{{.FACET_BINARY}} serve --templates-dir examples"

image:lab:
desc: "Build and push the linux/amd64 Facet image to the lab registry"
cmds:
- >-
docker build
--platform linux/amd64
--build-arg VERSION={{.LAB_VERSION}}
--build-arg GIT_COMMIT={{.LAB_COMMIT}}
--tag {{.LAB_REGISTRY}}/{{.APP_NAME}}:{{.LAB_VERSION}}
.
- docker push {{.LAB_REGISTRY}}/{{.APP_NAME}}:{{.LAB_VERSION}}

deploy:lab:
desc: "Build, push, and deploy Facet to the lab"
vars:
DEPLOY_VERSION: "{{.LAB_VERSION}}"
cmds:
- task: image:lab
vars:
LAB_VERSION: "{{.DEPLOY_VERSION}}"
- >-
helm upgrade --install {{.APP_NAME}} {{.CHART_DIR}}
--kube-context {{.LAB_CONTEXT}}
--namespace {{.LAB_NAMESPACE}}
--create-namespace
--values {{.CHART_DIR}}/values.lab.yaml
--set-string image.repository={{.LAB_REGISTRY}}/{{.APP_NAME}}
--set-string image.tag={{.DEPLOY_VERSION}}
--set-string image.pullPolicy=Always
--rollback-on-failure
--wait
--timeout 10m
- >-
kubectl --context {{.LAB_CONTEXT}}
rollout status deployment/{{.APP_NAME}}
--namespace {{.LAB_NAMESPACE}}
--timeout 5m
Comment on lines +177 to +193

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

minimum Helm version that introduced --rollback-on-failure flag

💡 Result:

The --rollback-on-failure flag was introduced in Helm 4.0.0 as a replacement for the deprecated --atomic flag [1][2][3]. This change was implemented to provide clearer terminology for the functionality, which automatically reverts a failed installation or upgrade [1]. While the functionality remains similar, the new flag is the standard in Helm 4 [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Taskfile references to helm image/version/context =="
rg -n "helm|Helm|helmfile|kubepi|LAB_|CHART_DIR|kube-context|kubectl" Taskfile.yml .github 2>/dev/null || true

echo
echo "== Helm references in tracked files =="
rg -n "helm (upgrade|install|up|version)|Helm|helmfile|helm3|helm4" -g '!vendor/**' -g '!dist/**' -g '!build/**' . || true

Repository: flanksource/facet

Length of output: 4594


🌐 Web query:

helm --rollback-on-failure flag available Helm v3 version

💡 Result:

The --rollback-on-failure flag is not available in Helm v3. It was introduced in Helm 4 as a replacement for the --atomic flag [1][2]. In Helm v3, the equivalent functionality is provided by the --atomic flag [3][2][4]. When --atomic is used with helm upgrade or helm install, Helm will automatically roll back the release to the previous successful state if the operation fails [2][4]. In Helm 4, the --atomic flag is deprecated in favor of --rollback-on-failure [1][3][2]. While --atomic remains available in some contexts for backward compatibility in early Helm 4 versions, it has been removed in others (such as the install command) [2]. Summary of flag usage: - Helm v3: Use --atomic. - Helm v4: Use --rollback-on-failure (replaces --atomic) [1][3][2].

Citations:


Use --atomic for Helm v3-compatible lab installs.

This repository is documented as supporting Helm 3.0+, and all CI Helm setup uses azure/setup-helm@v4 without pinning a Helm 4.x version. helm upgrade --install with --rollback-on-failure will fail on Helm v3; use --atomic instead unless the target lab Helm binary is guaranteed to be Helm 4+.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Taskfile.yml` around lines 177 - 193, Update the lab Helm upgrade command in
the deployment task to replace --rollback-on-failure with the Helm 3-compatible
--atomic flag, preserving the existing wait, timeout, and rollout status
behavior.


default:
desc: "Show available tasks"
Expand Down
35 changes: 35 additions & 0 deletions chart/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "facet.fullname" . }}
labels:
{{- include "facet.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "facet.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
20 changes: 20 additions & 0 deletions chart/values.lab.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
ingress:
enabled: true
className: traefik
annotations:
cert-manager.io/cluster-issuer: lab-ca
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: facet.c0a801af.sslip.io
paths:
- path: /
pathType: Prefix
- host: facet.lab
paths:
- path: /
pathType: Prefix
tls:
- secretName: facet-tls
hosts:
- facet.c0a801af.sslip.io
- facet.lab
Loading
Loading