do to formatters
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* ESLint rules for Display Object Guardrails
|
||||
*
|
||||
* Enforces display object boundaries and purity
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Rule 1: No IO in display objects
|
||||
'no-io-in-display-objects': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid IO imports in display objects',
|
||||
category: 'Display Objects',
|
||||
},
|
||||
messages: {
|
||||
message: 'DisplayObjects cannot import from api, services, page-queries, or view-models - see apps/website/lib/contracts/display-objects/DisplayObject.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const forbiddenPaths = [
|
||||
'@/lib/api/',
|
||||
'@/lib/services/',
|
||||
'@/lib/page-queries/',
|
||||
'@/lib/view-models/',
|
||||
'@/lib/presenters/',
|
||||
];
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (forbiddenPaths.some(path => importPath.includes(path)) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 2: No non-class display exports
|
||||
'no-non-class-display-exports': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid non-class exports in display objects',
|
||||
category: 'Display Objects',
|
||||
},
|
||||
messages: {
|
||||
message: 'Display Objects must be class-based and export only classes - see apps/website/lib/contracts/display-objects/DisplayObject.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
(node.declaration.type === 'FunctionDeclaration' ||
|
||||
(node.declaration.type === 'VariableDeclaration' &&
|
||||
!node.declaration.declarations.some(d => d.init && d.init.type === 'ClassExpression')))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
node.declaration.type !== 'ClassDeclaration' &&
|
||||
node.declaration.type !== 'ClassExpression') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function isInComment(node) {
|
||||
return false;
|
||||
}
|
||||
138
apps/website/eslint-rules/formatter-rules.js
Normal file
138
apps/website/eslint-rules/formatter-rules.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* ESLint rules for Formatter/Display Guardrails
|
||||
*
|
||||
* Enforces boundaries and purity for Formatters and Display Objects
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Rule 1: No IO in formatters/displays
|
||||
'no-io-in-display-objects': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid IO imports in formatters and displays',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters/Displays cannot import from api, services, page-queries, or view-models - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const forbiddenPaths = [
|
||||
'@/lib/api/',
|
||||
'@/lib/services/',
|
||||
'@/lib/page-queries/',
|
||||
'@/lib/view-models/',
|
||||
'@/lib/presenters/',
|
||||
];
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (forbiddenPaths.some(path => importPath.includes(path)) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 2: No non-class display exports
|
||||
'no-non-class-display-exports': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid non-class exports in formatters and displays',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters and Displays must be class-based and export only classes - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
(node.declaration.type === 'FunctionDeclaration' ||
|
||||
(node.declaration.type === 'VariableDeclaration' &&
|
||||
!node.declaration.declarations.some(d => d.init && d.init.type === 'ClassExpression')))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
node.declaration.type !== 'ClassDeclaration' &&
|
||||
node.declaration.type !== 'ClassExpression') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 3: Formatters must return primitives
|
||||
'formatters-must-return-primitives': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce that Formatters return primitive values for ViewData compatibility',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters used in ViewDataBuilders must return primitive values (string, number, boolean, null) - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isViewDataBuilder = filename.includes('/lib/builders/view-data/');
|
||||
|
||||
if (!isViewDataBuilder) return {};
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
// Check if calling a Formatter/Display method
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.name &&
|
||||
(node.callee.object.name.endsWith('Formatter') || node.callee.object.name.endsWith('Display'))) {
|
||||
|
||||
// If it's inside a ViewData object literal, it must be a primitive return
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
if (parent.type === 'Property' && parent.parent.type === 'ObjectExpression') {
|
||||
// This is a property in an object literal (likely ViewData)
|
||||
// We can't easily check the return type of the method at lint time without type info,
|
||||
// but we can enforce that it's not the whole object being assigned.
|
||||
if (node.callee.property.name === 'format' || node.callee.property.name.startsWith('format')) {
|
||||
// Good: calling a format method
|
||||
return;
|
||||
}
|
||||
|
||||
// If they are assigning the result of a non-format method, warn
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function isInComment(node) {
|
||||
return false;
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
const presenterContract = require('./presenter-contract');
|
||||
const rscBoundaryRules = require('./rsc-boundary-rules');
|
||||
const templatePurityRules = require('./template-purity-rules');
|
||||
const displayObjectRules = require('./display-object-rules');
|
||||
const displayObjectRules = require('./formatter-rules');
|
||||
const pageQueryRules = require('./page-query-rules');
|
||||
const servicesRules = require('./services-rules');
|
||||
const clientOnlyRules = require('./client-only-rules');
|
||||
@@ -30,7 +30,6 @@ const mutationContract = require('./mutation-contract');
|
||||
const serverActionsMustUseMutations = require('./server-actions-must-use-mutations');
|
||||
const viewDataLocation = require('./view-data-location');
|
||||
const viewDataBuilderContract = require('./view-data-builder-contract');
|
||||
const viewModelBuilderContract = require('./view-model-builder-contract');
|
||||
const singleExportPerFile = require('./single-export-per-file');
|
||||
const filenameMatchesExport = require('./filename-matches-export');
|
||||
const pageQueryMustUseBuilders = require('./page-query-must-use-builders');
|
||||
@@ -48,7 +47,6 @@ const serverActionsInterface = require('./server-actions-interface');
|
||||
const noDisplayObjectsInUi = require('./no-display-objects-in-ui');
|
||||
const viewDataBuilderImplements = require('./view-data-builder-implements');
|
||||
const viewDataBuilderImports = require('./view-data-builder-imports');
|
||||
const viewModelBuilderImplements = require('./view-model-builder-implements');
|
||||
const viewDataImplements = require('./view-data-implements');
|
||||
const viewModelImplements = require('./view-model-implements');
|
||||
const viewModelTaxonomy = require('./view-model-taxonomy');
|
||||
@@ -85,6 +83,7 @@ module.exports = {
|
||||
// Display Object Rules
|
||||
'display-no-domain-models': displayObjectRules['no-io-in-display-objects'],
|
||||
'display-no-business-logic': displayObjectRules['no-non-class-display-exports'],
|
||||
'formatters-must-return-primitives': displayObjectRules['formatters-must-return-primitives'],
|
||||
'no-display-objects-in-ui': noDisplayObjectsInUi,
|
||||
|
||||
// Page Query Rules
|
||||
@@ -139,8 +138,6 @@ module.exports = {
|
||||
'view-data-implements': viewDataImplements,
|
||||
|
||||
// View Model Rules
|
||||
'view-model-builder-contract': viewModelBuilderContract,
|
||||
'view-model-builder-implements': viewModelBuilderImplements,
|
||||
'view-model-implements': viewModelImplements,
|
||||
'view-model-taxonomy': viewModelTaxonomy,
|
||||
|
||||
@@ -222,6 +219,7 @@ module.exports = {
|
||||
// Display Objects
|
||||
'gridpilot-rules/display-no-domain-models': 'error',
|
||||
'gridpilot-rules/display-no-business-logic': 'error',
|
||||
'gridpilot-rules/formatters-must-return-primitives': 'error',
|
||||
'gridpilot-rules/no-display-objects-in-ui': 'error',
|
||||
|
||||
// Page Queries
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* ESLint rule to enforce View Model Builder contract
|
||||
*
|
||||
* View Model Builders must:
|
||||
* 1. Be classes named *ViewModelBuilder
|
||||
* 2. Have a static build() method
|
||||
* 3. Use 'satisfies ViewModelBuilder<...>' for static enforcement
|
||||
* 4. Accept View Data as parameter
|
||||
* 5. Return View Model
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce View Model Builder contract',
|
||||
category: 'Builders',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
notAClass: 'View Model Builders must be classes named *ViewModelBuilder',
|
||||
missingStaticBuild: 'View Model Builders must have a static build() method',
|
||||
missingSatisfies: 'View Model Builders must use "satisfies ViewModelBuilder<...>" for static type enforcement',
|
||||
invalidBuildSignature: 'build() method must accept View Data and return View Model',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isInViewModelBuilders = filename.includes('/lib/builders/view-models/');
|
||||
|
||||
if (!isInViewModelBuilders) return {};
|
||||
|
||||
let hasStaticBuild = false;
|
||||
let hasSatisfies = false;
|
||||
let hasCorrectSignature = false;
|
||||
|
||||
return {
|
||||
// Check class declaration
|
||||
ClassDeclaration(node) {
|
||||
const className = node.id?.name;
|
||||
|
||||
if (!className || !className.endsWith('ViewModelBuilder')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'notAClass',
|
||||
});
|
||||
}
|
||||
|
||||
// Check for static build method
|
||||
const staticBuild = node.body.body.find(member =>
|
||||
member.type === 'MethodDefinition' &&
|
||||
member.key.type === 'Identifier' &&
|
||||
member.key.name === 'build' &&
|
||||
member.static === true
|
||||
);
|
||||
|
||||
if (staticBuild) {
|
||||
hasStaticBuild = true;
|
||||
|
||||
// Check signature - should have at least one parameter
|
||||
if (staticBuild.value &&
|
||||
staticBuild.value.params &&
|
||||
staticBuild.value.params.length > 0) {
|
||||
hasCorrectSignature = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check for satisfies expression
|
||||
TSSatisfiesExpression(node) {
|
||||
if (node.typeAnnotation &&
|
||||
node.typeAnnotation.type === 'TSTypeReference' &&
|
||||
node.typeAnnotation.typeName.name === 'ViewModelBuilder') {
|
||||
hasSatisfies = true;
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
if (!hasStaticBuild) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'missingStaticBuild',
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasSatisfies) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'missingSatisfies',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasStaticBuild && !hasCorrectSignature) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'invalidBuildSignature',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* ESLint rule to enforce View Model Builder contract implementation
|
||||
*
|
||||
* View Model Builders in lib/builders/view-models/ must:
|
||||
* 1. Be classes named *ViewModelBuilder
|
||||
* 2. Implement the ViewModelBuilder<TInput, TOutput> interface
|
||||
* 3. Have a static build() method
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce View Model Builder contract implementation',
|
||||
category: 'Builders',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
notAClass: 'View Model Builders must be classes named *ViewModelBuilder',
|
||||
missingImplements: 'View Model Builders must implement ViewModelBuilder<TInput, TOutput> interface',
|
||||
missingBuildMethod: 'View Model Builders must have a static build() method',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isInViewModelBuilders = filename.includes('/lib/builders/view-models/');
|
||||
|
||||
if (!isInViewModelBuilders) return {};
|
||||
|
||||
let hasImplements = false;
|
||||
let hasBuildMethod = false;
|
||||
|
||||
return {
|
||||
// Check class declaration
|
||||
ClassDeclaration(node) {
|
||||
const className = node.id?.name;
|
||||
|
||||
if (!className || !className.endsWith('ViewModelBuilder')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'notAClass',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if class implements ViewModelBuilder interface
|
||||
if (node.implements && node.implements.length > 0) {
|
||||
for (const impl of node.implements) {
|
||||
// Handle GenericTypeAnnotation for ViewModelBuilder<TInput, TOutput>
|
||||
if (impl.expression.type === 'TSInstantiationExpression') {
|
||||
const expr = impl.expression.expression;
|
||||
if (expr.type === 'Identifier' && expr.name === 'ViewModelBuilder') {
|
||||
hasImplements = true;
|
||||
}
|
||||
} else if (impl.expression.type === 'Identifier') {
|
||||
// Handle simple ViewModelBuilder (without generics)
|
||||
if (impl.expression.name === 'ViewModelBuilder') {
|
||||
hasImplements = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for static build method
|
||||
const buildMethod = node.body.body.find(member =>
|
||||
member.type === 'MethodDefinition' &&
|
||||
member.key.type === 'Identifier' &&
|
||||
member.key.name === 'build' &&
|
||||
member.static === true
|
||||
);
|
||||
|
||||
if (buildMethod) {
|
||||
hasBuildMethod = true;
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
if (!hasImplements) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'missingImplements',
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasBuildMethod) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'missingBuildMethod',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
* 1. NOT contain the word "DTO" (DTOs are for API/Services)
|
||||
* 2. NOT define ViewData interfaces (ViewData must be in lib/view-data/)
|
||||
* 3. NOT import from DTO paths (DTOs belong to lib/types/generated/)
|
||||
* 4. ONLY import from allowed paths: lib/contracts/, lib/view-models/, lib/view-data/, lib/display-objects/
|
||||
* 4. ONLY import from allowed paths: lib/contracts/, lib/view-models/, lib/view-data/, lib/formatters/
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
@@ -22,7 +22,7 @@ module.exports = {
|
||||
noDtoInViewModel: 'ViewModels must not use the word "DTO". DTOs belong to the API/Service layer. Use plain logic-rich properties or ViewData types.',
|
||||
noDtoImport: 'ViewModels must not import from DTO paths. DTOs belong to lib/types/generated/. Import from lib/view-data/ or use plain properties instead.',
|
||||
noViewDataDefinition: 'ViewData must not be defined within ViewModel files. Import them from lib/view-data/ instead.',
|
||||
strictImport: 'ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/display-objects/. External imports are allowed. Found: {{importPath}}',
|
||||
strictImport: 'ViewModels can only import from lib/contracts/, lib/view-models/, lib/view-data/, or lib/formatters/. External imports are allowed. Found: {{importPath}}',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ module.exports = {
|
||||
'@/lib/contracts/',
|
||||
'@/lib/view-models/',
|
||||
'@/lib/view-data/',
|
||||
'@/lib/display-objects/',
|
||||
'@/lib/formatters/',
|
||||
];
|
||||
|
||||
const isAllowed = allowedPaths.some(path => importPath.startsWith(path));
|
||||
@@ -83,12 +83,12 @@ module.exports = {
|
||||
importPath.includes('/lib/contracts/') ||
|
||||
importPath.includes('/lib/view-models/') ||
|
||||
importPath.includes('/lib/view-data/') ||
|
||||
importPath.includes('/lib/display-objects/') ||
|
||||
importPath.includes('/lib/formatters/') ||
|
||||
// Also check for patterns like ../contracts/...
|
||||
importPath.includes('contracts') ||
|
||||
importPath.includes('view-models') ||
|
||||
importPath.includes('view-data') ||
|
||||
importPath.includes('display-objects') ||
|
||||
importPath.includes('formatters') ||
|
||||
// Allow relative imports to view models (e.g., ./InvoiceViewModel, ../ViewModelName)
|
||||
// This matches patterns like ./ViewModelName or ../ViewModelName
|
||||
/^\.\/[A-Z][a-zA-Z0-9]*ViewModel$/.test(importPath) ||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* ViewModel Builder Contract
|
||||
*
|
||||
* Purpose: Transform ViewData into ViewModels for client-side state management
|
||||
*
|
||||
* Rules:
|
||||
* - Deterministic and side-effect free
|
||||
* - No HTTP/API calls
|
||||
* - Input: ViewData (JSON-serializable template-ready data)
|
||||
* - Output: ViewModel (client-only class)
|
||||
* - Must be in lib/builders/view-models/
|
||||
* - Must be named *ViewModelBuilder
|
||||
* - Must have 'use client' directive
|
||||
* - Must implement static build() method
|
||||
* - Must use 'satisfies' for static type enforcement
|
||||
*/
|
||||
|
||||
import { ViewData } from '../view-data/ViewData';
|
||||
import { ViewModel } from '../view-models/ViewModel';
|
||||
|
||||
/**
|
||||
* ViewModel Builder Contract (Static)
|
||||
*
|
||||
* Usage:
|
||||
* export class MyViewModelBuilder {
|
||||
* static build(viewData: MyViewData): MyViewModel { ... }
|
||||
* }
|
||||
* MyViewModelBuilder satisfies ViewModelBuilder<MyViewData, MyViewModel>;
|
||||
*/
|
||||
export interface ViewModelBuilder<TViewData extends ViewData, TViewModel extends ViewModel> {
|
||||
build(viewData: TViewData): TViewModel;
|
||||
}
|
||||
@@ -1,22 +1,40 @@
|
||||
/**
|
||||
* Formatter contract
|
||||
*
|
||||
*
|
||||
* Deterministic, reusable, UI-only formatting/mapping logic.
|
||||
*
|
||||
* Based on DISPLAY_OBJECTS.md:
|
||||
* - Class-based
|
||||
* - Immutable
|
||||
*
|
||||
* Based on FORMATTERS.md:
|
||||
* - Stateless (Formatters) or Immutable (Display Objects)
|
||||
* - Deterministic
|
||||
* - Side-effect free
|
||||
* - No Intl.* or toLocale*
|
||||
* - No business rules
|
||||
*
|
||||
* Uncle Bob says: "Data structures should not have behavior."
|
||||
* Formatters ensure ViewData remains a dumb container of primitives.
|
||||
*/
|
||||
|
||||
export interface Formatter {
|
||||
/**
|
||||
* Format or map the display object
|
||||
*
|
||||
* @returns Primitive values only (strings, numbers, booleans)
|
||||
* Format or map the input to a primitive value
|
||||
*
|
||||
* @returns Primitive values only (strings, numbers, booleans, null)
|
||||
*/
|
||||
format(): unknown;
|
||||
format(): string | number | boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich Display Object contract (Client-only)
|
||||
*
|
||||
* Used by ViewModels to provide a rich API to the UI.
|
||||
*/
|
||||
export interface DisplayObject {
|
||||
/**
|
||||
* Primary primitive output
|
||||
*/
|
||||
format(): string | number | boolean | null;
|
||||
|
||||
/**
|
||||
* Multiple primitive variants
|
||||
*/
|
||||
variants?(): Record<string, string | number | boolean | null>;
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { JsonValue } from "../types/primitives";
|
||||
|
||||
/**
|
||||
* Base interface for ViewData objects
|
||||
*
|
||||
* All ViewData must be JSON-serializable for SSR.
|
||||
* This type ensures no class instances or functions are included.
|
||||
*
|
||||
* Note: We use 'any' here to allow complex DTO structures, but the
|
||||
* architectural rule is that these must be plain JSON objects.
|
||||
* Uncle Bob says: "Data structures should not have behavior."
|
||||
* ViewData is a dumb container for primitives and nested JSON only.
|
||||
*/
|
||||
export interface ViewData {
|
||||
[key: string]: JsonValue;
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Helper type to ensure a type is ViewData-compatible
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ViewData } from '@/lib/contracts/view-data/ViewData';
|
||||
*
|
||||
* ViewData for category icon media rendering.
|
||||
*/
|
||||
import { ViewData } from "../contracts/view-data/ViewData";
|
||||
|
||||
export interface CategoryIconViewData extends ViewData {
|
||||
buffer: string; // base64 encoded
|
||||
|
||||
Reference in New Issue
Block a user