diff --git a/.cursor/rules/REST_SERVICE.mdc b/.cursor/rules/REST_SERVICE.mdc index 40ffda8..f15c84c 100644 --- a/.cursor/rules/REST_SERVICE.mdc +++ b/.cursor/rules/REST_SERVICE.mdc @@ -15,32 +15,32 @@ 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, - 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({ +useRestService({ injector, root: 'api', port, api: { - GET: { /* endpoints */ }, - POST: { /* endpoints */ }, + GET: { + /* endpoints */ + }, + POST: { + /* endpoints */ + }, }, -}); +}) // Serve static frontend files useStaticFiles({ @@ -49,42 +49,42 @@ useStaticFiles({ path: '../frontend/dist', port, fallback: 'index.html', -}); +}) ``` ## API Type Definition ### 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'; -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 BoilerplateApi extends RestApi { +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,14 +95,9 @@ export interface BoilerplateApi extends RestApi { Use FuryStack's built-in actions when possible: ```typescript -import { - GetCurrentUser, - IsAuthenticated, - LoginAction, - LogoutAction, -} from '@furystack/rest-service'; - -useRestService({ +import { GetCurrentUser, IsAuthenticated, LoginAction, LogoutAction } from '@furystack/rest-service' + +useRestService({ injector, api: { GET: { @@ -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({ +useRestService({ injector, api: { GET: { - '/testQuery': Validate({ schema: BoilerplateApiSchemas, 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: BoilerplateApiSchemas, schemaName: 'TestUrlParamsEndpoint' })( - async (options) => JsonResult({ urlParamValue: options.getUrlParams().urlParam }) + '/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 }); - } + 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 BoilerplateApiSchemas from 'common/schemas/boilerplate-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: BoilerplateApiSchemas, + 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 @@ -275,13 +280,13 @@ addStore(injector, new InMemoryStore({ Handle errors during service startup: ```typescript -useRestService({ +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: @@ -353,7 +355,7 @@ yarn seed Set up CORS for frontend access: ```typescript -useRestService({ +useRestService({ injector, cors: { credentials: true, @@ -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 18cdf72..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(BoilerplateApiClient, 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/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-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 cf3df5d..27b86e2 100644 --- a/.github/workflows/check-version-bump.yml +++ b/.github/workflows/check-version-bump.yml @@ -21,11 +21,10 @@ 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 - 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..62e3153 --- /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 develop branch + if: github.ref == 'refs/heads/develop' + + 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 develop branch + uses: actions/checkout@v4 + with: + ref: develop + 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 develop branch + run: git push origin develop + + - name: Merge develop to master + run: | + git checkout master + git merge develop --no-edit + git push origin master + + - 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/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 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/.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/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/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 99% rename from common/schemas/boilerplate-api.json rename to common/schemas/stack-craft-api.json index 9e239dc..525aff7 100644 --- a/common/schemas/boilerplate-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/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/boilerplate-api.ts b/common/src/boilerplate-api.ts index ff2299b..39380ad 100644 --- a/common/src/boilerplate-api.ts +++ b/common/src/boilerplate-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/common/src/index.ts b/common/src/index.ts index 04ca3ab..227ee11 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -1,2 +1,2 @@ -export * from './boilerplate-api.js' export * from './models/index.js' +export * from './stack-craft-api.js' diff --git a/common/src/stack-craft-api.ts b/common/src/stack-craft-api.ts new file mode 100644 index 0000000..39380ad --- /dev/null +++ b/common/src/stack-craft-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/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/frontend/src/services/boilerplate-api-client.ts b/frontend/src/services/boilerplate-api-client.ts index bfd002d..364495c 100644 --- a/frontend/src/services/boilerplate-api-client.ts +++ b/frontend/src/services/boilerplate-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/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/stack-craft-api-client.ts b/frontend/src/services/stack-craft-api-client.ts new file mode 100644 index 0000000..364495c --- /dev/null +++ b/frontend/src/services/stack-craft-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', + }, + }) +} 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 }) 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"