Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -1,4 +1,3 @@
|
||||
import { createServer } from 'node:net';
|
||||
import { afterAll, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -11,6 +10,8 @@ import {
|
||||
spawnDetached,
|
||||
} from '../process-utils';
|
||||
|
||||
import { createServer } from 'node:net';
|
||||
|
||||
const pidsToCleanup: number[] = [];
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
interface ComponentInfo {
|
||||
name: string;
|
||||
exportPath: string;
|
||||
filePath: string;
|
||||
category: 'shadcn' | 'makerkit' | 'utils';
|
||||
category: 'shadcn' | 'makerkit' | 'base-ui' | 'hooks' | 'utils';
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -205,25 +206,32 @@ export class ComponentsTool {
|
||||
|
||||
private static determineCategory(
|
||||
filePath: string,
|
||||
): 'shadcn' | 'makerkit' | 'utils' {
|
||||
): ComponentInfo['category'] {
|
||||
if (filePath.includes('/shadcn/')) return 'shadcn';
|
||||
if (filePath.includes('/makerkit/')) return 'makerkit';
|
||||
if (filePath.includes('/base-ui/')) return 'base-ui';
|
||||
if (filePath.includes('/hooks/')) return 'hooks';
|
||||
return 'utils';
|
||||
}
|
||||
|
||||
private static async generateDescription(
|
||||
exportName: string,
|
||||
_filePath: string,
|
||||
category: 'shadcn' | 'makerkit' | 'utils',
|
||||
category: ComponentInfo['category'],
|
||||
): Promise<string> {
|
||||
const componentName = exportName.replace('./', '');
|
||||
|
||||
if (category === 'shadcn') {
|
||||
return this.getShadcnDescription(componentName);
|
||||
} else if (category === 'makerkit') {
|
||||
return this.getMakerkitDescription(componentName);
|
||||
} else {
|
||||
return this.getUtilsDescription(componentName);
|
||||
switch (category) {
|
||||
case 'shadcn':
|
||||
return this.getShadcnDescription(componentName);
|
||||
case 'makerkit':
|
||||
return this.getMakerkitDescription(componentName);
|
||||
case 'base-ui':
|
||||
return this.getBaseUiDescription(componentName);
|
||||
case 'hooks':
|
||||
return this.getHooksDescription(componentName);
|
||||
default:
|
||||
return this.getUtilsDescription(componentName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +292,21 @@ export class ComponentsTool {
|
||||
'Displays a form textarea or a component that looks like a textarea',
|
||||
tooltip:
|
||||
'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it',
|
||||
'context-menu':
|
||||
'A menu triggered by right-click or long-press for contextual actions',
|
||||
empty: 'An empty state placeholder component',
|
||||
pagination: 'Navigation component for paging through data sets',
|
||||
'native-select': 'A native HTML select element with consistent styling',
|
||||
toggle: 'A two-state button that can be either on or off',
|
||||
'menu-bar':
|
||||
'A horizontal menu bar with dropdown menus for application commands',
|
||||
'aspect-ratio': 'Displays content within a desired ratio',
|
||||
kbd: 'Keyboard shortcut indicator component',
|
||||
'button-group': 'Groups related buttons together with consistent spacing',
|
||||
'input-group': 'Groups an input with related addons or buttons',
|
||||
item: 'A generic list item component with consistent styling',
|
||||
field: 'A form field wrapper component with label and error handling',
|
||||
drawer: 'A panel that slides in from the edge of the screen',
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -296,8 +319,6 @@ export class ComponentsTool {
|
||||
if: 'Conditional rendering component that shows children only when condition is true',
|
||||
trans:
|
||||
'Internationalization component for translating text with interpolation support',
|
||||
sidebar:
|
||||
'Application sidebar component with navigation and collapsible functionality',
|
||||
'bordered-navigation-menu':
|
||||
'Navigation menu component with bordered styling',
|
||||
spinner: 'Loading spinner component with customizable size and styling',
|
||||
@@ -316,14 +337,29 @@ export class ComponentsTool {
|
||||
'Component for selecting application language/locale',
|
||||
stepper: 'Step-by-step navigation component for multi-step processes',
|
||||
'card-button': 'Clickable card component that acts as a button',
|
||||
'multi-step-form':
|
||||
'Multi-step form component with validation and navigation',
|
||||
'app-breadcrumbs': 'Application breadcrumb navigation component',
|
||||
'empty-state':
|
||||
'Component for displaying empty states with customizable content',
|
||||
marketing: 'Collection of marketing-focused components and layouts',
|
||||
'file-uploader':
|
||||
'File upload component with drag-and-drop and preview functionality',
|
||||
'navigation-schema': 'Schema and types for navigation configuration',
|
||||
'navigation-utils':
|
||||
'Utility functions for navigation path resolution and matching',
|
||||
'mode-toggle': 'Toggle button for switching between light and dark mode',
|
||||
'mobile-mode-toggle':
|
||||
'Mobile-optimized toggle for switching between light and dark mode',
|
||||
'lazy-render':
|
||||
'Component that defers rendering until visible in the viewport',
|
||||
'cookie-banner': 'GDPR-compliant cookie consent banner',
|
||||
'version-updater':
|
||||
'Component that checks for and prompts application updates',
|
||||
'oauth-provider-logo-image':
|
||||
'Displays the logo image for an OAuth provider',
|
||||
'copy-to-clipboard': 'Button component for copying text to the clipboard',
|
||||
'error-boundary': 'React error boundary component with fallback UI',
|
||||
'sidebar-navigation':
|
||||
'Sidebar navigation component with collapsible sections',
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -332,11 +368,30 @@ export class ComponentsTool {
|
||||
);
|
||||
}
|
||||
|
||||
private static getBaseUiDescription(componentName: string): string {
|
||||
const descriptions: Record<string, string> = {
|
||||
'csp-provider': 'Content Security Policy provider for Base UI components',
|
||||
};
|
||||
|
||||
return descriptions[componentName] || `Base UI component: ${componentName}`;
|
||||
}
|
||||
|
||||
private static getHooksDescription(componentName: string): string {
|
||||
const descriptions: Record<string, string> = {
|
||||
'hooks/use-async-dialog':
|
||||
'Hook for managing async dialog state with promise-based open/close',
|
||||
'hooks/use-mobile': 'Hook for detecting mobile viewport breakpoints',
|
||||
'use-supabase-upload':
|
||||
'Hook for uploading files to Supabase Storage with progress tracking',
|
||||
};
|
||||
|
||||
return descriptions[componentName] || `React hook: ${componentName}`;
|
||||
}
|
||||
|
||||
private static getUtilsDescription(componentName: string): string {
|
||||
const descriptions: Record<string, string> = {
|
||||
utils:
|
||||
'Utility functions for styling, class management, and common operations',
|
||||
'navigation-schema': 'Schema and types for navigation configuration',
|
||||
};
|
||||
|
||||
return descriptions[componentName] || `Utility module: ${componentName}`;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import postgres from 'postgres';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
const DATABASE_URL =
|
||||
process.env.DATABASE_URL ||
|
||||
@@ -360,7 +361,7 @@ export class DatabaseTool {
|
||||
|
||||
try {
|
||||
return await readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
throw new Error(`Schema file "${fileName}" not found`);
|
||||
}
|
||||
}
|
||||
@@ -457,7 +458,7 @@ export class DatabaseTool {
|
||||
// Fallback to schema files
|
||||
const enumContent = await this.getSchemaContent('01-enums.sql');
|
||||
return this.parseEnums(enumContent);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -609,7 +610,7 @@ export class DatabaseTool {
|
||||
onDelete: fk.delete_rule,
|
||||
onUpdate: fk.update_rule,
|
||||
}));
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -676,7 +677,7 @@ export class DatabaseTool {
|
||||
};
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { access, readFile, readdir } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { execFileAsync } from '../../lib/process-utils';
|
||||
import { type KitDbServiceDeps, createKitDbService } from './kit-db.service';
|
||||
@@ -16,6 +13,10 @@ import {
|
||||
KitDbStatusOutputSchema,
|
||||
} from './schema';
|
||||
|
||||
import { access, readFile, readdir } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
type TextContent = {
|
||||
type: 'text';
|
||||
text: string;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type {
|
||||
DbTool,
|
||||
KitDbMigrateInput,
|
||||
@@ -10,6 +8,8 @@ import type {
|
||||
KitDbStatusOutput,
|
||||
} from './schema';
|
||||
|
||||
import { join } from 'node:path';
|
||||
|
||||
type VariantFamily = 'supabase' | 'orm';
|
||||
|
||||
interface CommandResult {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
execFileAsync,
|
||||
@@ -26,6 +23,10 @@ import {
|
||||
KitMailboxStatusOutputSchema,
|
||||
} from './schema';
|
||||
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function registerKitDevTools(server: McpServer, rootPath?: string) {
|
||||
const service = createKitDevService(createKitDevDeps(rootPath));
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { EMAIL_TEMPLATE_RENDERERS } from '@kit/email-templates/registry';
|
||||
|
||||
import type { KitEmailsListOutput, KitEmailsReadOutput } from './schema';
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
export interface KitEmailsDeps {
|
||||
rootPath: string;
|
||||
readFile(filePath: string): Promise<string>;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { findWorkspaceRoot } from '../scanner';
|
||||
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('findWorkspaceRoot', () => {
|
||||
let tmp: string;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { envVariables } from './model';
|
||||
import { getEnvState } from './scanner';
|
||||
import type { EnvMode, ScanFs } from './types';
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export interface KitEnvDeps {
|
||||
rootPath: string;
|
||||
readFile(filePath: string): Promise<string>;
|
||||
|
||||
21
packages/mcp-server/src/tools/env/model.ts
vendored
21
packages/mcp-server/src/tools/env/model.ts
vendored
@@ -375,6 +375,16 @@ export const envVariables: EnvVariableModel[] = [
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLY',
|
||||
displayName: 'Enable Team Accounts Only and disable persoanl accounts.',
|
||||
description: 'Force disable personal accounts for pure B2B SaaS',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
|
||||
displayName: 'Enable Team Account Creation',
|
||||
@@ -405,6 +415,17 @@ export const envVariables: EnvVariableModel[] = [
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY',
|
||||
displayName: 'Enable Teams Accounts Only',
|
||||
description:
|
||||
'When enabled, disables personal accounts and only allows team accounts.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||
displayName: 'Enable Notifications',
|
||||
|
||||
8
packages/mcp-server/src/tools/env/scanner.ts
vendored
8
packages/mcp-server/src/tools/env/scanner.ts
vendored
@@ -1,7 +1,3 @@
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'path';
|
||||
|
||||
import { envVariables } from './model';
|
||||
import {
|
||||
AppEnvState,
|
||||
@@ -12,6 +8,10 @@ import {
|
||||
ScanOptions,
|
||||
} from './types';
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'path';
|
||||
|
||||
// Define precedence order for each mode
|
||||
const ENV_FILE_PRECEDENCE: Record<EnvMode, string[]> = {
|
||||
development: [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { Socket } from 'node:net';
|
||||
|
||||
import { execFileAsync } from '../../lib/process-utils';
|
||||
import {
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
KitEmailsSetReadStatusOutputSchema,
|
||||
} from './schema';
|
||||
|
||||
import { Socket } from 'node:net';
|
||||
|
||||
type TextContent = {
|
||||
type: 'text';
|
||||
text: string;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { z } from 'zod/v3';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
import { crossExecFileSync } from '../lib/process-utils';
|
||||
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export class MigrationsTool {
|
||||
private static _rootPath = process.cwd();
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
// Custom phase for organizing user stories
|
||||
interface CustomPhase {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { execFileAsync } from '../../lib/process-utils';
|
||||
import {
|
||||
@@ -12,6 +10,9 @@ import {
|
||||
KitPrerequisitesOutputSchema,
|
||||
} from './schema';
|
||||
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function registerKitPrerequisitesTool(
|
||||
server: McpServer,
|
||||
rootPath?: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod/v3';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
interface PromptTemplate {
|
||||
name: string;
|
||||
|
||||
@@ -9,8 +9,6 @@ function createDeps(
|
||||
overrides: Partial<RunChecksDeps> = {},
|
||||
scripts: Record<string, string> = {
|
||||
typecheck: 'tsc --noEmit',
|
||||
'lint:fix': 'eslint . --fix',
|
||||
'format:fix': 'prettier . --write',
|
||||
test: 'vitest run',
|
||||
},
|
||||
): RunChecksDeps {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { execFileAsync } from '../../lib/process-utils';
|
||||
import {
|
||||
@@ -9,6 +7,9 @@ import {
|
||||
} from './run-checks.service';
|
||||
import { RunChecksInputSchema, RunChecksOutputSchema } from './schema';
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function registerRunChecksTool(server: McpServer, rootPath?: string) {
|
||||
const service = createRunChecksService(createRunChecksDeps(rootPath));
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import * as z from 'zod/v3';
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
interface ScriptInfo {
|
||||
name: string;
|
||||
@@ -93,7 +94,7 @@ export class ScriptsTool {
|
||||
lint: {
|
||||
category: 'linting',
|
||||
description:
|
||||
'Run ESLint to check code quality and enforce coding standards',
|
||||
'Run Oxlint to check code quality and enforce coding standards',
|
||||
usage:
|
||||
'CRITICAL: Run after writing code to ensure code quality. Must pass before commits.',
|
||||
importance: 'medium',
|
||||
@@ -102,7 +103,7 @@ export class ScriptsTool {
|
||||
'lint:fix': {
|
||||
category: 'linting',
|
||||
description:
|
||||
'Run ESLint with auto-fix to automatically resolve fixable issues',
|
||||
'Run Oxlint with auto-fix to automatically resolve fixable issues',
|
||||
usage:
|
||||
'Use to automatically fix linting issues. Run before manual fixes.',
|
||||
importance: 'high',
|
||||
@@ -110,14 +111,14 @@ export class ScriptsTool {
|
||||
},
|
||||
format: {
|
||||
category: 'linting',
|
||||
description: 'Check code formatting with Prettier across all files',
|
||||
description: 'Check code formatting with Oxfmt across all files',
|
||||
usage: 'Verify code follows consistent formatting standards.',
|
||||
importance: 'high',
|
||||
},
|
||||
'format:fix': {
|
||||
category: 'linting',
|
||||
description:
|
||||
'Auto-format all code with Prettier to ensure consistent styling',
|
||||
'Auto-format all code with Oxfmt to ensure consistent styling',
|
||||
usage: 'Use to automatically format code. Run before commits.',
|
||||
importance: 'high',
|
||||
healthcheck: true,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { access, readFile, stat } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { execFileAsync } from '../../lib/process-utils';
|
||||
import {
|
||||
@@ -10,6 +7,10 @@ import {
|
||||
} from './kit-status.service';
|
||||
import { KitStatusInputSchema, KitStatusOutputSchema } from './schema';
|
||||
|
||||
import { access, readFile, stat } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function registerKitStatusTool(server: McpServer, rootPath?: string) {
|
||||
return server.registerTool(
|
||||
'kit_status',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { KitStatusInput, KitStatusOutput } from './schema';
|
||||
|
||||
import { join } from 'node:path';
|
||||
|
||||
interface VariantDescriptor {
|
||||
variant: string;
|
||||
variant_family: string;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import path from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -6,6 +5,8 @@ import {
|
||||
createKitTranslationsService,
|
||||
} from '../kit-translations.service';
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
function createDeps(
|
||||
files: Record<string, string>,
|
||||
directories: string[],
|
||||
@@ -91,7 +92,7 @@ function createDeps(
|
||||
|
||||
describe('KitTranslationsService.list', () => {
|
||||
it('lists and flattens translations with missing namespace fallback', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({
|
||||
@@ -122,7 +123,7 @@ describe('KitTranslationsService.list', () => {
|
||||
|
||||
describe('KitTranslationsService.update', () => {
|
||||
it('updates nested translation keys', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -143,7 +144,7 @@ describe('KitTranslationsService.update', () => {
|
||||
});
|
||||
|
||||
it('rejects paths outside locales root', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -164,7 +165,7 @@ describe('KitTranslationsService.update', () => {
|
||||
});
|
||||
|
||||
it('rejects namespace path segments', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -187,7 +188,7 @@ describe('KitTranslationsService.update', () => {
|
||||
|
||||
describe('KitTranslationsService.stats', () => {
|
||||
it('computes coverage using base locale keys', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({
|
||||
@@ -213,7 +214,7 @@ describe('KitTranslationsService.stats', () => {
|
||||
|
||||
describe('KitTranslationsService.addNamespace', () => {
|
||||
it('creates namespace JSON in all locale directories', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -237,7 +238,7 @@ describe('KitTranslationsService.addNamespace', () => {
|
||||
});
|
||||
|
||||
it('throws if namespace already exists', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -253,7 +254,7 @@ describe('KitTranslationsService.addNamespace', () => {
|
||||
});
|
||||
|
||||
it('throws if no locales exist', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
@@ -264,7 +265,7 @@ describe('KitTranslationsService.addNamespace', () => {
|
||||
});
|
||||
|
||||
it('rejects path traversal in namespace', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -286,7 +287,7 @@ describe('KitTranslationsService.addNamespace', () => {
|
||||
|
||||
describe('KitTranslationsService.addLocale', () => {
|
||||
it('creates locale directory with namespace files', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({ hello: 'Hello' }),
|
||||
@@ -310,7 +311,7 @@ describe('KitTranslationsService.addLocale', () => {
|
||||
});
|
||||
|
||||
it('throws if locale already exists', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -326,7 +327,7 @@ describe('KitTranslationsService.addLocale', () => {
|
||||
});
|
||||
|
||||
it('works when no namespaces exist yet', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
@@ -337,7 +338,7 @@ describe('KitTranslationsService.addLocale', () => {
|
||||
});
|
||||
|
||||
it('rejects path traversal in locale', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
@@ -354,7 +355,7 @@ describe('KitTranslationsService.addLocale', () => {
|
||||
|
||||
describe('KitTranslationsService.removeNamespace', () => {
|
||||
it('deletes namespace files from all locales', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -377,7 +378,7 @@ describe('KitTranslationsService.removeNamespace', () => {
|
||||
});
|
||||
|
||||
it('throws if namespace does not exist', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -393,7 +394,7 @@ describe('KitTranslationsService.removeNamespace', () => {
|
||||
});
|
||||
|
||||
it('rejects path traversal', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
@@ -406,7 +407,7 @@ describe('KitTranslationsService.removeNamespace', () => {
|
||||
|
||||
describe('KitTranslationsService.removeLocale', () => {
|
||||
it('deletes entire locale directory', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -426,7 +427,7 @@ describe('KitTranslationsService.removeLocale', () => {
|
||||
});
|
||||
|
||||
it('throws if locale does not exist', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
@@ -437,7 +438,7 @@ describe('KitTranslationsService.removeLocale', () => {
|
||||
});
|
||||
|
||||
it('throws when trying to delete base locale', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps(
|
||||
{
|
||||
[`${localesRoot}/en/common.json`]: JSON.stringify({}),
|
||||
@@ -454,7 +455,7 @@ describe('KitTranslationsService.removeLocale', () => {
|
||||
});
|
||||
|
||||
it('rejects path traversal', async () => {
|
||||
const localesRoot = '/repo/apps/web/public/locales';
|
||||
const localesRoot = '/repo/apps/web/i18n/messages';
|
||||
const deps = createDeps({}, [localesRoot]);
|
||||
|
||||
const service = createKitTranslationsService(deps);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type {
|
||||
KitTranslationsAddLocaleInput,
|
||||
KitTranslationsAddLocaleSuccess,
|
||||
@@ -15,6 +13,8 @@ import type {
|
||||
KitTranslationsUpdateSuccess,
|
||||
} from './schema';
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
export interface KitTranslationsDeps {
|
||||
rootPath: string;
|
||||
readFile(filePath: string): Promise<string>;
|
||||
@@ -408,7 +408,7 @@ export class KitTranslationsService {
|
||||
}
|
||||
|
||||
private getLocalesRoot() {
|
||||
return path.resolve(this.deps.rootPath, 'apps', 'web', 'public', 'locales');
|
||||
return path.resolve(this.deps.rootPath, 'apps', 'web', 'i18n', 'messages');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user