diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f87e4712..09cc658e4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,10 +22,10 @@ jobs: # Download and cache dependencies - restore_cache: - keys: - - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + keys: + - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- - run: cd .clj-kondo && cat config.edn - run: cat .cljfmt.edn @@ -108,7 +108,7 @@ jobs: working_directory: "./frontend" command: | yarn install - yarn run compile + yarn run build:app:assets clojure -M:dev:shadow-cljs release main yarn playwright install --with-deps chromium yarn e2e:test @@ -126,7 +126,6 @@ jobs: PENPOT_TEST_REDIS_URI: "redis://localhost/1" - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} - + paths: + - ~/.m2 + key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} diff --git a/.gitignore b/.gitignore index caf638f38..b0b2074d8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ /deploy /docker/images/bundle* /exporter/target +/frontend/.storybook/preview-body.html +/frontend/.storybook/preview-head.html /frontend/cypress/fixtures/validuser.json /frontend/cypress/videos/*/ /frontend/cypress/videos/*/ @@ -68,7 +70,6 @@ /web clj-profiler/ node_modules -frontend/.storybook/preview-body.html /test-results/ /playwright-report/ /blob-report/ diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 05df881bd..961b3a4fb 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -149,6 +149,11 @@ http { proxy_pass http://127.0.0.1:6060/ws/notifications; } + location /storybook { + alias /home/penpot/penpot/frontend/storybook-static/; + autoindex on; + } + location / { location ~ ^/github/penpot-files/(?[a-zA-Z0-9\-\_\.]+) { proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file; diff --git a/frontend/package.json b/frontend/package.json index 9a527d9c0..7e816f8c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,28 +17,28 @@ "@vitejs/plugin-react": "^4.2.0" }, "scripts": { - "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "build:app:assets": "node ./scripts/build-app-assets.js", + "build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build", + "build:storybook:assets": "node ./scripts/build-storybook-assets.js", + "build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook", + "e2e:server": "node ./scripts/e2e-server.js", + "e2e:test": "playwright test --project default", "fmt:clj": "cljfmt fix --parallel=true src/ test/", - "fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js", + "fmt:clj:check": "cljfmt check --parallel=false src/ test/", "fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w", + "fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js", + "lint:clj": "clj-kondo --parallel --lint src/", "lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss", "lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w", - "lint:clj": "clj-kondo --parallel --lint src/", + "test": "yarn run test:compile && yarn run test:run", "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", "test:run": "node target/tests.cjs", "test:watch": "clojure -M:dev:shadow-cljs watch test", - "test": "yarn run test:compile && yarn run test:run", "translations": "node ./scripts/translations.js", - "translations:find-unused": "node ./scripts/find-unused-translations.js", - "compile": "node ./scripts/compile.js", - "compile:cljs": "clojure -M:dev:shadow-cljs compile main", - "watch": "node ./scripts/watch.js", - "e2e:server": "node ./scripts/e2e-server.js", - "e2e:test": "playwright test --project default", - "storybook:compile": "yarn run compile && clojure -M:dev:shadow-cljs compile storybook", - "storybook:server": "yarn run storybook dev -p 6006 --no-open", - "storybook:watch": "concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"yarn run storybook:server\" \"yarn run watch\"", - "storybook:build": "yarn run storybook:compile && storybook build" + "watch": "yarn run watch:app:assets", + "watch:app:assets": "node ./scripts/watch.js", + "watch:storybook": "concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"", + "watch:storybook:assets": "node ./scripts/watch-storybook.js" }, "devDependencies": { "@playwright/test": "1.44.1", diff --git a/frontend/.storybook/preview-head.html b/frontend/resources/templates/preview-head.mustache similarity index 51% rename from frontend/.storybook/preview-head.html rename to frontend/resources/templates/preview-head.mustache index e943079ee..fdbcabf55 100644 --- a/frontend/.storybook/preview-head.html +++ b/frontend/resources/templates/preview-head.mustache @@ -1,4 +1,4 @@ - + \ No newline at end of file + diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index 4b72301e8..b8fbd0de5 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -82,6 +82,31 @@ export async function compileSassDebug(worker) { return `${result.css}\n`; } +export async function compileSassStorybook(worker) { + const limitFn = pLimit(4); + const sourceDir = ph.join("src", "app", "main", "ui", "ds"); + + const dsFiles = (await fs.readdir(sourceDir, { recursive: true })) + .filter(isSassFile) + .map((filename) => ph.join(sourceDir, filename)); + const procs = [compileSass(worker, "resources/styles/main-default.scss", {})]; + + for (let path of dsFiles) { + const proc = limitFn(() => compileSass(worker, path, { modules: true })); + procs.push(proc); + } + + const result = await Promise.all(procs); + return result.reduce( + (acc, item) => { + acc.index[item.outputPath] = item.css; + acc.items.push(item.outputPath); + return acc; + }, + { index: {}, items: [] }, + ); +} + export async function compileSassAll(worker) { const limitFn = pLimit(4); const sourceDir = "src"; @@ -379,6 +404,15 @@ async function generateTemplates() { ); await fs.writeFile("./.storybook/preview-body.html", content); + content = await renderTemplate( + "resources/templates/preview-head.mustache", + { + manifest: manifest, + }, + partials, + ); + await fs.writeFile("./.storybook/preview-head.html", content); + content = await renderTemplate("resources/templates/render.mustache", { manifest: manifest, translations: JSON.stringify(translations), @@ -394,6 +428,22 @@ async function generateTemplates() { await fs.writeFile("./resources/public/rasterizer.html", content); } +export async function compileStorybookStyles() { + const worker = startWorker(); + const start = process.hrtime(); + + log.info("init: compile storybook styles"); + let result = await compileSassStorybook(worker); + result = concatSass(result); + + await fs.mkdir("./resources/public/css", { recursive: true }); + await fs.writeFile("./resources/public/css/ds.css", result); + + const end = process.hrtime(start); + log.info("done: compile storybook styles", `(${ppt(end)})`); + worker.terminate(); +} + export async function compileStyles() { const worker = startWorker(); const start = process.hrtime(); diff --git a/frontend/scripts/build b/frontend/scripts/build index 4ac406cce..60d685fd2 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -4,6 +4,8 @@ set -ex +export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no}; + export CURRENT_VERSION=$1; export BUILD_DATE=$(date -R); export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)}; @@ -20,9 +22,16 @@ rm -rf target/dist; clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1 -yarn run compile || exit 1; +yarn run build:app:assets || exit 1; + mkdir -p target/dist; rsync -avr resources/public/ target/dist/ sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html; sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html; + +if [ "$INCLUDE_STORYBOOK" = "yes"]; then + # build storybook + yarn run build:storybook || exit 1; + rsync -avr storybook-static/ target/dist/storybook-static; +fi diff --git a/frontend/scripts/compile.js b/frontend/scripts/build-app-assets.js similarity index 100% rename from frontend/scripts/compile.js rename to frontend/scripts/build-app-assets.js diff --git a/frontend/scripts/build-storybook-assets.js b/frontend/scripts/build-storybook-assets.js new file mode 100644 index 000000000..c0eb37a36 --- /dev/null +++ b/frontend/scripts/build-storybook-assets.js @@ -0,0 +1,7 @@ +import * as h from "./_helpers.js"; + +await h.compileStorybookStyles(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); diff --git a/frontend/scripts/watch-storybook.js b/frontend/scripts/watch-storybook.js new file mode 100644 index 000000000..a82a66932 --- /dev/null +++ b/frontend/scripts/watch-storybook.js @@ -0,0 +1,86 @@ +import fs from "node:fs/promises"; +import ph from "node:path"; + +import log from "fancy-log"; +import * as h from "./_helpers.js"; +import ppt from "pretty-time"; + +const worker = h.startWorker(); +let sass = null; + +async function compileSassAll() { + const start = process.hrtime(); + log.info("init: compile storybook styles"); + + sass = await h.compileSassStorybook(worker); + let output = await h.concatSass(sass); + await fs.writeFile("./resources/public/css/ds.css", output); + + const end = process.hrtime(start); + log.info("done: compile storybook styles", `(${ppt(end)})`); +} + +async function compileSass(path) { + const start = process.hrtime(); + log.info("changed:", path); + const result = await h.compileSass(worker, path, { modules: true }); + sass.index[result.outputPath] = result.css; + + const output = h.concatSass(sass); + await fs.writeFile("./resources/public/css/ds.css", output); + + const end = process.hrtime(start); + log.info("done:", `(${ppt(end)})`); +} + +await fs.mkdir("./resources/public/css/", { recursive: true }); +await compileSassAll(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); + +log.info("watch: scss src (~)"); + +h.watch("src", h.isSassFile, async function (path) { + const isPartial = ph.basename(path).startsWith("_"); + const isCommon = isPartial || ph.dirname(path).endsWith("/ds"); + + if (isCommon) { + await compileSassAll(path); + } else { + await compileSass(path); + } +}); + +log.info("watch: scss: resources (~)"); +h.watch("resources/styles", h.isSassFile, async function (path) { + log.info("changed:", path); + await compileSassAll(); +}); + +log.info("watch: templates (~)"); +h.watch("resources/templates", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: translations (~)"); +h.watch("translations", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: assets (~)"); +h.watch( + ["resources/images", "resources/fonts", "resources/plugins-runtime"], + null, + async function (path) { + log.info("changed:", path); + await h.compileSvgSprites(); + await h.copyAssets(); + await h.compileTemplates(); + }, +); + +worker.terminate(); diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index ebd3062c5..608357a29 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -93,7 +93,7 @@ {:entries []} :components - {:exports {:default app.main.ui.ds/default} + {:exports {default app.main.ui.ds/default} :depends-on #{:base}}} :compiler-options diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 119e8e7aa..12cc3eeb8 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,15 +1,14 @@ import { defineConfig } from "vite"; -import { configDefaults } from 'vitest/config' +import { configDefaults } from "vitest/config"; import { resolve } from "path"; // https://vitejs.dev/config/ export default defineConfig({ test: { - exclude: [...configDefaults.exclude, 'target/**', 'resources/**'], - environment: 'jsdom' + exclude: [...configDefaults.exclude, "target/**", "resources/**"], + environment: "jsdom", }, - resolve: { alias: { "@target": resolve(__dirname, "./target/storybook"),