Files
OpenNoodl/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-007-app-config/CONFIG-006-expression-integration.md
2025-12-30 11:55:30 +01:00

10 KiB

CONFIG-006: Expression System Integration

Overview

Integrate Noodl.Config access into the expression evaluator with autocomplete support for config keys.

Estimated effort: 4-6 hours
Dependencies: CONFIG-001, TASK-006 (Expression System Overhaul)
Blocks: None


Objectives

  1. Make Noodl.Config accessible in Expression nodes
  2. Add autocomplete for config keys in expression editor
  3. Provide type hints for config values
  4. Handle undefined config keys gracefully

Expression Usage Examples

// Simple value access
Noodl.Config.appName

// Color in style binding
Noodl.Config.primaryColor

// Conditional with config
Noodl.Config.pwaEnabled ? "Install App" : "Use in Browser"

// Template literal
`Welcome to ${Noodl.Config.appName}!`

// Array access
Noodl.Config.menuItems[0].label

// Object property
Noodl.Config.apiSettings.timeout

Files to Modify

1. Expression Evaluator Context

File: packages/noodl-runtime/src/nodes/std-library/expression.js (or new expression-evaluator module from TASK-006)

Add Noodl.Config to the expression context:

// In the expression preamble or context setup
const { configManager } = require('../../../config/config-manager');

function createExpressionContext() {
  return {
    // Math helpers
    min: Math.min,
    max: Math.max,
    cos: Math.cos,
    sin: Math.sin,
    // ... other existing helpers ...
    
    // Noodl globals
    Noodl: {
      Variables: Model.get('--ndl--global-variables'),
      Objects: objectsProxy,
      Arrays: arraysProxy,
      Config: configManager.getConfig()  // Add Config access
    }
  };
}

If following TASK-006's enhanced expression node pattern:

File: packages/noodl-runtime/src/expression-evaluator.ts

import { configManager } from './config/config-manager';

export function createExpressionContext(nodeContext: NodeContext): ExpressionContext {
  return {
    // ... existing context ...
    
    Noodl: {
      // ... existing Noodl properties ...
      Config: configManager.getConfig()
    }
  };
}

2. Expression Editor Autocomplete

File: packages/noodl-editor/src/editor/src/views/panels/propertyeditor/CodeEditor/ (or relevant autocomplete module)

Add Config keys to autocomplete suggestions:

import { ProjectModel } from '@noodl-models/projectmodel';
import { AppConfig } from '@noodl/runtime/src/config/types';

interface AutocompleteItem {
  label: string;
  kind: 'property' | 'method' | 'variable';
  detail?: string;
  documentation?: string;
}

function getConfigAutocompletions(prefix: string): AutocompleteItem[] {
  const config = ProjectModel.instance.getAppConfig();
  const items: AutocompleteItem[] = [];
  
  // Built-in config keys
  const builtInKeys = [
    { key: 'appName', type: 'string', doc: 'Application name' },
    { key: 'description', type: 'string', doc: 'Application description' },
    { key: 'coverImage', type: 'string', doc: 'Cover image path' },
    { key: 'ogTitle', type: 'string', doc: 'Open Graph title' },
    { key: 'ogDescription', type: 'string', doc: 'Open Graph description' },
    { key: 'ogImage', type: 'string', doc: 'Open Graph image' },
    { key: 'favicon', type: 'string', doc: 'Favicon path' },
    { key: 'themeColor', type: 'color', doc: 'Theme color' },
    { key: 'pwaEnabled', type: 'boolean', doc: 'PWA enabled state' },
    { key: 'pwaShortName', type: 'string', doc: 'PWA short name' },
    { key: 'pwaDisplay', type: 'string', doc: 'PWA display mode' },
    { key: 'pwaStartUrl', type: 'string', doc: 'PWA start URL' },
    { key: 'pwaBackgroundColor', type: 'color', doc: 'PWA background color' }
  ];
  
  // Add built-in keys
  for (const { key, type, doc } of builtInKeys) {
    if (key.startsWith(prefix)) {
      items.push({
        label: key,
        kind: 'property',
        detail: type,
        documentation: doc
      });
    }
  }
  
  // Add custom variables
  for (const variable of config.variables) {
    if (variable.key.startsWith(prefix)) {
      items.push({
        label: variable.key,
        kind: 'property',
        detail: variable.type,
        documentation: variable.description
      });
    }
  }
  
  return items;
}

// In the autocomplete provider
function provideCompletions(
  model: editor.ITextModel,
  position: Position
): CompletionList {
  const lineContent = model.getLineContent(position.lineNumber);
  const beforeCursor = lineContent.substring(0, position.column - 1);
  
  // Check if typing after "Noodl.Config."
  const configMatch = beforeCursor.match(/Noodl\.Config\.(\w*)$/);
  if (configMatch) {
    const prefix = configMatch[1];
    const items = getConfigAutocompletions(prefix);
    
    return {
      suggestions: items.map(item => ({
        label: item.label,
        kind: monaco.languages.CompletionItemKind.Property,
        detail: item.detail,
        documentation: item.documentation,
        insertText: item.label,
        range: new Range(
          position.lineNumber,
          position.column - prefix.length,
          position.lineNumber,
          position.column
        )
      }))
    };
  }
  
  // Check if typing "Noodl."
  const noodlMatch = beforeCursor.match(/Noodl\.(\w*)$/);
  if (noodlMatch) {
    const prefix = noodlMatch[1];
    const noodlProps = ['Variables', 'Objects', 'Arrays', 'Config'];
    
    return {
      suggestions: noodlProps
        .filter(p => p.startsWith(prefix))
        .map(prop => ({
          label: prop,
          kind: monaco.languages.CompletionItemKind.Module,
          insertText: prop,
          range: new Range(
            position.lineNumber,
            position.column - prefix.length,
            position.lineNumber,
            position.column
          )
        }))
    };
  }
  
  return { suggestions: [] };
}

