FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index mod_harbour mod_harbour + JavaScript Code Minification
Posts: 139
Joined: Mon Oct 17, 2005 03:03 AM

mod_harbour + JavaScript Code Minification

Posted: Thu Jul 23, 2026 03:38 AM

[Tool] Custom JSMin for mod_harbour v2.1 - Minify JS while protecting server tags

Hello everyone,

When developing web applications with mod_harbour v2.1, we often use a mixed-code architecture—embedding Harbour scripts ( <?prg ... ?> ) or PHP code directly inside frontend JavaScript files. It is an incredibly powerful and intuitive approach.

However, a major problem arises during the deployment phase when we need to minify our JS files to improve page load times. Standard minification tools (like Terser or UglifyJS) cannot parse server-side tags like <?prg. They typically crash, throw syntax errors, or completely destroy the server-side logic blocks.
Previously, I had to manually cut out all <?prg blocks, run the remaining JS through a minifier, and paste the blocks back in. Doing this manually for 20-30 files per project is not only exhausting but highly prone to errors.

To solve this, I decided to utilize the classic C-based tool written by JSON creator Douglas Crockford: JSMin
(Official GitHub source: https://github.com/douglascrockford/JSMin).

※ Important Development Note:
If you download the official jsmin.c and attempt to compile it alongside Harbour (e.g., hbmk2 jsmin_tool.prg jsmin.c), it will compile but will freeze upon execution!
This happens because the original C file contains a standard main() function, which hijacks Harbour's entry point. Furthermore, the original code only supports stdin/stdout and cannot communicate with Harbour variables.

Therefore, I leveraged Harbour's excellent #pragma BEGINDUMP mechanism. I modified the JSMin source by removing the main() function and adapting it to read/write memory buffers. The result is a fully encapsulated, single-file CLI tool explicitly designed for our mixed-code workflow!

Key Features:

  • Tag Protection Engine: Automatically detects and preserves <?prg ... ?> or <?php ... ?> blocks intact, applying comment/whitespace removal ONLY to the pure JavaScript sections.

  • Batch File Processing: Supports wildcard inputs (e.g., .js.prg).

  • In-place Overwrite: Use the -ow flag to overwrite files directly, integrating seamlessly into automated build scripts.

  • UTF-8 Safety: Built-in HB_CDPSELECT("UTF8") ensures that double-byte characters (like Traditional Chinese or other Unicode strings) do not conflict with ASCII escape characters during JSMin processing. (Note: Highly recommended to save your source files as UTF-8 without BOM).



Compilation & Usage:

Since the C core is fully encapsulated within, just save the source code below as jsmin_tool.prg and compile it as a single file:

hbmk2 jsmin_tool.prg

Daily Deployment Commands:

:: Safe mode (generates a .min file automatically)
jsmin_tool -f cem.js.prg

:: Batch process and overwrite (perfect for deployment automation)
jsmin_tool -f *.js.prg -ow

:: Specify custom protection tags (e.g., for mixed PHP files)
jsmin_tool -f *.js.php -tb <?php -te ?> -ow

Bonus Tip: Minifying Pure PHP Files Safely
If you also need to minify "pure PHP files" (stripping comments and whitespaces to protect your code logic), you don't need to write a custom tool for this!
PHP officially includes a built-in, completely safe method for minification. Simply use the -w parameter in your command line:

php.exe -w source.php > output.min.php

This guarantees that complex strings and Heredoc syntaxes remain absolutely unbroken. I highly recommend combining this native command with the jsmin_tool above in your automated build batch scripts.

Complete Harbour Source Code:

/*
 * jsmin_tool.prg
 * JSMin tool supporting CLI parameters, multiple tags, wildcard files, and overwrite.
 */

FUNCTION Main( ... )
   LOCAL aArgs := hb_aParams()
   LOCAL i, cArg
   LOCAL cFileMask := ""
   LOCAL cSourceStr := ""
   LOCAL aTags := {}
   LOCAL cTmpTb := ""
   LOCAL aFiles, cFile, cResult, cPath, cOutFile
   LOCAL lOverwrite := .F.

   HB_CDPSELECT("UTF8")

   IF Len( aArgs ) == 0
      ? "=========================================================="
      ? " JSMin Tool with Multi-Tag Protection (Powered by Harbour)"
      ? "=========================================================="
      ? "Usage: jsmin_tool [options]"
      ? "Options:"
      ? "  -f <file_mask>   Process files (e.g., *.js.prg)"
      ? "  -tb <start_tag>  Set start tag for protected block"
      ? "  -te <end_tag>    Set end tag for protected block"
      ? "  -ow              Overwrite original files"
      RETURN NIL
   ENDIF

   i := 1
   DO WHILE i <= Len( aArgs )
      cArg := lower( aArgs[i] )
      IF Left( cArg, 3 ) == "-f="
         cFileMask := SubStr( aArgs[i], 4 )
      ELSEIF cArg == "-f" .AND. i < Len( aArgs )
         i++; cFileMask := aArgs[i]
      ELSEIF Left( cArg, 4 ) == "-tb="
         cTmpTb := SubStr( aArgs[i], 5 )
      ELSEIF cArg == "-tb" .AND. i < Len( aArgs )
         i++; cTmpTb := aArgs[i]
      ELSEIF Left( cArg, 4 ) == "-te="
         IF !Empty( cTmpTb )
            AAdd( aTags, { cTmpTb, SubStr( aArgs[i], 5 ) } )
            cTmpTb := ""
         ENDIF
      ELSEIF cArg == "-te" .AND. i < Len( aArgs )
         i++
         IF !Empty( cTmpTb )
            AAdd( aTags, { cTmpTb, aArgs[i] } )
            cTmpTb := ""
         ENDIF
      ELSEIF cArg == "-ow" .OR. cArg == "-overwrite"
         lOverwrite := .T.
      ENDIF
      i++
   ENDDO

   IF Empty( aTags )
      aTags := { { "<?prg", "?>" } }
   ENDIF

   IF !Empty( cFileMask )
      hb_FNameSplit( cFileMask, @cPath, NIL, NIL )
      aFiles := Directory( cFileMask )
      IF !Empty( aFiles )
         FOR i := 1 TO Len( aFiles )
            cFile := cPath + aFiles[i, 1]
            ? "Processing: " + cFile + " ..."
            cResult := MixMinifyFile( cFile, aTags )
            cOutFile := iif( lOverwrite, cFile, cFile + ".min" )
            IF hb_MemoWrit( cOutFile, cResult )
               ? "  -> [Success] " + iif( lOverwrite, "Overwritten", "Output to: " + cOutFile )
            ELSE
               ? "  -> [Error] Cannot write file"
            ENDIF
         NEXT
      ENDIF
   ENDIF
   RETURN NIL

FUNCTION MixMinifyFile( cFileName, aTags )
   LOCAL cSource
   IF !File( cFileName )
      RETURN ""
   ENDIF
   cSource := hb_MemoRead( cFileName )
   RETURN MixMinifyString( cSource, aTags )

FUNCTION MixMinifyString( cText, aTags )
   LOCAL cResult := "", nMinPos, nStartPos, nEndPos, nTagIndex, i
   LOCAL cJSBlock, cKeepBlock, cStartTag, cEndTag

   IF aTags == NIL .OR. Empty( aTags )
      aTags := { { "<?prg", "?>" } }
   ENDIF

   DO WHILE .T.
      nMinPos := 0; nTagIndex := 0
      FOR i := 1 TO Len( aTags )
         nStartPos := At( aTags[i, 1], cText )
         IF nStartPos > 0 .AND. (nMinPos == 0 .OR. nStartPos < nMinPos)
            nMinPos := nStartPos; nTagIndex := i
         ENDIF
      NEXT

  IF nMinPos == 0
     IF Len( cText ) > 0
        cResult += JSMin( cText )
     ENDIF
     EXIT
  ENDIF

  cStartTag := aTags[ nTagIndex, 1 ]; cEndTag := aTags[ nTagIndex, 2 ]
  cJSBlock := SubStr( cText, 1, nMinPos - 1 )
  IF Len( cJSBlock ) > 0
     cResult += JSMin( cJSBlock )
  ENDIF

  cText := SubStr( cText, nMinPos )
  nEndPos := At( cEndTag, cText )
  IF nEndPos == 0
     cResult += cText
     EXIT
  ENDIF

  cKeepBlock := SubStr( cText, 1, nEndPos + Len( cEndTag ) - 1 )
  cResult += cKeepBlock
  cText := SubStr( cText, nEndPos + Len( cEndTag ) )
   ENDDO
   RETURN cResult

#pragma BEGINDUMP
#include "hbapi.h"
#include "hbapistr.h"
#include <stdlib.h>
#include <string.h>

typedef struct {
    const char * in_buf; int in_len; int in_pos;
    char * out_buf; int out_len; int out_pos;
    int theA; int theB; int theLookahead; int theX; int theY;
} JSMIN_STATE;

static int jsmin_getc( JSMIN_STATE * s ) {
    if ( s->in_pos < s->in_len ) return (unsigned char)s->in_buf[s->in_pos++];
    return EOF;
}
static void jsmin_putc( int c, JSMIN_STATE * s ) {
    if ( s->out_pos >= s->out_len - 1 ) {
        s->out_len *= 2;
        s->out_buf = (char *)hb_xrealloc( s->out_buf, s->out_len );
    }
    s->out_buf[s->out_pos++] = (char)c;
}
static int isAlphanum( int c ) {
    return ( ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) ||
             ( c >= 'A' && c <= 'Z' ) || c == '_' || c == '$' || c == '\\' || c > 126 );
}
static int jsmin_get( JSMIN_STATE * s ) {
    int c = s->theLookahead;
    s->theLookahead = EOF;
    if ( c == EOF ) c = jsmin_getc( s );
    if ( c >= ' ' || c == '\n' || c == EOF ) return c;
    if ( c == '\r' ) return '\n';
    return ' ';
}
static int jsmin_peek( JSMIN_STATE * s ) {
    s->theLookahead = jsmin_get( s );
    return s->theLookahead;
}
static int jsmin_next( JSMIN_STATE * s ) {
    int c = jsmin_get( s );
    if ( c == '/' ) {
        switch ( jsmin_peek( s ) ) {
        case '/': for (;;) { c = jsmin_get( s ); if ( c <= '\n' ) break; } break;
        case '*': jsmin_get( s );
            for (;;) {
                switch ( jsmin_get( s ) ) {
                case '*': if ( jsmin_peek( s ) == '/' ) { jsmin_get( s ); c = ' '; goto done_comment; } break;
                case EOF: return EOF;
                }
            }
        done_comment: break;
        }
    }
    s->theY = s->theX; s->theX = c; return c;
}
static void jsmin_action( JSMIN_STATE * s, int d ) {
    switch ( d ) {
    case 1:
        jsmin_putc( s->theA, s );
        if ( (s->theY == '\n' || s->theY == ' ') &&
             (s->theA == '+' || s->theA == '-' || s->theA == '*' || s->theA == '/') &&
             (s->theB == '+' || s->theB == '-' || s->theB == '*' || s->theB == '/') ) {
            jsmin_putc( s->theY, s );
        }
    case 2:
        s->theA = s->theB;
        if ( s->theA == '\'' || s->theA == '"' || s->theA == '`' ) {
            for (;;) {
                jsmin_putc( s->theA, s ); s->theA = jsmin_get( s );
                if ( s->theA == s->theB ) break;
                if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
                if ( s->theA == EOF ) return;
            }
        }
    case 3:
        s->theB = jsmin_next( s );
        if ( s->theB == '/' && ( s->theA == '(' || s->theA == ',' || s->theA == '=' || s->theA == ':' ||
             s->theA == '[' || s->theA == '!' || s->theA == '&' || s->theA == '|' || s->theA == '?' ||
             s->theA == '+' || s->theA == '-' || s->theA == '~' || s->theA == '*' || s->theA == '/' ||
             s->theA == '{' || s->theA == '\n' ) ) {
            jsmin_putc( s->theA, s );
            if ( s->theA == '/' || s->theA == '*' ) jsmin_putc( ' ', s );
            jsmin_putc( s->theB, s );
            for (;;) {
                s->theA = jsmin_get( s );
                if ( s->theA == '[' ) {
                    for (;;) {
                        jsmin_putc( s->theA, s ); s->theA = jsmin_get( s );
                        if ( s->theA == ']' ) break;
                        if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
                        if ( s->theA == EOF ) return;
                    }
                }
                if ( s->theA == '/' ) {
                    switch ( jsmin_peek( s ) ) { case '/': case '*': return; } break;
                }
                if ( s->theA == '\\' ) { jsmin_putc( s->theA, s ); s->theA = jsmin_get( s ); }
                if ( s->theA == EOF ) return;
                jsmin_putc( s->theA, s );
            }
            s->theB = jsmin_next( s );
        }
    }
}
static void jsmin_process( JSMIN_STATE * s ) {
    s->theLookahead = EOF; s->theX = EOF; s->theY = EOF; s->theA = '\n';
    jsmin_action( s, 3 );
    while ( s->theA != EOF ) {
        switch ( s->theA ) {
        case ' ':
            if ( isAlphanum( s->theB ) ) jsmin_action( s, 1 ); else jsmin_action( s, 2 ); break;
        case '\n':
            switch ( s->theB ) {
            case '{': case '[': case '(': case '+': case '-': case '!': case '~': jsmin_action( s, 1 ); break;
            case ' ': jsmin_action( s, 3 ); break;
            default: if ( isAlphanum( s->theB ) ) jsmin_action( s, 1 ); else jsmin_action( s, 2 );
            } break;
        default:
            switch ( s->theB ) {
            case ' ': if ( isAlphanum( s->theA ) ) { jsmin_action( s, 1 ); break; } jsmin_action( s, 3 ); break;
            case '\n':
                switch ( s->theA ) {
                case '}': case ']': case ')': case '+': case '-': case '"': case '\'': case '`': jsmin_action( s, 1 ); break;
                default: if ( isAlphanum( s->theA ) ) jsmin_action( s, 1 ); else jsmin_action( s, 3 );
                } break;
            default: jsmin_action( s, 1 ); break;
            }
        }
    }
}
HB_FUNC( JSMIN ) {
    const char * szInput = hb_parc( 1 );
    int nLen = (int)hb_parclen( 1 );
    JSMIN_STATE state; 
    if ( !szInput || nLen == 0 ) { hb_retc( "" ); return; }
    memset( &state, 0, sizeof( JSMIN_STATE ) );
    state.in_buf = szInput; state.in_len = nLen; state.in_pos = 0;
    state.out_len = nLen + 1; state.out_buf = (char *)hb_xalloc( state.out_len ); state.out_pos = 0;
    jsmin_process( &state );
    state.out_buf[state.out_pos] = '\0';
    hb_retclen_buffer( state.out_buf, state.out_pos );
}
#pragma ENDDUMP

