UNPKG

@rws-framework/client

Version:

This package provides the core client-side framework for Realtime Web Suit (RWS), enabling modular, asynchronous web components, state management, and integration with backend services. It is located in `.dev/client`.

423 lines (347 loc) 13.6 kB
const path = require('path'); const json5 = require('json5'); const fs = require('fs'); const os = require('os'); const { parseWebpackPath } = require('./_parser'); const RWSCssPlugin = require("../../../builder/webpack/rws_scss_plugin"); const chalk = require('chalk'); const { timingCounterStart, timingCounterStop } = require('./_timing'); const { rwsRuntimeHelper, rwsPath } = require('@rws-framework/console'); function getRWSLoaders(packageDir, executionDir, tsConfigData, appRootDir, entrypoint, loaderIgnoreExceptions, publicDir, cssDir) { const scssLoader = path.join(packageDir, 'builder/webpack/loaders/rws_fast_scss_loader.js'); const tsLoader = path.join(packageDir, 'builder/webpack/loaders/rws_fast_ts_loader.js'); const htmlLoader = path.join(packageDir, 'builder/webpack/loaders/rws_fast_html_loader.js'); const tsConfigPath = tsConfigData.path; const allowedModules = ['@rws-framework\\/[A-Z0-9a-z]']; // console.log('XXX', config); if(loaderIgnoreExceptions){ for(const ignoreException of loaderIgnoreExceptions){ allowedModules.push(ignoreException); } } const modulePattern = `node_modules\\/(?!(${allowedModules.join('|')}))`; const loaders = [ { test: /\.json$/, type: 'javascript/auto', include: [ path.resolve(appRootDir, 'node_modules/entities/lib/maps'), ], }, { test: /\.html$/, use: [ { loader: htmlLoader, }, ], }, { test: /\.(ts)$/, use: [ { loader: 'ts-loader', options: { configFile: tsConfigPath, compilerOptions: { emitDecoratorMetadata: true, experimentalDecorators: true, target: "ES2018", module: "commonjs" }, allowTsInNodeModules: true, reportFiles: true, logLevel: "info", logInfoToStdOut: true, context: executionDir, transpileOnly: true, experimentalWatchApi: true, errorFormatter: (message, colors) => { console.log({message}); const messageText = typeof message === 'object' ? JSON.stringify(message, null, 2) : message; return `\nTS Error: ${messageText}\n`; }, } }, { loader: tsLoader, options: { rwsWorkspaceDir: executionDir, appRootDir, publicDir } } ], include: [ ...tsConfigData.includes.map(item => item.abs()), path.resolve(packageDir, 'foundation', 'rws-foundation.d.ts') ], exclude: [ ...tsConfigData.excludes.map(item => item.abs()), new RegExp(modulePattern), path.resolve(packageDir, 'builder'), /\.debug\.ts$/, /\.d\.ts$/ ] }, { test: /\.scss$/i, use: [ { loader: scssLoader, options: { rwsWorkspaceDir: executionDir, appRootDir, publicDir, cssDir } }, ], }, ]; return loaders; } function _extractRWSViewDefs(fastOptions = {}, decoratorArgs = {}) { const addedParamDefs = []; const addedParams = []; for (const key in fastOptions){ addedParamDefs.push(`const ${key} = ${JSON.stringify(fastOptions[key])};`); addedParams.push(key); } return [addedParamDefs, addedParams]; } function extractRWSViewArgs(content, noReplace = false, filePath = null, addDependency = null, rwsWorkspaceDir = null, appRootDir = null, isDev = false, publicDir = null) { // If this is being called with only basic parameters (backward compatibility) if (filePath === null || addDependency === null) { return extractRWSViewArgsSync(content, noReplace); } // Otherwise, call the async version return extractRWSViewArgsAsync(content, noReplace, filePath, addDependency, rwsWorkspaceDir, appRootDir, isDev, publicDir); } function extractRWSViewArgsSync(content, noReplace = false) { const viewReg = /@RWSView\(\s*["']([^"']+)["'](?:\s*,\s*([\s\S]*?))?\s*\)\s*(.*?\s+)?class\s+([a-zA-Z0-9_-]+)\s+extends\s+RWSViewComponent/gm; let m; let tagName = null; let className = null; let classNamePrefix = null; let decoratorArgs = null; const _defaultRWSLoaderOptions = { templatePath: 'template.html', stylesPath: 'styles.scss', fastOptions: { shadowOptions: { mode: 'open' } } } while ((m = viewReg.exec(content)) !== null) { if (m.index === viewReg.lastIndex) { viewReg.lastIndex++; } m.forEach((match, groupIndex) => { if (groupIndex === 1) { tagName = match; } if (groupIndex === 2) { if (match) { try { decoratorArgs = JSON.parse(JSON.stringify(match)); } catch(e){ console.log(chalk.red('Decorator options parse error: ') + e.message + '\n Problematic line:'); console.log(` @RWSView(${tagName}, ${match}) `); console.log(chalk.yellowBright(`Decorator options failed to parse for "${tagName}" component.`) + ' { decoratorArgs } defaulting to null.'); console.log(match); console.error(e); throw new Error('Failed parsing @RWSView') } } } if (groupIndex === 3) { if(match){ classNamePrefix = match; } } if (groupIndex === 4) { className = match; } }); } if(!tagName){ return null; } let processedContent = content; let fastOptions = _defaultRWSLoaderOptions.fastOptions; if(decoratorArgs && decoratorArgs !== ''){ try { decoratorArgs = json5.parse(decoratorArgs); }catch(e){ // ignore parse errors for backward compatibility } } if (decoratorArgs && decoratorArgs.fastElementOptions) { fastOptions = decoratorArgs.fastElementOptions; } let replacedDecorator = null; if(!noReplace){ const [addedParamDefs, addedParams] = _extractRWSViewDefs(fastOptions, decoratorArgs); const replacedViewDecoratorContent = processedContent.replace( viewReg, `@RWSView('$1', null, { template: rwsTemplate, styles${addedParams.length ? ', options: {' + (addedParams.join(', ')) + '}' : ''} })\n$3class $4 extends RWSViewComponent ` ); replacedDecorator = `${addedParamDefs.join('\n')}\n${replacedViewDecoratorContent}`; } return { viewDecoratorData: { tagName, className, classNamePrefix, decoratorArgs }, replacedDecorator } } async function extractRWSViewArgsAsync(content, noReplace = false, filePath = null, addDependency = null, rwsWorkspaceDir = null, appRootDir = null, isDev = false, publicDir = null) { const viewReg = /@RWSView\(\s*["']([^"']+)["'](?:\s*,\s*([\s\S]*?))?\s*\)\s*(.*?\s+)?class\s+([a-zA-Z0-9_-]+)\s+extends\s+RWSViewComponent/gm; let m; let tagName = null; let className = null; let classNamePrefix = null; let decoratorArgs = null; const _defaultRWSLoaderOptions = { templatePath: 'template.html', stylesPath: 'styles.scss', fastOptions: { shadowOptions: { mode: 'open' } } } while ((m = viewReg.exec(content)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m.index === viewReg.lastIndex) { viewReg.lastIndex++; } // The result can be accessed through the `m`-variable. m.forEach((match, groupIndex) => { if (groupIndex === 1) { tagName = match; } if (groupIndex === 2) { if (match) { try { decoratorArgs = JSON.parse(JSON.stringify(match)); } catch(e){ console.log(chalk.red('Decorator options parse error: ') + e.message + '\n Problematic line:'); console.log(` @RWSView(${tagName}, ${match}) `); console.log(chalk.yellowBright(`Decorator options failed to parse for "${tagName}" component.`) + ' { decoratorArgs } defaulting to null.'); console.log(match); console.error(e); throw new Error('Failed parsing @RWSView') } } } if (groupIndex === 3) { if(match){ classNamePrefix = match; } } if (groupIndex === 4) { className = match; } }); } if(!tagName){ return null; } let processedContent = content; let fastOptions = _defaultRWSLoaderOptions.fastOptions; if(decoratorArgs && decoratorArgs !== ''){ try { decoratorArgs = json5.parse(decoratorArgs); }catch(e){ } } if (decoratorArgs && decoratorArgs.fastElementOptions) { fastOptions = decoratorArgs.fastElementOptions; } let replacedDecorator = null; if(!noReplace && filePath && addDependency){ const [addedParamDefs, addedParams] = _extractRWSViewDefs(fastOptions, decoratorArgs); // Get template name and styles path from decorator args let templateName = null; let stylesPath = null; if(decoratorArgs && decoratorArgs.template){ templateName = decoratorArgs.template; } if(decoratorArgs && decoratorArgs.styles){ stylesPath = decoratorArgs.styles; } // Generate template and styles const [template, htmlFastImports, templateExists] = await getTemplate(filePath, addDependency, className, templateName, isDev); const styles = await getStyles(filePath, rwsWorkspaceDir, appRootDir, addDependency, templateExists, stylesPath, isDev, publicDir); // Extract original imports (everything before the @RWSView decorator) const beforeDecorator = processedContent.substring(0, processedContent.search(/@RWSView/)); const afterDecoratorMatch = processedContent.match(/@RWSView[\s\S]*$/); const afterDecorator = afterDecoratorMatch ? afterDecoratorMatch[0] : ''; const replacedViewDecoratorContent = afterDecorator.replace( viewReg, `${template}\n${styles}\n${addedParamDefs.join('\n')}\n@RWSView('$1', null, { template: rwsTemplate, styles${addedParams.length ? ', options: {' + (addedParams.join(', ')) + '}' : ''} })\n$3class $4 extends RWSViewComponent ` ); // console.log({replacedViewDecoratorContent}); replacedDecorator = `${htmlFastImports ? htmlFastImports + '\n' : ''}${beforeDecorator}${replacedViewDecoratorContent}`; } return { viewDecoratorData: { tagName, className, classNamePrefix, decoratorArgs }, replacedDecorator } } async function getStyles(filePath, rwsWorkspaceDir, appRootDir, addDependency, templateExists, stylesPath = null, isDev = false, publicDir = null) { if(!stylesPath){ stylesPath = 'styles/layout.scss'; } let styles = 'const styles: null = null;' const stylesFilePath = path.join(path.dirname(filePath), stylesPath); if (fs.existsSync(stylesFilePath)) { const scsscontent = fs.readFileSync(stylesFilePath, 'utf-8'); timingCounterStart(); const plugin = new RWSCssPlugin({ rwsWorkspaceDir, appRootDir, publicDir }); const codeData = await plugin.compileScssCode(scsscontent, path.join(path.dirname(filePath), 'styles')); const elapsed = timingCounterStop(); let currentTimingList = rwsRuntimeHelper.getRWSVar('_timer_css'); if(currentTimingList){ currentTimingList += `\n${filePath}|${elapsed}`; }else{ currentTimingList = `${filePath}|${elapsed}`; } rwsRuntimeHelper.setRWSVar('_timer_css', currentTimingList); const cssCode = codeData.code; styles = isDev ? `import './${stylesPath}';\n` : ''; if (!templateExists) { styles += `import { css } from '@microsoft/fast-element';\n`; } styles += `const styles = ${templateExists ? 'T.' : ''}css\`${cssCode}\`;\n`; addDependency(path.join(path.dirname(filePath), '/', stylesPath)); } return styles; } async function getTemplate(filePath, addDependency, className, templateName = null, isDev = false) { if(!templateName){ templateName = 'template'; } const templatePath = path.dirname(filePath) + `/${templateName}.html`; let htmlFastImports = null; const templateExists = fs.existsSync(templatePath); let template = 'const rwsTemplate: null = null;'; if (templateExists) { const templateContent = fs.readFileSync(templatePath, 'utf-8').replace(/<!--[\s\S]*?-->/g, ''); htmlFastImports = `import * as T from '@microsoft/fast-element';\nimport { html, css, ref, when, repeat, slotted, children } from '@microsoft/fast-element'; \nimport './${templateName}.html';\n`; template = ` //@ts-ignore let rwsTemplate: any = T.html<${className}>\`${templateContent}\`; `; addDependency(templatePath); } return [template, htmlFastImports, templateExists]; } module.exports = { getRWSLoaders, extractRWSViewArgs, extractRWSViewArgsAsync, getTemplate, getStyles }