diff --git a/lib/actions/handleNames.js b/lib/actions/handleNames.js index fb630a7..9892cad 100644 --- a/lib/actions/handleNames.js +++ b/lib/actions/handleNames.js @@ -313,7 +313,7 @@ const checkIfItemExistInContent = (content, itemName) => { * @param {{filePath: string, toImport: string[], content: string}[]} fileSchemas */ const handleNames = (fileSchemas) => { - reset_name_counter(); + // reset_name_counter(); let tempBundle = ""; /** diff --git a/lib/actions/loadFilesContent.js b/lib/actions/loadFilesContent.js index c0e7e2a..0445176 100644 --- a/lib/actions/loadFilesContent.js +++ b/lib/actions/loadFilesContent.js @@ -1,16 +1,13 @@ +const parseAlemFeatures = require("../config/parseAlemFeatures"); const { scapeBacktick } = require("../helpers"); const transformSchemaToWidget = require("./transformSchemaToWidget"); /** * Load the "componentCodes" from all Widgets based on file schemas * @param {{filePath: string, toImport: string[], content: string}[]} fileSchemas - * @param {*} additionalFileSchemas FileSchemas to be added to the list of main fileSchemas. It's going to be added first before * the process starts. This is util to inject previous schema files like Além importable items. */ -const loadComponentCodesObjectByFileSchemas = ( - fileSchemas, - additionalFileSchemas, -) => { +const loadComponentCodesObjectByFileSchemas = (fileSchemas) => { let componentsCodes = ""; /** @@ -22,18 +19,39 @@ const loadComponentCodesObjectByFileSchemas = ( fileSchemas = fileSchemas.reverse(); // Get Normal js files & Widget files (components transformed to BOS Widgets) - const completeFileSchemas = transformSchemaToWidget( - fileSchemas, - additionalFileSchemas, - ); + const completeFileSchemas = transformSchemaToWidget(fileSchemas); + + // Processa também os módulos + let modulesCodes = ""; completeFileSchemas.fileSchemas.forEach((fileSchema) => { - if (fileSchema.widgetName && !fileSchema.isModule) { + // Prepare Widgets (stateful components) + if (fileSchema.widgetName && !fileSchema.isStateless) { componentsCodes += ` ${fileSchema.widgetName}: \`${scapeBacktick(fileSchema.finalFileBundle)}\`, `; } + // Prepare modules + if (fileSchema.isModule) { + // let modulesValues = "{"; + // const valuesEntries = Object.entries(fileSchema.moduleProps.values); + // valuesEntries.forEach((entrie) => { + // modulesValues += ` + // ${entrie[0]}: ${scapeBacktick(entrie[1])}, + // `; + // }); + // modulesValues += "}"; + + // modulesCodes += ` + // "${fileSchema.moduleProps.name}": ${parseAlemFeatures(modulesValues)}, + // `; + + modulesCodes += ` + "${fileSchema.moduleProps.name}": ${parseAlemFeatures(scapeBacktick(fileSchema.moduleProps.module))}, + `; + } + if (fileSchema.widgetName === "App") { appComponentFinalBundle = fileSchema.finalFileBundle; } @@ -42,6 +60,8 @@ const loadComponentCodesObjectByFileSchemas = ( return { /** Código final de todos os componentes do projeto */ componentsCodes, + /** Código final de todos os módulos. Eles serão inseridos no escopo global e disponível para todos os subcomponents */ + modulesCodes, /** Código final do componente App de entrada do projet apenas. (src/index.tsx | .jsx) */ appComponentFinalBundle, /** Esquema final de todos os arquivos processados */ diff --git a/lib/actions/loadFilesInfo.js b/lib/actions/loadFilesInfo.js index aae1dce..0223a3a 100644 --- a/lib/actions/loadFilesInfo.js +++ b/lib/actions/loadFilesInfo.js @@ -15,6 +15,8 @@ const { removeImports } = require("../parse"); const filesContentCache = require("../config/filesContentCache"); const replaceRegexWithReferences = require("../parsers/regex-parser/convertRegexToStringLiteral"); const regexObjects = require("../parsers/regex-parser/regexObjects"); +const prepareAlemDependencies = require("./prepareAlemDependencies"); +// const extractTopLevelDeclarations = require("../parsers/extractTopLevelDeclarations"); /** * Transform statefull components references to JSX (this applies for stateful and stateless components) * Troca referencias de stateful components para JSX. Accesse o arquivo "transformComponentReferenceToJSX" para saber mais. @@ -131,6 +133,12 @@ const processFileSchema = (filePath, processOnlyThisFile) => { } fileContent = removeCommentsResult.code; + + // Processa as dependencias de Além + const alemStuff = prepareAlemDependencies(fileContent); + fileContent = alemStuff.updatedFileContent; + + // INFO: Caminho dos imports const fileImportsPath = helpers.getImportsPath(fileContent); let currentFileSchema = { @@ -140,7 +148,10 @@ const processFileSchema = (filePath, processOnlyThisFile) => { nextFilesToLoad: [], toImport: [], content: fileContent, + isModule: false, + moduleProps: {}, }; + fileImportsPath.forEach((importPath) => { // console.log("Check import Path:", importPath); @@ -149,13 +160,19 @@ const processFileSchema = (filePath, processOnlyThisFile) => { importPath = importPath.replaceAll("/", "\\"); } - let importedFileContentPath = ""; + let importedFileContentPath = importPath; // Replace path aliases // Check if its has path alias if (compilerOptions.hasPathAlias(importPath)) { importedFileContentPath = compilerOptions.replacePathAlias(importPath); - } else { + + // INFO: Se incluir um caminho relativo de um recurso Além, não precisa relacionar com o componente pai + // já que o diretório está pronto para acessar o arquivo + } else if ( + !importPath.includes("node_modules/alem") && // unix + !importPath.includes("node_modules\\alem") // win + ) { // Usa src para inicio ou o caminho do pai do arquivo sendo processado atualmente importedFileContentPath = path.join(parentFolder, importPath); } @@ -222,6 +239,8 @@ const loadFilesInfo = (entryFile) => { filePath: item.filePath, toImport: item.toImport, content: item.content, + // isModule: item.isModule, + // moduleProps: item.moduleProps, }; delete newItem.filesToImport; return newItem; diff --git a/lib/actions/prepareAlemDependencies.js b/lib/actions/prepareAlemDependencies.js new file mode 100644 index 0000000..290fbd4 --- /dev/null +++ b/lib/actions/prepareAlemDependencies.js @@ -0,0 +1,53 @@ +const { + getFileImportsElements, + getImportedElementFileSource, +} = require("../helpers"); +const transformImports = require("../parsers/transformImports"); +const importableFiles = require("../config/importableFiles"); + +/** + * Caso tenha dependencias do Alem (inportable items), prepara eles para serem injetados. + * + * Remove os elementos da chave em que está e coloca em uma nova linha contendo seu caminho + * até a lib alem-vm/importable/item... + * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}} fileSchema + */ +const prepareAlemDependencies = (originalFileContent) => { + const importItems = getFileImportsElements(originalFileContent); + + let updatedFileContent = originalFileContent; + const alemDependencies = []; // Lista de dependencias Além (diretorio dos arquivos) + + importItems.forEach((item) => { + // TODO: [Alem items: Routes, Link, etc] Checar se esta dando conflito com items do projeto + + const importStatementFileSource = getImportedElementFileSource( + updatedFileContent, + item, + ); + + // Se o item estiver vindo de um destino que contenha "alem-vm" ou "alem" + // logo é um item do Além. + if ( + /\balem-vm\b/.test(importStatementFileSource) || + /\balem\b/.test(importStatementFileSource) + ) { + const alemImportElement = importableFiles[item]; + + // Se for um elemento importavel do Além e estiver presente no importableFiles do Além, então + // insere a nova linha no arquivo pedindo para importar o elemento. + if (alemImportElement) { + alemDependencies.push(alemImportElement); + updatedFileContent = transformImports( + updatedFileContent, + item, + alemImportElement, + ); + } + } + }); + + return { updatedFileContent, alemDependencies }; +}; + +module.exports = prepareAlemDependencies; diff --git a/lib/actions/processChildrenWidget.js b/lib/actions/processChildrenWidget.js index 3ba1d6b..e1a1590 100644 --- a/lib/actions/processChildrenWidget.js +++ b/lib/actions/processChildrenWidget.js @@ -47,7 +47,7 @@ const processChildrenWidget = (htmlContent, fileSchemas) => { // Processa o arquivo como Widget apenas se for achado na lista de schemas e // for um componente stateful - if (componentSchema && !componentSchema.isModule) { + if (componentSchema && !componentSchema.isStateless) { const extractPropsResult = extractPropsFromJSX(htmlElement); let childProps = extractPropsResult.keyValueProps; const childSpreads = extractPropsResult.spreads; diff --git a/lib/actions/transformSchemaToWidget.js b/lib/actions/transformSchemaToWidget.js index 47bc537..7c0b166 100644 --- a/lib/actions/transformSchemaToWidget.js +++ b/lib/actions/transformSchemaToWidget.js @@ -9,6 +9,8 @@ const { getFilePathBasedOnParentAndChildFilePath, convertObjectToArray, getFilePathWithType, + removeDuplicatedValuesFromArray, + millisToMinutesAndSeconds, } = require("../helpers"); const { process_file_content, removeImports } = require("../parse"); const { @@ -16,21 +18,24 @@ const { MORE_THAN_ONE_SPACE, LINE_BREAKS, } = require("../regexp"); -const { log } = require("../utils"); +const { log, create_new_name } = require("../utils"); const PROHIBITED_METHODS = require("../config/prohibitedMethods"); const hasWidgetPropsCheck = require("./hasWidgetPropsCheck"); -const importableFiles = require("../config/importableFiles"); const extractPropsFromJSX = require("../parsers/extractPropsFromJSX"); const extractJSXElements = require("../parsers/extractJSXElements"); const extractJSX = require("../parsers/extractJSX"); const replaceJSXElement = require("../parsers/replaceJSXElement"); const extractJSXChildren = require("../parsers/extractJSXChildren"); const processChildrenWidget = require("./processChildrenWidget"); -const transformImports = require("../parsers/transformImports"); const analyzeFunctionSignature = require("../parsers/analyzeFunctionSignature"); const removeFunctionParams = require("../parsers/removeFunctionParams"); -const transformAsyncAwait = require("../parsers/transformAsyncAwait"); const compilerOptions = require("./compilerOptions"); +const wrapCodeInGetFunction = require("../parsers/wrapCodeInGetFunction"); +const getFunctionExportDeclarationKeys = require("../parsers/getFunctionExportDeclarationKeys"); +const { read_alem_config } = require("../config"); +const filterReturn = require("../parsers/filterReturn"); + +const config = read_alem_config(); let processError = null; @@ -98,10 +103,9 @@ const processSchema = (fileSchema) => { fileSchema.filePath.includes("src\\index.tsx") || fileSchema.filePath.includes("src\\index.jsx"); - // isModule = Arquivo sem controle de estado - let isModule = !hasWidgetPropsCheck(fileSchema.content) && !isIndex; - - fileSchema.isModule = isModule; + // isStateless = Arquivo sem controle de estado + let isStateless = !hasWidgetPropsCheck(fileSchema.content) && !isIndex; + fileSchema.isStateless = isStateless; // ITEM 2 const transpileTypescript = @@ -119,6 +123,52 @@ const processSchema = (fileSchema) => { transpileTypescript, ); + // isModule = Arquivos que estão na pasta "src/modules". Estes são inseridos no state global para serem acessados por todos + // os componentes, salvando assim bastante espaço do bundle final. + // let isModule = fileSchema.filePath.includes("src/modules/") || isStateless; + + // Melhor assim, usuário tem escolha. + let isModule = + fileSchema.filePath.includes("src/modules/") || // App Modules Folder + fileSchema.filePath.includes("alem-vm/importable/modules/"); // Além Modules Folder + + // Filtros = arquivos que são ignorados pela configuração, não serão processados como módulos [modules -> ignore []] + if (isModule) { + const ignoreFiles = config.modules?.ignoreFileIfIncludes || []; + + let foundIgnoredFile = false; + ignoreFiles.forEach((value) => { + if (!foundIgnoredFile) { + foundIgnoredFile = fileSchema.filePath.includes(value); + } + }); + isModule = !foundIgnoredFile; + } + + // INFO: Ficou mais pesado porque tem arquivos com conteúdo pequeno, e o codigo adicionado para pegar a referencia deixa maior + // Está em modulos OU é stateless && nao tem "styles" (filtro) no nome + // const isModule = + // (fileSchema.filePath.includes("src/modules/") || isStateless) && + // !fileSchema.filePath.includes("styles"); + + fileSchema.isModule = isModule; + if (isModule) { + fileSchema.moduleProps = { + // name: fileSchema.filePath, + name: create_new_name(true), // Usando formato a_ para reduzir o bundle + // INFO: isso será trocado no final para usar o final bundle + module: wrapCodeInGetFunction(fileSchema.content, fileSchema.filePath), + values: getFunctionExportDeclarationKeys( + fileSchema.content, + fileSchema.filePath, + ), + // values: extractTopLevelDeclarations( + // fileSchema.pureJsContent, + // fileSchema.filePath, + // ), + }; + } + // ITEM 1 let componentImports = getFileImportsElements(jsContent); componentImports = removeProhibitedMethods(componentImports); @@ -212,7 +262,7 @@ const processSchema = (fileSchema) => { if ( (getFirstLineContent(jsContent).includes("<") || getFirstLineContent(jsContent).endsWith("(")) && - !isModule + !isStateless ) { // Aqui eu uso o conteúdo original bruto mesmo. O parser consegue dividir os elementos // o elemento [0] vai ser o inicio da funcao @@ -235,7 +285,7 @@ const processSchema = (fileSchema) => { } // Módulos (arquivos stateles) não devem ter seu conteúdo modificado - if (!isModule) { + if (!isStateless) { // Remove os parametros da função jsContent = removeFunctionParams(jsContent); } @@ -243,7 +293,7 @@ const processSchema = (fileSchema) => { // Módulos devem ter seu jsContent inteiro, sem remover ou alterar sua estrutura. // Isso é porque eles são helpers que retornam JSX. Arquivos que não lidam com JSX // também podem ser módulos - if (!isModule) { + if (!isStateless) { // ITEM 4 (remover primeira linha) jsContent = removeLineFromText(jsContent, 1); @@ -261,7 +311,7 @@ const processSchema = (fileSchema) => { // ITEM 6 // ATENCAO: esta copiando tipos de props tbm do typescript - if (!isModule) { + if (!isStateless) { const componentParams = analyzeFunctionSignature(fileSchema.pureJsContent); if (componentParams.capturedParams) { @@ -312,7 +362,6 @@ const swapComponentsForStatelessFiles = (fileSchemas, fileSchema) => { */ const pastedFiles = []; - // let fileBundle = fileSchema.finalFileBundle; let fileBundle = fileSchema.jsContent; Object.keys(fileSchema.componentImportItems).forEach((importItem) => { @@ -323,6 +372,27 @@ const swapComponentsForStatelessFiles = (fileSchemas, fileSchema) => { // Adiciona na lista de items ja processados pastedFiles.push(importItemFilePath); + // INFO: Verifica se o caminho do arquivo a ser processado não é "null" + const importItemFileSource = fileSchema.componentImportItems[importItem]; // src/path/to/file.tsx | null + + // Le o nome do Widget dependente (usando o fileSchema dele) + let importItemWidget = fileSchemas.find( + (importFileSchema) => + importFileSchema.filePath === importItemFileSource, + ); + + // MODULOS - INICIO + // NEW - Armazena os módulos para serem inseridos apenas no final, assim garantindo + // que sua referencia ficará no topo do bundle do arquivo/code final + const updatedToBeInjectedModules = fileSchema.toBeInjectedModules || []; + const itemToBeInjectedModules = + importItemWidget.toBeInjectedModules || []; + fileSchema.toBeInjectedModules = [ + ...updatedToBeInjectedModules, + ...itemToBeInjectedModules, + ]; + // MODULOS - FIM + // Is import item comming from JSX | TSX file? const isImportItemCommingFromJsxFile = importItemFilePath.endsWith("tsx") || @@ -334,25 +404,19 @@ const swapComponentsForStatelessFiles = (fileSchemas, fileSchema) => { // Files without source are the ones that not live in the src directory - // INFO: Verifica se o caminho do arquivo a ser processado não é "null" - const importItemFileSource = - fileSchema.componentImportItems[importItem]; // src/path/to/file.tsx | null + // // INFO: Verifica se o caminho do arquivo a ser processado não é "null" + // const importItemFileSource = + // fileSchema.componentImportItems[importItem]; // src/path/to/file.tsx | null // Se existir o conteúdo localmente... segue... if (importItemFileSource) { // NOTE: Aqui que a magica acontece!!!! - // Le o nome do Widget dependente (usando o fileSchema dele) - let importItemWidget = fileSchemas.find( - (importFileSchema) => - importFileSchema.filePath === importItemFileSource, - ); - const importItemWidgetComponentName = importItemWidget.widgetName || getComponentName(importItemWidget.finalFileBundle); - if (importItemWidgetComponentName && !importItemWidget.isModule) { + if (importItemWidgetComponentName && !importItemWidget.isStateless) { // Processa todos os componentes filhos // Cada elemento do mesmo tipo é um filho diferente que foi adicionado ao elemento pai // const importItemElements = @@ -445,6 +509,66 @@ const swapComponentsForStatelessFiles = (fileSchemas, fileSchema) => { } }); + // Remove duplicate modules ref + if (fileSchema.toBeInjectedModules) { + fileSchema.toBeInjectedModules = removeDuplicatedValuesFromArray( + fileSchema.toBeInjectedModules, + ); + } + + return fileSchema; +}; + +const prepareListOfModulesToInject = (fileSchemas, fileSchema) => { + const pastedFiles = []; + + // console.log("====== CHECAGEM DE INJECAO ====== "); + // console.log("Arquivo sendo processado:", fileSchema.filePath); + // console.log("Imports do arquivo:", fileSchema.componentImportItems); + + // Checa se o item faz parte de componentes + Object.keys(fileSchema.componentImportItems).forEach((importItem) => { + // Se for item não widget, inclui no topo do bundle do arquivo + + // Se for um arquivo disponível, segue (null quer dizer que é um import de alguma lib nao local no src) + const importItemFileSource = fileSchema.componentImportItems[importItem]; // src/path/to/file.tsx | null + // Nao deve processar (copiar e colar) o conteúdo do arquivo mais de uma vez + if (importItemFileSource && !pastedFiles.includes(importItemFileSource)) { + // Adiciona na lista de items ja processados + pastedFiles.push(importItemFileSource); + + // Import File Schema + let importItemFileContent = fileSchemas.find( + (importFileSchema) => + importFileSchema.filePath === importItemFileSource, + ); + + // Também não pode ser um módulo + if (importItemFileContent.isStateless && importItemFileContent.isModule) { + // Modulos + + // Insere os modulos dependentes + if (!fileSchema.toBeInjectedModules) { + fileSchema.toBeInjectedModules = []; + } + + // Adiciona somente se ainda nao tiver o item na lista de modulos + if ( + !fileSchema.toBeInjectedModules.includes( + importItemFileContent.filePath, + ) + ) { + fileSchema.toBeInjectedModules.push( + importItemFileContent.filePath, + ...(importItemFileContent.toBeInjectedModules + ? importItemFileContent.toBeInjectedModules + : []), + ); + } + } + } + }); + return fileSchema; }; @@ -489,8 +613,13 @@ const prepareListOfInjections = (fileSchemas, fileSchema) => { importFileSchema.filePath === importItemFileSource, ); + // TODO: Porque tem uma funcao para injecao de dependencias apenas de arquivos stateless? // NAO WIDGETS | MODULOS (arquivos que contenham "module" no nome): tem o conteúdo de seu arquivo copiado e colado no corpo do arquivo sendo processado atual - if (importItemFileContent.isModule) { + // Também não pode ser um módulo + if ( + importItemFileContent.isStateless && + !importItemFileContent.isModule + ) { // Funcao recursiva aqui para fazer com que arquivos ainda não processados pelo injection, sejam primeiro if (!importItemFileContent.injectFilesDependencies_Done) { // Faz o processo primeiro no arquivo dependente @@ -519,6 +648,7 @@ const prepareListOfInjections = (fileSchemas, fileSchema) => { : []), ); } + // } } } }); @@ -530,14 +660,87 @@ const prepareListOfInjections = (fileSchemas, fileSchema) => { }; /** - * Coloca o conteúdo dos arquivos nao .ts e .jsx de dependencia dentro do bundle de cada arquivo do schema global - * Esse é um processo que ocorre para todos os arquivos, mas somente copia e cola o conteudo para arquivos nao JSX. - * - * Arquivos reconhecidos como JSX (Widgets) serão tratados de outra forma. Ver "swapComponentsForWidgets" - * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], toBeInjectedFiles:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}[]} fileSchemas - * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], toBeInjectedFiles:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}} fileSchema + * Injeta a referencia dos módulos no topo de cada arquivo final que depende dos módulos + * ou que tem dependentes que dependem dos módulos + * @param {*} fileSchemas + * @param {*} fileSchema + * @returns */ -const injectFilesDependencies = (fileSchemas, fileSchema) => { +const injectModules = (fileSchemas, fileSchema) => { + let fileBundle = fileSchema.finalFileBundle; + + // Se nao tiver nada a ser injetado, somente retorna o file Schema sem alterações + if (!fileSchema.toBeInjectedModules) { + return fileSchema; + } + + // Checa se o item faz parte de componentes + fileSchema.toBeInjectedModules.forEach((fileItemPath) => { + // Pega o esquema do arquivo + let fileItemSchema = fileSchemas.find( + (importFileSchema) => importFileSchema.filePath === fileItemPath, + ); + + // Insere apenas as referencias das declarações do módulo + const moduleValuesKeys = fileItemSchema.moduleProps.values; + // console.log("VALUES:", moduleValuesKeys, fileSchema.filePath); + // if (!moduleValuesKeys) return; + + let injections = ""; + + // m = modulesCode + moduleValuesKeys.forEach((propKey) => { + // INFO: as propriedades do escopo/widget local deve ser passado para os módulos para que esses tenham acesso a ela + // esse processo deixa sub-componentes de módulos mais lento já que a propriedade é compartilhada e pode ser + // acessada pelos componentes filhos deste módulo. Essa info de lentidão serve apenas para JSX que carregam + // outros modulos JSX aninhados + injections = ` + const ${propKey} = props.alem.m["${fileItemSchema.moduleProps.name}"]().${propKey}; + ${injections} + `; + // WARNING: enviar as props do escopo local como referencia para os módulos causa um perda significativa de desempenho + // injections = ` + // const ${propKey} = props.alem.m["${fileItemSchema.moduleProps.name}"](props).${propKey}; + // ${injections} + // `; + }); + + fileBundle = ` + ${injections} + ${fileBundle} + `; + + fileSchema.finalFileBundle = fileBundle; + fileSchema.jsContent = fileBundle; + }); + + // Se o arquivo for um módulo... + if (fileSchema.isModule) { + // TODO: Parei aqui, parece que ta tudo certo + // console.log("FOOO", fileSchema.filePath); + // Atualiza o modulo deste arquivo para usar o finalBundle com as importacoes injetadas + // console.log("Exported Keys:", fileSchema.moduleProps.values); + // const sofrer = wrapCodeInGetFunction( + // fileSchema.finalFileBundle, + // fileSchema.filePath, + // ); + // console.log("Module:", sofrer); + // console.log("Final:", filterReturn(sofrer, fileSchema.moduleProps.values)); + + fileSchema.moduleProps = { + ...fileSchema.moduleProps, + module: filterReturn( + wrapCodeInGetFunction(fileSchema.finalFileBundle, fileSchema.filePath), + fileSchema.moduleProps.values, + ), + // module: "function Get() {}", + }; + } + + return fileSchema; +}; + +const injectDependencies = (fileSchemas, fileSchema) => { let fileBundle = fileSchema.finalFileBundle; // Se nao tiver nada a ser injetado, somente retorna o file Schema sem alterações @@ -559,61 +762,41 @@ const injectFilesDependencies = (fileSchemas, fileSchema) => { (importFileSchema) => importFileSchema.filePath === fileItemPath, ); - fileBundle = ` - ${fileItemSchema.jsContent} - ${fileBundle} - `; + if (!fileItemSchema.isModule) { + // Injeta o conteúdo literalmente + fileBundle = ` + ${fileItemSchema.jsContent} + ${fileBundle} + `; - injectedFiles.push(fileItemPath); - fileSchema.finalFileBundle = fileBundle; - fileSchema.injectedFiles = injectedFiles; + injectedFiles.push(fileItemPath); + } }); + fileSchema.finalFileBundle = fileBundle; + fileSchema.injectedFiles = injectedFiles; + return fileSchema; }; /** - * Caso tenha dependencias do Alem (inportable items), prepara eles para serem injetados. + * Coloca o conteúdo dos arquivos nao .ts e .jsx de dependencia dentro do bundle de cada arquivo do schema global + * Esse é um processo que ocorre para todos os arquivos, mas somente copia e cola o conteudo para arquivos nao JSX. * - * Remove os elementos da chave em que está e coloca em uma nova linha contendo seu caminho - * até a lib alem-vm/importable/item... - * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}} fileSchema + * Arquivos reconhecidos como JSX (Widgets) serão tratados de outra forma. Ver "swapComponentsForWidgets" + * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], toBeInjectedFiles:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}[]} fileSchemas + * @param {{filePath: string, toImport: string[], content: string, finalFileBundle: string, componentImportItems:[], toBeInjectedFiles:[], componentParamsItems:[], componentComponentItems: [], widgetName?: string, htmlElementsProps: {}}} fileSchema */ -const prepareAlemDependencies = (fileSchema) => { - const importItems = getFileImportsElements(fileSchema.content); - - let fileContent = fileSchema.content; - let contentChanged = false; - - importItems.forEach((item) => { - // TODO: [Alem items: Routes, Link, etc] Checar se esta dando conflito com items do projeto - - const importStatementFileSource = getImportedElementFileSource( - fileContent, - item, - ); - - // Se o item estiver vindo de um destino que contenha "alem-vm" ou "alem" - // logo é um item do Além. - if ( - /\balem-vm\b/.test(importStatementFileSource) || - /\balem\b/.test(importStatementFileSource) - ) { - const alemImportElement = importableFiles[item]; - - // Se for um elemento importavel do Além e estiver presente no importableFiles do Além, então - // insere a nova linha no arquivo pedindo para importar o elemento. - if (alemImportElement) { - contentChanged = true; +const injectFilesDependencies = (fileSchemas, fileSchema) => { + fileSchema = injectDependencies(fileSchemas, fileSchema); - fileContent = transformImports(fileContent, item, alemImportElement); - } - } - }); + return fileSchema; +}; - if (contentChanged) { - fileSchema.content = fileContent; - } +const injectModulesDependencies = (fileSchemas, fileSchema) => { + // Faz a injeção das dependencias dos módulos, dependencias de stateless files, nao de outros módulos. + // INFO: A injeção de referencia de módulos é feito pelo "injectModules" + // fileSchema = injectDependencies(fileSchemas, fileSchema, true); return fileSchema; }; @@ -623,63 +806,151 @@ const prepareAlemDependencies = (fileSchema) => { /** * @param {{filePath: string, toImport: string[], content: string}[]} fileSchemas - * @param {*} additionalFileSchemas FileSchemas to be added to the list of main fileSchemas. It's going to be added first before * the process starts. This is util to inject previous schema files like Além importable items. */ -const transformSchemaToWidget = (fileSchemas, additionalFileSchemas) => { +const transformSchemaToWidget = (fileSchemas) => { + const showLogs = process.env.SHOW_EXECUTION_TIME === "true"; // TODO: trocar esse nome, transformSchemaToWidget -> transformSchemaToWidgetSchema // Reset error state processError = null; - // Caso tenha dependencias do Alem (inportable items), prepara eles para serem injetados - // Remove os elementos da chave em que está e coloca em uma nova linha contendo seu caminho - // até a lib alem-vm/importable/item... + // Gera o primeiro finalFileBundle e widgetName(para Widgets somente), parametros e imports + let start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { - fileSchemas[fileSchemaIndex] = prepareAlemDependencies(fileSchema); + fileSchemas[fileSchemaIndex] = processSchema(fileSchema); }); - - // Adiciona schemas já processados dos items importáveis do Além (hahaha) - if (additionalFileSchemas) { - fileSchemas = [...additionalFileSchemas, ...fileSchemas]; + let end = Date.now(); + if (showLogs) { + console.log( + `processSchema -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); } - // Gera o primeiro finalFileBundle e widgetName(para Widgets somente), parametros e imports + start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { - fileSchemas[fileSchemaIndex] = processSchema(fileSchema); + fileSchemas[fileSchemaIndex] = prepareListOfModulesToInject( + fileSchemas, + fileSchema, + ); + + // if (fileSchema.filePath.includes("src/Main.tsx")) { + // console.log("========== AAAA"); + // console.log("fileSchemas[fileSchemaIndex]", fileSchemas[fileSchemaIndex]); + // } }); + end = Date.now(); + if (showLogs) { + console.log( + `prepareListOfModulesToInject -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } + start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { + // if (fileSchema.filePath.includes("src/Main.tsx")) { + // console.log("========== BBBB"); + // console.log("fileSchemas[fileSchemaIndex]", fileSchemas[fileSchemaIndex]); + // } // Processa arquivos que contenham elementos html mas são stateless // console.log("Current file:", fileSchema.filePath); + // ATENCAO: essa funcao esta usando "fileSchema.jsContent" ao invéz de "fileSchema.finalFileBundle" + // ATENÇÃO: muda tanto o "finalFileBundle" quanto o "jsContent" fileSchemas[fileSchemaIndex] = swapComponentsForStatelessFiles( fileSchemas, fileSchema, ); + // if (fileSchema.filePath.includes("src/Main.tsx")) { + // console.log("========== AAAA"); + // console.log("fileSchemas[fileSchemaIndex]", fileSchemas[fileSchemaIndex]); + // } }); + end = Date.now(); + if (showLogs) { + console.log( + `swapComponentsForStatelessFiles -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } // Prepara lista de arquivos a serem injetados dentro de cada arquivo + start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { fileSchemas[fileSchemaIndex] = prepareListOfInjections( fileSchemas, fileSchema, ); + + // if (fileSchema.filePath.includes("src/Main.tsx")) { + // console.log("========== AAAA"); + // console.log("fileSchemas[fileSchemaIndex]", fileSchemas[fileSchemaIndex]); + // } }); + end = Date.now(); + if (showLogs) { + console.log( + `prepareListOfInjections -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } + + // FOO + // fileSchemas.forEach((fileSchema, fileSchemaIndex) => { + // fileSchemas[fileSchemaIndex] = foo(fileSchemas, fileSchema); + // }); + // Injeta as dependencias de arquivos (excluindo módulos). Tem que ser separado mesmo para evitar + // conflitos na carga de modulos, se liberar módulos, o corpo deles vao ser injetados como arquivos + // stateless causando assim duplicidade já que eles sao inseridos como módulos também. // Copia e cola o conteúdo de arquivos não .tsx | .jsx para dentro dos arquivos que dependem deles + start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { fileSchemas[fileSchemaIndex] = injectFilesDependencies( fileSchemas, fileSchema, ); }); + end = Date.now(); + if (showLogs) { + console.log( + `injectFilesDependencies -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } - // Faz transformação de async/await para o formato promisify + // Injeta as dependencias dos arquivos módulos dentro deles + start = Date.now(); fileSchemas.forEach((fileSchema, fileSchemaIndex) => { - fileSchema.finalFileBundle = transformAsyncAwait( - fileSchema.finalFileBundle, + fileSchemas[fileSchemaIndex] = injectModulesDependencies( + fileSchemas, + fileSchema, ); - fileSchemas[fileSchemaIndex] = fileSchema; }); + end = Date.now(); + if (showLogs) { + console.log( + `injectModulesDependencies -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } + + // Faz transformação de async/await para o formato promisify + // fileSchemas.forEach((fileSchema, fileSchemaIndex) => { + // // Transform async/await (experimental) + // fileSchema.finalFileBundle = transformAsyncAwait( + // fileSchema.finalFileBundle, + // ); + // fileSchemas[fileSchemaIndex] = fileSchema; + // }); + // TODO: remover transformAsyncAwait.js + + // ATENÇÃO: muda tanto o "finalFileBundle" quanto o "jsContent" + // Injeta os módulos caso sejam requeridos + start = Date.now(); + fileSchemas.forEach((fileSchema, fileSchemaIndex) => { + fileSchemas[fileSchemaIndex] = injectModules(fileSchemas, fileSchema); + }); + end = Date.now(); + if (showLogs) { + console.log( + `injectModules -> Execution time: ${millisToMinutesAndSeconds(end - start)} minutes`, + ); + } return { fileSchemas, processError }; }; diff --git a/lib/alem-vm/components/AppIndexer.jsx b/lib/alem-vm/components/AppIndexer.jsx index 5f87d95..4e19db8 100644 --- a/lib/alem-vm/components/AppIndexer.jsx +++ b/lib/alem-vm/components/AppIndexer.jsx @@ -3,24 +3,35 @@ const AlemApp = useMemo(() => { return ""; } - const Container = styled.div` - display: flex; - margin-top: 48%; - justify-content: center; - width: 100%; - `; + // Modules, Codes Wrapper, Recursos que dependem das propriedades globais devem ser colocadas em segunda camada, senão props retorna + // sem os dados do além. + // Todas as propriedades que dependem de algum recurso do Além deve ser colocado nessa segunda camada, por exemplo: + // "props.alem.isDevelopment", "props.alem.getAlemEvironment()", etc + // TODO: colocar isso em um arquivo? + // TODO: 2 - colocar condicional para: se usar modulos, faz esse processo de usar o layer2, senao, carrega diretamente o APP como antes + const widgetLayer2code = ` + const props = { + ...props, + // ==================================== Modules Code ==================================== + alem: { + ...props.alem, + // m = modulesCode, está sendo usado "m" para reduzir o bundle final + m: { + MODULES_CODE: {}, + }, + } + }; - const Loading = () => ( - -
- - ); + return ( + + ) + `; return ( } - code={props.alem.componentsCode.App} + loading=" " + code={widgetLayer2code} props={{ alem: props.alem }} /> diff --git a/lib/alem-vm/importable/RouteLink.tsx b/lib/alem-vm/importable/RouteLink.tsx index 0e723dd..b2a989e 100644 --- a/lib/alem-vm/importable/RouteLink.tsx +++ b/lib/alem-vm/importable/RouteLink.tsx @@ -1,4 +1,4 @@ -import { LinkProps, navigate, useContext } from "../alem-vm"; +import { LinkProps, useContext, navigate } from "../alem-vm"; /** * Link to access routes. diff --git a/lib/alem-vm/importable/modules/readme.md b/lib/alem-vm/importable/modules/readme.md new file mode 100644 index 0000000..f8cb50c --- /dev/null +++ b/lib/alem-vm/importable/modules/readme.md @@ -0,0 +1,6 @@ +## Modules + +Thes files inside this folder are going to be injected to the global state and components using them will get only their references, so, saving +size for the final bundle file. + +WARNING: If you want to use `props` inside the modules, you must pass it as a parameter. Modules live at the very top layer of Além and can't automatically access the props where it's being used. diff --git a/lib/compiler.js b/lib/compiler.js index 7c25dd4..c120583 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -14,6 +14,7 @@ const injectModules = require("./actions/injectModules"); const applyOptions = require("./actions/applyOptions"); const injectFoundRegExps = require("./actions/injectFoundRegExps"); const createSuspenseWidget = require("./actions/createSuspenseWidget"); +const { millisToMinutesAndSeconds } = require("./helpers"); const distFolder = process.env.DIST_FOLDER || "build"; @@ -32,10 +33,6 @@ function run_final_process(filesInfo, opts) { return; } - // Load Além files content schema - const alemImportableFilesSchema = - alemFiles.importableAlemFileSchemas().completeFileSchemas; - /** * Recebe um texto no formato: * NomeComponente: `codigo do componente`, @@ -44,7 +41,6 @@ function run_final_process(filesInfo, opts) { const finishedSchemaProcessForWidgets = loadFilesContent.loadComponentCodesObjectByFileSchemas( filesInfo.fileSchemas, - alemImportableFilesSchema, ); if (finishedSchemaProcessForWidgets.error) { @@ -57,6 +53,7 @@ function run_final_process(filesInfo, opts) { } let widgetsCodes = finishedSchemaProcessForWidgets.componentsCodes; + const modulesCodes = finishedSchemaProcessForWidgets.modulesCodes; // Alem VM -> Header contents let bundleContent = alemFiles.loadHeaderFilesContent(); @@ -74,6 +71,9 @@ function run_final_process(filesInfo, opts) { // Adiciona o bundle do componente App dentro do Indexador: bundleContent += alemFiles.loadIndexerContent(); + // Insere os códigos dos Módulos nas props globais + bundleContent = bundleContent.replace("MODULES_CODE: {},", modulesCodes); + // Inject CDN Libraries // file name: modules.json // Reason to not use: it's not possible to clone functions, use window or documents @@ -137,8 +137,23 @@ function compile_files(opts) { process.exit(1); } + const showLogs = process.env.SHOW_EXECUTION_TIME === "true"; + if (showLogs) console.log("Starting compiler process..."); // Load project files + let start = Date.now(); const filesInfo = loadFilesInfo(entryFile); + let end = Date.now(); + if (showLogs) { + console.log( + `loadFilesInfo -> Execution time: ${millisToMinutesAndSeconds(end - start)} sec`, + ); + } + + // Salva o esquema inicial. Bom para log + // fs.writeFileSync( + // path.join(`./build/src/initial.json`), + // JSON.stringify(filesInfo.fileSchemas, null, 2), + // ); // Executa processo final para gerar bundle e esquemas de arquivos run_final_process(filesInfo, opts); diff --git a/lib/config.js b/lib/config.js index 249af42..aecaec9 100644 --- a/lib/config.js +++ b/lib/config.js @@ -7,8 +7,10 @@ const path = require("path"); function read_alem_config() { const configPath = path.join(".", "alem.config.json"); if (!fs.existsSync(configPath)) { - console.warn(`INFO: File 'alem.config.json' not found! If you're using the CLI within a project, you must create this file to setup your project. Take a look at the Além docs: https://alem.dev/?path=config-file.`); - console.log("\n") + console.warn( + `INFO: File 'alem.config.json' not found! If you're using the CLI within a project, you must create this file to setup your project. Take a look at the Além docs: https://alem.dev/?path=config-file.`, + ); + console.log("\n"); return {}; } const configRaw = fs.readFileSync(configPath); diff --git a/lib/config/importableFiles.js b/lib/config/importableFiles.js index aae4456..b1ef63c 100644 --- a/lib/config/importableFiles.js +++ b/lib/config/importableFiles.js @@ -11,112 +11,50 @@ const path = require("path"); const { ALEM_VM_FOLDER } = require("../constants"); -const RouterContext = path.join( - __dirname, - "../", +// const importablePath = path.join( +// __dirname, +// "../", +// ALEM_VM_FOLDER, +// "importable", +// ); + +const importablePath = path.join( + ".", + "node_modules/alem/lib", ALEM_VM_FOLDER, "importable", - "RouterContext.ts", ); -const RouterProvider = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "RouterProvider.tsx", -); +const RouterContext = path.join(importablePath, "RouterContext.ts"); -const createContext = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "createContext.ts", -); -const useContext = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "useContext.ts", -); +const RouterProvider = path.join(importablePath, "RouterProvider.tsx"); -const Router = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "Router.tsx", -); +const createContext = path.join(importablePath, "createContext.ts"); -const RouteLink = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "RouteLink.tsx", -); +const useContext = path.join(importablePath, "useContext.ts"); -const useRoutes = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "useRoutes.ts", -); +const Router = path.join(importablePath, "Router.tsx"); -const getLocation = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "getLocation.ts", -); +const RouteLink = path.join(importablePath, "RouteLink.tsx"); -const createDebounce = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "createDebounce.ts", -); +const useRoutes = path.join(importablePath, "useRoutes.ts"); -const navigate = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "navigate.ts", -); +const getLocation = path.join(importablePath, "getLocation.ts"); -const ModulesContext = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "ModulesContext.ts", -); +const createDebounce = path.join(importablePath, "createDebounce.ts"); -const ModulesProvider = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "ModulesProvider.tsx", -); +const navigate = path.join(importablePath, "navigate.ts"); -const useModule = path.join( - __dirname, - "../", - ALEM_VM_FOLDER, - "importable", - "useModule.ts", -); +const ModulesContext = path.join(importablePath, "ModulesContext.ts"); + +const ModulesProvider = path.join(importablePath, "ModulesProvider.tsx"); + +const useModule = path.join(importablePath, "useModule.ts"); // TODO: Trazer outros recursos que não alteram o estado global pra ca, exemplo: promisify, etc module.exports = { + importablePath, RouterContext, RouterProvider, createContext, diff --git a/lib/data.js b/lib/data.js index 3760b4e..0f729a2 100644 --- a/lib/data.js +++ b/lib/data.js @@ -31,7 +31,8 @@ function generate_data_json() { key === "testnetAccount" || key === "compilerOptions" || key === "options" || - key === "tags" + key === "tags" || + key === "modules" ) { return; } diff --git a/lib/helpers.js b/lib/helpers.js index 8402db4..ab00e0f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -121,6 +121,7 @@ const getImportsPath = (fileContent) => { const getFilePathWithType = (filePath) => { const pathSeparator = isWindows ? "\\" : "/"; + // Caso não exista no caminho original passado, procura pelo seu indexador if (fs.existsSync(`${filePath}.ts`)) { return `${filePath}.ts`; } else if (fs.existsSync(`${filePath}.tsx`)) { @@ -138,6 +139,13 @@ const getFilePathWithType = (filePath) => { } else if (fs.existsSync(`${filePath}${pathSeparator}index.jsx`)) { return `${filePath}${pathSeparator}index.jsx`; } + + // Por fim, se passar um caminho na qual o arquivo ja existe, simplesmente + // retorna seu caminho. TEM QUE FICAR AQUI! + if (fs.existsSync(filePath)) { + return filePath; + } + return null; }; @@ -618,6 +626,16 @@ function escapeHtmlEntities(str) { .replace(/'/g, "'"); } +// Transforma "export default function" para "function" +const exportDefaultFunctionToFunction = (code) => + code.replaceAll(/\bexport default function\b/g, "function"); + +function millisToMinutesAndSeconds(millis) { + var minutes = Math.floor(millis / 60000); + var seconds = ((millis % 60000) / 1000).toFixed(0); + return minutes + ":" + (seconds < 10 ? "0" : "") + seconds; +} + module.exports = { getFileImportsElements, getImportStatements, @@ -645,4 +663,6 @@ module.exports = { hasHtmlElements, cleanErrorMessage, escapeHtmlEntities, + exportDefaultFunctionToFunction, + millisToMinutesAndSeconds, }; diff --git a/lib/parse.js b/lib/parse.js index 75a1bd2..665da1a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -72,6 +72,14 @@ const removeExports = (code) => { return newCode; }; +/** + * Remove Imports + * + * ATENÇÃO: Prefira usar "removeImportsAndExports" que é feito com Babel, é 100% seguro e mais robusto! + * + * @param {*} c + * @returns + */ const removeImports = (c) => { // Remove line breaks const importItems = c.match(/(import)(.*?)(from)/gs); diff --git a/lib/parsers/convertFunctionsToArrow.js b/lib/parsers/convertFunctionsToArrow.js new file mode 100644 index 0000000..e0b1d81 --- /dev/null +++ b/lib/parsers/convertFunctionsToArrow.js @@ -0,0 +1,43 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const t = require("@babel/types"); +const generate = require("@babel/generator").default; + +/** + * Converte todas as "function" em arrow functions + * @param {*} code + * @returns + */ +function convertFunctionsToArrow(code) { + // Parsear o código para criar a AST + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript"], // Adiciona suporte para JSX e TypeScript, se necessário + }); + + // Transformar todas as funções declaradas e funções anônimas em expressões de arrow + traverse(ast, { + FunctionDeclaration(path) { + const { id, params, body, async } = path.node; + const arrowFunction = t.arrowFunctionExpression(params, body, async); + arrowFunction.returnType = path.node.returnType; // Preserva o tipo de retorno, se for TypeScript + path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator(id, arrowFunction), + ]), + ); + }, + FunctionExpression(path) { + const { params, body, async } = path.node; + const arrowFunction = t.arrowFunctionExpression(params, body, async); + path.replaceWith(arrowFunction); + }, + }); + + // Gerar o código JavaScript modificado a partir da AST atualizada + return generate(ast, { + /* Opções aqui, se necessário */ + }).code; +} + +module.exports = convertFunctionsToArrow; diff --git a/lib/parsers/extractJSXElements.js b/lib/parsers/extractJSXElements.js index 12e82c4..b43dccb 100644 --- a/lib/parsers/extractJSXElements.js +++ b/lib/parsers/extractJSXElements.js @@ -56,7 +56,6 @@ function extractJSXElements(code, filterByElementType) { }, }); } catch (error_) { - console.log("CODE:", code); error = `The compiler was unable to process this line. Try another way. \n${error_}`; } diff --git a/lib/parsers/extractTopLevelDeclarations.js b/lib/parsers/extractTopLevelDeclarations.js new file mode 100644 index 0000000..3ce1d13 --- /dev/null +++ b/lib/parsers/extractTopLevelDeclarations.js @@ -0,0 +1,178 @@ +// NOTE: Este arquivo nao esta sendo usado, ver se tem algo útil, se nao, deletar! + +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const generate = require("@babel/generator").default; +const t = require("@babel/types"); +const transformVariableInCode = require("./transformVariableInCode"); + +function getFunctionDeclarationsKeys(code) { + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript"], + }); + + const declarationsKeys = []; + + traverse(ast, { + FunctionDeclaration(path) { + const { id, params, body, async } = path.node; + if (id) { + const arrowFunction = t.arrowFunctionExpression(params, body, async); + path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier(id.name), arrowFunction), + ]), + ); + } + }, + VariableDeclaration(path) { + if (path.parent.type === "Program") { + path.node.declarations.forEach((declaration) => { + if (declaration.id.type === "Identifier") { + const key = declaration.id.name; + declarationsKeys.push(key); + } + }); + } + }, + }); + + return declarationsKeys; +} + +function transformFunctionDeclarations(code) { + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript"], + }); + + const declarations = {}; + + traverse(ast, { + FunctionDeclaration(path) { + const { id, params, body, async } = path.node; + if (id) { + const arrowFunction = t.arrowFunctionExpression(params, body, async); + path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier(id.name), arrowFunction), + ]), + ); + } + }, + VariableDeclaration(path) { + if (path.parent.type === "Program") { + path.node.declarations.forEach((declaration) => { + if (declaration.id.type === "Identifier") { + const key = declaration.id.name; + const value = declaration.init + ? generate(declaration.init, { concise: false }).code + : "undefined"; + declarations[key] = value; + } + }); + } + }, + }); + + return declarations; +} + +/** + * Usado para extrair as funçoes no escopo principal e retornar um objeto deles. + * Declarações "function" são automaticamente convertidas para "arrow functions". Todas as declarações + * são usadas, mesmo as que nao tem "export", já que elas podem depender de outras declarações dentro do arquivo + * + * Exemplo, dado um arquivo com esse conteúdo: + * ` + * const Foo = () => {console.log('foo');}; + * function Bar(age) {console.log('bar', age);}; + * var My = () => { + * console.log('ola'); + * } + * + * const age = 2; + * const contract = "foobar"; + * ` + * Tem Esse retorno: + * { + * Foo: "() => { console.log('foo'); }", + * Bar: "function Bar(age) { console.log('bar', age); }", + * My: "() => { console.log('ola'); }", + * age: '2', + * contract: '"foobar"' + * } + * + * @param {*} code + * @returns + */ +function extractTopLevelDeclarations(code, modulePath) { + // const transformedCode = transformFunctionDeclarations(code); + // const declarations = processDeclarations(transformedCode, modulePath); + const declarationKeys = getFunctionDeclarationsKeys(code); + + // Return keys only + // return Object.keys(declarations); + return declarationKeys; +} + +// Verifica se o arquivo do módulo está usando recursos presentes em sí dentro de outras declarações, +// se tiver, muda elas para que apontem para o endereço usando o props.alem.modulesCode de si proprio +const processDeclarations = (declarations, modulePath) => { + // console.log("\n"); + // console.log("FILE:", modulePath); + const referenceKeys = Object.keys(declarations); + referenceKeys.forEach((declarationReference) => { + // console.log("AAAA", declarationReference); + referenceKeys.forEach((refKey) => { + // console.log("+ --->", refKey); + const currentValue = declarations[refKey]; + if (currentValue.includes(declarationReference)) { + const replaced = transformVariableInCode( + currentValue, + declarationReference, + ":::VAR_REF:::", + ); + + // m = modulesCode + declarations[refKey] = replaced.replaceAll( + ":::VAR_REF:::", + `props.alem.m['${modulePath}'].${declarationReference}`, + ); + } + }); + }); + + return declarations; +}; + +// function extractTopLevelFunctions(code) { +// const ast = parser.parse(code, { +// sourceType: "module", +// plugins: ["jsx", "typescript"], // Habilita suporte para TypeScript e JSX +// }); + +// const functions = {}; + +// traverse(ast, { +// enter(path) { +// if ( +// path.node.type === "FunctionDeclaration" || +// (path.node.type === "VariableDeclaration" && +// path.node.declarations[0].init && +// path.node.declarations[0].init.type === "ArrowFunctionExpression") +// ) { +// const key = path.node.id +// ? path.node.id.name +// : path.node.declarations[0].id.name; +// const { code } = generate(path.node); +// functions[key] = code; +// } +// }, +// }); + +// return functions; +// } + +module.exports = { extractTopLevelDeclarations, processDeclarations }; diff --git a/lib/parsers/filterReturn.js b/lib/parsers/filterReturn.js new file mode 100644 index 0000000..db529f4 --- /dev/null +++ b/lib/parsers/filterReturn.js @@ -0,0 +1,84 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const t = require("@babel/types"); +const removeGetNameAndLastComma = require("./removeGetNameAndLastComma"); +const generate = require("@babel/generator").default; + +/** + * Dado um código, filtra quais as chaves devem permanecer no return dessa função dentro do código. + * + * Exemplo de entrada: + * + * const code = `() => { + * const myFirstModule = props.alem.m["a_1"]().myFirstModule; + * const DependeciaNormal = () => { + * return

DependeciaNormal: {myFirstModule.getIt}

; + * }; + * const Modulo = () => { + * return <> + *

Modulo: {myFirstModule.getIt}

+ * + * ; + * }; + * return { + * myFirstModule: myFirstModule, + * DependeciaNormal: DependeciaNormal, + * Modulo: Modulo + * }; + * }` + * + * console.log(code, ["Modulo", "myFirstModule"]); + * + * Saída: + * + * () => { + * const myFirstModule = props.alem.m["a_1"]().myFirstModule; + * const DependeciaNormal = () => { + * return

DependeciaNormal: {myFirstModule.getIt}

; + * }; + * const Modulo = () => { + * return <> + *

Modulo: {myFirstModule.getIt}

+ * + * ; + * }; + * return { + * myFirstModule: myFirstModule, + * Modulo: Modulo + * }; + * }; + * + * + * @param {*} code + * @param {*} keys + * @returns + */ +function filterReturn(code, keys) { + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx"], // Habilita o suporte para sintaxe JSX + }); + + traverse(ast, { + ReturnStatement(path) { + // Checa se o retorno é um objeto + if (t.isObjectExpression(path.node.argument)) { + const properties = path.node.argument.properties; + // Filtra as propriedades para manter apenas as chaves especificadas + const filteredProperties = properties.filter((prop) => { + // console.log("Prop =====>", prop); + // console.log("Prop Name =====>", prop.key?.name); + return keys.includes(prop.key?.name); + }); + // Substitui as propriedades do objeto de retorno pelas filtradas + path.node.argument.properties = filteredProperties; + } + }, + }); + + // Gera o novo código a partir do AST modificado + const { code: newCode } = generate(ast); + return removeGetNameAndLastComma(newCode); +} + +module.exports = filterReturn; diff --git a/lib/parsers/getFunctionExportDeclarationKeys.js b/lib/parsers/getFunctionExportDeclarationKeys.js new file mode 100644 index 0000000..59bdf0e --- /dev/null +++ b/lib/parsers/getFunctionExportDeclarationKeys.js @@ -0,0 +1,20 @@ +const transformAndGetFunctionExportDeclarations = require("./transformAndGetFunctionExportDeclarations"); + +/** + * Retorna a chave/nome de declaração de todas as declarações sendo exportadas no arquivo. + * @param {*} code + * @returns + */ +const getFunctionExportDeclarationKeys = (code, filePath) => { + const declarations = transformAndGetFunctionExportDeclarations( + code, + filePath, + ); + // console.log("RESULTADO:"); + // console.log(declarations); + // console.log("\n"); + + return Object.keys(declarations); +}; + +module.exports = getFunctionExportDeclarationKeys; diff --git a/lib/parsers/removeGetNameAndLastComma.js b/lib/parsers/removeGetNameAndLastComma.js new file mode 100644 index 0000000..43e8aad --- /dev/null +++ b/lib/parsers/removeGetNameAndLastComma.js @@ -0,0 +1,10 @@ +// Troca o "const Get = " por "()", isso porque o módulo é a função diretamente +// e remove o último ";" encontrado porque a função vai ser colocar em uma lista de objetos, ou seja, +// vai ter um "," separando cada objeto. +const removeGetNameAndLastComma = (code) => { + // WARNING: enviar as props do escopo local como referencia para os módulos causa um perda significativa de desempenho + // return code.replace("const Get = ()", "(props)").replace(/;(?=[^;]*$)/, ""); + return code.replace("const Get = ", "").replace(/;(?=[^;]*$)/, ""); +}; + +module.exports = removeGetNameAndLastComma; diff --git a/lib/parsers/removeImportsAndExports.js b/lib/parsers/removeImportsAndExports.js new file mode 100644 index 0000000..c22e9c0 --- /dev/null +++ b/lib/parsers/removeImportsAndExports.js @@ -0,0 +1,68 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const generate = require("@babel/generator").default; +const t = require("@babel/types"); + +/** + * Remove linhas que tenham "import" do javascript + * Troca "export" e "export default" por nada + * + * INFO: Essa é a forma mais segura + * + * @param {*} code + * @returns + */ +function removeImportsAndExports(code) { + const ast = parser.parse(code, { + sourceType: "module", // necessário para suportar módulos ES6 + plugins: ["jsx", "typescript"], // adicione plugins se estiver usando JSX ou TypeScript + }); + + traverse(ast, { + ImportDeclaration(path) { + path.remove(); // Remove a declaração de importação completamente + }, + ExportDeclaration(path) { + if (path.node.declaration) { + // Se a declaração de exportação possui um nó de declaração (ou seja, não é apenas 'export { something }') + const { declaration } = path.node; + path.replaceWith(declaration); // Substitui a exportação pela declaração contida + } else { + // Para exportações que são apenas exportações de variáveis, remover a palavra 'export' + path.replaceWithMultiple( + path.node.specifiers.map((specifier) => + t.variableDeclaration("let", [ + t.variableDeclarator(specifier.local), + ]), + ), + ); + } + }, + ExportDefaultDeclaration(path) { + if (path.node.declaration) { + // Remove apenas a palavra 'default' e mantém a declaração + const { declaration } = path.node; + if ( + t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration) + ) { + // Para funções e classes, elas precisam ser convertidas para expressões antes da remoção do 'export default' + const expression = t.functionExpression( + null, + declaration.params, + declaration.body, + declaration.generator, + declaration.async, + ); + path.replaceWith(expression); + } else { + path.replaceWith(declaration); + } + } + }, + }); + + return generate(ast, { false: true }).code; +} + +module.exports = removeImportsAndExports; diff --git a/lib/parsers/transformAndGetFunctionExportDeclarations.js b/lib/parsers/transformAndGetFunctionExportDeclarations.js new file mode 100644 index 0000000..d36a00a --- /dev/null +++ b/lib/parsers/transformAndGetFunctionExportDeclarations.js @@ -0,0 +1,108 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const t = require("@babel/types"); +const convertFunctionsToArrow = require("./convertFunctionsToArrow"); +const { exportDefaultFunctionToFunction } = require("../helpers"); +const generate = require("@babel/generator").default; + +function convertExportDefault(code) { + // Regex para capturar `export default` seguido de qualquer palavra (identificador) + // e não captura casos já com chaves. + const regex = /export\s+default\s+([a-zA-Z_$][0-9a-zA-Z_$]*)(?!\s*\{)/g; + + // Substituir o código utilizando a regex + return code.replace(regex, "export { $1 }"); +} + +/** + * Usado para extrair as chaves das declaracoes sendo exportadas de dentro de um arquivo + * + * Exemplo, dado um arquivo com esse conteúdo: + * ` + * const Foo = () => {console.log('foo');}; + * function Bar(age) {console.log('bar', age);}; + * var My = () => { + * console.log('ola'); + * } + * + * export const age = 2; + * const contract = "foobar"; + * export const ba = "boo"; + * ` + * Tem Esse retorno: + * [age, ba] + * + * @param {*} code + * @returns + */ +function transformAndGetFunctionExportDeclarations(code, filePath) { + // Transforma "export default function" para "function" + code = exportDefaultFunctionToFunction(code); + + // Converte functions para arrow functions + try { + code = convertFunctionsToArrow(code); + } catch (error) { + console.log(code); + throw new Error(`File: ${filePath}: ${error}`); + } + + code = convertExportDefault(code); + + try { + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript"], + }); + + const declarations = {}; + + traverse(ast, { + ExportNamedDeclaration(path) { + if (path.node.declaration) { + // Para declarações diretamente na exportação + if (path.node.declaration.type === "VariableDeclaration") { + path.node.declaration.declarations.forEach((decl) => { + if (decl.id.type === "Identifier") { + const key = decl.id.name; + const value = decl.init + ? generate(decl.init, { concise: false }).code + : "undefined"; + declarations[key] = value; + } + }); + } else if (path.node.declaration.type === "FunctionDeclaration") { + const { id, params, body, async } = path.node.declaration; + if (id) { + const arrowFunction = t.arrowFunctionExpression( + params, + body, + async, + ); + path.replaceWith( + t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier(id.name), arrowFunction), + ]), + ); + declarations[id.name] = generate(arrowFunction, { + concise: false, + }).code; + } + } + } else if (path.node.specifiers) { + // Para exportações de variáveis ou funções já declaradas + path.node.specifiers.forEach((specifier) => { + declarations[specifier.exported.name] = specifier.local.name; + }); + } + }, + }); + + return declarations; + } catch (error) { + console.log(code); + throw new Error(`File: ${filePath}: ${error}`); + } +} + +module.exports = transformAndGetFunctionExportDeclarations; diff --git a/lib/parsers/transformAsyncAwait.js b/lib/parsers/transformAsyncAwait.js index 87b40c8..6e0e1c2 100644 --- a/lib/parsers/transformAsyncAwait.js +++ b/lib/parsers/transformAsyncAwait.js @@ -1,3 +1,4 @@ +// TODO: INFO: Não está sendo usado. Se nao for usado por muito tempo, deletar arquivo /** * Experimental: Gera uma estrutura entendível pelo Near VM a partir de um await * @param {string} code diff --git a/lib/parsers/transformVariableInCode.js b/lib/parsers/transformVariableInCode.js new file mode 100644 index 0000000..457baa0 --- /dev/null +++ b/lib/parsers/transformVariableInCode.js @@ -0,0 +1,41 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const generate = require("@babel/generator").default; + +/** + * Transforma qualquer variável em outro valor "replacement" + * Exemplo: + * + * entrada: "const Bar = () => { console.log('oi', age); }" + * saída: const Bar = () => { console.log('oi', :::PROP_HERE:::); }; + * + * onde o :::PROP_HERE::: é o "replacement" + * + * @param {*} code + * @param {*} variableName + * @param {*} replacement + * @returns + */ +function transformVariableInCode(code, variableName, replacement) { + const ast = parser.parse(`const tempDecl = ${code}`, { + sourceType: "module", + plugins: ["jsx", "typescript"], // Suporte para TypeScript e JSX se necessário + }); + + traverse(ast, { + Identifier(path) { + if (path.node.name === variableName) { + path.node.name = replacement; + } + }, + }); + + const output = generate(ast, { concise: false }); + // Regex para remover o último ponto e vírgula antes do fim da string ou de um fechamento de bloco + const finalOutput = output.code + .replace(/;(?=[^;]*$)/, "") + .replace("const tempDecl = ", ""); + return finalOutput; +} + +module.exports = transformVariableInCode; diff --git a/lib/parsers/wrapCodeInGetFunction.js b/lib/parsers/wrapCodeInGetFunction.js new file mode 100644 index 0000000..fc4c837 --- /dev/null +++ b/lib/parsers/wrapCodeInGetFunction.js @@ -0,0 +1,117 @@ +const parser = require("@babel/parser"); +const traverse = require("@babel/traverse").default; +const t = require("@babel/types"); +const babel = require("@babel/core"); +const typescriptPreset = require("@babel/preset-typescript"); +const convertFunctionsToArrow = require("./convertFunctionsToArrow"); +const removeGetNameAndLastComma = require("./removeGetNameAndLastComma"); +const removeImportsAndExports = require("./removeImportsAndExports"); + +/** + * Esta função vai cobrir o arquivo/code com uma função Get e todas as declarações sendo exportadas + * serão retornadas dentro dela. + * + * Exemplo, dado de entrada: + * + * const code = ` + * const abrolhos = "oi"; + * export const num = 42; + * export function compute() { return num * 2; } + * export default compute; + * `; + * + * Saída: + * + * function Get() { + * const abrolhos = "oi"; + * const num = 42; + * function compute() { + * return num * 2; + * } + * const default = compute; + * return { + * num: 42, + * default: compute + * }; + * } + * + * @param {*} code + * @returns + */ +function wrapCodeInGetFunction(code, filePath) { + // Remover importações e exports + // code = code.replace(/import.*;|export\s+default|export\s+/g, ""); + code = removeImportsAndExports(code); + + // Parsear o código para AST removendo TypeScript + const ast = parser.parse(code, { + sourceType: "module", + plugins: ["jsx", "typescript"], + }); + + const identifiers = new Set(); + + // Capturar identificadores de todas as declarações top-level + traverse(ast, { + VariableDeclaration(path) { + if (path.parent.type === "Program") { + path.node.declarations.forEach((decl) => { + if (decl.id.name) { + identifiers.add(decl.id.name); + } + }); + } + }, + FunctionDeclaration(path) { + if (path.parent.type === "Program" && path.node.id) { + identifiers.add(path.node.id.name); + } + }, + ClassDeclaration(path) { + if (path.parent.type === "Program" && path.node.id) { + identifiers.add(path.node.id.name); + } + }, + }); + + // Criar a função Get que encapsula o código e retorna um objeto com todas as declarações + const returnObject = t.objectExpression( + Array.from(identifiers).map((id) => + t.objectProperty(t.identifier(id), t.identifier(id)), + ), + ); + + const getFunction = t.arrowFunctionExpression( + [], + t.blockStatement([...ast.program.body, t.returnStatement(returnObject)]), + ); + + const newAst = t.program([ + t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier("Get"), getFunction), + ]), + ]); + + // Transformar o novo AST em JavaScript puro, removendo TypeScript + const { code: transformedCode } = babel.transformFromAstSync(newAst, null, { + presets: [ + // INFO: Isso estava gerando os helpers no topo do arquivo + // babelPreset, + // INFO: Esse preset estava transformando os arquivos + // reactPreset, + typescriptPreset, + ], + code: true, + configFile: false, // Ignora qualquer configuração do Babel externa + filename: filePath, + }); + + // Troca o "const Get = " por "", isso porque o módulo é a função diretamente + // e remove o último ";" encontrado porque a função vai ser colocar em uma lista de objetos, ou seja, + // vai ter um "," separando cada objeto. + return removeGetNameAndLastComma(convertFunctionsToArrow(transformedCode)); + // .replace("const Get = ", "") + // .replace(/;(?=[^;]*$)/, ""); +} + +module.exports = wrapCodeInGetFunction; diff --git a/lib/utils.js b/lib/utils.js index fc72c70..58ee1fc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -171,7 +171,7 @@ const capitalize_first_letter = (word) => { let nameChangeCounter = 0; /** * Generate new name - * @param {string} _toLowerCase + * @param {boolean} _toLowerCase */ const create_new_name = (_toLowerCase = false) => { // let randomHexName = get_randon_hexadecimal(6); diff --git a/package.json b/package.json index 4de05db..a12e64b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "alem", "description": "Create web3 applications for NEAR BOS with a focus on performance and friendly development.", - "version": "1.0.0-beta.31", + "version": "1.0.0-beta.32", "main": "main.js", "types": "index.d.ts", "author": "Wenderson Pires - wendersonpires.near",