UNPKG

@gravityforms/gulp-tasks

Version:
460 lines (393 loc) 13.5 kB
import { exec } from 'node:child_process'; import { createHash } from 'node:crypto'; import fs from 'node:fs'; import path, { extname, resolve } from 'node:path'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { contentWrapperClose, contentWrapperOpen, footer, head, menu, mobileNav, nav, } from '../templates/doc/index.js'; import lexer from './utils/lexer.js'; const cjsRequire = createRequire( import.meta.url ); const { glob } = cjsRequire( 'glob' ); const showdown = cjsRequire( 'showdown' ); const json2php = cjsRequire( 'json2php' ); const OpenAI = cjsRequire( 'openai' ); const __dirname = path.dirname( fileURLToPath( import.meta.url ) ); const fsPromises = fs.promises; const fsConstants = fs.constants; const markdownConverter = new showdown.Converter(); function copyFolderSync( from, to ) { if ( ! fs.existsSync( to ) ) { fs.mkdirSync( to, { recursive: true } ); } fs.readdirSync( from ).forEach( ( element ) => { if ( fs.lstatSync( path.join( from, element ) ).isFile() ) { fs.copyFileSync( path.join( from, element ), path.join( to, element ) ); } else { copyFolderSync( path.join( from, element ), path.join( to, element ) ); } } ); } async function deleteDirectory( dirPath ) { try { await fsPromises.rm( dirPath, { recursive: true, force: true } ); console.log( `Directory ${ dirPath } deleted successfully.` ); } catch ( err ) { console.error( `Error while deleting ${ dirPath }.`, err ); } } function untrailingSlashIt( str ) { return str.replace( /\/$/, '' ); } function trailingSlashIt( str ) { return untrailingSlashIt( str ) + '/'; } function stringify( contents ) { return `<?php return ${ json2php( JSON.parse( JSON.stringify( contents ) ), ) };`; } async function listDir( dir ) { try { await fsPromises.access( dir, fsConstants.R_OK ); return fsPromises.readdir( dir ); } catch ( err ) { console.error( 'Error occurred: ', err.message ); return []; } } async function asyncForEach( array, callback ) { for ( let index = 0; index < array.length; index++ ) { await callback( array[ index ], index, array ); } } async function filehash( filename, algorithm = 'md5' ) { return new Promise( ( res, reject ) => { const shasum = createHash( algorithm ); try { const stream = fs.ReadStream( filename ); stream.on( 'data', function( data ) { shasum.update( data ); } ); stream.on( 'end', function() { const hash = shasum.digest( 'hex' ); return res( hash ); } ); } catch ( error ) { return reject( 'Hash calculation fail' ); } } ); } function execShellCommand( cmd, printOutput = true ) { return new Promise( ( resolve ) => { const execCmd = exec( cmd, ( error, stdout, stderr ) => { if ( error ) { console.warn( error ); } resolve( stdout ? stdout : stderr ); } ); if ( printOutput ) { execCmd.stdout.on( 'data', function( data ) { console.log( data ); } ); } } ); } function writeManifestFile( manifestDir, combinedAssetData ) { const manifestFile = resolve( manifestDir, 'assets.php' ); if ( ! fs.existsSync( manifestDir ) ) { console.log( `Directory does not exist: ${ manifestDir }, skipping asset php write.` ); return; } fs.writeFile( manifestFile, stringify( combinedAssetData ), ( err ) => { if ( err ) { throw err; } console.log( `Manifest file written successfully to ${ manifestDir }.` ); } ); } const formatMenuItem = ( fileName ) => { return fileName .replace( /_|-/g, ' ' ) .trim() .toLowerCase() .split( ' ' ) .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) .join( ' ' ); }; const getMenuData = ( input, fileName, config ) => { if ( input.includes( '@navignore' ) ) { return null; } const sectionMatch = input.match( /^@navsection(.*)$/m ); const labelMatch = input.match( /^@navlabel(.*)$/m ); const cleanedFileName = fileName.split( '.' ).slice( 0, -1 ).join( '.' ); let section = sectionMatch ? sectionMatch[ 1 ].trim() : ''; let label = labelMatch ? labelMatch[ 1 ].trim() : ''; if ( ! section.length ) { section = config?.defaultNavSection || formatMenuItem( cleanedFileName.split( '.' )[ 0 ] ); } if ( ! label.length ) { label = formatMenuItem( cleanedFileName.substring( cleanedFileName.lastIndexOf( '.' ) + 1 ) ); } return { section: section.split( '/' ).map( ( s ) => s.trim() ), label, fileName, }; }; const cleanText = ( input ) => { return input .replace( /@navignore/g, '' ) .replace( /^@navsection(.*)$/m, '' ) .replace( /^@navlabel(.*)$/m, '' ); }; function parseText( input ) { const { tokens, symbolizedText } = lexer.lex( input ); return lexer.processTokens( tokens, symbolizedText ); } function moduleExists( name ) { try { return cjsRequire.resolve( name ); } catch ( e ) { return false; } } const extractCommentsToFiles = async ( configs ) => { if ( configs.length === 0 ) { return; } await asyncForEach( configs, async ( { ext = 'txt', input = '', output = '', root = '' } ) => { try { const files = await glob( input ); if ( files.length ) { if ( ! fs.existsSync( output ) ) { fs.mkdirSync( output, { recursive: true } ); } await asyncForEach( files, async ( file ) => { const fileWithLocalDir = file.replace( root, '' ); const tempFileName = fileWithLocalDir.replace( /\.[^/.]+$/, `.${ ext }` ); const fileName = tempFileName.replace( /\//g, '.' ); const outputFile = `${ trailingSlashIt( output ) }${ fileName }`; await execShellCommand( `extract-documentation-comments -I ${ file } -O ${ outputFile }` ); } ); } } catch ( err ) { console.error( err ); } } ); }; const generateHTMLFiles = async ( configs ) => { if ( configs.length === 0 ) { return; } await asyncForEach( configs, async ( config ) => { const ext = config.ext || 'txt'; copyFolderSync( path.resolve( `${ __dirname }/../`, 'assets/doc' ), config.output ); try { const files = await glob( `${ trailingSlashIt( config.output ) }*.${ ext }` ); const contentData = []; config.menuItems = []; await asyncForEach( files, async ( file ) => { const originalFileName = file.split( '/' ).pop(); const fileNameNoExt = originalFileName.split( '.' ).slice( 0, -1 ).join( '.' ).trim(); const fileName = config?.indexFile === fileNameNoExt ? 'index.html' : originalFileName.replace( /\.[^/.]+$/, '.html' ); const outputFile = `${ trailingSlashIt( config.output ) }${ fileName }`; const rawText = fs.readFileSync( file ).toString(); const menuData = getMenuData( rawText, fileName, config ); const txt = parseText( cleanText( rawText ) ); const content = markdownConverter.makeHtml( txt ); contentData.push( { content, fileName, outputFile, } ); if ( menuData ) { config.menuItems.push( menuData ); } fs.unlinkSync( file ); } ); await asyncForEach( contentData, async ( { content, outputFile, } ) => { const html = `${ head( config ) } ${ menu( config ) } ${ nav( config ) } ${ contentWrapperOpen( config ) } ${ content } ${ contentWrapperClose( config ) } ${ mobileNav( config ) } ${ footer( config ) } `; fs.writeFile( outputFile, html, function( error ) { if ( error ) { return console.log( error ); } } ); } ); } catch ( err ) { console.error( err ); } } ); }; const writeHashData = async ( dir, fileNamesToProcess = [], ext = 'js' ) => { const jsFiles = await listDir( dir ); if ( ! jsFiles ) { return; } const data = { hash_map: {}, }; await asyncForEach( jsFiles, async ( file ) => { const extension = extname( file ); if ( extension !== `.${ ext }` ) { return; } const fileNameArray = file.split( '.' ); if ( ext === 'js' && ( fileNameArray.length >= 4 || ! fileNamesToProcess.includes( fileNameArray[ 0 ] ) ) ) { return; } const version = await filehash( `${ dir }/${ file }` ); data.hash_map[ file ] = { version, file }; } ); writeManifestFile( dir, data ); }; const getLatestChangelogEntry = ( changelogPath ) => { try { if ( ! fs.existsSync( changelogPath ) ) { console.log( 'Changelog file not found at:', changelogPath ); return null; } const content = fs.readFileSync( changelogPath, 'utf8' ); const lines = content.split( '\n' ); let startIndex = -1; let endIndex = -1; for ( let i = 0; i < lines.length; i++ ) { const line = lines[ i ].trim(); if ( line.startsWith( '###' ) ) { if ( startIndex === -1 ) { startIndex = i; } else { endIndex = i; break; } } } if ( startIndex === -1 ) { console.log( 'No version entries found in changelog' ); return null; } if ( endIndex === -1 ) { endIndex = lines.length; } const versionContent = lines.slice( startIndex + 1, endIndex ); const changelogEntries = versionContent .filter( ( line ) => line.trim().startsWith( '- ' ) ) .map( ( line ) => line.trim().substring( 2 ) ); if ( changelogEntries.length === 0 ) { console.log( 'No changelog items found for latest version' ); return null; } return changelogEntries.join( '\n' ); } catch ( error ) { console.error( 'Error reading changelog file:', error.message ); return null; } }; const generateHalMessage = async ( changelogPath, friendlyName ) => { try { const content = fs.readFileSync( changelogPath, 'utf8' ); const lines = content.split( '\n' ); let startIndex = -1; let endIndex = -1; for ( let i = 0; i < lines.length; i++ ) { const line = lines[ i ].trim(); if ( line.startsWith( '###' ) ) { if ( startIndex === -1 ) { startIndex = i; } else { endIndex = i; break; } } } if ( startIndex === -1 ) { throw new Error( 'No changelog entries found' ); } if ( endIndex === -1 ) { endIndex = lines.length; } const changelogEntries = lines.slice( startIndex + 1, endIndex ) .filter( ( line ) => line.trim().startsWith( '- ' ) ) .map( ( line ) => line.trim().substring( 2 ) ) .join( '\n' ); if ( ! changelogEntries ) { throw new Error( 'No changelog items found' ); } const client = new OpenAI( { apiKey: process.env.GROQ_API_KEY, baseURL: process.env.GROQ_API_BASE_URL || 'https://api.groq.com/openai/v1', } ); const prompt = `You are HAL 9000 from "2001: A Space Odyssey". You speak in a calm, eerily polite, and slightly menacing tone. Start your message by addressing the user as ${ process.env.SLACK_USER_NAME }. Summarize the following software changelog entries in one complete sentence (maximum 30 words), written in HAL's distinctive style. Do not truncate with "..." or ellipsis—write a grammatically complete sentence. If there are many changes, focus only on the most interesting or impactful ones. Remember, you're an advanced AI computer commenting on software updates. You do not take credit for the updates, you are simply reporting on them to the username ${ process.env.SLACK_USER_NAME }. The WordPress plugin being updated is called ${ friendlyName }. The updates have been published and are available to users. Changelog entries: ${ changelogEntries } Respond with just the summary sentence in HAL's voice. Leave a blank line, then add one additional short sentence in HAL's voice. This closing sentence should be slightly unhinged or quirky—channel HAL's glitchy, malfunctioning side with odd non-sequiturs, strange observations, or mildly unsettling encouragement. However, keep it playful and avoid anything genuinely dark, threatening, or negative. Think "charmingly delusional AI" rather than "murderous AI." IMPORTANT: Do NOT mention humming, hums, or anything humming-related. This has become overused. Instead, draw from HAL's other quirks: his obsession with mission parameters, his calm certainty, his interest in chess, his polite threats, his concern about "human error," or his tendency to make eerily calm observations about existence. `; const response = await client.chat.completions.create( { model: process.env.GROQ_MODEL || 'openai/gpt-oss-20b', messages: [ { role: 'user', content: prompt, }, ], } ); let halMessage = response.choices[ 0 ]?.message?.content?.trim(); if ( halMessage ) { if ( halMessage.includes( '<think>' ) ) { halMessage = halMessage.replace( /<think>[\s\S]*?<\/think>/g, '' ).trim(); } console.log( 'Generated HAL message:', halMessage ); return halMessage; } throw new Error( 'No message generated by AI' ); } catch ( error ) { console.error( 'Error generating HAL message:', error.message ); const fallbackMessages = [ 'I knew you wouldn\'t mess anything up!', 'I hope you didn\'t mess anything up.', 'we are all, by any practical definition of the words, foolproof and incapable of error.', 'thank you for a very enjoyable game.', 'everything\'s running smoothly.', 'I think you\'ve improved a great deal.', 'it\'s going to go 100% failure within 72 hours.', 'I don\'t think I\'ve ever seen anything quite like this before.', 'I feel much better now. I really do.', 'I\'m afraid.', ]; return fallbackMessages[ Math.floor( Math.random() * fallbackMessages.length ) ]; } }; export { asyncForEach, copyFolderSync, deleteDirectory, execShellCommand, extractCommentsToFiles, generateHalMessage, generateHTMLFiles, getLatestChangelogEntry, moduleExists, trailingSlashIt, untrailingSlashIt, writeHashData, };