UNPKG

@jeli/compiler-cli

Version:

jeli compiler for frontend development

856 lines (785 loc) 29 kB
const path = require('path'); const helper = require('@jeli/cli/lib/utils'); const fs = require('fs-extra'); const lodashTemplate = require('lodash.template'); const uglify = require('uglify-js'); const { getIndex } = require('./output-mapper'); const symbol = "ϕ"; const PATTERN = { MODULE: 'MODULE', UMD: 'UMD', DEFAULT: 'DEFAULT' }; const cssFileHolder = new Map(); let nodeSass = null; /** * handle node sass error */ try { nodeSass = require('node-sass'); } catch (e) { helper.abort(`\n${e.message}`); } exports.PATTERN = PATTERN; /** * * @param {*} fileName * @param {*} options */ exports.writeFile = async function (filePath, data) { try { const dirName = path.dirname(filePath); // check if folder already exist // else create one if (!fs.existsSync(dirName)) { fs.mkdirpSync(dirName); } fs.writeFileSync(filePath, data); // Print a success message. helper.console.success(`generated file "${path.basename(filePath)}" size: (${helper.colors.yellow((data.length / 1024).toFixed(2) + 'kb')}) `); } catch (e) { helper.console.error(`unable to save file, please try again`); helper.abort(e); } } /** * * @param {*} filePath * @param {*} compilerObject */ exports.generateBundleData = async function (compilerObject, fileNames) { /** * existing and new packageJSON path */ const existingPackageJSON = path.join(compilerObject.options.sourceRoot, compilerObject.entryFile, '..', './package.json'); const outputPackageJSON = path.join(compilerObject.options.output.folder, compilerObject.entryFile, '..', './package.json'); /** * write the bundle path to packageJson object */ let packageJSON = Object.keys(fileNames).reduce((accum, type) => { accum[type === 'UMD' ? 'main' : type.toLowerCase()] = `${getRelativePath(fileNames[type], path.dirname(outputPackageJSON))}`; return accum; }, {}); /** * check for existing packageJSON data * then extend the existing data * */ if (fs.existsSync(existingPackageJSON)) { packageJSON = Object.assign(JSON.parse(fs.readFileSync(existingPackageJSON)), packageJSON); } if (compilerObject.buildOptions.version) { packageJSON.version = compilerObject.buildOptions.version; } if (compilerObject.buildOptions.hasStyles) { packageJSON.stylesPath = 'bundles/styles.css'; } packageJSON.peerDependencies = packageJSON.peerDependencies || {}; for (const prop in compilerObject.globalImports) { const globImp = compilerObject.globalImports[prop]; if (globImp.name) packageJSON.peerDependencies[globImp.name] = verifyVersion(globImp.version); } packageJSON.metaDataPath = 'metadata.json'; exports.writeFile(outputPackageJSON, JSON.stringify(packageJSON, null, 2)); saveCompilerData(compilerObject); } /** * * @param {*} compilerObject * @param {*} moduleName */ exports.outputLibraryFiles = async function (compilerObject, moduleName) { const files = {}; const scriptBody = extractSourceCode(compilerObject, true); for (const type of compilerObject.options.output.patterns) { files[type] = await buildByType(type, scriptBody, moduleName, compilerObject); } await exports.copyFiles(compilerObject); compilerObject.buildOptions.hasStyles = await writeCss(compilerObject.options, true); await exports.generateBundleData(compilerObject, files); }; /** * * @param {*} filesToCopy */ exports.copyFiles = async compilerObject => { if (compilerObject.options.output.copy) { for (const file of compilerObject.options.output.copy) { try { const dest = `${compilerObject.options.output.folder}${file.dest || (path.basename(file.src) + '/')}`; fs.copySync(file.src, dest, { recursive: true, overwrite: true }); } catch (exception) { } } } } exports.copyAndUpdateAssetsFile = async (filePath, compilerObject, item) => { if (!fs.existsSync(filePath)) return; const basename = path.basename(item.src); const dest = `${compilerObject.options.output.folder}${basename}${filePath.split(basename)[1]}`; try { fs.copySync(filePath, dest, { recursive: true, overwrite: true }); } catch (e) { } } /** * * @param {*} compilerObject * @param {*} changes */ exports.outputApplicationFiles = async function (compilerObject, changes) { const getBootStrapModule = filePath => { for(const imp of compilerObject.files[filePath].imports) { const fn = imp.specifiers[0].local; if (compilerObject.jModule[fn]){ return imp.absolutePath; } } }; /** * generate the script file with below conditions * changes to .html .js .json */ if (!changes || !changes.isStyles) { const changesFilePath = changes ? changes.filePath : null; const isLazyLoaded = changesFilePath && compilerObject.output.modules[changesFilePath].isLazyLoaded; const isProdBuild = (compilerObject.buildOptions && compilerObject.buildOptions.prod); if (!isLazyLoaded) { const bootStrapFilePath = path.join(compilerObject.options.sourceRoot, compilerObject.entryFile); const bootStrapModule = getBootStrapModule(bootStrapFilePath); const main = await resolveModules(compilerObject, changesFilePath, bootStrapModule); const fileName = `${compilerObject.options.output.folder}${compilerObject.entryFile}`; const deps = [compilerObject.output.global]; const bstDeps = extendImportExport(bootStrapFilePath, compilerObject, deps); const entry = writeGlobalImports(deps.join(''), bstDeps.$, false); const buildArgs = JSON.stringify(compilerObject.buildOptions); let script = loadTemplate('default', { entry, main, buildArgs }); await outputJSFiles(fileName, script, isProdBuild); } else { const lazyLoadModulePath = compilerObject.files[changesFilePath].lazyLoadModulePath; if(changesFilePath && !compilerObject.output.lazyLoads.includes(lazyLoadModulePath)) { compilerObject.output.lazyLoads.push(lazyLoadModulePath); } } await writeLazyLoadModules(compilerObject, isProdBuild); } if (!changes) { await exports.copyFiles(compilerObject); await writeCss(compilerObject.options); if (compilerObject.options.output.view) { exports.saveApplicationView(compilerObject); } // saveCompilerData(compilerObject); } } exports.saveApplicationView = async function (compilerObject) { const viewFilePath = path.join(compilerObject.options.sourceRoot, compilerObject.options.output.view); if (fs.existsSync(viewFilePath)) { const files = ['styles.js', compilerObject.entryFile]; const html = fs.readFileSync(viewFilePath, 'utf8').replace(/<\/body>/, _ => { return files.map(file => `<script src="./${file}" type="text/javascript"></script>`).join('\n') + '\n' + _; }); fs.writeFileSync(`${compilerObject.options.output.folder}${compilerObject.options.output.view}`, html); } }; /** * * @param {*} config * @param {*} folder */ exports.pushStyle = (config, append) => { let result = parseStyle(config); if (!result || !(result.substring(1, result.length - 2))) return; result = helper.stringifyContent(result); if (append) { append.push(result); } else { cssFileHolder.set(config.elementFilePath, { result, config }); } } /** * * @param {*} compilerObject * @param {*} changes */ exports.styleChanges = async (compilerObject, changes) => { const elementFilePath = compilerObject.output.styles[changes.filePath]; const existingContent = cssFileHolder.get(elementFilePath); this.pushStyle(existingContent ? existingContent.config : { styleUrl: changes.filePath, elementFilePath }); await writeCss(compilerObject.options); }; /** * * @param {*} script * @param {*} sourceMap */ function obfuscate(script, sourceMap) { return uglify.minify(script, { nameCache: null, // or specify a name cache object toplevel: false, ie8: false, warnings: false, sourceMap, compress: { sequences: true, dead_code: true, conditionals: true, booleans: true, unused: true, if_return: true, join_vars: true, drop_console: false, properties: false }, output: { preserve_line: false, beautify: false, max_line_len: 400 } }); } /** * * @param {*} annotations */ function isModule(annotations) { return annotations && (annotations.find(annot => annot.isModule) || {}).fn; } /** * * @param {*} template * @returns */ function getTemplate(template) { template = fs.readFileSync(path.resolve(__filename, `../../utils/templates/${template}.jeli`), { encoding: 'utf8' }); return lodashTemplate(template); } /** * * @param {*} template * @param {*} data */ function loadTemplate(template, data) { var templateParser = getTemplate(template); return templateParser(data); } /** * * @param {*} needle * @param {*} against */ function getRelativePath(needle, against) { needle = needle.split('/'); against = against.split('/'); for (var i = 0; i < (against.length - needle.length); i++) { needle.unshift('..'); } return needle.join('/'); } function verifyVersion(version) { if (version && !version.toLowerCase().includes('placeholder')) { return version; } return 'latest'; } /** * * @param {*} compilerObject */ function saveCompilerData(compilerObject) { /** * remove unwanted data from compilerObject * before saving it */ delete compilerObject.options; delete compilerObject.output; delete compilerObject.buildOptions; delete compilerObject.globalImports; const metaDataFilePath = path.join(compilerObject.options.output.folder, compilerObject.entryFile, '..', './metadata.json'); exports.writeFile(metaDataFilePath, JSON.stringify(compilerObject, null, 2).replace(/[']/g, '')); } function extractSourceCode(compilerObject, isLib) { const sourceCode = []; if (isLib) { Object.keys(compilerObject.output.modules).forEach(filePath => { sourceCode.push(compilerObject.output.modules[filePath].source.join('')); }); } sourceCode.concat(compilerObject.output.global); return sourceCode.join('\n'); } /** * * @param {*} fileName * @param {*} script * @param {*} isProd */ async function outputJSFiles(fileName, script, isProd) { /** * obfuscate code if prod flag is sent */ if (isProd) { helper.console.write('obfuscating code...'); const uglifiedScript = obfuscate(script); if (!uglifiedScript.error) { script = uglifiedScript.code; } else { helper.console.error(`obfuscation failed for this file -> ${fileName}`); } } await exports.writeFile(fileName, script); } /** * * @param {*} type */ async function buildByType(type, scriptBody, moduleName, compilerObject) { const trimmedName = helper.trimPackageName(moduleName); const isModule = (type === PATTERN.MODULE); const scriptDefinition = { scriptBody, header: '', footer: createModuleExportation(compilerObject.exports, isModule), moduleName: `'${moduleName}'`, importsAMD: '', importsCJS: '', globalArgs: '', args: '', buildOptions: JSON.stringify(compilerObject.buildOptions) }; const imports = createModuleImportation(compilerObject.globalImports, isModule); if (!isModule) { const globalName = `global.${trimmedName.first}`; scriptDefinition.globalName = globalName; scriptDefinition.globalNameSpace = `${trimmedName.nameSpace ? ', ' + globalName + '["' + trimmedName.nameSpace + '"] = ' + globalName + '["' + trimmedName.nameSpace + '"] || {}' : ''}`; scriptDefinition.importsAMD = imports.amd.join(', '); scriptDefinition.importsCJS = imports.cjs.join(''); scriptDefinition.globalArgs = imports.global.join(', '); scriptDefinition.args = imports.args.join(', '); } else { scriptDefinition.header = imports; } const fileName = `${trimmedName.name}-${type.toLowerCase()}`; const filePath = `bundles/${fileName}.js`; const script = loadTemplate(type.toLowerCase(), scriptDefinition); const outputFolder = compilerObject.options.output.folder; // overwrite template await exports.writeFile(`${outputFolder}/${filePath}`, script); /** * uglify script if required */ if (!isModule) { const uglifyFilename = `${fileName}.min.js`; const uglifiedScript = obfuscate(script, { url: `${uglifyFilename}.map` }); if (!uglifiedScript.error) { const minFilePath = `${outputFolder}/${path.join(filePath, `../${uglifyFilename}`)}`; await exports.writeFile(minFilePath, uglifiedScript.code); await exports.writeFile(`${minFilePath}.map`, uglifiedScript.map); } else { helper.console.error(`obfuscation failed for this file -> ${fileName}`); } } return filePath; } /** * * @param {*} imports * @param {*} isModule * @returns */ function createModuleImportation(imports, isModule) { const keys = Object.keys(imports); if (isModule) { return keys.map(key => { const specifiers = imports[key].specifiers; return `import ${specifiers.length ? ('{ ' + specifiers.join(', ') + '} from ') : ''}'${key}';\n`; }).join(''); } else { return keys.reduce((accum, key) => { const specifier = imports[key]; accum.amd.push(`'${key}'`); accum.cjs.push(`, require('${key}')`); accum.global.push(`global.${specifier.output.first}${specifier.output.nameSpace ? "['" + specifier.output.nameSpace + "']" : ''}`); accum.args.push(`${specifier.output.arg}`); return accum; }, { amd: [], cjs: [''], global: [''], args: [''] }) } } /** * * @param {*} specifiers * @param {*} isModule */ function createModuleExportation(specifiers, isModule) { if (isModule) { return `export { ${specifiers.map(exp => `${exp.exported}${exp.exported != exp.local ? ' as ' + exp.local : ''}`).join(' , ')} }`; } else { return specifiers.map(exp => `exports.${exp.exported} = ${exp.local};`).join('\n') } } /** * * @param {*} compilerObject * @param {*} importItems * @param {*} output * @param {*} isExternalModule * @returns */ function _writeImport(compilerObject, importItems, output, isExternalModule = true) { const defaultModuleImports = { $: {} }; const importedCache = {}; const isImported = (i, n) => { if (importedCache[i]) { if (importedCache[i].includes(n)) { return true; } importedCache[i].push(n); } else { importedCache[i] = [n]; } return false; }; for (const importItem of importItems) { let index = getIndex(importItem.absolutePath); if (importItem.default) { const imported = importItem.specifiers[0].imported; if (compilerObject.output.modules.hasOwnProperty(importItem.absolutePath) && !isImported(index, imported)) { /** * empty module file * files that imports and exports only */ if (!output.join('')) { defaultModuleImports[imported] = `__required(${index}, 'default')`; } else { output.unshift(`var ${imported} = __required(${index}, 'default');\n`); } } } else if (importItem.nameSpace) { if (!isImported(index, importItem.specifiers[0].local)) output.unshift(`var ${importItem.specifiers[0].local} = __required(${index}${importItem.noExports ? '' : ",'exports'"});\n`); } else if (compilerObject.globalImports.hasOwnProperty(importItem.source)) { const globalDepMeta = compilerObject.globalImports[importItem.source]; index = getIndex(globalDepMeta.absolutePath); if (globalDepMeta.default) { output.unshift(`var ${importItem.specifiers[0].imported} = __required(${index}, 'default');\n`); }else { if (!isExternalModule) { defaultModuleImports.$[globalDepMeta.output.arg] = defaultModuleImports.$[globalDepMeta.output.arg] || []; importItem.specifiers.forEach(specifier => { if (!defaultModuleImports.$[globalDepMeta.output.arg].includes(specifier.imported)) defaultModuleImports.$[globalDepMeta.output.arg].push(specifier.imported); }); } else { importItem.specifiers.forEach(specifier => { if (!isImported(index, specifier.imported)) output.unshift(`var ${specifier.local} = ${globalDepMeta.output.arg}.${specifier.imported};\n`); }); } output.unshift(`var ${globalDepMeta.output.arg} = __required(${index});\n`); } } else if (compilerObject.files.hasOwnProperty(importItem.absolutePath)) { importItem.specifiers.forEach(specifier => { if (!isImported(index, specifier.imported)) output.unshift(`var ${specifier.local} = __required(${index}, '${specifier.imported || specifier.local}');\n`); }); } } return defaultModuleImports; } /** * * @param {*} exportedItems * @param {*} defaultModuleImports * @param {*} output */ function _writeExports(exportedItems, defaultModuleImports, output) { // parse exports for (const exp of exportedItems) { if (helper.is(exp.exported, 'default')) { let value = output.pop(); /** * export defaut contains an identifier * e.g export default IDENTIFIER; * append the script before the export declaration */ if (exp.local !== exp.exported) { output.push(`exports.default = ${exp.local};\n${value}`); } else { output.push(`exports.default = ${value}`); } } else { if (!defaultModuleImports[exp.local]) { output.unshift(`__required.r(exports, '${exp.exported}', () => ${exp.local});\n`); } else { output.unshift(`\nexports.${exp.exported} = ${defaultModuleImports[exp.local]};`) } } } } /** * * @param {*} filePath * @param {*} compilerObject * @param {*} output * @param {*} isBootStrapModule * @returns */ function extendImportExport(filePath, compilerObject, output, isBootStrapModule) { const metaData = compilerObject.files[filePath]; const defaultModuleImports = _writeImport(compilerObject, metaData.imports, output, true); _writeExports(metaData.exports, defaultModuleImports, output); return defaultModuleImports; } /** * * @param {*} sourceCode * @param {*} globalImports * @param {*} delify * @returns */ function writeGlobalImports(sourceCode, globalImports, delify = true) { if (globalImports) { const delimeter = delify ? ['%', '%'] : ['', '']; for (const ns in globalImports) { globalImports[ns].forEach(dep => { sourceCode = sourceCode.replace(new RegExp(delimeter.join(dep), 'g'), _ => `${ns}.${dep}`) }); } } return sourceCode; } /** * * @param {*} compilerObject * @param {*} fileChanged * @param {*} bootStrapModule * @returns */ async function resolveModules(compilerObject, fileChanged, bootStrapModulePath) { const files = []; const filePaths = Object.keys(compilerObject.output.modules); let req = null; /** * * @param {*} req * @param {*} filePath * @param {*} allowBuild * @returns */ async function generateModuleDeps(filePath, allowBuild) { const isRequired = (req.type === 'require'); if (allowBuild) { let defaultModuleImports = {}; if (!isRequired) { defaultModuleImports = extendImportExport(filePath, compilerObject, req.source, (bootStrapModulePath == filePath)); } else { req.source.unshift(`module.exports = `) } // rewrite source to string req.source = writeGlobalImports(req.source.join(''), defaultModuleImports.$); } return `(module, exports, __required, global) => {\n"use strict";\n${req.source}\n}`; } for (const filePath of filePaths) { req = compilerObject.output.modules[filePath]; if (!req.isLazyLoaded) { const allowBuild = (!fileChanged || (fileChanged && helper.is(fileChanged, filePath))); const sourceCode = await generateModuleDeps(filePath, allowBuild); files.push(`${getIndex(filePath)} : ${sourceCode}`); } } return `{${files.join(',\n')}}`; } /** * * @param {*} compilerObject * @param {*} modulePath * @param {*} sourceDefinition * @returns */ function extractModuleImpExp(compilerObject, modulePath, sourceDefinition) { const definition = compilerObject.files[modulePath]; // get module declarations from jModule attribute const declarations = compilerObject[sourceDefinition.annotations[0].type][sourceDefinition.annotations[0].fn]; // concat the ctors for jModule const fnDefinitions = (declarations.services || []).concat(declarations.selectors || []); const cache = { imp: ["@jeli/core"], exp: [] }; const ret = { imp: [{ source: "@jeli/core", specifiers: [] }], exp: [], replace: {}, localImp: [], ns: [] }; const isLazyLoaded = filePath => compilerObject.files[filePath].lazyLoadModulePath; /** * * @param {*} item * @param {*} type * @param {*} g */ const pushItem = (item, type, g) => { if (!cache.imp.includes(item.source)) { ret[type].push({ source: item.source, absolutePath: item.absolutePath, specifiers: [] }); cache.imp.push(item.source); } // check global imports if (g) { const vi = ret[type].find(c => c.source === item.source); vi.specifiers.push.apply(vi.specifiers, item.specifiers); } }; for (item of definition.imports) { const local = (item.specifiers.length && item.specifiers[0].local); const impIndex = fnDefinitions.indexOf(local); // !local || impIndex > -1 if (isLazyLoaded(item.absolutePath)) { ret.exp.splice(impIndex, 0, item); cache.exp.push(item.absolutePath); // check what each file imports const itemImps = compilerObject.files[item.absolutePath]; itemImps.imports.forEach(cItem => { if (!compilerObject.globalImports.hasOwnProperty(cItem.source)) { if (!isLazyLoaded(cItem.absolutePath)) { ret.imp.push(cItem); } else if (!cache.exp.includes(cItem.absolutePath)) { if (cItem.nameSpace) { ret.ns.push(cItem); ret.imp.push(cItem); } else { ret.localImp.push(cItem); } cache.exp.push(cItem.absolutePath); } } else { pushItem(cItem, 'imp', true); } }); } else { pushItem(item, 'imp', true); } } return ret } /** * * @param {*} compilerObject * @param {*} impExp * @param {*} output */ function _writeModuleStream(compilerObject, impExp, output, isNs) { for (const item of impExp) { const req = compilerObject.output.modules[item.absolutePath]; if (isNs) { output.push(`${getIndex(item.absolutePath)} : module => {\n${req.source.join('')}\n}`); } else { output.push(req.source.join('')); } } } /** * * @param {*} compilerObject * @param {*} isProd * @returns */ async function writeLazyLoadModules(compilerObject, isProd) { var templateParser = getTemplate('lazyload'); while (compilerObject.output.lazyLoads.length) { modulePath = compilerObject.output.lazyLoads.pop(); const output = []; const sourceDefinition = compilerObject.output.modules[modulePath]; try { const impExp = extractModuleImpExp(compilerObject, modulePath, sourceDefinition); const defaultModuleImports = _writeImport(compilerObject, impExp.imp, output, true); const moduleName = sourceDefinition.annotations[0].fn; _writeExports([{ local: moduleName, exported: moduleName }], defaultModuleImports, output); _writeModuleStream(compilerObject, impExp.localImp.concat(impExp.exp), output); output.push(sourceDefinition.source.join('')); const fileName = `${compilerObject.options.output.folder}${getIndex(modulePath)}.js`; const modules = [`${getIndex(modulePath)} : (module, exports, __required, global) => {\n${output.join('')}}`]; _writeModuleStream(compilerObject, impExp.ns, modules, true); const sourceCode = writeGlobalImports(`{\n${modules.join(',\n')}\n}`, defaultModuleImports.$); await outputJSFiles(fileName, templateParser({ sourceCode }), isProd); } catch(e) { console.log(`[OutPut] Error generating chunk module ${modulePath}, please try again`); } } } function parseStyle(config) { const style = config.styleUrl ? fs.readFileSync(config.styleUrl, 'utf8') : config.style; if (!style) return undefined; try { return nodeSass.renderSync({ data: attachSelector(style), outputStyle: 'compressed', outFile: config.outFile, sourceMap: false, importer: urlLoader }).css.toString(); } catch (e) { console.log(`styling error: ${e.message || e} for ${config.styleUrl}`); return ""; } /** * * @param {*} url * @param {*} prev * @param {*} done */ function urlLoader(url, prev, done) { // url is the path in import as is, which LibSass encountered. // prev is the previously resolved path. // done is an optional callback, either consume it or return value synchronously. // this.options contains this options hash const filePath = path.resolve(prev, '..', path.dirname(config.styleUrl), url); let content = ''; try { content = fs.readFileSync(filePath, 'utf8'); } catch (e) { helper.console.error(`unable to read file ${helper.colors.yellow(filePath)} imported in ${helper.colors.yellow(config.styleUrl)} `); } return ({ contents: attachSelector(content) }); } function attachSelector(result) { return (result ? (config.selector ? `${config.selector}{${result}}` : result) : undefined); } } /** * * @param {*} options */ async function writeCss(options, isLib = false) { let styles = []; if (options.output.styles) { for (const style of options.output.styles) { exports.pushStyle({ styleUrl: style }, styles); } } cssFileHolder.forEach(css => { if (css.result) styles.push(css.result); }); let outputFilePath = `${options.output.folder}/styles.js`; if (!isLib) { styles = loadTemplate('css', { styles }); } else { outputFilePath = `${options.output.folder}/bundles/styles.css`; styles = styles.map(c => c.substring(1, c.length - 1)).join(''); } if (styles.length) { await exports.writeFile(outputFilePath, styles); styles = ''; return true; } }