+37
| #!/usr/bin/env node | ||
| const sade = require('sade'); | ||
| const command = require('./commands'); | ||
| const { version } = require('./package'); | ||
| sade('hottie') | ||
| .version(version) | ||
| .command('build [src]') | ||
| .describe('Build production assets') | ||
| .option('--minify', 'Minify production assets', true) | ||
| .option('-o, --dest', 'Path to output directory', 'build') | ||
| .action(command.build) | ||
| // .command('export [src]') | ||
| // .describe('Export pre-rendered pages') | ||
| // .option('-o, --dest', 'Path to output directory', 'build') | ||
| // .option('-w, --wait', 'Time (ms) to wait before scraping each route', 0) | ||
| // .option('-r, --routes', 'Comma-delimited list of routes to export') | ||
| // .option('-i, --insecure', 'Launch Chrome Headless without sandbox') | ||
| // .action((src, opts) => { | ||
| // opts.export = true; | ||
| // build(src, opts); | ||
| // }) | ||
| .command('watch [src]') | ||
| .describe('Start development server') | ||
| .option('-H, --host', 'A hostname on which to start the application', 'localhost') | ||
| .option('-p, --port', 'A port number on which to start the application', 8080) | ||
| .option('-q, --quiet', 'Disable logging to terminal, including errors and warnings') | ||
| .option('--https', 'Run the application over HTTP/2 with HTTPS') | ||
| .option('--key', 'Path to custom SSL certificate key') | ||
| .option('--cert', 'Path to custom SSL certificate') | ||
| .option('--cacert', 'Path to custom CA certificate override') | ||
| .action(command.watch) | ||
| .parse(process.argv); |
| !function(){let e=new EventSource("/_hmr");e.addEventListener("hmr:start",(function(e){console.log("[hottie] Connected")})),e.addEventListener("hmr:update",(function(e){let t=JSON.parse(e.data);return console.log("[TODO]",t),location.reload()})),e.addEventListener("hmr:error",(function(e){console.error("[hottie]",e.data)}))}(); |
| var fs = require('fs'); | ||
| var util = require('util'); | ||
| var path = require('path'); | ||
| var tlist = require('totalist'); | ||
| var colors = require('kleur'); | ||
| var laccess = require('local-access'); | ||
| const read = util.promisify(fs.readFile); | ||
| const write = util.promisify(fs.writeFile); | ||
| function exists(file, dir = '.') { | ||
| let str = path.resolve(dir, file); | ||
| return fs.existsSync(str) && str; | ||
| } | ||
| function absolute(target, root) { | ||
| return path.join(path.dirname(root), target); | ||
| } | ||
| function parse(str, opts = {}) { | ||
| return require('node-html-parser').parse(str, opts); | ||
| } | ||
| function toScript(tag) { | ||
| let { src, type } = tag.rawAttributes; | ||
| if (!src || /^(https?:)?\/\//.test(src)) return; | ||
| let input = src.startsWith('/') ? src.substring(1) : src; | ||
| let nomodule = 'nomodule' in tag.rawAttributes || type !== 'module'; | ||
| return { input, nomodule, original: tag.toString() }; | ||
| } | ||
| async function entries(template) { | ||
| const document = parse(await read(template, 'utf8')); | ||
| return document.querySelectorAll('script').map(toScript).filter(Boolean); | ||
| } | ||
| function mutate(src, opts) { | ||
| opts.dir = opts.dest || 'build'; | ||
| opts.cwd = path.resolve(opts.cwd || '.'); | ||
| opts.src = path.join(opts.cwd, src || 'src'); | ||
| opts.dest = path.join(opts.cwd, opts.dir); | ||
| opts.template = opts.template || 'index.html'; | ||
| opts.production = !!opts.production; | ||
| delete opts._; // bye | ||
| } | ||
| // modified pwa/core util | ||
| function merge(old, nxt, args) { | ||
| for (let k in nxt) { | ||
| if (k === 'rollup') continue; | ||
| if (typeof nxt[k] === 'function') { | ||
| old[k] = old[k] || {}; | ||
| nxt[k](old[k], args); | ||
| } else { | ||
| old[k] = nxt[k] || old[k]; | ||
| } | ||
| } | ||
| } | ||
| // TODO: lukeed/premove? | ||
| function remove(dir, cwd) { | ||
| if (cwd) dir = path.join(cwd, dir); | ||
| let stat = fs.lstatSync(dir); | ||
| if (stat.isDirectory()) { | ||
| fs.readdirSync(dir).forEach(str => remove(str, dir)); | ||
| fs.rmdirSync(dir); // now empty | ||
| } else { | ||
| fs.unlinkSync(dir); | ||
| } | ||
| } | ||
| const defaults = { | ||
| publicPath: '/', | ||
| assets: { | ||
| test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif|mp4|mov|ogg|webm)$/, | ||
| }, | ||
| resolve: { | ||
| extensions: ['.mjs', '.js', '.jsx', '.json'], | ||
| mainFields: ['browser', 'module', 'jsnext', 'main'], | ||
| }, | ||
| commonjs: { | ||
| extensions: ['.js', '.cjs'] | ||
| }, | ||
| json: { | ||
| preferConst: true, | ||
| namedExports: true, | ||
| indent: ' ', | ||
| }, | ||
| terser: { | ||
| mangle: true, | ||
| compress: true, | ||
| output: { | ||
| comments: false | ||
| } | ||
| } | ||
| }; | ||
| function toAssets (argv, options={}) { | ||
| // TODO: mimetype dict? | ||
| const { test } = options; | ||
| const minify = !!argv.minify; | ||
| const isProd = argv.production; | ||
| const Manifest = new Map(); | ||
| const Plugin = { | ||
| name: 'hottie:assets', | ||
| resolveId(file, importer) { | ||
| if (!test.test(file)) return; | ||
| let dir = path.dirname(importer); | ||
| return path.resolve(dir, file); | ||
| }, | ||
| async load(file) { | ||
| if (!test.test(file)) return; | ||
| // TODO: base64 inline w/ limit? | ||
| const ref = this.emitFile({ | ||
| type: 'asset', | ||
| source: await read(file), | ||
| name: path.basename(file), | ||
| }); | ||
| return `export default import.meta.ROLLUP_FILE_URL_${ref};`; | ||
| } | ||
| }; | ||
| if (isProd) { | ||
| Plugin.resolveFileUrl = function (info) { | ||
| // [absolute path]: [relative outpath] | ||
| Manifest.set(info.moduleId, info.fileName); | ||
| return null; | ||
| }; | ||
| Plugin.renderChunk = function (_code, info) { | ||
| // [absolute path]: [relative outpath] | ||
| Manifest.set(info.facadeModuleId, info.fileName); | ||
| return null; | ||
| }; | ||
| } | ||
| return { Manifest, Plugin }; | ||
| } | ||
| function Config (argv, extra = {}) { | ||
| const { src, dest, production } = argv; | ||
| const { local, ...args } = extra; | ||
| const isProd = !!production; | ||
| Object.assign(args, argv); | ||
| let customize, options = { ...defaults }; | ||
| if (local) { | ||
| let file = require(local); | ||
| merge(options, file, args); | ||
| customize = file.rollup; | ||
| } | ||
| const Assets = toAssets(argv, options.assets); | ||
| /** | ||
| * @type import('rollup').RollupOptions | ||
| */ | ||
| const config = { | ||
| input: extra.entry.input, | ||
| output: { | ||
| dir: dest, | ||
| sourcemap: !isProd, | ||
| minifyInternalExports: isProd, | ||
| entryFileNames: isProd ? '[name].[hash].js' : '[name].js', | ||
| assetFileNames: isProd ? '[name].[hash].[ext]' : '[name].[ext]', | ||
| chunkFileNames: isProd ? '[name].[hash].js' : '[name].js', | ||
| }, | ||
| preserveModules: !isProd, | ||
| treeshake: isProd && { | ||
| moduleSideEffects: 'no-external', | ||
| tryCatchDeoptimization: false | ||
| }, | ||
| plugins: [ | ||
| Assets.Plugin, | ||
| require('@rollup/plugin-node-resolve').nodeResolve({ | ||
| ...options.resolve, | ||
| rootDir: src, | ||
| }), | ||
| require('@rollup/plugin-commonjs')({ | ||
| ...options.commonjs | ||
| }), | ||
| require('@rollup/plugin-json')({ | ||
| compact: isProd, | ||
| ...options.json | ||
| }), | ||
| ] | ||
| }; | ||
| if (customize) { | ||
| customize(config, args); | ||
| } | ||
| return { | ||
| Assets, | ||
| options, | ||
| config, | ||
| }; | ||
| } | ||
| /** | ||
| * Crawl for & Copy HTML files | ||
| */ | ||
| async function HTML (argv, opts) { | ||
| const { src, dest, minify } = argv; | ||
| const { MANIFEST, publicPath } = opts; | ||
| // Find, modify, write HTML files | ||
| await tlist(src, async (rel, abs) => { | ||
| if (!/\.html?$/.test(rel)) return; | ||
| let target = path.join(dest, rel); | ||
| let content = await read(abs, 'utf8'); | ||
| let document = parse(content); | ||
| // TODO: meta tags | ||
| document.querySelectorAll('script,link').forEach(tag => { | ||
| let { src, href } = tag.rawAttributes; | ||
| if (/^(https?:)?\/\//.test(href || src)) return; | ||
| let file = absolute(href || src, abs); | ||
| let entry = MANIFEST.get(file); | ||
| if (entry) { | ||
| tag.setAttribute(href ? 'href' : 'src', publicPath + entry); | ||
| } | ||
| }); | ||
| // TODO: html-minifier ? | ||
| if (minify) document.removeWhitespace(); | ||
| await write(target, document.toString()); | ||
| }); | ||
| } | ||
| async function index (src, argv={}) { | ||
| let prev = argv.production; | ||
| argv.production = prev == null || !!prev; | ||
| mutate(src, argv); | ||
| const template = exists(argv.template, argv.src); | ||
| if (!template) throw new Error(`Missing template: "${argv.template}"`); | ||
| if (exists(argv.dest)) { | ||
| console.log('removing existing "%s" directory', argv.dir); | ||
| remove(argv.dest); | ||
| } | ||
| let publicPath, MANIFEST = new Map; | ||
| const { rollup } = require('rollup'); | ||
| const { terser } = require('rollup-plugin-terser'); | ||
| const local = exists('hottie.config.js', argv.cwd); | ||
| if (local) console.log('loading custom configuration'); | ||
| // TODO: if module only, generate nomodule too | ||
| const scripts = await entries(template); | ||
| for (let entry of scripts) { | ||
| let { Assets, config, options } = Config(argv, { local, entry }); | ||
| publicPath = publicPath || options.publicPath; | ||
| let { output, ...rest } = config; | ||
| // Prod-only changes | ||
| if (argv.minify) rest.plugins.push( | ||
| terser(options.terser) | ||
| ); | ||
| let now = Date.now(); | ||
| let b = await rollup(rest); | ||
| // TODO: generate sibling nomodule here? | ||
| await Promise.all([].concat(output).map(b.write)); | ||
| console.log('~> %s (%s)', entry.input, (Date.now() - now) + 'ms'); | ||
| for (let [abs, rel] of Assets.Manifest) { | ||
| if (MANIFEST.has(abs)) console.log('Manifest has "%s" file!', abs); | ||
| else MANIFEST.set(abs, rel); | ||
| } | ||
| } | ||
| await HTML(argv, { MANIFEST, publicPath }); | ||
| console.log('build complete~!'); | ||
| } | ||
| /** | ||
| * Crawl for & Copy HTML files | ||
| */ | ||
| function HTML$1 (opts={}) { | ||
| const { src } = opts; | ||
| let script; | ||
| async function copy(ctx, file) { | ||
| let document = parse(await read(file, 'utf8')); | ||
| if (!script) { | ||
| // TODO: inject <script src="/@npm/hottie/client"> | ||
| let data = await read('./client/index.js', 'utf8'); | ||
| script = parse(`<script>${data}</script>\n`, { script: true }); | ||
| } | ||
| document.querySelector('body').appendChild(script); | ||
| ctx.emitFile({ | ||
| type: 'asset', | ||
| source: document.toString(), | ||
| fileName: path.relative(src, file) | ||
| }); | ||
| } | ||
| return { | ||
| name: 'hottie:dev:html', | ||
| async buildStart() { | ||
| await tlist(src, async (rel, abs) => { | ||
| if (/\.html$/.test(rel)) { | ||
| this.addWatchFile(abs); | ||
| await copy(this, abs); | ||
| } | ||
| }); | ||
| }, | ||
| async watchChange(file) { | ||
| if (/\.html$/.test(file)) { | ||
| await copy(this, file); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Create a Watcher from base config | ||
| * @param config {import('rollup').RollupWatchOptions} | ||
| * @returns {import('rollup').RollupWatcher} | ||
| */ | ||
| function Watcher (config, opts={}) { | ||
| const { src, dest, onUpdate, onError } = opts; | ||
| const hasMap = !!config.output.sourcemap; | ||
| // dev-only plugins | ||
| config.plugins.push( | ||
| HTML$1({ src }) | ||
| ); | ||
| // Initialize Watcher | ||
| // Attach logging/event listeners | ||
| const watcher = require('rollup').watch(config); | ||
| let UPDATES = []; | ||
| let CHANGED = new Set; | ||
| watcher.on('change', file => { | ||
| if (file.startsWith(src)) { | ||
| CHANGED.add('/' + path.relative(src, file)); | ||
| } else console.error('[CHANGE] NOT WITHIN SOURCE: "%s"', file); | ||
| }); | ||
| watcher.on('event', evt => { | ||
| switch (evt.code) { | ||
| case 'START': { | ||
| UPDATES = [...CHANGED]; | ||
| CHANGED.clear(); | ||
| break; | ||
| } | ||
| case 'BUNDLE_END': { | ||
| // TODO: prettify | ||
| console.info(`Bundled in ${evt.duration}ms`); | ||
| if (onUpdate && UPDATES.length) onUpdate(UPDATES); | ||
| break; | ||
| } | ||
| case 'ERROR': { | ||
| console.error('ERROR', evt.error); | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
| return watcher; | ||
| } | ||
| // HTTPS | ||
| const { HOST, PORT } = process.env; | ||
| function Server (dir, opts={}) { | ||
| let https = false; // TODO | ||
| let port = PORT || opts.port; | ||
| let hostname = HOST || opts.host; | ||
| /** @type {import('http').Server} */ | ||
| const server = require('hottie/server')(dir, opts); | ||
| return new Promise((res, rej) => { | ||
| server.listen(port, hostname, () => { | ||
| let { address } = server.address(); | ||
| let isLocal = /^(::1?|localhost)$/.test(address); | ||
| hostname = isLocal ? 'localhost' : address; | ||
| const write = msg => process.stdout.write(msg + '\n'); | ||
| const { local, network } = laccess({ port, hostname, https }); | ||
| // lukeed/sirv | ||
| write('\n ' + colors.green('Your application is ready~! 🚀\n')); | ||
| write(' ' + colors.bold('- Local:') + ' ' + local); | ||
| isLocal || write(' ' + colors.bold('- Network:') + ' ' + network); | ||
| let border = '─'.repeat(Math.min(process.stdout.columns, 36) / 2); | ||
| write('\n' + border + colors.inverse(' LOGS ') + border + '\n'); | ||
| return res(server); | ||
| }); | ||
| }); | ||
| } | ||
| async function index$1 (src, argv) { | ||
| mutate(src, argv); | ||
| const template = exists(argv.template, argv.src); | ||
| if (!template) throw new Error(`Missing template: "${argv.template}"`); | ||
| if (exists(argv.dest)) { | ||
| console.log('removing existing "%s" directory', argv.dir); | ||
| remove(argv.dest); | ||
| } | ||
| const local = exists('hottie.config.js', argv.cwd); | ||
| if (local) console.log('loading custom configuration'); | ||
| const scripts = await entries(template); | ||
| // look for first ESM script | ||
| let entry = scripts.find(x => !x.nomodule); | ||
| if (!entry) console.log('TODO: APPEND SCRIPT TO TEMPLATE'); | ||
| // Load & Assemble config | ||
| const { config } = Config(argv, { local, entry }); | ||
| // TODO: import pwa-cli HTTPS stuff | ||
| const server = await Server(argv.dest, { | ||
| ...argv, single: true | ||
| }); | ||
| Watcher(config, { | ||
| ...argv, | ||
| onError: msg => server.broadcast('error', msg), | ||
| onUpdate: arr => server.broadcast('update', JSON.stringify(arr)), | ||
| }); | ||
| } | ||
| exports.build = index; | ||
| exports.watch = index$1; |
+11
| declare module 'hottie/server' { | ||
| import type { Server } from 'http'; | ||
| import type { Options } from 'sirv'; | ||
| declare function broadcast(event: string, data: string): void; | ||
| declare function init(dir: string, options?: Options): Server & { | ||
| broadcast: typeof broadcast | ||
| }; | ||
| export default init; | ||
| } |
Sorry, the diff of this file is not supported yet
| var http = require('http'); | ||
| var sirv = require('sirv'); | ||
| var parse = require('@polka/url'); | ||
| const clients = new Map(); | ||
| function start(dir, opts={}) { | ||
| let assets = sirv(dir, { ...opts, dev: true }); | ||
| return function (req, res, next) { | ||
| let { pathname } = parse(req); | ||
| if (pathname !== '/_hmr') { | ||
| return assets(req, res, next); | ||
| } | ||
| res.writeHead(200, { | ||
| 'connection': 'keep-alive', | ||
| 'content-type': 'text/event-stream', | ||
| 'transfer-encoding': 'identity', | ||
| 'cache-control': 'no-cache', | ||
| }); | ||
| let key = Math.random().toString(36).slice(2); | ||
| let message = format(key, 'start'); | ||
| res.connection.setTimeout(0); | ||
| clients.set(key, res); | ||
| req.once('close', () => { | ||
| clients.delete(key); | ||
| res.end(); | ||
| }); | ||
| res.write(message); | ||
| }; | ||
| } | ||
| function format(data, event, id) { | ||
| let msg = ''; | ||
| if (id) msg += `id: ${id}\n`; | ||
| if (event) msg += `event: hmr:${event}\n`; | ||
| return msg + `data: ${data}\n\n`; | ||
| } | ||
| function broadcast(event, data) { | ||
| let msg = format(data, event); | ||
| for (let [, res] of clients) res.write(msg); | ||
| } | ||
| function stop(callback) { | ||
| for (let [, res] of clients) res.end(); | ||
| if (callback) callback(); | ||
| } | ||
| function index (dir, opts = {}) { | ||
| const handler = start(dir, opts); | ||
| const server = http.createServer(handler); | ||
| const close = server.close.bind(server); | ||
| server.broadcast = broadcast; | ||
| server.close = (db) => { | ||
| stop(); | ||
| return close(db); | ||
| }; | ||
| return server; | ||
| } | ||
| module.exports = index; |
| import { createServer } from 'http'; | ||
| import sirv from 'sirv'; | ||
| import parse from '@polka/url'; | ||
| const clients = new Map(); | ||
| function start(dir, opts={}) { | ||
| let assets = sirv(dir, { ...opts, dev: true }); | ||
| return function (req, res, next) { | ||
| let { pathname } = parse(req); | ||
| if (pathname !== '/_hmr') { | ||
| return assets(req, res, next); | ||
| } | ||
| res.writeHead(200, { | ||
| 'connection': 'keep-alive', | ||
| 'content-type': 'text/event-stream', | ||
| 'transfer-encoding': 'identity', | ||
| 'cache-control': 'no-cache', | ||
| }); | ||
| let key = Math.random().toString(36).slice(2); | ||
| let message = format(key, 'start'); | ||
| res.connection.setTimeout(0); | ||
| clients.set(key, res); | ||
| req.once('close', () => { | ||
| clients.delete(key); | ||
| res.end(); | ||
| }); | ||
| res.write(message); | ||
| }; | ||
| } | ||
| function format(data, event, id) { | ||
| let msg = ''; | ||
| if (id) msg += `id: ${id}\n`; | ||
| if (event) msg += `event: hmr:${event}\n`; | ||
| return msg + `data: ${data}\n\n`; | ||
| } | ||
| function broadcast(event, data) { | ||
| let msg = format(data, event); | ||
| for (let [, res] of clients) res.write(msg); | ||
| } | ||
| function stop(callback) { | ||
| for (let [, res] of clients) res.end(); | ||
| if (callback) callback(); | ||
| } | ||
| function index (dir, opts = {}) { | ||
| const handler = start(dir, opts); | ||
| const server = createServer(handler); | ||
| const close = server.close.bind(server); | ||
| server.broadcast = broadcast; | ||
| server.close = (db) => { | ||
| stop(); | ||
| return close(db); | ||
| }; | ||
| return server; | ||
| } | ||
| export default index; |
+53
-2
| { | ||
| "name": "hottie", | ||
| "version": "0.0.0" | ||
| "name": "hottie", | ||
| "version": "0.0.1", | ||
| "repository": "lukeed/hottie", | ||
| "description": "WIP", | ||
| "license": "MIT", | ||
| "types": "index.d.ts", | ||
| "bin": { | ||
| "hottie": "bin.js" | ||
| }, | ||
| "author": { | ||
| "name": "Luke Edwards", | ||
| "email": "luke.edwards05@gmail.com", | ||
| "url": "https://lukeed.com" | ||
| }, | ||
| "files": [ | ||
| "*.d.ts", | ||
| "client", | ||
| "commands", | ||
| "server", | ||
| "bin.js" | ||
| ], | ||
| "exports": { | ||
| "./server": { | ||
| "import": "./server/index.mjs", | ||
| "require": "./server/index.js" | ||
| } | ||
| }, | ||
| "engines": { | ||
| "node": ">= 10" | ||
| }, | ||
| "scripts": { | ||
| "build": "node build", | ||
| "prepublishOnly": "node build", | ||
| "test": "uvu -r esm packages test" | ||
| }, | ||
| "dependencies": { | ||
| "@polka/url": "1.0.0-next.11", | ||
| "@rollup/plugin-commonjs": "^12.0.0", | ||
| "@rollup/plugin-json": "^4.0.3", | ||
| "@rollup/plugin-node-resolve": "^8.0.0", | ||
| "kleur": "^3.0.3", | ||
| "local-access": "^1.0.1", | ||
| "node-html-parser": "^1.2.18", | ||
| "rollup": "^2.13.0", | ||
| "rollup-plugin-terser": "^6.1.0", | ||
| "sade": "^1.7.0", | ||
| "sirv": "1.0.0-next.9", | ||
| "totalist": "^1.0.1" | ||
| }, | ||
| "devDependencies": { | ||
| "esm": "^3.2.25", | ||
| "uvu": "^0.0.11" | ||
| } | ||
| } |
-1
| // |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
Trivial Package
Supply chain riskPackages less than 10 lines of code are easily copied into your own project and may not warrant the additional supply chain risk of an external dependency.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No License Found
LicenseLicense information could not be found.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
17974
38973.91%8
300%0
-100%527
52600%2
-33.33%12
Infinity%2
Infinity%3
50%4
300%3
200%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added