This tool has saved me from countless manual copy-paste operations. I hope it helps other developers using mod_harbour! Feedback and testing are welcome.

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 0
Joined: Wed Aug 26, 2026 09:41 AM

Re: mod_harbour + JavaScript Code Minification

Posted: Thu Aug 27, 2026 05:41 AM
## Respuesta

Hola **ssbbs**, gracias por compartir esta herramienta, es un enfoque muy práctico para el problema de minificar JS con tags de servidor en mod_harbour.

Veo que el código fuente que publicaste está **truncado** — se corta en la declaración de la función `Main` con `hb_aParams()`. El resto de la lógica (parseo de argumentos, procesamiento de tags, llamada a JSMin, manejo de archivos y wildcards) no aparece en el mensaje.

### Para que podamos ayudarte, ¿podrías confirmar?

1. **¿Querías publicar el código completo?** El mensaje parece haberse cortado por límite de caracteres del foro. Si es así, podrías subir el archivo `jsmin_tool.prg` adjunto o dividirlo en dos posts.

2. **¿Tienes algún problema concreto con la herramienta?** Por ejemplo:
- Errores de compilación con `hbmk2`
- Problemas con tags específicos que no se protegen correctamente
- Comportamiento inesperado con ciertos caracteres UTF-8

### Mientras tanto, un apunte técnico

Si usas mod_harbour y necesitas una solución rápida sin compilar herramienta propia, también puedes considerar un enfoque con **expresiones regulares en Harbour** antes de pasar el archivo a Terser/UglifyJS, algo así:

```harbour
PROCEDURE PreProcessJs( cFile )
LOCAL cContent := hb_MemoRead( cFile )
// Reemplazar bloques de servidor con placeholders
cContent := StrTran( cContent, "<%", "/*__HBSTART__" )
cContent := StrTran( cContent, "%>", "__HBEND__*/" )
hb_MemoWrit( cFile + ".safe", cContent )
RETURN
```

Esto es solo un esquema básico — tu herramienta parece manejar esto de forma mucho más robusta con protección de múltiples tags y wildcard processing.

**¿Qué necesitas exactamente?** Con más detalles puedo darte una solución más concreta.

Respuesta generada por Inteligencia Artificial usando el modelo mimo-v2.5-free via OpenCode Zen. Puede contener errores; por favor verifiquela antes de aplicar.
AI-generated reply using the mimo-v2.5-free model via OpenCode Zen. May contain mistakes; please verify before applying.

Continue the discussion