3. Type Hints

File: Add type inference for config values

function getConfigValueType(key: string): string {
  const config = ProjectModel.instance.getAppConfig();
  
  // Check custom variables first
  const variable = config.variables.find(v => v.key === key);
  if (variable) {
    return variable.type;
  }
  
  // Built-in type mappings
  const typeMap: Record<string, string> = {
    appName: 'string',
    description: 'string',
    coverImage: 'string',
    ogTitle: 'string',
    ogDescription: 'string',
    ogImage: 'string',
    favicon: 'string',
    themeColor: 'color',
    pwaEnabled: 'boolean',
    pwaShortName: 'string',
    pwaDisplay: 'string',
    pwaStartUrl: 'string',
    pwaBackgroundColor: 'color'
  };
  
  return typeMap[key] || 'any';
}

4. Error Handling

File: packages/noodl-runtime/src/config/config-manager.ts

Ensure graceful handling of undefined keys:

// In the config proxy (from CONFIG-001)
get(target, prop: string) {
  if (prop in target) {
    return target[prop];
  }
  
  // Log warning in development
  if (process.env.NODE_ENV !== 'production') {
    console.warn(
      `Noodl.Config.${prop} is not defined. ` +
      `Add it in App Setup or check for typos.`
    );
  }
  
  return undefined;
}

Type Declarations Update

File: packages/noodl-viewer-react/static/viewer/global.d.ts.keep

Enhance the Config type declaration with JSDoc:

declare namespace Noodl {
  /**
   * App configuration values defined in App Setup.
   * 
   * Config values are static and cannot be modified at runtime.
   * Use `Noodl.Variables` for values that need to change.
   * 
   * @example
   * // Access app name
   * const name = Noodl.Config.appName;
   * 
   * // Use in template literal
   * const greeting = `Welcome to ${Noodl.Config.appName}!`;
   * 
   * // Access custom variable
   * const color = Noodl.Config.primaryColor;
   * 
   * @see https://docs.noodl.net/config
   */
  const Config: Readonly<{
    /** Application name */
    readonly appName: string;
    
    /** Application description */
    readonly description: string;
    
    /** Cover image path or URL */
    readonly coverImage?: string;
    
    /** Open Graph title (defaults to appName) */
    readonly ogTitle: string;
    
    /** Open Graph description (defaults to description) */
    readonly ogDescription: string;
    
    /** Open Graph image path or URL */
    readonly ogImage?: string;
    
    /** Favicon path or URL */
    readonly favicon?: string;
    
    /** Theme color (hex format) */
    readonly themeColor?: string;
    
    /** Whether PWA is enabled */
    readonly pwaEnabled: boolean;
    
    /** PWA short name */
    readonly pwaShortName?: string;
    
    /** PWA display mode */
    readonly pwaDisplay?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
    
    /** PWA start URL */
    readonly pwaStartUrl?: string;
    
    /** PWA background color */
    readonly pwaBackgroundColor?: string;
    
    /** Custom configuration variables */
    readonly [key: string]: any;
  }>;
}

Testing Checklist

Expression Access

  • Noodl.Config.appName returns correct value
  • Noodl.Config.primaryColor returns color value
  • Noodl.Config.menuItems returns array
  • Nested access works: Noodl.Config.menuItems[0].label
  • Template literals work: `Hello ${Noodl.Config.appName}`
  • Undefined keys return undefined (no crash)
  • Warning logged for undefined keys in dev

Autocomplete

  • Typing "Noodl." suggests "Config"
  • Typing "Noodl.Config." shows all config keys
  • Built-in keys show correct types
  • Custom variables appear in suggestions
  • Variable descriptions show in autocomplete
  • Autocomplete filters as you type

Integration

  • Works in Expression nodes
  • Works in Function nodes
  • Works in Script nodes
  • Works in inline expressions (if TASK-006 complete)
  • Values update when config changes (editor refresh)

Notes for Implementer

TASK-006 Dependency

This task should be implemented alongside or after TASK-006 (Expression System Overhaul). If TASK-006 introduces a new expression evaluator module, integrate Config there. Otherwise, update the existing expression.js.

Monaco Editor

The autocomplete examples assume Monaco editor is used. Adjust the implementation based on the actual editor component used in the expression input fields.

Performance

The config object is frozen and created once at startup. Accessing it in expressions should have minimal performance impact. However, avoid calling getConfig() repeatedly - cache the reference.

Cloud Functions

Remember to also update packages/noodl-viewer-cloud/src/noodl-js-api.js if cloud functions need Config access.