From 9f90d68159388a3289aff6d9f08d24c70cd975c3 Mon Sep 17 00:00:00 2001
From: Gallay Lajos
Date: Sun, 1 Feb 2026 11:23:50 +0100
Subject: [PATCH 1/4] furystack consolidations
---
.github/workflows/build-test.yml | 4 ++--
.github/workflows/check-version-bump.yml | 2 +-
.yarn/versions/2d4efe43.yml | 2 +-
.yarn/versions/544e081e.yml | 2 +-
.yarn/versions/5e7c1747.yml | 2 +-
.yarn/versions/bf3e3cf4.yml | 2 +-
.yarn/versions/cee96777.yml | 2 +-
.yarn/versions/e54633dc.yml | 2 +-
README.md | 4 ++--
...boilerplate-api.json => stack-craft-api.json} | 0
common/src/bin/create-schemas.ts | 4 ++--
common/src/index.ts | 2 +-
.../{boilerplate-api.ts => stack-craft-api.ts} | 2 +-
frontend/index.html | 2 +-
frontend/src/components/layout.tsx | 2 +-
frontend/src/environment-options.ts | 2 +-
package.json | 12 ++++++------
service/src/service.ts | 16 ++++++++--------
18 files changed, 32 insertions(+), 32 deletions(-)
rename common/schemas/{boilerplate-api.json => stack-craft-api.json} (100%)
rename common/src/{boilerplate-api.ts => stack-craft-api.ts} (93%)
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 60079c2..d7336fc 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -18,8 +18,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install
- - name: Prettier check
- run: yarn prettier:check
+ - name: Format check
+ run: yarn format:check
- name: Build
run: yarn build
env:
diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml
index cf3df5d..e95671c 100644
--- a/.github/workflows/check-version-bump.yml
+++ b/.github/workflows/check-version-bump.yml
@@ -21,7 +21,7 @@ jobs:
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Check version bumps
diff --git a/.yarn/versions/2d4efe43.yml b/.yarn/versions/2d4efe43.yml
index 6e6773b..b833b78 100644
--- a/.yarn/versions/2d4efe43.yml
+++ b/.yarn/versions/2d4efe43.yml
@@ -1,5 +1,5 @@
releases:
common: patch
frontend: patch
- furystack-boilerplate-app: patch
+ stack-craft: patch
service: patch
diff --git a/.yarn/versions/544e081e.yml b/.yarn/versions/544e081e.yml
index 6e6773b..b833b78 100644
--- a/.yarn/versions/544e081e.yml
+++ b/.yarn/versions/544e081e.yml
@@ -1,5 +1,5 @@
releases:
common: patch
frontend: patch
- furystack-boilerplate-app: patch
+ stack-craft: patch
service: patch
diff --git a/.yarn/versions/5e7c1747.yml b/.yarn/versions/5e7c1747.yml
index e27b4cb..f885e24 100644
--- a/.yarn/versions/5e7c1747.yml
+++ b/.yarn/versions/5e7c1747.yml
@@ -1,2 +1,2 @@
releases:
- furystack-boilerplate-app: patch
+ stack-craft: patch
diff --git a/.yarn/versions/bf3e3cf4.yml b/.yarn/versions/bf3e3cf4.yml
index 6e6773b..b833b78 100644
--- a/.yarn/versions/bf3e3cf4.yml
+++ b/.yarn/versions/bf3e3cf4.yml
@@ -1,5 +1,5 @@
releases:
common: patch
frontend: patch
- furystack-boilerplate-app: patch
+ stack-craft: patch
service: patch
diff --git a/.yarn/versions/cee96777.yml b/.yarn/versions/cee96777.yml
index 6e6773b..b833b78 100644
--- a/.yarn/versions/cee96777.yml
+++ b/.yarn/versions/cee96777.yml
@@ -1,5 +1,5 @@
releases:
common: patch
frontend: patch
- furystack-boilerplate-app: patch
+ stack-craft: patch
service: patch
diff --git a/.yarn/versions/e54633dc.yml b/.yarn/versions/e54633dc.yml
index 6e6773b..b833b78 100644
--- a/.yarn/versions/e54633dc.yml
+++ b/.yarn/versions/e54633dc.yml
@@ -1,5 +1,5 @@
releases:
common: patch
frontend: patch
- furystack-boilerplate-app: patch
+ stack-craft: patch
service: patch
diff --git a/README.md b/README.md
index 68490d1..9ca9e41 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# boilerplate
+# Stack Craft
-Boilerplate app with common type api definitions, a furystack-based backend service and a Shades-based single page application.
+Example web app with common type API definitions, a FuryStack-based backend service and a Shades-based single page application.
# Usage
diff --git a/common/schemas/boilerplate-api.json b/common/schemas/stack-craft-api.json
similarity index 100%
rename from common/schemas/boilerplate-api.json
rename to common/schemas/stack-craft-api.json
diff --git a/common/src/bin/create-schemas.ts b/common/src/bin/create-schemas.ts
index 19556ad..940c7aa 100644
--- a/common/src/bin/create-schemas.ts
+++ b/common/src/bin/create-schemas.ts
@@ -21,8 +21,8 @@ export const entityValues: SchemaGenerationSetting[] = [
export const apiValues: SchemaGenerationSetting[] = [
{
- inputFile: './src/boilerplate-api.ts',
- outputFile: './schemas/boilerplate-api.json',
+ inputFile: './src/stack-craft-api.ts',
+ outputFile: './schemas/stack-craft-api.json',
type: '*',
},
]
diff --git a/common/src/index.ts b/common/src/index.ts
index 04ca3ab..0105f60 100644
--- a/common/src/index.ts
+++ b/common/src/index.ts
@@ -1,2 +1,2 @@
-export * from './boilerplate-api.js'
+export * from './stack-craft-api.js'
export * from './models/index.js'
diff --git a/common/src/boilerplate-api.ts b/common/src/stack-craft-api.ts
similarity index 93%
rename from common/src/boilerplate-api.ts
rename to common/src/stack-craft-api.ts
index ff2299b..39380ad 100644
--- a/common/src/boilerplate-api.ts
+++ b/common/src/stack-craft-api.ts
@@ -5,7 +5,7 @@ export type TestQueryEndpoint = { query: { param1: string }; result: { param1Val
export type TestUrlParamsEndpoint = { url: { urlParam: string }; result: { urlParamValue: string } }
export type TestPostBodyEndpoint = { body: { value: string }; result: { bodyValue: string } }
-export interface BoilerplateApi extends RestApi {
+export interface StackCraftApi extends RestApi {
GET: {
'/isAuthenticated': { result: { isAuthenticated: boolean } }
'/currentUser': { result: User }
diff --git a/frontend/index.html b/frontend/index.html
index 1a0137a..604c7ff 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,7 +2,7 @@
- FuryStack Boilerplate App
+ Stack Craft
diff --git a/frontend/src/components/layout.tsx b/frontend/src/components/layout.tsx
index bd5fce1..15a388f 100644
--- a/frontend/src/components/layout.tsx
+++ b/frontend/src/components/layout.tsx
@@ -24,7 +24,7 @@ export const Layout = Shade({
backgroundColor: injector.getInstance(ThemeProviderService).theme.background.default,
}}
>
-
+
)
diff --git a/frontend/src/environment-options.ts b/frontend/src/environment-options.ts
index 609206b..333643c 100644
--- a/frontend/src/environment-options.ts
+++ b/frontend/src/environment-options.ts
@@ -4,6 +4,6 @@ export const environmentOptions = {
// appVersion: process.env.APP_VERSION as string,
// buildDate: new Date(process.env.BUILD_DATE as string),
// serviceUrl: process.env.SERVICE_URL as string,
- repository: 'http://github.com/furystack/boilerplate',
+ repository: 'http://github.com/furystack/stack-craft',
serviceUrl: 'http://localhost:9090/api', //process.env.REPOSITORY as string,
}
diff --git a/package.json b/package.json
index cbc2a49..7ccedec 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
- "name": "furystack-boilerplate-app",
+ "name": "stack-craft",
"version": "1.0.2",
- "description": "example web app based on furystack",
+ "description": "Example web app based on FuryStack",
"main": "service/src/index.ts",
- "repository": "https://github.com/furystack/boilerplate.git",
+ "repository": "https://github.com/furystack/stack-craft.git",
"author": "Gallay Lajos ",
"license": "GPL-2.0-only",
"private": true,
@@ -62,9 +62,9 @@
"clean": "rimraf service/dist frontend/dist **/tsconfig.tsbuildinfo tsconfig.tsbuildinfo common/dist",
"lint": "eslint .",
"bumpVersions": "yarn version check --interactive",
- "applyVersionBumps": "yarn version apply --all && echo TODO: Upgrade changelogs",
- "prettier": "prettier --write .",
- "prettier:check": "prettier --check ."
+ "applyReleaseChanges": "yarn version apply --all && yarn changelog apply && yarn format",
+ "format": "prettier --write .",
+ "format:check": "prettier --check ."
},
"engines": {
"node": ">=18.0.0"
diff --git a/service/src/service.ts b/service/src/service.ts
index 7fc89c0..144834a 100644
--- a/service/src/service.ts
+++ b/service/src/service.ts
@@ -10,9 +10,9 @@ import {
useRestService,
useStaticFiles,
} from '@furystack/rest-service'
-import type { BoilerplateApi } from 'common'
+import type { StackCraftApi } from 'common'
import { User } from 'common'
-import BoilerplateApiSchemas from 'common/schemas/boilerplate-api.json' with { type: 'json' }
+import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' }
import { injector } from './config.js'
import { attachShutdownHandler } from './shutdown-handler.js'
@@ -22,13 +22,13 @@ useHttpAuthentication(injector, {
getUserStore: (sm) => sm.getStoreFor(User, 'username'),
getSessionStore: (sm) => sm.getStoreFor(DefaultSession, 'sessionId'),
})
-useRestService({
+useRestService({
injector,
root: 'api',
port,
- name: 'Boilerplate Service',
+ name: 'Stack Craft Service',
version: '1.0.0',
- description: 'API for Furystack Boilerplate Application containing simple authentication and example endpoints',
+ description: 'API for Stack Craft application containing simple authentication and example endpoints',
cors: {
credentials: true,
origins: ['http://localhost:8080'],
@@ -38,17 +38,17 @@ useRestService({
GET: {
'/currentUser': GetCurrentUser,
'/isAuthenticated': IsAuthenticated,
- '/testQuery': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestQueryEndpoint' })(async (options) =>
+ '/testQuery': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestQueryEndpoint' })(async (options) =>
JsonResult({ param1Value: options.getQuery().param1 }),
),
- '/testUrlParams/:urlParam': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestUrlParamsEndpoint' })(
+ '/testUrlParams/:urlParam': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestUrlParamsEndpoint' })(
async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam }),
),
},
POST: {
'/login': LoginAction,
'/logout': LogoutAction,
- '/testPostBody': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestPostBodyEndpoint' })(
+ '/testPostBody': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestPostBodyEndpoint' })(
async (options) => {
const body = await options.getBody()
return JsonResult({ bodyValue: body.value })
From 2e62c5e2edea00f9c94fe256889332e21ad0f9a3 Mon Sep 17 00:00:00 2001
From: Gallay Lajos
Date: Sun, 1 Feb 2026 11:27:10 +0100
Subject: [PATCH 2/4] chore: FuryStack consolidation for stack-craft
- Rename prettier scripts to format/format:check
- Fix all boilerplate references to stack-craft:
- Package name and repository URL
- API type renamed from BoilerplateApi to StackCraftApi
- API client renamed from BoilerplateApiClient to StackCraftApiClient
- Schema file renamed
- Service name and description updated
- Header title updated
- HTML title updated
- Environment options updated
- Add changelog plugin configuration
- Remove old version files (referenced wrong package name)
- Update cursor rules documentation
- Update GitHub Actions to use setup-node@v4
---
.cursor/rules/REST_SERVICE.mdc | 28 ++--
.cursor/rules/TESTING_GUIDELINES.mdc | 2 +-
.yarn/plugins/@yarnpkg/plugin-changelog.cjs | 124 ++++++++++++++++++
.yarn/versions/2d4efe43.yml | 5 -
.yarn/versions/544e081e.yml | 5 -
.yarn/versions/5e7c1747.yml | 2 -
.yarn/versions/bf3e3cf4.yml | 5 -
.yarn/versions/cee96777.yml | 5 -
.yarn/versions/e54633dc.yml | 5 -
.yarnrc.yml | 13 ++
common/schemas/stack-craft-api.json | 2 +-
frontend/src/services/session.ts | 6 +-
...pi-client.ts => stack-craft-api-client.ts} | 6 +-
yarn.lock | 50 +++----
14 files changed, 184 insertions(+), 74 deletions(-)
create mode 100644 .yarn/plugins/@yarnpkg/plugin-changelog.cjs
delete mode 100644 .yarn/versions/2d4efe43.yml
delete mode 100644 .yarn/versions/544e081e.yml
delete mode 100644 .yarn/versions/5e7c1747.yml
delete mode 100644 .yarn/versions/bf3e3cf4.yml
delete mode 100644 .yarn/versions/cee96777.yml
delete mode 100644 .yarn/versions/e54633dc.yml
rename frontend/src/services/{boilerplate-api-client.ts => stack-craft-api-client.ts} (72%)
diff --git a/.cursor/rules/REST_SERVICE.mdc b/.cursor/rules/REST_SERVICE.mdc
index 40ffda8..91d49d0 100644
--- a/.cursor/rules/REST_SERVICE.mdc
+++ b/.cursor/rules/REST_SERVICE.mdc
@@ -15,8 +15,8 @@ alwaysApply: false
The service entry point (`service/src/service.ts`) sets up the REST API:
```typescript
-import type { BoilerplateApi } from 'common';
-import BoilerplateApiSchemas from 'common/schemas/boilerplate-api.json' with { type: 'json' };
+import type { StackCraftApi } from 'common';
+import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' };
import {
useHttpAuthentication,
@@ -32,7 +32,7 @@ useHttpAuthentication(injector, {
});
// Set up REST API
-useRestService({
+useRestService({
injector,
root: 'api',
port,
@@ -56,7 +56,7 @@ useStaticFiles({
### Define API in Common Package
-The API contract is defined in `common/src/boilerplate-api.ts`:
+The API contract is defined in `common/src/stack-craft-api.ts`:
```typescript
import type { RestApi } from '@furystack/rest';
@@ -74,7 +74,7 @@ export type CreateUserEndpoint = {
};
// Define the API interface
-export interface BoilerplateApi extends RestApi {
+export interface StackCraftApi extends RestApi {
GET: {
'/isAuthenticated': { result: { isAuthenticated: boolean } };
'/currentUser': { result: User };
@@ -102,7 +102,7 @@ import {
LogoutAction,
} from '@furystack/rest-service';
-useRestService({
+useRestService({
injector,
api: {
GET: {
@@ -124,19 +124,19 @@ Create custom actions with proper typing:
```typescript
import { JsonResult, Validate } from '@furystack/rest-service';
-useRestService({
+useRestService({
injector,
api: {
GET: {
- '/testQuery': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestQueryEndpoint' })(
+ '/testQuery': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestQueryEndpoint' })(
async (options) => JsonResult({ param1Value: options.getQuery().param1 })
),
- '/testUrlParams/:urlParam': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestUrlParamsEndpoint' })(
+ '/testUrlParams/:urlParam': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestUrlParamsEndpoint' })(
async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam })
),
},
POST: {
- '/testPostBody': Validate({ schema: BoilerplateApiSchemas, schemaName: 'TestPostBodyEndpoint' })(
+ '/testPostBody': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestPostBodyEndpoint' })(
async (options) => {
const body = await options.getBody();
return JsonResult({ bodyValue: body.value });
@@ -155,10 +155,10 @@ Use the `Validate` wrapper with JSON schemas:
```typescript
import { Validate, JsonResult } from '@furystack/rest-service';
-import BoilerplateApiSchemas from 'common/schemas/boilerplate-api.json' with { type: 'json' };
+import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' };
const endpoint = Validate({
- schema: BoilerplateApiSchemas,
+ schema: StackCraftApiSchemas,
schemaName: 'MyEndpointType',
})(async (options) => {
// Request is validated before reaching here
@@ -275,7 +275,7 @@ addStore(injector, new InMemoryStore({
Handle errors during service startup:
```typescript
-useRestService({
+useRestService({
injector,
// ... config
}).catch((err) => {
@@ -353,7 +353,7 @@ yarn seed
Set up CORS for frontend access:
```typescript
-useRestService({
+useRestService({
injector,
cors: {
credentials: true,
diff --git a/.cursor/rules/TESTING_GUIDELINES.mdc b/.cursor/rules/TESTING_GUIDELINES.mdc
index 18cdf72..7fe6e1d 100644
--- a/.cursor/rules/TESTING_GUIDELINES.mdc
+++ b/.cursor/rules/TESTING_GUIDELINES.mdc
@@ -134,7 +134,7 @@ describe('SessionService', () => {
const mockApiClient = {
call: vi.fn(),
};
- injector.setExplicitInstance(BoilerplateApiClient, mockApiClient);
+ injector.setExplicitInstance(StackCraftApiClient, mockApiClient);
sessionService = injector.getInstance(SessionService);
});
diff --git a/.yarn/plugins/@yarnpkg/plugin-changelog.cjs b/.yarn/plugins/@yarnpkg/plugin-changelog.cjs
new file mode 100644
index 0000000..c6a055b
--- /dev/null
+++ b/.yarn/plugins/@yarnpkg/plugin-changelog.cjs
@@ -0,0 +1,124 @@
+/* eslint-disable */
+//prettier-ignore
+module.exports = {
+name: "@yarnpkg/plugin-changelog",
+factory: function (require) {
+"use strict";var plugin=(()=>{var z=Object.defineProperty;var ce=Object.getOwnPropertyDescriptor;var le=Object.getOwnPropertyNames;var ge=Object.prototype.hasOwnProperty;var N=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,o)=>(typeof require<"u"?require:t)[o]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var pe=(e,t)=>{for(var o in t)z(e,o,{get:t[o],enumerable:!0})},he=(e,t,o,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of le(t))!ge.call(e,i)&&i!==o&&z(e,i,{get:()=>t[i],enumerable:!(s=ce(t,i))||s.enumerable});return e};var de=e=>he(z({},"__esModule",{value:!0}),e);var $e={};pe($e,{default:()=>Oe});var se=N("@yarnpkg/cli"),M=N("@yarnpkg/core"),f=N("@yarnpkg/fslib"),$=N("clipanion");function W(e,t,o){let s=`## [${t}] - ${o}
+
+`;for(let i of e.sections)i.isEmpty||(s+=`### ${i.name}
+`,s+=`${i.content.trim()}
+
+`);return s}var Z={heading:1,other:2,list:3};function me(e){let t=e.trim();if(!t)return"other";let o=t.split(`
+`)[0].trim();return/^#{2,}/.test(o)?"heading":/^[-*+]/.test(o)||/^\d+\./.test(o)?"list":"other"}function fe(e){let t=e.trim();return/^[-*+]/.test(t)||/^\d+\./.test(t)}function ee(e){if(e.length===0)return"";let t=e.map(l=>({content:l.trim(),type:me(l)}));t.sort((l,h)=>Z[l.type]-Z[h.type]);let o=t.filter(l=>l.type!=="list"),s=t.filter(l=>l.type==="list"),i=[];for(let l of o)i.push(l.content);if(s.length>0){let l=[];for(let h of s){let a=h.content.split(`
+`);for(let r of a)r.trim()&&(fe(r)||/^\s+/.test(r))&&l.push(r)}l.length>0&&i.push(l.join(`
+`))}return i.join(`
+
+`)}var te={major:3,minor:2,patch:1};function Y(e){if(e.length===0)return{packageName:"",versionType:"patch",sections:[],hasPlaceholders:!1};if(e.length===1)return e[0];let{packageName:t}=e[0],o=e.some(a=>a.hasPlaceholders),s=e.reduce((a,r)=>{let c=te[r.versionType]??0,m=te[a]??0;return c>m?r.versionType:a},"patch"),i=new Map,l=[];for(let a of e)for(let r of a.sections){i.has(r.name)||(i.set(r.name,[]),l.push(r.name));let c=r.content.trim();if(!c)continue;let m=i.get(r.name);m.some(p=>p.trim().toLowerCase()===c.toLowerCase())||m.push(c)}let h=l.map(a=>{let r=i.get(a)??[],c=ee(r);return{name:a,content:c?`${c}
+`:"",isEmpty:!c}});return{packageName:t,versionType:s,sections:h,hasPlaceholders:o}}var n={BREAKING_CHANGES:"\u{1F4A5} Breaking Changes",DEPRECATED:"\u{1F5D1}\uFE0F Deprecated",FEATURES:"\u2728 Features",BUG_FIXES:"\u{1F41B} Bug Fixes",DOCUMENTATION:"\u{1F4DA} Documentation",PERFORMANCE:"\u26A1 Performance",REFACTORING:"\u267B\uFE0F Refactoring",TESTS:"\u{1F9EA} Tests",BUILD:"\u{1F4E6} Build",CI:"\u{1F477} CI",DEPENDENCIES:"\u2B06\uFE0F Dependencies",CHORES:"\u{1F527} Chores"};function G(e,t={}){let o=[];return t.expectedVersionType&&e.versionType!==t.expectedVersionType&&o.push(`Version type mismatch: changelog has "${e.versionType}" but manifest expects "${t.expectedVersionType}". Run 'yarn changelog create --force' to regenerate.`),e.versionType==="major"&&!e.sections.some(i=>i.name===n.BREAKING_CHANGES&&!i.isEmpty)&&o.push(`Major release requires filled "${n.BREAKING_CHANGES}" section`),e.sections.filter(i=>!i.isEmpty).length===0&&o.push("At least one section must have content"),o}function ne(e,t){let o=[];return e||o.push(`${t}: Missing package name heading. Expected a heading like "# @furystack/package-name" at the start of the file.`),{isValid:o.length===0,errors:o}}function oe(e,t){let o=e.versionType!==t,i=G(e,{expectedVersionType:t}).filter(l=>!l.includes("Version type mismatch"));return{shouldRegenerate:o||i.length>0,hasVersionMismatch:o,contentErrors:i}}var ue="patch",Ee="/,Ce=/^# (.+)$/m,Pe=/^## (.+)$/;function A(e){let t=e.split(`
+`),s=e.match(ye)?.[1]??ue,l=e.match(Ce)?.[1]??"",h=e.includes(Ee),a=[],r=null;for(let c of t){let m=c.match(Pe);m?(r&&a.push(r),r={name:m[1],content:"",isEmpty:!0}):r&&!c.trim().startsWith("`,xe={[n.BREAKING_CHANGES]:"Describe breaking changes (BREAKING CHANGE:)",[n.DEPRECATED]:"Describe deprecated features. Double-check if they are annotated with a `@deprecated` jsdoc tag.",[n.FEATURES]:"Describe your shiny new features (feat:)",[n.BUG_FIXES]:"Describe the nasty little bugs that has been eradicated (fix:)",[n.DOCUMENTATION]:"Describe documentation changes (docs:)",[n.PERFORMANCE]:"Describe performance improvements (perf:)",[n.REFACTORING]:"Describe code refactoring (refactor:)",[n.TESTS]:"Describe test changes (test:)",[n.BUILD]:"Describe build system changes (build:)",[n.CI]:"Describe CI configuration changes (ci:)",[n.DEPENDENCIES]:"Describe dependency updates (deps:)",[n.CHORES]:"Describe other changes (chore:)"},De="",Se=[n.BREAKING_CHANGES,n.DEPRECATED,n.FEATURES,n.BUG_FIXES,n.DOCUMENTATION,n.PERFORMANCE,n.REFACTORING,n.TESTS,n.BUILD,n.CI,n.DEPENDENCIES,n.CHORES],Ae=[n.DEPRECATED,n.FEATURES,n.BUG_FIXES,n.DOCUMENTATION,n.PERFORMANCE,n.REFACTORING,n.TESTS,n.BUILD,n.CI,n.DEPENDENCIES,n.CHORES],ve=[n.FEATURES,n.BUG_FIXES,n.DOCUMENTATION,n.PERFORMANCE,n.REFACTORING,n.TESTS,n.BUILD,n.CI,n.DEPENDENCIES,n.CHORES];function Ie(e,t=!1){let o=xe[e],s=`## ${e}
+`;return t&&(s+=`
+${De}`),s}function be(e){return(e==="major"?Se:e==="minor"?Ae:ve).map(o=>{let s=o===n.BREAKING_CHANGES;return Ie(o,s)}).join(`
+
+`)}function q(e,t){let o=be(t);return`
+# ${e}
+
+${we}
+
+${o}
+`}function L(e,t){return`${re(e)}.${t}.md`}function Q(e,t,o){let s=o||Te;return t==="major"?`
+# ${e}
+
+## ${n.BREAKING_CHANGES}
+- ${s}
+
+## ${n.DEPENDENCIES}
+- ${s}
+`:`
+# ${e}
+
+## ${n.DEPENDENCIES}
+- ${s}
+`}var j=class extends ie.BaseCommand{static paths=[["changelog","check"]];static usage=U.Command.Usage({description:"Validate changelog entries for all version manifests",details:`
+ This command validates that:
+ - Every release in \`.yarn/versions/*.yml\` has a changelog file
+ - Major releases have filled BREAKING CHANGES sections
+ - At least one section (Added/Changed/Fixed) has content
+ `,examples:[["Validate changelogs","yarn changelog check"]]});verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Show verbose output"});async execute(){let t=await H.Configuration.find(this.context.cwd,this.context.plugins),{project:o}=await H.Project.find(t,this.context.cwd),s=w.ppath.join(o.cwd,F),i=w.ppath.join(o.cwd,I);if(!await w.xfs.existsPromise(s))return this.context.stdout.write(`No .yarn/versions directory found. Nothing to check.
+`),0;let h=(await w.xfs.readdirPromise(s)).filter(c=>c.endsWith(".yml"));if(h.length===0)return this.context.stdout.write(`No version manifests found. Nothing to check.
+`),0;let a=[],r=0;for(let c of h){let m=w.ppath.join(s,c),D=await w.xfs.readFilePromise(m,"utf8"),p=V(D,m);this.verbose&&this.context.stdout.write(`Checking manifest: ${c}
+`);for(let d of p.releases){let C=L(d.packageName,p.id),g=w.ppath.join(i,C);if(!await w.xfs.existsPromise(g)){a.push(`Missing changelog for ${d.packageName} (manifest: ${p.id}). Run 'yarn changelog create' to generate it.`);continue}let E=await w.xfs.readFilePromise(g,"utf8"),R=A(E),T=G(R,{expectedVersionType:d.versionType});if(T.length>0)for(let b of T)a.push(`${d.packageName} (${C}): ${b}`);else this.verbose&&this.context.stdout.write(` \u2713 ${d.packageName}
+`);r++}}if(a.length>0){this.context.stderr.write(`
+Changelog validation failed:
+
+`);for(let c of a)this.context.stderr.write(` \u2717 ${c}
+`);return this.context.stderr.write(`
+Found ${a.length} error(s).
+`),1}return this.context.stdout.write(`
+\u2713 All ${r} changelog(s) are valid.
+`),0}};var ae=N("@yarnpkg/cli"),K=N("@yarnpkg/core"),u=N("@yarnpkg/fslib"),v=N("clipanion");var B=class extends ae.BaseCommand{static paths=[["changelog","create"]];static usage=v.Command.Usage({description:"Generate changelog drafts from version manifests",details:`
+ This command reads all version manifests in \`.yarn/versions/*.yml\`
+ and generates draft changelog files in \`.yarn/changelogs/\`.
+
+ Each draft includes sections for Added, Changed, and Fixed entries.
+ For major/minor releases, additional sections are included.
+
+ Existing changelog drafts are not overwritten unless --force is used.
+
+ Use --dependabot to auto-fill changelogs for dependency updates.
+ The --message option can provide a custom message (e.g., PR title).
+ `,examples:[["Generate changelog drafts","yarn changelog create"],["Regenerate mismatched/invalid changelogs","yarn changelog create --force"],["Generate for Dependabot PR","yarn changelog create --dependabot"],["Generate with custom message",'yarn changelog create --dependabot -m "Bump lodash from 4.17.20 to 4.17.21"']]});verbose=v.Option.Boolean("-v,--verbose",!1,{description:"Show verbose output"});force=v.Option.Boolean("-f,--force",!1,{description:"Regenerate changelogs with mismatched version types or invalid entries"});dependabot=v.Option.Boolean("--dependabot",!1,{description:"Auto-fill changelog for dependency updates (Dependabot PRs)"});message=v.Option.String("-m,--message",{description:"Custom message for the changelog entry (used with --dependabot)"});async execute(){let t=await K.Configuration.find(this.context.cwd,this.context.plugins),{project:o}=await K.Project.find(t,this.context.cwd),s=u.ppath.join(o.cwd,F),i=u.ppath.join(o.cwd,I);if(await u.xfs.mkdirPromise(i,{recursive:!0}),!await u.xfs.existsPromise(s))return this.context.stdout.write(`No .yarn/versions directory found. Nothing to do.
+`),0;let h=(await u.xfs.readdirPromise(s)).filter(D=>D.endsWith(".yml"));if(h.length===0)return this.context.stdout.write(`No version manifests found. Nothing to do.
+`),0;let a=0,r=0,c=0;for(let D of h){let p=u.ppath.join(s,D),d=await u.xfs.readFilePromise(p,"utf8"),C=V(d,p);this.verbose&&this.context.stdout.write(`Processing manifest: ${D}
+`);for(let g of C.releases){let E=L(g.packageName,C.id),R=u.ppath.join(i,E);if(await u.xfs.existsPromise(R)){let b=await u.xfs.readFilePromise(R,"utf8"),k=A(b),P=oe(k,g.versionType);if(this.force&&P.shouldRegenerate){let x=this.dependabot?Q(g.packageName,g.versionType,this.message):q(g.packageName,g.versionType);await u.xfs.writeFilePromise(R,x);let S=[];P.hasVersionMismatch&&S.push(`${k.versionType} \u2192 ${g.versionType}`),P.contentErrors.length>0&&S.push(...P.contentErrors),this.context.stdout.write(` Regenerated: ${E} (${S.join(", ")})
+`),r++;continue}if(this.verbose)if(P.shouldRegenerate){let x=[];P.hasVersionMismatch&&x.push(`version mismatch: ${k.versionType} vs ${g.versionType}`),P.contentErrors.length>0&&x.push(...P.contentErrors.map(S=>S.toLowerCase())),this.context.stdout.write(` Skipping ${g.packageName} (${x.join("; ")}, use --force to regenerate)
+`)}else this.context.stdout.write(` Skipping ${g.packageName} (already exists)
+`);c++;continue}let T=this.dependabot?Q(g.packageName,g.versionType,this.message):q(g.packageName,g.versionType);await u.xfs.writeFilePromise(R,T),this.context.stdout.write(` Created: ${E} (${g.versionType})
+`),a++}}let m=[`Created ${a}`];return r>0&&m.push(`regenerated ${r}`),m.push(`skipped ${c}`),this.context.stdout.write(`
+Done! ${m.join(", ")} changelog draft(s).
+`),0}};var ke={commands:[B,j,_]},Oe=ke;return de($e);})();
+return plugin;
+}
+};
diff --git a/.yarn/versions/2d4efe43.yml b/.yarn/versions/2d4efe43.yml
deleted file mode 100644
index b833b78..0000000
--- a/.yarn/versions/2d4efe43.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-releases:
- common: patch
- frontend: patch
- stack-craft: patch
- service: patch
diff --git a/.yarn/versions/544e081e.yml b/.yarn/versions/544e081e.yml
deleted file mode 100644
index b833b78..0000000
--- a/.yarn/versions/544e081e.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-releases:
- common: patch
- frontend: patch
- stack-craft: patch
- service: patch
diff --git a/.yarn/versions/5e7c1747.yml b/.yarn/versions/5e7c1747.yml
deleted file mode 100644
index f885e24..0000000
--- a/.yarn/versions/5e7c1747.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-releases:
- stack-craft: patch
diff --git a/.yarn/versions/bf3e3cf4.yml b/.yarn/versions/bf3e3cf4.yml
deleted file mode 100644
index b833b78..0000000
--- a/.yarn/versions/bf3e3cf4.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-releases:
- common: patch
- frontend: patch
- stack-craft: patch
- service: patch
diff --git a/.yarn/versions/cee96777.yml b/.yarn/versions/cee96777.yml
deleted file mode 100644
index b833b78..0000000
--- a/.yarn/versions/cee96777.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-releases:
- common: patch
- frontend: patch
- stack-craft: patch
- service: patch
diff --git a/.yarn/versions/e54633dc.yml b/.yarn/versions/e54633dc.yml
deleted file mode 100644
index b833b78..0000000
--- a/.yarn/versions/e54633dc.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-releases:
- common: patch
- frontend: patch
- stack-craft: patch
- service: patch
diff --git a/.yarnrc.yml b/.yarnrc.yml
index 03b3254..76fd3ad 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -1,3 +1,16 @@
+changesetBaseRefs:
+ - develop
+ - origin/develop
+ - master
+ - origin/master
+ - main
+ - origin/main
+
nodeLinker: node-modules
+plugins:
+ - checksum: 26e8fe1580b68848fd4498cd646f44a38bfee77658c4678777003ff5ff01977e4113cf01a3809aa2727a46aa5342d2afe8dbe610800da3b6850f74af7d0ab1fa
+ path: .yarn/plugins/@yarnpkg/plugin-changelog.cjs
+ spec: 'https://raw.githubusercontent.com/furystack/furystack/refs/heads/develop/packages/yarn-plugin-changelog/bundles/%40yarnpkg/plugin-changelog.js'
+
yarnPath: .yarn/releases/yarn-4.12.0.cjs
diff --git a/common/schemas/stack-craft-api.json b/common/schemas/stack-craft-api.json
index 9e239dc..525aff7 100644
--- a/common/schemas/stack-craft-api.json
+++ b/common/schemas/stack-craft-api.json
@@ -82,7 +82,7 @@
"required": ["body", "result"],
"additionalProperties": false
},
- "BoilerplateApi": {
+ "StackCraftApi": {
"type": "object",
"properties": {
"GET": {
diff --git a/frontend/src/services/session.ts b/frontend/src/services/session.ts
index 7b894b4..c479800 100644
--- a/frontend/src/services/session.ts
+++ b/frontend/src/services/session.ts
@@ -3,7 +3,7 @@ import { Injectable, Injected } from '@furystack/inject'
import { NotyService } from '@furystack/shades-common-components'
import { ObservableValue, usingAsync } from '@furystack/utils'
import type { User } from 'common'
-import { BoilerplateApiClient } from './boilerplate-api-client.js'
+import { StackCraftApiClient } from './stack-craft-api-client.js'
export type SessionState = 'initializing' | 'offline' | 'unauthenticated' | 'authenticated'
@@ -101,8 +101,8 @@ export class SessionService implements IdentityContext {
return currentUser as unknown as TUser
}
- @Injected(BoilerplateApiClient)
- declare private api: BoilerplateApiClient
+ @Injected(StackCraftApiClient)
+ declare private api: StackCraftApiClient
@Injected(NotyService)
declare private readonly notys: NotyService
diff --git a/frontend/src/services/boilerplate-api-client.ts b/frontend/src/services/stack-craft-api-client.ts
similarity index 72%
rename from frontend/src/services/boilerplate-api-client.ts
rename to frontend/src/services/stack-craft-api-client.ts
index bfd002d..364495c 100644
--- a/frontend/src/services/boilerplate-api-client.ts
+++ b/frontend/src/services/stack-craft-api-client.ts
@@ -1,11 +1,11 @@
import { Injectable } from '@furystack/inject'
import { createClient } from '@furystack/rest-client-fetch'
-import type { BoilerplateApi } from 'common'
+import type { StackCraftApi } from 'common'
import { environmentOptions } from '../environment-options.js'
@Injectable({ lifetime: 'singleton' })
-export class BoilerplateApiClient {
- public call = createClient({
+export class StackCraftApiClient {
+ public call = createClient({
endpointUrl: environmentOptions.serviceUrl,
requestInit: {
credentials: 'include',
diff --git a/yarn.lock b/yarn.lock
index 349a1fb..ec4abbc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2633,31 +2633,6 @@ __metadata:
languageName: node
linkType: hard
-"furystack-boilerplate-app@workspace:.":
- version: 0.0.0-use.local
- resolution: "furystack-boilerplate-app@workspace:."
- dependencies:
- "@eslint/js": "npm:^9.39.2"
- "@playwright/test": "npm:^1.58.0"
- "@types/node": "npm:^25.0.10"
- "@vitest/coverage-v8": "npm:^4.0.18"
- eslint: "npm:^9.39.2"
- eslint-config-prettier: "npm:^10.1.8"
- eslint-plugin-import: "npm:2.32.0"
- eslint-plugin-jsdoc: "npm:^62.4.0"
- eslint-plugin-playwright: "npm:^2.5.0"
- eslint-plugin-prettier: "npm:^5.5.5"
- husky: "npm:^9.1.7"
- lint-staged: "npm:^16.2.7"
- prettier: "npm:^3.8.1"
- rimraf: "npm:^6.1.2"
- typescript: "npm:^5.9.3"
- typescript-eslint: "npm:^8.53.1"
- vite: "npm:^7.3.1"
- vitest: "npm:^4.0.18"
- languageName: unknown
- linkType: soft
-
"generator-function@npm:^2.0.0":
version: 2.0.1
resolution: "generator-function@npm:2.0.1"
@@ -4506,6 +4481,31 @@ __metadata:
languageName: node
linkType: hard
+"stack-craft@workspace:.":
+ version: 0.0.0-use.local
+ resolution: "stack-craft@workspace:."
+ dependencies:
+ "@eslint/js": "npm:^9.39.2"
+ "@playwright/test": "npm:^1.58.0"
+ "@types/node": "npm:^25.0.10"
+ "@vitest/coverage-v8": "npm:^4.0.18"
+ eslint: "npm:^9.39.2"
+ eslint-config-prettier: "npm:^10.1.8"
+ eslint-plugin-import: "npm:2.32.0"
+ eslint-plugin-jsdoc: "npm:^62.4.0"
+ eslint-plugin-playwright: "npm:^2.5.0"
+ eslint-plugin-prettier: "npm:^5.5.5"
+ husky: "npm:^9.1.7"
+ lint-staged: "npm:^16.2.7"
+ prettier: "npm:^3.8.1"
+ rimraf: "npm:^6.1.2"
+ typescript: "npm:^5.9.3"
+ typescript-eslint: "npm:^8.53.1"
+ vite: "npm:^7.3.1"
+ vitest: "npm:^4.0.18"
+ languageName: unknown
+ linkType: soft
+
"stackback@npm:0.0.2":
version: 0.0.2
resolution: "stackback@npm:0.0.2"
From 731c6fd14d9325fab4f75bf9abb9b13f43adc8d3 Mon Sep 17 00:00:00 2001
From: Gallay Lajos
Date: Sun, 1 Feb 2026 12:00:12 +0100
Subject: [PATCH 3/4] ci: add deployment infrastructure and boilerplate API
- Add check-changelog workflow for PR validation
- Add publish-to-dockerhub workflow for container deployment
- Add Dockerfile for containerized deployment
- Add boilerplate API client and types
- Update cursor rules formatting
- Simplify check-version-bump workflow
---
.cursor/rules/REST_SERVICE.mdc | 250 +++++++++---------
.cursor/rules/TESTING_GUIDELINES.mdc | 194 +++++++-------
.github/workflows/check-changelog.yml | 28 ++
.github/workflows/check-version-bump.yml | 1 -
.github/workflows/publish-to-dockerhub.yml | 107 ++++++++
.yarn/versions/2d4efe43.yml | 5 +
.yarn/versions/544e081e.yml | 5 +
.yarn/versions/5e7c1747.yml | 2 +
.yarn/versions/bf3e3cf4.yml | 5 +
.yarn/versions/cee96777.yml | 5 +
.yarn/versions/e54633dc.yml | 5 +
Dockerfile | 30 +++
common/src/boilerplate-api.ts | 20 ++
common/src/index.ts | 2 +-
.../src/services/boilerplate-api-client.ts | 15 ++
15 files changed, 451 insertions(+), 223 deletions(-)
create mode 100644 .github/workflows/check-changelog.yml
create mode 100644 .github/workflows/publish-to-dockerhub.yml
create mode 100644 .yarn/versions/2d4efe43.yml
create mode 100644 .yarn/versions/544e081e.yml
create mode 100644 .yarn/versions/5e7c1747.yml
create mode 100644 .yarn/versions/bf3e3cf4.yml
create mode 100644 .yarn/versions/cee96777.yml
create mode 100644 .yarn/versions/e54633dc.yml
create mode 100644 Dockerfile
create mode 100644 common/src/boilerplate-api.ts
create mode 100644 frontend/src/services/boilerplate-api-client.ts
diff --git a/.cursor/rules/REST_SERVICE.mdc b/.cursor/rules/REST_SERVICE.mdc
index 91d49d0..f15c84c 100644
--- a/.cursor/rules/REST_SERVICE.mdc
+++ b/.cursor/rules/REST_SERVICE.mdc
@@ -15,21 +15,17 @@ alwaysApply: false
The service entry point (`service/src/service.ts`) sets up the REST API:
```typescript
-import type { StackCraftApi } from 'common';
-import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' };
+import type { StackCraftApi } from 'common'
+import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' }
-import {
- useHttpAuthentication,
- useRestService,
- useStaticFiles,
-} from '@furystack/rest-service';
-import { injector } from './config.js';
+import { useHttpAuthentication, useRestService, useStaticFiles } from '@furystack/rest-service'
+import { injector } from './config.js'
// Set up authentication
useHttpAuthentication(injector, {
getUserStore: (sm) => sm.getStoreFor(User, 'username'),
getSessionStore: (sm) => sm.getStoreFor(DefaultSession, 'sessionId'),
-});
+})
// Set up REST API
useRestService({
@@ -37,10 +33,14 @@ useRestService({
root: 'api',
port,
api: {
- GET: { /* endpoints */ },
- POST: { /* endpoints */ },
+ GET: {
+ /* endpoints */
+ },
+ POST: {
+ /* endpoints */
+ },
},
-});
+})
// Serve static frontend files
useStaticFiles({
@@ -49,7 +49,7 @@ useStaticFiles({
path: '../frontend/dist',
port,
fallback: 'index.html',
-});
+})
```
## API Type Definition
@@ -59,32 +59,32 @@ useStaticFiles({
The API contract is defined in `common/src/stack-craft-api.ts`:
```typescript
-import type { RestApi } from '@furystack/rest';
-import type { User } from './models/index.js';
+import type { RestApi } from '@furystack/rest'
+import type { User } from './models/index.js'
// Define endpoint types
export type GetUserEndpoint = {
- url: { id: string };
- result: User;
-};
+ url: { id: string }
+ result: User
+}
export type CreateUserEndpoint = {
- body: { username: string; email: string };
- result: User;
-};
+ body: { username: string; email: string }
+ result: User
+}
// Define the API interface
export interface StackCraftApi extends RestApi {
GET: {
- '/isAuthenticated': { result: { isAuthenticated: boolean } };
- '/currentUser': { result: User };
- '/users/:id': GetUserEndpoint;
- };
+ '/isAuthenticated': { result: { isAuthenticated: boolean } }
+ '/currentUser': { result: User }
+ '/users/:id': GetUserEndpoint
+ }
POST: {
- '/login': { result: User; body: { username: string; password: string } };
- '/logout': { result: unknown };
- '/users': CreateUserEndpoint;
- };
+ '/login': { result: User; body: { username: string; password: string } }
+ '/logout': { result: unknown }
+ '/users': CreateUserEndpoint
+ }
}
```
@@ -95,12 +95,7 @@ export interface StackCraftApi extends RestApi {
Use FuryStack's built-in actions when possible:
```typescript
-import {
- GetCurrentUser,
- IsAuthenticated,
- LoginAction,
- LogoutAction,
-} from '@furystack/rest-service';
+import { GetCurrentUser, IsAuthenticated, LoginAction, LogoutAction } from '@furystack/rest-service'
useRestService({
injector,
@@ -114,7 +109,7 @@ useRestService({
'/logout': LogoutAction,
},
},
-});
+})
```
### Custom Endpoint Actions
@@ -122,29 +117,29 @@ useRestService({
Create custom actions with proper typing:
```typescript
-import { JsonResult, Validate } from '@furystack/rest-service';
+import { JsonResult, Validate } from '@furystack/rest-service'
useRestService({
injector,
api: {
GET: {
- '/testQuery': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestQueryEndpoint' })(
- async (options) => JsonResult({ param1Value: options.getQuery().param1 })
+ '/testQuery': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestQueryEndpoint' })(async (options) =>
+ JsonResult({ param1Value: options.getQuery().param1 }),
),
'/testUrlParams/:urlParam': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestUrlParamsEndpoint' })(
- async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam })
+ async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam }),
),
},
POST: {
'/testPostBody': Validate({ schema: StackCraftApiSchemas, schemaName: 'TestPostBodyEndpoint' })(
async (options) => {
- const body = await options.getBody();
- return JsonResult({ bodyValue: body.value });
- }
+ const body = await options.getBody()
+ return JsonResult({ bodyValue: body.value })
+ },
),
},
},
-});
+})
```
## Request Validation
@@ -154,17 +149,17 @@ useRestService({
Use the `Validate` wrapper with JSON schemas:
```typescript
-import { Validate, JsonResult } from '@furystack/rest-service';
-import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' };
+import { Validate, JsonResult } from '@furystack/rest-service'
+import StackCraftApiSchemas from 'common/schemas/stack-craft-api.json' with { type: 'json' }
const endpoint = Validate({
schema: StackCraftApiSchemas,
schemaName: 'MyEndpointType',
})(async (options) => {
// Request is validated before reaching here
- const body = await options.getBody();
- return JsonResult({ success: true });
-});
+ const body = await options.getBody()
+ return JsonResult({ success: true })
+})
```
### Generate Schemas
@@ -185,34 +180,40 @@ The schema generator is configured in `common/src/bin/create-schemas.ts`.
Set up the injector with stores and services in `service/src/config.ts`:
```typescript
-import { addStore, InMemoryStore } from '@furystack/core';
-import { FileSystemStore } from '@furystack/filesystem-store';
-import { Injector } from '@furystack/inject';
-import { useLogging, VerboseConsoleLogger } from '@furystack/logging';
-import { getRepository } from '@furystack/repository';
-import { usePasswordPolicy } from '@furystack/security';
-import { DefaultSession } from '@furystack/rest-service';
-import { User } from 'common';
+import { addStore, InMemoryStore } from '@furystack/core'
+import { FileSystemStore } from '@furystack/filesystem-store'
+import { Injector } from '@furystack/inject'
+import { useLogging, VerboseConsoleLogger } from '@furystack/logging'
+import { getRepository } from '@furystack/repository'
+import { usePasswordPolicy } from '@furystack/security'
+import { DefaultSession } from '@furystack/rest-service'
+import { User } from 'common'
-export const injector = new Injector();
+export const injector = new Injector()
// Set up logging
-useLogging(injector, VerboseConsoleLogger);
+useLogging(injector, VerboseConsoleLogger)
// Add stores
-addStore(injector, new FileSystemStore({
- model: User,
- primaryKey: 'username',
- fileName: join(process.cwd(), 'users.json'),
-}));
-
-addStore(injector, new InMemoryStore({
- model: DefaultSession,
- primaryKey: 'sessionId',
-}));
+addStore(
+ injector,
+ new FileSystemStore({
+ model: User,
+ primaryKey: 'username',
+ fileName: join(process.cwd(), 'users.json'),
+ }),
+)
+
+addStore(
+ injector,
+ new InMemoryStore({
+ model: DefaultSession,
+ primaryKey: 'sessionId',
+ }),
+)
// Set up password policy
-usePasswordPolicy(injector);
+usePasswordPolicy(injector)
```
### Authorization
@@ -220,22 +221,20 @@ usePasswordPolicy(injector);
Define authorization functions for data sets:
```typescript
-import { isAuthenticated } from '@furystack/core';
-import type { AuthorizationResult, DataSetSettings } from '@furystack/repository';
+import { isAuthenticated } from '@furystack/core'
+import type { AuthorizationResult, DataSetSettings } from '@furystack/repository'
export const authorizedOnly = async (options: { injector: Injector }): Promise => {
- const isAllowed = await isAuthenticated(options.injector);
- return isAllowed
- ? { isAllowed }
- : { isAllowed, message: 'You are not authorized' };
-};
+ const isAllowed = await isAuthenticated(options.injector)
+ return isAllowed ? { isAllowed } : { isAllowed, message: 'You are not authorized' }
+}
export const authorizedDataSet: Partial> = {
authorizeAdd: authorizedOnly,
authorizeGet: authorizedOnly,
authorizeRemove: authorizedOnly,
authorizeUpdate: authorizedOnly,
-};
+}
```
## Store Types
@@ -245,14 +244,17 @@ export const authorizedDataSet: Partial> = {
Use for persistent data stored as JSON files:
```typescript
-import { FileSystemStore } from '@furystack/filesystem-store';
-
-addStore(injector, new FileSystemStore({
- model: User,
- primaryKey: 'username',
- tickMs: 30 * 1000, // Save interval
- fileName: join(process.cwd(), 'users.json'),
-}));
+import { FileSystemStore } from '@furystack/filesystem-store'
+
+addStore(
+ injector,
+ new FileSystemStore({
+ model: User,
+ primaryKey: 'username',
+ tickMs: 30 * 1000, // Save interval
+ fileName: join(process.cwd(), 'users.json'),
+ }),
+)
```
### InMemoryStore
@@ -260,12 +262,15 @@ addStore(injector, new FileSystemStore({
Use for session data or temporary storage:
```typescript
-import { InMemoryStore } from '@furystack/core';
+import { InMemoryStore } from '@furystack/core'
-addStore(injector, new InMemoryStore({
- model: DefaultSession,
- primaryKey: 'sessionId',
-}));
+addStore(
+ injector,
+ new InMemoryStore({
+ model: DefaultSession,
+ primaryKey: 'sessionId',
+ }),
+)
```
## Error Handling
@@ -279,9 +284,9 @@ useRestService({
injector,
// ... config
}).catch((err) => {
- console.error(err);
- process.exit(1);
-});
+ console.error(err)
+ process.exit(1)
+})
```
### Graceful Shutdown
@@ -291,20 +296,20 @@ Implement graceful shutdown handling:
```typescript
// service/src/shutdown-handler.ts
export const attachShutdownHandler = async (injector: Injector): Promise => {
- const logger = getLogger(injector).withScope('ShutdownHandler');
-
+ const logger = getLogger(injector).withScope('ShutdownHandler')
+
const shutdown = async (signal: string) => {
- await logger.information({ message: `Received ${signal}, shutting down...` });
- await injector[Symbol.asyncDispose]();
- process.exit(0);
- };
-
- process.on('SIGTERM', () => void shutdown('SIGTERM'));
- process.on('SIGINT', () => void shutdown('SIGINT'));
-};
+ await logger.information({ message: `Received ${signal}, shutting down...` })
+ await injector[Symbol.asyncDispose]()
+ process.exit(0)
+ }
+
+ process.on('SIGTERM', () => void shutdown('SIGTERM'))
+ process.on('SIGINT', () => void shutdown('SIGINT'))
+}
// In service.ts
-void attachShutdownHandler(injector);
+void attachShutdownHandler(injector)
```
## Data Seeding
@@ -315,29 +320,26 @@ Create a seed script for initial data:
```typescript
// service/src/seed.ts
-import { StoreManager } from '@furystack/core';
-import { PasswordAuthenticator, PasswordCredential } from '@furystack/security';
-import { User } from 'common';
-import { injector } from './config.js';
+import { StoreManager } from '@furystack/core'
+import { PasswordAuthenticator, PasswordCredential } from '@furystack/security'
+import { User } from 'common'
+import { injector } from './config.js'
export const seed = async (i: Injector): Promise => {
- const sm = i.getInstance(StoreManager);
- const userStore = sm.getStoreFor(User, 'username');
- const pwcStore = sm.getStoreFor(PasswordCredential, 'userName');
-
+ const sm = i.getInstance(StoreManager)
+ const userStore = sm.getStoreFor(User, 'username')
+ const pwcStore = sm.getStoreFor(PasswordCredential, 'userName')
+
// Create default user credentials
- const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential(
- 'testuser',
- 'password'
- );
-
+ const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential('testuser', 'password')
+
// Save to stores
- await pwcStore.add(cred);
- await userStore.add({ username: 'testuser', roles: [] });
-};
+ await pwcStore.add(cred)
+ await userStore.add({ username: 'testuser', roles: [] })
+}
-await seed(injector);
-await injector[Symbol.asyncDispose]();
+await seed(injector)
+await injector[Symbol.asyncDispose]()
```
Run with:
@@ -361,7 +363,7 @@ useRestService({
headers: ['cache', 'content-type'],
},
// ... rest of config
-});
+})
```
## Summary
diff --git a/.cursor/rules/TESTING_GUIDELINES.mdc b/.cursor/rules/TESTING_GUIDELINES.mdc
index 7fe6e1d..498237c 100644
--- a/.cursor/rules/TESTING_GUIDELINES.mdc
+++ b/.cursor/rules/TESTING_GUIDELINES.mdc
@@ -18,7 +18,7 @@ The project uses Vitest for unit testing with workspace-based configuration:
```typescript
// vitest.config.mts
-import { defineConfig } from 'vitest/config';
+import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
@@ -48,7 +48,7 @@ export default defineConfig({
},
],
},
-});
+})
```
### Playwright for E2E Tests
@@ -57,8 +57,8 @@ E2E tests use Playwright with the service auto-started:
```typescript
// playwright.config.ts
-import type { PlaywrightTestConfig } from '@playwright/test';
-import { devices } from '@playwright/test';
+import type { PlaywrightTestConfig } from '@playwright/test'
+import { devices } from '@playwright/test'
const config: PlaywrightTestConfig = {
testDir: 'e2e',
@@ -76,7 +76,7 @@ const config: PlaywrightTestConfig = {
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
],
-};
+}
```
## Unit Testing with Vitest
@@ -98,22 +98,22 @@ service/src/
Use `describe`, `it`, and `expect` from Vitest:
```typescript
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
describe('MyService', () => {
describe('methodName', () => {
it('should do something when condition', () => {
// Arrange
- const input = 'test';
-
+ const input = 'test'
+
// Act
- const result = myFunction(input);
-
+ const result = myFunction(input)
+
// Assert
- expect(result).toBe('expected');
- });
- });
-});
+ expect(result).toBe('expected')
+ })
+ })
+})
```
### Testing Services
@@ -121,27 +121,27 @@ describe('MyService', () => {
Test service methods with mocked dependencies:
```typescript
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { Injector } from '@furystack/inject';
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { Injector } from '@furystack/inject'
describe('SessionService', () => {
- let injector: Injector;
- let sessionService: SessionService;
-
+ let injector: Injector
+ let sessionService: SessionService
+
beforeEach(() => {
- injector = new Injector();
+ injector = new Injector()
// Set up mocks
const mockApiClient = {
call: vi.fn(),
- };
- injector.setExplicitInstance(StackCraftApiClient, mockApiClient);
- sessionService = injector.getInstance(SessionService);
- });
-
+ }
+ injector.setExplicitInstance(StackCraftApiClient, mockApiClient)
+ sessionService = injector.getInstance(SessionService)
+ })
+
it('should initialize with unauthenticated state', async () => {
- expect(sessionService.state.getValue()).toBe('initializing');
- });
-});
+ expect(sessionService.state.getValue()).toBe('initializing')
+ })
+})
```
### Mocking with Vitest
@@ -149,24 +149,24 @@ describe('SessionService', () => {
Use `vi.fn()` for function mocks and `vi.spyOn()` for spying:
```typescript
-import { vi } from 'vitest';
+import { vi } from 'vitest'
// Mock a function
-const mockFn = vi.fn().mockReturnValue('mocked');
+const mockFn = vi.fn().mockReturnValue('mocked')
// Mock with implementation
-const mockFn = vi.fn().mockImplementation((arg) => `result: ${arg}`);
+const mockFn = vi.fn().mockImplementation((arg) => `result: ${arg}`)
// Mock async function
-const mockAsync = vi.fn().mockResolvedValue({ data: 'test' });
+const mockAsync = vi.fn().mockResolvedValue({ data: 'test' })
// Spy on method
-const spy = vi.spyOn(service, 'method');
+const spy = vi.spyOn(service, 'method')
// Verify calls
-expect(mockFn).toHaveBeenCalled();
-expect(mockFn).toHaveBeenCalledWith('arg');
-expect(mockFn).toHaveBeenCalledTimes(2);
+expect(mockFn).toHaveBeenCalled()
+expect(mockFn).toHaveBeenCalledWith('arg')
+expect(mockFn).toHaveBeenCalledTimes(2)
```
### Testing Observable Values
@@ -174,21 +174,21 @@ expect(mockFn).toHaveBeenCalledTimes(2);
Test ObservableValue subscriptions:
```typescript
-import { ObservableValue } from '@furystack/utils';
+import { ObservableValue } from '@furystack/utils'
describe('ObservableValue', () => {
it('should notify subscribers on value change', () => {
- const observable = new ObservableValue('initial');
- const values: string[] = [];
-
- const subscription = observable.subscribe((value) => values.push(value));
- observable.setValue('updated');
-
- expect(values).toEqual(['initial', 'updated']);
-
- subscription.dispose();
- });
-});
+ const observable = new ObservableValue('initial')
+ const values: string[] = []
+
+ const subscription = observable.subscribe((value) => values.push(value))
+ observable.setValue('updated')
+
+ expect(values).toEqual(['initial', 'updated'])
+
+ subscription.dispose()
+ })
+})
```
## E2E Testing with Playwright
@@ -209,26 +209,26 @@ e2e/
Use Playwright's test API:
```typescript
-import { expect, test } from '@playwright/test';
+import { expect, test } from '@playwright/test'
test.describe('Feature Name', () => {
test('should do something', async ({ page }) => {
// Navigate
- await page.goto('/');
-
+ await page.goto('/')
+
// Find elements
- const element = page.locator('selector');
-
+ const element = page.locator('selector')
+
// Assert visibility
- await expect(element).toBeVisible();
-
+ await expect(element).toBeVisible()
+
// Interact
- await element.click();
-
+ await element.click()
+
// Assert result
- await expect(page.locator('.result')).toHaveText('Expected');
- });
-});
+ await expect(page.locator('.result')).toHaveText('Expected')
+ })
+})
```
### Locating Shades Components
@@ -238,21 +238,21 @@ Use shadow DOM component names as selectors:
```typescript
test('should interact with Shades components', async ({ page }) => {
// Locate by shadow DOM name
- const loginForm = page.locator('shade-login form');
- await expect(loginForm).toBeVisible();
-
+ const loginForm = page.locator('shade-login form')
+ await expect(loginForm).toBeVisible()
+
// Locate inputs within components
- const usernameInput = loginForm.locator('input[name="userName"]');
- const passwordInput = loginForm.locator('input[name="password"]');
-
+ const usernameInput = loginForm.locator('input[name="userName"]')
+ const passwordInput = loginForm.locator('input[name="password"]')
+
// Fill inputs
- await usernameInput.type('testuser');
- await passwordInput.type('password');
-
+ await usernameInput.type('testuser')
+ await passwordInput.type('password')
+
// Click buttons
- const submitButton = page.locator('button', { hasText: 'Login' });
- await submitButton.click();
-});
+ const submitButton = page.locator('button', { hasText: 'Login' })
+ await submitButton.click()
+})
```
### Authentication Flow Test
@@ -260,36 +260,36 @@ test('should interact with Shades components', async ({ page }) => {
Example of testing login/logout:
```typescript
-import { expect, test } from '@playwright/test';
+import { expect, test } from '@playwright/test'
test.describe('Authentication', () => {
test('Login and logout roundtrip', async ({ page }) => {
- await page.goto('/');
-
+ await page.goto('/')
+
// Wait for login form
- const loginForm = page.locator('shade-login form');
- await expect(loginForm).toBeVisible();
-
+ const loginForm = page.locator('shade-login form')
+ await expect(loginForm).toBeVisible()
+
// Fill credentials
- await loginForm.locator('input[name="userName"]').type('testuser');
- await loginForm.locator('input[name="password"]').type('password');
-
+ await loginForm.locator('input[name="userName"]').type('testuser')
+ await loginForm.locator('input[name="password"]').type('password')
+
// Submit
- await page.locator('button', { hasText: 'Login' }).click();
-
+ await page.locator('button', { hasText: 'Login' }).click()
+
// Verify logged in state
- const welcomeTitle = page.locator('hello-world div h2');
- await expect(welcomeTitle).toBeVisible();
- await expect(welcomeTitle).toHaveText('Hello, testuser !');
-
+ const welcomeTitle = page.locator('hello-world div h2')
+ await expect(welcomeTitle).toBeVisible()
+ await expect(welcomeTitle).toHaveText('Hello, testuser !')
+
// Logout
- const logoutButton = page.locator('shade-app-bar button >> text="Log Out"');
- await logoutButton.click();
-
+ const logoutButton = page.locator('shade-app-bar button >> text="Log Out"')
+ await logoutButton.click()
+
// Verify logged out
- await expect(page.locator('shade-login form')).toBeVisible();
- });
-});
+ await expect(page.locator('shade-login form')).toBeVisible()
+ })
+})
```
### Waiting for Elements
@@ -298,14 +298,14 @@ Use Playwright's auto-waiting or explicit waits:
```typescript
// Auto-wait (recommended)
-await expect(element).toBeVisible();
+await expect(element).toBeVisible()
// Explicit wait
-await page.waitForSelector('selector');
-await page.waitForLoadState('networkidle');
+await page.waitForSelector('selector')
+await page.waitForLoadState('networkidle')
// Wait for response
-await page.waitForResponse('**/api/endpoint');
+await page.waitForResponse('**/api/endpoint')
```
## Running Tests
diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml
new file mode 100644
index 0000000..29c231c
--- /dev/null
+++ b/.github/workflows/check-changelog.yml
@@ -0,0 +1,28 @@
+name: Changelog checks
+on:
+ push:
+ branches-ignore:
+ - 'release/**'
+ - 'master'
+ - 'develop'
+ pull_request:
+ branches:
+ - develop
+jobs:
+ check:
+ name: Check changelog completion
+ timeout-minutes: 5
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+ - name: Check changelog entries
+ run: yarn changelog check
+ env:
+ CI: true
diff --git a/.github/workflows/check-version-bump.yml b/.github/workflows/check-version-bump.yml
index e95671c..27b86e2 100644
--- a/.github/workflows/check-version-bump.yml
+++ b/.github/workflows/check-version-bump.yml
@@ -25,7 +25,6 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Check version bumps
- continue-on-error: true ## Set this to false once versioning has been set up
run: yarn version check
env:
CI: true
diff --git a/.github/workflows/publish-to-dockerhub.yml b/.github/workflows/publish-to-dockerhub.yml
new file mode 100644
index 0000000..c3d502c
--- /dev/null
+++ b/.github/workflows/publish-to-dockerhub.yml
@@ -0,0 +1,107 @@
+name: Release to Docker Hub
+
+on:
+ workflow_dispatch:
+ # No inputs - when you trigger, it releases
+
+permissions:
+ contents: write # Push branches/tags
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ # Only allow running from master branch
+ if: github.ref == 'refs/heads/master'
+
+ steps:
+ - uses: actions/create-github-app-token@v1
+ id: app-token
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
+ - name: Checkout master branch
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ fetch-depth: 0 # Full history for merging
+ token: ${{ steps.app-token.outputs.token }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+
+ - name: Install dependencies
+ run: yarn install --immutable
+
+ - name: Build
+ run: yarn build
+
+ - name: Lint
+ run: yarn lint
+
+ - name: Test
+ run: yarn test:unit
+
+ - name: Apply release changes
+ run: yarn applyReleaseChanges
+
+ - name: Get version from package.json
+ id: version
+ run: |
+ VERSION=$(node -p "require('./package.json').version")
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Release version: $VERSION"
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Commit changes if any
+ run: |
+ git add -A
+ if git diff --staged --quiet; then
+ echo "No changes to commit"
+ else
+ git commit -m "chore: release v${{ steps.version.outputs.version }}"
+ fi
+
+ - name: Push master branch
+ run: git push origin master
+
+ - name: Sync to develop
+ run: |
+ git checkout develop
+ git merge master --no-edit
+ git push origin develop
+
+ - name: Create and push release tag
+ continue-on-error: true
+ run: |
+ git tag "v${{ steps.version.outputs.version }}"
+ git push origin "v${{ steps.version.outputs.version }}"
+
+ - name: Docker login
+ env:
+ DOCKER_USER: ${{ secrets.DOCKER_USER }}
+ DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+ run: |
+ docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ install: true
+
+ - name: Build and push Docker image
+ run: |
+ docker build --platform linux/arm64,linux/amd64 \
+ --tag furystack/stack-craft:v${{ steps.version.outputs.version }} \
+ --tag furystack/stack-craft:latest \
+ . --push
diff --git a/.yarn/versions/2d4efe43.yml b/.yarn/versions/2d4efe43.yml
new file mode 100644
index 0000000..b833b78
--- /dev/null
+++ b/.yarn/versions/2d4efe43.yml
@@ -0,0 +1,5 @@
+releases:
+ common: patch
+ frontend: patch
+ stack-craft: patch
+ service: patch
diff --git a/.yarn/versions/544e081e.yml b/.yarn/versions/544e081e.yml
new file mode 100644
index 0000000..b833b78
--- /dev/null
+++ b/.yarn/versions/544e081e.yml
@@ -0,0 +1,5 @@
+releases:
+ common: patch
+ frontend: patch
+ stack-craft: patch
+ service: patch
diff --git a/.yarn/versions/5e7c1747.yml b/.yarn/versions/5e7c1747.yml
new file mode 100644
index 0000000..f885e24
--- /dev/null
+++ b/.yarn/versions/5e7c1747.yml
@@ -0,0 +1,2 @@
+releases:
+ stack-craft: patch
diff --git a/.yarn/versions/bf3e3cf4.yml b/.yarn/versions/bf3e3cf4.yml
new file mode 100644
index 0000000..b833b78
--- /dev/null
+++ b/.yarn/versions/bf3e3cf4.yml
@@ -0,0 +1,5 @@
+releases:
+ common: patch
+ frontend: patch
+ stack-craft: patch
+ service: patch
diff --git a/.yarn/versions/cee96777.yml b/.yarn/versions/cee96777.yml
new file mode 100644
index 0000000..b833b78
--- /dev/null
+++ b/.yarn/versions/cee96777.yml
@@ -0,0 +1,5 @@
+releases:
+ common: patch
+ frontend: patch
+ stack-craft: patch
+ service: patch
diff --git a/.yarn/versions/e54633dc.yml b/.yarn/versions/e54633dc.yml
new file mode 100644
index 0000000..b833b78
--- /dev/null
+++ b/.yarn/versions/e54633dc.yml
@@ -0,0 +1,5 @@
+releases:
+ common: patch
+ frontend: patch
+ stack-craft: patch
+ service: patch
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3865221
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,30 @@
+FROM node:lts-alpine as base
+
+COPY --chown=node:node /common /home/node/app/common
+COPY --chown=node:node /frontend /home/node/app/frontend
+COPY --chown=node:node /service /home/node/app/service
+
+COPY --chown=node:node /package.json /home/node/app/package.json
+COPY --chown=node:node /.yarn/releases /home/node/app/.yarn/releases
+
+COPY --chown=node:node /yarn.lock /home/node/app/yarn.lock
+COPY --chown=node:node /tsconfig.json /home/node/app/tsconfig.json
+COPY --chown=node:node /.yarnrc.yml /home/node/app/.yarnrc.yml
+
+WORKDIR /home/node/app
+
+RUN yarn workspaces focus service --production
+
+FROM node:lts-alpine as runner
+
+RUN apk upgrade -U \
+ && rm -rf /var/cache/*
+
+COPY --chown=node:node --from=base /home/node/app /home/node/app
+
+USER node
+
+EXPOSE 9090
+WORKDIR /home/node/app
+
+ENTRYPOINT ["yarn", "start:service"]
diff --git a/common/src/boilerplate-api.ts b/common/src/boilerplate-api.ts
new file mode 100644
index 0000000..39380ad
--- /dev/null
+++ b/common/src/boilerplate-api.ts
@@ -0,0 +1,20 @@
+import type { RestApi } from '@furystack/rest'
+import type { User } from './models/index.js'
+
+export type TestQueryEndpoint = { query: { param1: string }; result: { param1Value: string } }
+export type TestUrlParamsEndpoint = { url: { urlParam: string }; result: { urlParamValue: string } }
+export type TestPostBodyEndpoint = { body: { value: string }; result: { bodyValue: string } }
+
+export interface StackCraftApi extends RestApi {
+ GET: {
+ '/isAuthenticated': { result: { isAuthenticated: boolean } }
+ '/currentUser': { result: User }
+ '/testQuery': TestQueryEndpoint
+ '/testUrlParams/:urlParam': TestUrlParamsEndpoint
+ }
+ POST: {
+ '/login': { result: User; body: { username: string; password: string } }
+ '/logout': { result: unknown }
+ '/testPostBody': TestPostBodyEndpoint
+ }
+}
diff --git a/common/src/index.ts b/common/src/index.ts
index 0105f60..227ee11 100644
--- a/common/src/index.ts
+++ b/common/src/index.ts
@@ -1,2 +1,2 @@
-export * from './stack-craft-api.js'
export * from './models/index.js'
+export * from './stack-craft-api.js'
diff --git a/frontend/src/services/boilerplate-api-client.ts b/frontend/src/services/boilerplate-api-client.ts
new file mode 100644
index 0000000..364495c
--- /dev/null
+++ b/frontend/src/services/boilerplate-api-client.ts
@@ -0,0 +1,15 @@
+import { Injectable } from '@furystack/inject'
+import { createClient } from '@furystack/rest-client-fetch'
+import type { StackCraftApi } from 'common'
+import { environmentOptions } from '../environment-options.js'
+
+@Injectable({ lifetime: 'singleton' })
+export class StackCraftApiClient {
+ public call = createClient({
+ endpointUrl: environmentOptions.serviceUrl,
+ requestInit: {
+ credentials: 'include',
+ mode: 'cors',
+ },
+ })
+}
From 50fffec701736ab5f88f11633bd1ff87129c0427 Mon Sep 17 00:00:00 2001
From: Gallay Lajos
Date: Sun, 1 Feb 2026 15:50:47 +0100
Subject: [PATCH 4/4] deploy workflow fix
---
.github/workflows/publish-to-dockerhub.yml | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/publish-to-dockerhub.yml b/.github/workflows/publish-to-dockerhub.yml
index c3d502c..62e3153 100644
--- a/.github/workflows/publish-to-dockerhub.yml
+++ b/.github/workflows/publish-to-dockerhub.yml
@@ -11,8 +11,8 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
- # Only allow running from master branch
- if: github.ref == 'refs/heads/master'
+ # Only allow running from develop branch
+ if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/create-github-app-token@v1
@@ -21,10 +21,10 @@ jobs:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- - name: Checkout master branch
+ - name: Checkout develop branch
uses: actions/checkout@v4
with:
- ref: master
+ ref: develop
fetch-depth: 0 # Full history for merging
token: ${{ steps.app-token.outputs.token }}
@@ -69,14 +69,14 @@ jobs:
git commit -m "chore: release v${{ steps.version.outputs.version }}"
fi
- - name: Push master branch
- run: git push origin master
+ - name: Push develop branch
+ run: git push origin develop
- - name: Sync to develop
+ - name: Merge develop to master
run: |
- git checkout develop
- git merge master --no-edit
- git push origin develop
+ git checkout master
+ git merge develop --no-edit
+ git push origin master
- name: Create and push release tag
continue-on-error: true