UNPKG

@litexa/core

Version:

Litexa, a programming language for writing Alexa skills

328 lines (274 loc) 10.6 kB
### # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ### ### The project info object is a merge of the information derived from the project config file, and information scanned from the project data, like lists of asset and code files. ### fs = require 'fs' path = require 'path' globalModulesPath = require('global-modules') debug = require('debug')('litexa-project-info') LoggingChannel = require './loggingChannel' class ProjectInfo constructor: ({jsonConfig, @variant, logger = new LoggingChannel({logPrefix: 'project info'}), @doNotParseExtensions = false}) -> @variant = @variant ? "development" for k, v of jsonConfig @[k] = v @litexaRoot = path.join @root, "litexa" debug "litexa root is #{@litexaRoot}" @logger = logger @DEPLOY = @deployments?[@variant]?.DEPLOY ? {} @disableAssetReferenceValidation = @deployments?[@variant]?.disableAssetReferenceValidation # Direct Public Side-Effect @parseDirectory jsonConfig parseDirectory: -> unless fs.existsSync(@litexaRoot) or @root == '--mockRoot' throw new Error "Cannot initialize ProjectInfo no litexa sub directory found at #{@litexaRoot}" # compiled summary of package/extension info, to be sent in each response packageInfo = require '../../package.json' @userAgent = "#{packageInfo.name}/#{packageInfo.version} Node/#{process.version}" @parseExtensions() debug "beginning languages parse" @languages = {} @languages.default = @parseLanguage(@litexaRoot, 'default') @languagesRoot = path.join @litexaRoot, 'languages' if fs.existsSync @languagesRoot filter = (f) => fullPath = path.join @languagesRoot, f return false unless fs.lstatSync(fullPath).isDirectory() return false if f[0] == '.' return true languages = ( f for f in fs.readdirSync(@languagesRoot) when filter(f) ) for lang in languages @languages[lang] = @parseLanguage(path.join(@languagesRoot, lang), lang) # check for a localization summary file in the project's root dir for type in ['json', 'js'] localizationFilePath = path.join(@root, "localization.#{type}") if fs.existsSync localizationFilePath @localization = require(localizationFilePath) # if skill has no localization file, let's add a blank localization container # (to be populated by toLocalization() calls) unless @localization? @localization = { intents: {}, speech: {} } parseExtensions: -> @extensions = {} @extensionOptions = @extensionOptions ? {} return if @root == '--mockRoot' or @doNotParseExtensions lib = require '../parser/parserlib.coffee' deployModules = path.join @litexaRoot, 'node_modules' scanForExtensions = (modulesRoot) => debug "scanning for extensions at #{modulesRoot}" # this is fine, no extension modules to scan return unless fs.existsSync modulesRoot for moduleName in fs.readdirSync modulesRoot if moduleName.charAt(0) == '@' scopePath = path.join modulesRoot, moduleName for scopedModule in fs.readdirSync scopePath scopedModuleName = path.join moduleName, scopedModule scanModuleForExtension(scopedModuleName, modulesRoot) else scanModuleForExtension(moduleName, modulesRoot) scanModuleForExtension = (moduleName, modulesRoot) => if @extensions[moduleName] # this extension was already loaded - ignore duplicate # (probably installed locally as well as globally) return modulePath = path.join modulesRoot, moduleName debug "looking in #{modulePath}" # attempt to load any of the supported types found = false extensionFile = "" for type in ['coffee', 'js'] extensionFile = path.join modulePath, "litexa.extension.#{type}" debug extensionFile if fs.existsSync extensionFile found = true break # fine, this is not an extension module unless found debug "module #{moduleName} did not contain litexa.extension.js/coffee, skipping for extensions" return debug "loading extension `#{moduleName}`" # add extension name and version to userAgent, to be included in responses try extensionPackageInfo = require path.join modulePath, 'package.json' @userAgent += " #{moduleName}/#{extensionPackageInfo.version}" catch err console.warn "WARNING: Failed to load a package.json for the extension module at #{modulePath}/package.json, while looking for its version number. Is it missing?" extension = require extensionFile extension.__initialized = false extension.__location = modulesRoot extension.__deployable = modulesRoot == deployModules options = @extensionOptions[moduleName] ? {} @extensions[moduleName] = extension options, lib if @extensions[moduleName].language?.lib? for k, v of @extensions[moduleName].language.lib if k of lib throw new Error "extension `#{moduleName}` wanted to add type `#{k}` to lib, but it was already there. That extension is unfortunately not compatible with this project." lib[k] = v scanForExtensions(x) for x in [ deployModules path.join @root, 'node_modules' path.join @root, 'modules' globalModulesPath ] parseLanguage: (root, lang) -> debug "parsing language at #{root}" def = assetProcessors: {} convertedAssets: root: path.join @root, '.deploy', 'converted-assets', lang files: [] assets: root: path.join root, 'assets' files: [] code: root: root files: [] return if @root == '--mockRoot' fileBlacklist = [ 'package.json' 'package-lock.json' 'tsconfig.json' 'tslint.json' 'mocha.opts' '.mocharc.json' '.DS_Store' ] # collect all the files in the litexa directory # as inputs for the litexa compiler codeExtensionsWhitelist = [ '.litexa' '.coffee' '.js' '.json' ] codeFilter = (f) -> fullPath = path.join def.code.root, f return false unless fs.lstatSync(fullPath).isFile() return false if f[0] == '.' extension = path.extname f return false unless extension in codeExtensionsWhitelist return true def.code.files = ( f for f in fs.readdirSync(def.code.root) when codeFilter(f) ) assetExtensionsWhitelist = [ '.png' '.jpg' '.svg' '.mp3' '.otf' '.json' '.jpeg' '.txt' '.html' '.css' '.js' '.map' '.glb' '.m4a' '.mp4' '.ico' '.ogg' ] for kind, info of @extensions continue unless info.assetPipeline? for proc, procIndex in info.assetPipeline # @TODO: Validate processor here? # Create a clone of our processor, so as not to override previous languages' inputs/outputs. clone = {} Object.assign(clone, proc) name = clone.name ? "#{kind}[#{procIndex}]" unless clone.listOutputs? throw new Error "asset processor #{procIndex} from extension #{kind} doesn't have a listOutputs function." def.assetProcessors[name] = clone clone.inputs = [] clone.outputs = [] clone.options = @plugins?[kind] # collect all the assets if fs.existsSync def.assets.root # we support direct copy for some built in types logger = @logger assetFilter = (f) -> fullPath = path.join def.assets.root, f return false unless fs.lstatSync(fullPath).isFile() return false if f[0] == '.' extension = path.extname f unless extension in assetExtensionsWhitelist return false return true def.assets.files = [] processDirectory = (root) -> debug "processing asset dir #{root}" for f in fs.readdirSync(root) continue if f in fileBlacklist f = path.join root, f stat = fs.statSync f if stat.isDirectory() processDirectory f continue f = path.relative def.assets.root, f debug "processing asset file #{f}" processed = false if assetFilter(f) def.assets.files.push f processed = true # check whether any extensions would produce # usable assets from this file for kind, proc of def.assetProcessors outputs = proc.listOutputs assetName: f assetsRoot: def.assets.root targetRoot: null options: proc.options if outputs?.length > 0 debug "#{kind}: #{f} -> #{outputs}" processed = true proc.inputs.push f for o in outputs proc.outputs.push o if (o in def.assets.files) or (o in def.convertedAssets.files) throw new Error "Asset processor #{kind} would produce a duplicate file #{o}. Please resolve this before continuing by either deleting the duplicate or determining whether you have multiple asset processors that create the same output." def.convertedAssets.files.push o unless processed logger.warning "Unsupported internally or by extensions, skipping asset: #{f}" processDirectory def.assets.root debug "project info: \n #{JSON.stringify def, null, 2}" return def filesForLanguage: (lang) -> result = {} for type, info of @languages.default list = result[type] = {} for name in info.files list[name] = path.join info.root, name if lang of @languages for type, info of @languages[lang] list = result[type] for name in info.files list[name] = path.join info.root, name result ProjectInfo.createMock = -> config = { root: "--mockRoot" name: "mockProject" isMock: true } return new ProjectInfo {jsonConfig: config, variant: "mockTesting"} module.exports = ProjectInfo