working on making nojs workable
This commit is contained in:
288
src/server/conditional-parser.test.ts
Normal file
288
src/server/conditional-parser.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
parseConditionals,
|
||||
type ConditionalContext
|
||||
} from "./conditional-parser";
|
||||
|
||||
describe("parseConditionals", () => {
|
||||
const baseContext: ConditionalContext = {
|
||||
isAuthenticated: true,
|
||||
privilegeLevel: "user",
|
||||
userId: "test-user",
|
||||
currentDate: new Date("2025-06-01"),
|
||||
featureFlags: { "beta-feature": true },
|
||||
env: { NODE_ENV: "development", VERCEL_ENV: "development" }
|
||||
};
|
||||
|
||||
it("should show content for authenticated users", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
|
||||
<div class="conditional-content"><p>Secret content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Secret content");
|
||||
expect(result).not.toContain("conditional-block");
|
||||
});
|
||||
|
||||
it("should hide content for anonymous users when condition is authenticated", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
|
||||
<div class="conditional-content"><p>Secret content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const anonContext: ConditionalContext = {
|
||||
...baseContext,
|
||||
isAuthenticated: false,
|
||||
privilegeLevel: "anonymous"
|
||||
};
|
||||
const result = parseConditionals(html, anonContext);
|
||||
expect(result).not.toContain("Secret content");
|
||||
});
|
||||
|
||||
it("should evaluate admin-only content", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
|
||||
<div class="conditional-content"><p>Admin panel</p></div>
|
||||
</div>
|
||||
`;
|
||||
const userResult = parseConditionals(html, baseContext);
|
||||
expect(userResult).not.toContain("Admin panel");
|
||||
|
||||
const adminContext: ConditionalContext = {
|
||||
...baseContext,
|
||||
privilegeLevel: "admin"
|
||||
};
|
||||
const adminResult = parseConditionals(html, adminContext);
|
||||
expect(adminResult).toContain("Admin panel");
|
||||
});
|
||||
|
||||
it("should handle date before condition", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="date" data-condition-value="before:2026-01-01" data-show-when="true">
|
||||
<div class="conditional-content"><p>Available until 2026</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Available until 2026");
|
||||
});
|
||||
|
||||
it("should handle date after condition", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="date" data-condition-value="after:2024-01-01" data-show-when="true">
|
||||
<div class="conditional-content"><p>Available after 2024</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Available after 2024");
|
||||
});
|
||||
|
||||
it("should handle date between condition", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="date" data-condition-value="between:2025-01-01,2025-12-31" data-show-when="true">
|
||||
<div class="conditional-content"><p>2025 content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("2025 content");
|
||||
});
|
||||
|
||||
it("should handle feature flag conditions", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="feature" data-condition-value="beta-feature" data-show-when="true">
|
||||
<div class="conditional-content"><p>Beta content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Beta content");
|
||||
});
|
||||
|
||||
it("should hide content when feature flag is false", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="feature" data-condition-value="disabled-feature" data-show-when="true">
|
||||
<div class="conditional-content"><p>Disabled content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Disabled content");
|
||||
});
|
||||
|
||||
it("should handle showWhen=false (inverted logic)", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="false">
|
||||
<div class="conditional-content"><p>Not authenticated content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Not authenticated content");
|
||||
|
||||
const anonContext: ConditionalContext = {
|
||||
...baseContext,
|
||||
isAuthenticated: false,
|
||||
privilegeLevel: "anonymous"
|
||||
};
|
||||
const anonResult = parseConditionals(html, anonContext);
|
||||
expect(anonResult).toContain("Not authenticated content");
|
||||
});
|
||||
|
||||
it("should handle multiple conditional blocks", () => {
|
||||
const html = `
|
||||
<p>Public content</p>
|
||||
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
|
||||
<div class="conditional-content"><p>Auth content</p></div>
|
||||
</div>
|
||||
<p>More public</p>
|
||||
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
|
||||
<div class="conditional-content"><p>Admin content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Public content");
|
||||
expect(result).toContain("Auth content");
|
||||
expect(result).toContain("More public");
|
||||
expect(result).not.toContain("Admin content");
|
||||
});
|
||||
|
||||
it("should handle empty HTML", () => {
|
||||
const result = parseConditionals("", baseContext);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle HTML with no conditionals", () => {
|
||||
const html = "<p>Regular content</p>";
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toBe(html);
|
||||
});
|
||||
|
||||
it("should default to hiding unknown condition types", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="unknown" data-condition-value="something" data-show-when="true">
|
||||
<div class="conditional-content"><p>Unknown type content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Unknown type content");
|
||||
});
|
||||
|
||||
it("should handle complex nested HTML in conditional content", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">
|
||||
<div class="conditional-content">
|
||||
<h2>Title</h2>
|
||||
<ul><li>Item 1</li><li>Item 2</li></ul>
|
||||
<pre><code>console.log('test');</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("<h2>Title</h2>");
|
||||
expect(result).toContain("<ul><li>Item 1</li>");
|
||||
expect(result).toContain("<code>console.log('test');</code>");
|
||||
});
|
||||
|
||||
it("should handle env condition with exact match", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">
|
||||
<div class="conditional-content"><p>Dev mode content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Dev mode content");
|
||||
});
|
||||
|
||||
it("should hide env condition when value doesn't match", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="true">
|
||||
<div class="conditional-content"><p>Prod content</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Prod content");
|
||||
});
|
||||
|
||||
it("should handle env condition with wildcard (*) for any truthy value", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:*" data-show-when="true">
|
||||
<div class="conditional-content"><p>Any env set</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Any env set");
|
||||
});
|
||||
|
||||
it("should hide env condition when variable is undefined", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="NONEXISTENT_VAR:*" data-show-when="true">
|
||||
<div class="conditional-content"><p>Should not show</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Should not show");
|
||||
});
|
||||
|
||||
it("should handle env condition with inverted logic", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="false">
|
||||
<div class="conditional-content"><p>Not production</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Not production");
|
||||
});
|
||||
|
||||
it("should handle malformed env condition format", () => {
|
||||
const html = `
|
||||
<div class="conditional-block" data-condition-type="env" data-condition-value="INVALID_FORMAT" data-show-when="true">
|
||||
<div class="conditional-content"><p>Invalid format</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).not.toContain("Invalid format");
|
||||
});
|
||||
|
||||
// Inline conditional tests
|
||||
it("should handle inline conditional span for authenticated users", () => {
|
||||
const html = `<p>The domain is <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">localhost</span>.</p>`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("The domain is localhost.");
|
||||
expect(result).not.toContain("conditional-inline");
|
||||
expect(result).not.toContain("data-condition-type");
|
||||
});
|
||||
|
||||
it("should hide inline conditional when condition is false", () => {
|
||||
const html = `<p>The domain is <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="true">freno.me</span>.</p>`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toBe("<p>The domain is .</p>");
|
||||
});
|
||||
|
||||
it("should handle inline auth conditionals", () => {
|
||||
const html = `<p>Welcome <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">back</span>!</p>`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Welcome back!");
|
||||
});
|
||||
|
||||
it("should handle multiple inline conditionals in same paragraph", () => {
|
||||
const html = `<p>Domain: <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:development" data-show-when="true">localhost</span>, User: <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">logged in</span></p>`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Domain: localhost");
|
||||
expect(result).toContain("User: logged in");
|
||||
});
|
||||
|
||||
it("should handle mixed block and inline conditionals", () => {
|
||||
const html = `
|
||||
<p>Text with <span class="conditional-inline" data-condition-type="auth" data-condition-value="authenticated" data-show-when="true">inline</span> conditional.</p>
|
||||
<div class="conditional-block" data-condition-type="privilege" data-condition-value="admin" data-show-when="true">
|
||||
<div class="conditional-content"><p>Block conditional</p></div>
|
||||
</div>
|
||||
`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Text with inline conditional.");
|
||||
expect(result).not.toContain("Block conditional"); // user is not admin
|
||||
});
|
||||
|
||||
it("should handle inline conditional with showWhen=false", () => {
|
||||
const html = `<p>Status: <span class="conditional-inline" data-condition-type="env" data-condition-value="NODE_ENV:production" data-show-when="false">not production</span></p>`;
|
||||
const result = parseConditionals(html, baseContext);
|
||||
expect(result).toContain("Status: not production");
|
||||
});
|
||||
});
|
||||
309
src/server/conditional-parser.ts
Normal file
309
src/server/conditional-parser.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Server-side conditional parser for blog content
|
||||
* Evaluates conditional blocks and returns processed HTML
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get safe environment variables for conditional evaluation
|
||||
* Only exposes non-sensitive variables that are safe to use in content conditionals
|
||||
*/
|
||||
export function getSafeEnvVariables(): Record<string, string | undefined> {
|
||||
return {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
VERCEL_ENV: process.env.VERCEL_ENV
|
||||
// Add other safe, non-sensitive env vars here as needed
|
||||
// DO NOT expose API keys, secrets, database URLs, etc.
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConditionalContext {
|
||||
isAuthenticated: boolean;
|
||||
privilegeLevel: "admin" | "user" | "anonymous";
|
||||
userId: string | null;
|
||||
currentDate: Date;
|
||||
featureFlags: Record<string, boolean>;
|
||||
env: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
interface ConditionalBlock {
|
||||
fullMatch: string;
|
||||
conditionType: string;
|
||||
conditionValue: string;
|
||||
showWhen: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML and evaluate conditional blocks (both block and inline)
|
||||
* @param html - Raw HTML from database
|
||||
* @param context - Evaluation context (user, date, features)
|
||||
* @returns Processed HTML with conditionals evaluated
|
||||
*/
|
||||
export function parseConditionals(
|
||||
html: string,
|
||||
context: ConditionalContext
|
||||
): string {
|
||||
if (!html) return html;
|
||||
|
||||
let processedHtml = html;
|
||||
|
||||
// First, process block-level conditionals (div elements)
|
||||
processedHtml = processBlockConditionals(processedHtml, context);
|
||||
|
||||
// Then, process inline conditionals (span elements)
|
||||
processedHtml = processInlineConditionals(processedHtml, context);
|
||||
|
||||
return processedHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process block-level conditional divs
|
||||
*/
|
||||
function processBlockConditionals(
|
||||
html: string,
|
||||
context: ConditionalContext
|
||||
): string {
|
||||
// Regex to match conditional blocks
|
||||
// Matches: <div class="conditional-block" data-condition-type="..." data-condition-value="..." data-show-when="...">...</div>
|
||||
const conditionalRegex =
|
||||
/<div\s+[^>]*class="[^"]*conditional-block[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/div>/gi;
|
||||
|
||||
let processedHtml = html;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset regex lastIndex
|
||||
conditionalRegex.lastIndex = 0;
|
||||
|
||||
// Collect all matches first to avoid regex state issues
|
||||
const matches: ConditionalBlock[] = [];
|
||||
while ((match = conditionalRegex.exec(html)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
conditionType: match[1],
|
||||
conditionValue: match[2],
|
||||
showWhen: match[3],
|
||||
content: match[4]
|
||||
});
|
||||
}
|
||||
|
||||
// Process each conditional block
|
||||
for (const block of matches) {
|
||||
const shouldShow = evaluateCondition(
|
||||
block.conditionType,
|
||||
block.conditionValue,
|
||||
block.showWhen === "true",
|
||||
context
|
||||
);
|
||||
|
||||
if (shouldShow) {
|
||||
// Keep content, but remove conditional wrapper
|
||||
// Extract content from inner <div class="conditional-content">
|
||||
const innerContentRegex =
|
||||
/<div\s+class="conditional-content">([\s\S]*?)<\/div>/i;
|
||||
const innerMatch = block.fullMatch.match(innerContentRegex);
|
||||
const innerContent = innerMatch ? innerMatch[1] : block.content;
|
||||
|
||||
processedHtml = processedHtml.replace(block.fullMatch, innerContent);
|
||||
} else {
|
||||
// Remove entire block
|
||||
processedHtml = processedHtml.replace(block.fullMatch, "");
|
||||
}
|
||||
}
|
||||
|
||||
return processedHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process inline conditional spans
|
||||
*/
|
||||
function processInlineConditionals(
|
||||
html: string,
|
||||
context: ConditionalContext
|
||||
): string {
|
||||
// Regex to match inline conditionals
|
||||
// Matches: <span class="conditional-inline" data-condition-type="..." data-condition-value="..." data-show-when="...">...</span>
|
||||
const inlineRegex =
|
||||
/<span\s+[^>]*class="[^"]*conditional-inline[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/span>/gi;
|
||||
|
||||
let processedHtml = html;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset regex lastIndex
|
||||
inlineRegex.lastIndex = 0;
|
||||
|
||||
// Collect all matches first
|
||||
const matches: ConditionalBlock[] = [];
|
||||
while ((match = inlineRegex.exec(html)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
conditionType: match[1],
|
||||
conditionValue: match[2],
|
||||
showWhen: match[3],
|
||||
content: match[4]
|
||||
});
|
||||
}
|
||||
|
||||
// Process each inline conditional
|
||||
for (const inline of matches) {
|
||||
const shouldShow = evaluateCondition(
|
||||
inline.conditionType,
|
||||
inline.conditionValue,
|
||||
inline.showWhen === "true",
|
||||
context
|
||||
);
|
||||
|
||||
if (shouldShow) {
|
||||
// Keep content, remove span wrapper
|
||||
processedHtml = processedHtml.replace(inline.fullMatch, inline.content);
|
||||
} else {
|
||||
// Remove entire inline span
|
||||
processedHtml = processedHtml.replace(inline.fullMatch, "");
|
||||
}
|
||||
}
|
||||
|
||||
return processedHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single condition
|
||||
*/
|
||||
function evaluateCondition(
|
||||
conditionType: string,
|
||||
conditionValue: string,
|
||||
showWhen: boolean,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
let conditionMet = false;
|
||||
|
||||
switch (conditionType) {
|
||||
case "auth":
|
||||
conditionMet = evaluateAuthCondition(conditionValue, context);
|
||||
break;
|
||||
case "privilege":
|
||||
conditionMet = evaluatePrivilegeCondition(conditionValue, context);
|
||||
break;
|
||||
case "date":
|
||||
conditionMet = evaluateDateCondition(conditionValue, context);
|
||||
break;
|
||||
case "feature":
|
||||
conditionMet = evaluateFeatureCondition(conditionValue, context);
|
||||
break;
|
||||
case "env":
|
||||
conditionMet = evaluateEnvCondition(conditionValue, context);
|
||||
break;
|
||||
default:
|
||||
// Unknown condition type - default to hiding content for safety
|
||||
conditionMet = false;
|
||||
}
|
||||
|
||||
// Apply showWhen logic: if showWhen is true, show when condition is met
|
||||
// If showWhen is false, show when condition is NOT met
|
||||
return showWhen ? conditionMet : !conditionMet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate authentication condition
|
||||
*/
|
||||
function evaluateAuthCondition(
|
||||
value: string,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
switch (value) {
|
||||
case "authenticated":
|
||||
return context.isAuthenticated;
|
||||
case "anonymous":
|
||||
return !context.isAuthenticated;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate privilege level condition
|
||||
*/
|
||||
function evaluatePrivilegeCondition(
|
||||
value: string,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
return context.privilegeLevel === value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate date-based condition
|
||||
* Supports: "before:YYYY-MM-DD", "after:YYYY-MM-DD", "between:YYYY-MM-DD,YYYY-MM-DD"
|
||||
*/
|
||||
function evaluateDateCondition(
|
||||
value: string,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
try {
|
||||
const now = context.currentDate.getTime();
|
||||
|
||||
if (value.startsWith("before:")) {
|
||||
const dateStr = value.substring(7);
|
||||
const targetDate = new Date(dateStr).getTime();
|
||||
return now < targetDate;
|
||||
}
|
||||
|
||||
if (value.startsWith("after:")) {
|
||||
const dateStr = value.substring(6);
|
||||
const targetDate = new Date(dateStr).getTime();
|
||||
return now > targetDate;
|
||||
}
|
||||
|
||||
if (value.startsWith("between:")) {
|
||||
const dateRange = value.substring(8).split(",");
|
||||
if (dateRange.length !== 2) return false;
|
||||
|
||||
const startDate = new Date(dateRange[0].trim()).getTime();
|
||||
const endDate = new Date(dateRange[1].trim()).getTime();
|
||||
return now >= startDate && now <= endDate;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error parsing date condition:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate feature flag condition
|
||||
*/
|
||||
function evaluateFeatureCondition(
|
||||
value: string,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
return context.featureFlags[value] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate environment variable condition
|
||||
* Format: "ENV_VAR_NAME:expected_value" or "ENV_VAR_NAME:*" for any truthy value
|
||||
*/
|
||||
function evaluateEnvCondition(
|
||||
value: string,
|
||||
context: ConditionalContext
|
||||
): boolean {
|
||||
try {
|
||||
// Parse format: "VAR_NAME:expected_value"
|
||||
const colonIndex = value.indexOf(":");
|
||||
if (colonIndex === -1) return false;
|
||||
|
||||
const varName = value.substring(0, colonIndex).trim();
|
||||
const expectedValue = value.substring(colonIndex + 1).trim();
|
||||
|
||||
const actualValue = context.env[varName];
|
||||
|
||||
// If expected value is "*", check if variable exists and is truthy
|
||||
if (expectedValue === "*") {
|
||||
return !!actualValue;
|
||||
}
|
||||
|
||||
// Otherwise, check for exact match
|
||||
return actualValue === expectedValue;
|
||||
} catch (error) {
|
||||
console.error("Error parsing env condition:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
24
src/server/feature-flags.ts
Normal file
24
src/server/feature-flags.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Feature flag system for conditional content
|
||||
* Centralized configuration for feature toggles
|
||||
*/
|
||||
|
||||
export interface FeatureFlags {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export function getFeatureFlags(): FeatureFlags {
|
||||
return {
|
||||
// TODO: Add feature flags here
|
||||
"beta-features": process.env.ENABLE_BETA_FEATURES === "true",
|
||||
"new-editor": false,
|
||||
"premium-content": true,
|
||||
"seasonal-event": false,
|
||||
"maintenance-mode": false
|
||||
};
|
||||
}
|
||||
|
||||
export function isFeatureEnabled(featureName: string): boolean {
|
||||
const flags = getFeatureFlags();
|
||||
return flags[featureName] === true;
|
||||
}
|
||||
Reference in New Issue
Block a user