diff --git a/apps/chat/apps/web/Dockerfile b/apps/chat/apps/web/Dockerfile index 7b6358aa3..6cb765e14 100644 --- a/apps/chat/apps/web/Dockerfile +++ b/apps/chat/apps/web/Dockerfile @@ -43,6 +43,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config COPY packages/shared-api-client ./packages/shared-api-client COPY packages/shared-stores ./packages/shared-stores COPY packages/shared-app-onboarding ./packages/shared-app-onboarding +COPY packages/shared-pwa ./packages/shared-pwa + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ # Copy chat packages COPY apps/chat/packages ./apps/chat/packages diff --git a/apps/clock/apps/web/Dockerfile b/apps/clock/apps/web/Dockerfile index 50590255d..c71d424e7 100644 --- a/apps/clock/apps/web/Dockerfile +++ b/apps/clock/apps/web/Dockerfile @@ -43,6 +43,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config COPY packages/shared-api-client ./packages/shared-api-client COPY packages/shared-stores ./packages/shared-stores COPY packages/shared-app-onboarding ./packages/shared-app-onboarding +COPY packages/shared-pwa ./packages/shared-pwa + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ # Copy clock packages and web COPY apps/clock/packages ./apps/clock/packages diff --git a/apps/nutriphi/apps/web/Dockerfile b/apps/nutriphi/apps/web/Dockerfile index 3b719d044..469d062c6 100644 --- a/apps/nutriphi/apps/web/Dockerfile +++ b/apps/nutriphi/apps/web/Dockerfile @@ -42,6 +42,10 @@ COPY packages/shared-utils ./packages/shared-utils COPY packages/shared-error-tracking ./packages/shared-error-tracking COPY packages/shared-vite-config ./packages/shared-vite-config COPY packages/shared-tsconfig ./packages/shared-tsconfig +COPY packages/shared-pwa ./packages/shared-pwa + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ # Copy nutriphi packages and web COPY apps/nutriphi/packages ./apps/nutriphi/packages diff --git a/apps/photos/apps/web/Dockerfile b/apps/photos/apps/web/Dockerfile index 4a5816064..9e60a6bec 100644 --- a/apps/photos/apps/web/Dockerfile +++ b/apps/photos/apps/web/Dockerfile @@ -50,6 +50,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config COPY packages/shared-api-client ./packages/shared-api-client COPY packages/shared-stores ./packages/shared-stores COPY packages/shared-app-onboarding ./packages/shared-app-onboarding +COPY packages/shared-pwa ./packages/shared-pwa + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ # Copy photos shared package COPY apps/photos/packages/shared ./apps/photos/packages/shared diff --git a/apps/playground/apps/web/Dockerfile b/apps/playground/apps/web/Dockerfile index a283893f5..0b40a0a5d 100644 --- a/apps/playground/apps/web/Dockerfile +++ b/apps/playground/apps/web/Dockerfile @@ -27,6 +27,9 @@ COPY packages/shared-branding ./packages/shared-branding COPY packages/shared-i18n ./packages/shared-i18n COPY packages/shared-icons ./packages/shared-icons +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ + # Copy llm-playground service COPY apps/playground/apps/web ./apps/playground/apps/web diff --git a/apps/presi/apps/web/Dockerfile b/apps/presi/apps/web/Dockerfile index 6e7a1c907..90c77b685 100644 --- a/apps/presi/apps/web/Dockerfile +++ b/apps/presi/apps/web/Dockerfile @@ -43,6 +43,14 @@ COPY packages/shared-ui ./packages/shared-ui COPY packages/shared-error-tracking ./packages/shared-error-tracking COPY packages/shared-utils ./packages/shared-utils COPY packages/shared-app-onboarding ./packages/shared-app-onboarding +COPY packages/shared-pwa ./packages/shared-pwa +COPY packages/shared-vite-config ./packages/shared-vite-config + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ + +# Copy presi shared package +COPY apps/presi/packages/shared ./apps/presi/packages/shared # Copy presi web COPY apps/presi/apps/web ./apps/presi/apps/web diff --git a/apps/skilltree/apps/web/Dockerfile b/apps/skilltree/apps/web/Dockerfile index 2e642dbe7..0e417c589 100644 --- a/apps/skilltree/apps/web/Dockerfile +++ b/apps/skilltree/apps/web/Dockerfile @@ -34,6 +34,11 @@ COPY packages/shared-error-tracking ./packages/shared-error-tracking COPY packages/shared-vite-config ./packages/shared-vite-config COPY packages/shared-api-client ./packages/shared-api-client COPY packages/shared-app-onboarding ./packages/shared-app-onboarding +COPY packages/shared-ui ./packages/shared-ui +COPY packages/shared-pwa ./packages/shared-pwa + +# Copy patches (referenced by pnpm-lock.yaml) +COPY patches/ ./patches/ # Copy skilltree web COPY apps/skilltree/apps/web ./apps/skilltree/apps/web diff --git a/apps/todo/apps/backend/src/task/dto/index.ts b/apps/todo/apps/backend/src/task/dto/index.ts index b98414aad..2b34f860c 100644 --- a/apps/todo/apps/backend/src/task/dto/index.ts +++ b/apps/todo/apps/backend/src/task/dto/index.ts @@ -1,3 +1,4 @@ export * from './create-task.dto'; export * from './update-task.dto'; export * from './query-tasks.dto'; +export * from './reorder-tasks.dto'; diff --git a/apps/todo/apps/backend/src/task/dto/reorder-tasks.dto.ts b/apps/todo/apps/backend/src/task/dto/reorder-tasks.dto.ts new file mode 100644 index 000000000..7d93b3669 --- /dev/null +++ b/apps/todo/apps/backend/src/task/dto/reorder-tasks.dto.ts @@ -0,0 +1,11 @@ +import { IsArray, IsString, IsOptional } from 'class-validator'; + +export class ReorderTasksDto { + @IsArray() + @IsString({ each: true }) + taskIds: string[]; + + @IsOptional() + @IsString() + projectId?: string | null; +} diff --git a/apps/todo/apps/backend/src/task/task.controller.ts b/apps/todo/apps/backend/src/task/task.controller.ts index d384a2f1e..1e3996d52 100644 --- a/apps/todo/apps/backend/src/task/task.controller.ts +++ b/apps/todo/apps/backend/src/task/task.controller.ts @@ -3,7 +3,7 @@ import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nes import { UseCredits } from '@manacore/nestjs-integration'; import { CreditOperationType } from '@manacore/credit-operations'; import { TaskService } from './task.service'; -import { CreateTaskDto, UpdateTaskDto, QueryTasksDto } from './dto'; +import { CreateTaskDto, UpdateTaskDto, QueryTasksDto, ReorderTasksDto } from './dto'; @Controller('tasks') @UseGuards(JwtAuthGuard) @@ -121,12 +121,8 @@ export class TaskController { } @Put('reorder') - async reorder( - @CurrentUser() user: CurrentUserData, - @Body('taskIds') taskIds: string[], - @Body('projectId') projectId?: string | null - ) { - const tasks = await this.taskService.reorder(user.userId, taskIds, projectId); + async reorder(@CurrentUser() user: CurrentUserData, @Body() dto: ReorderTasksDto) { + const tasks = await this.taskService.reorder(user.userId, dto.taskIds, dto.projectId); return { tasks }; } } diff --git a/package.json b/package.json index 7fd2a71e5..145d9f976 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "clean": "turbo run clean", "format": "prettier --config .prettierrc.json --write \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"", "format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"", + "validate:dockerfiles": "node scripts/validate-dockerfiles.mjs", "setup:env": "node scripts/generate-env.mjs", "setup:db": "./scripts/setup-databases.sh", "setup:db:chat": "./scripts/setup-databases.sh chat", diff --git a/scripts/validate-dockerfiles.mjs b/scripts/validate-dockerfiles.mjs new file mode 100755 index 000000000..0d4bb1a93 --- /dev/null +++ b/scripts/validate-dockerfiles.mjs @@ -0,0 +1,238 @@ +#!/usr/bin/env node + +/** + * Validate that all web app Dockerfiles include COPY statements + * for every workspace dependency listed in package.json. + */ + +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +const ROOT = resolve(import.meta.dirname, '..'); + +// Build a map of package name -> directory path (relative to repo root) +// for both packages/* and apps/*/packages/* +function buildPackageMap() { + const map = new Map(); + + // packages/* + const packagesDir = join(ROOT, 'packages'); + if (existsSync(packagesDir)) { + for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const pkgJsonPath = join(packagesDir, entry.name, 'package.json'); + if (!existsSync(pkgJsonPath)) continue; + try { + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + map.set(pkg.name, `packages/${entry.name}`); + } catch {} + } + } + + // apps/*/packages/* + const appsDir = join(ROOT, 'apps'); + if (existsSync(appsDir)) { + for (const appEntry of readdirSync(appsDir, { withFileTypes: true })) { + if (!appEntry.isDirectory()) continue; + const appPkgsDir = join(appsDir, appEntry.name, 'packages'); + if (!existsSync(appPkgsDir)) continue; + for (const pkgEntry of readdirSync(appPkgsDir, { withFileTypes: true })) { + if (!pkgEntry.isDirectory()) continue; + const pkgJsonPath = join(appPkgsDir, pkgEntry.name, 'package.json'); + if (!existsSync(pkgJsonPath)) continue; + try { + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + map.set(pkg.name, `apps/${appEntry.name}/packages/${pkgEntry.name}`); + } catch {} + } + } + } + + return map; +} + +// Extract workspace deps from package.json +function getWorkspaceDeps(pkgJsonPath) { + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + const deps = []; + for (const section of ['dependencies', 'devDependencies']) { + if (!pkg[section]) continue; + for (const [name, version] of Object.entries(pkg[section])) { + if (typeof version === 'string' && version.startsWith('workspace:')) { + deps.push(name); + } + } + } + return deps; +} + +// Extract COPY paths from Dockerfile (only from builder stage, before RUN install) +function getDockerfileCopyPaths(dockerfilePath) { + const content = readFileSync(dockerfilePath, 'utf8'); + const lines = content.split('\n'); + const copyPaths = new Set(); + let hasPatchesCopy = false; + + for (const line of lines) { + const trimmed = line.trim(); + // Match COPY statements like: COPY packages/shared-utils ./packages/shared-utils + // or COPY apps/zitare/packages/content ./apps/zitare/packages/content + const copyMatch = trimmed.match(/^COPY\s+((?:packages|apps)\/\S+)/); + if (copyMatch) { + copyPaths.add(copyMatch[1]); + } + // Check for patches copy + if (trimmed.match(/^COPY\s+patches[\s/]/)) { + hasPatchesCopy = true; + } + } + + return { copyPaths, hasPatchesCopy }; +} + +// Extract @scope/package imports from a source file +function extractImports(filePath) { + if (!existsSync(filePath)) return []; + const content = readFileSync(filePath, 'utf8'); + const imports = new Set(); + // Match import ... from '@scope/package' and import '@scope/package' + // Also match dynamic imports and require + const patterns = [ + /from\s+['"](@[^'"/]+\/[^'"/]+)/g, + /import\s*\(\s*['"](@[^'"/]+\/[^'"/]+)/g, + /import\s+['"](@[^'"/]+\/[^'"/]+)/g, + /require\s*\(\s*['"](@[^'"/]+\/[^'"/]+)/g, + ]; + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + imports.add(match[1]); + } + } + return [...imports]; +} + +function main() { + const packageMap = buildPackageMap(); + const appsDir = join(ROOT, 'apps'); + let hasErrors = false; + const results = []; + + // Find all web app Dockerfiles + const appDirs = readdirSync(appsDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + + for (const appName of appDirs) { + const dockerfilePath = join(appsDir, appName, 'apps', 'web', 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + const pkgJsonPath = join(appsDir, appName, 'apps', 'web', 'package.json'); + if (!existsSync(pkgJsonPath)) { + results.push({ + path: `apps/${appName}/apps/web/Dockerfile`, + errors: ['package.json not found in same directory'], + warnings: [], + depCount: 0, + }); + hasErrors = true; + continue; + } + + const workspaceDeps = getWorkspaceDeps(pkgJsonPath); + const { copyPaths, hasPatchesCopy } = getDockerfileCopyPaths(dockerfilePath); + const errors = []; + const warnings = []; + + // Check each workspace dep has a corresponding COPY + for (const dep of workspaceDeps) { + const dirPath = packageMap.get(dep); + if (!dirPath) { + warnings.push(`UNKNOWN PACKAGE: ${dep} (not found in workspace)`); + continue; + } + // Check if any COPY path matches or is a parent directory of this package + // e.g. "apps/calendar/packages" covers "apps/calendar/packages/shared" + const found = [...copyPaths].some( + (cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath) + ); + if (!found) { + errors.push(`MISSING: ${dep} → add: COPY ${dirPath} ./${dirPath}`); + } + } + + // Check patches + if (!hasPatchesCopy) { + errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/'); + } + + // Check for imports in hooks.server.ts and vite.config.ts not in package.json + const hooksPath = join(appsDir, appName, 'apps', 'web', 'src', 'hooks.server.ts'); + const vitePath = join(appsDir, appName, 'apps', 'web', 'vite.config.ts'); + + for (const filePath of [hooksPath, vitePath]) { + const imports = extractImports(filePath); + const fileName = filePath.includes('hooks.server') ? 'hooks.server.ts' : 'vite.config.ts'; + for (const imp of imports) { + // Only check workspace packages (ones we know about) + if (packageMap.has(imp) && !workspaceDeps.includes(imp)) { + warnings.push(`NOT IN package.json: ${imp} (imported in ${fileName})`); + } + } + } + + if (errors.length > 0) hasErrors = true; + + results.push({ + path: `apps/${appName}/apps/web/Dockerfile`, + errors, + warnings, + depCount: workspaceDeps.length, + }); + } + + // Print results + for (const result of results) { + const totalIssues = result.errors.length + result.warnings.length; + if (totalIssues === 0) { + console.log(`\u2713 ${result.path} - all ${result.depCount} deps covered`); + } else { + if (result.errors.length > 0) { + console.log( + `\u2717 ${result.path} - ${result.errors.length} issue${result.errors.length !== 1 ? 's' : ''}:` + ); + for (const err of result.errors) { + console.log(` ${err}`); + } + } + if (result.warnings.length > 0) { + if (result.errors.length === 0) { + console.log( + `\u26A0 ${result.path} - ${result.warnings.length} warning${result.warnings.length !== 1 ? 's' : ''}:` + ); + } + for (const warn of result.warnings) { + console.log(` \u26A0 ${warn}`); + } + } + if (result.errors.length === 0 && result.warnings.length > 0) { + // Already printed the warning header above + } + } + } + + // Summary + const errorCount = results.reduce((sum, r) => sum + r.errors.length, 0); + const warnCount = results.reduce((sum, r) => sum + r.warnings.length, 0); + console.log(''); + console.log( + `Checked ${results.length} Dockerfiles: ${errorCount} error${errorCount !== 1 ? 's' : ''}, ${warnCount} warning${warnCount !== 1 ? 's' : ''}` + ); + + if (hasErrors) { + process.exit(1); + } +} + +main();