UNPKG

elm-spa

Version:
256 lines (255 loc) 13 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.build = void 0; const path_1 = __importDefault(require("path")); const file_1 = require("../file"); const config_1 = __importDefault(require("../config")); const File = __importStar(require("../file")); const readline_1 = require("readline"); const routes_1 = __importDefault(require("../templates/routes")); const pages_1 = __importDefault(require("../templates/pages")); const page_1 = __importDefault(require("../templates/page")); const request_1 = __importDefault(require("../templates/request")); const model_1 = __importDefault(require("../templates/model")); const msg_1 = __importDefault(require("../templates/msg")); const child_process_1 = __importDefault(require("child_process")); const params_1 = __importDefault(require("../templates/params")); const terser_1 = __importDefault(require("terser")); const terminal_1 = require("../terminal"); const utils_1 = require("../templates/utils"); const _common_1 = require("./_common"); const elm = require('node-elm-compiler'); exports.build = ({ env, runElmMake }) => () => Promise.all([ createMissingDefaultFiles(), _common_1.createMissingAddTemplates() ]) .then(createGeneratedFiles) .then(runElmMake ? compileMainElm(env) : _ => ` ${terminal_1.check} ${terminal_1.bold}elm-spa${terminal_1.reset} generated new files.`); const createMissingDefaultFiles = async () => { const toAction = async (filepath) => { const [inDefaults, inSrc] = await Promise.all([ file_1.exists(path_1.default.join(config_1.default.folders.defaults.dest, ...filepath)), file_1.exists(path_1.default.join(config_1.default.folders.src, ...filepath)) ]); if (inSrc && inDefaults) { return ['DELETE_FROM_DEFAULTS', filepath]; } else if (!inSrc) { return ['CREATE_IN_DEFAULTS', filepath]; } else { return ['DO_NOTHING', filepath]; } }; const actions = await Promise.all(config_1.default.defaults.map(toAction)); const performDefaultFileAction = ([action, relative]) => action === 'CREATE_IN_DEFAULTS' ? createDefaultFile(relative) : action === 'DELETE_FROM_DEFAULTS' ? deleteFromDefaults(relative) : Promise.resolve(); const createDefaultFile = async (relative) => File.copyFile(path_1.default.join(config_1.default.folders.defaults.src, ...relative), path_1.default.join(config_1.default.folders.defaults.dest, ...relative)); const deleteFromDefaults = async (relative) => File.remove(path_1.default.join(config_1.default.folders.defaults.dest, ...relative)); return Promise.all(actions.map(performDefaultFileAction)); }; const getFilepathSegments = async (entries) => { const contents = await Promise.all(entries.map(e => File.read(e.filepath))); return Promise.all(entries.map(async (entry, i) => { const c = contents[i]; const kind = await (utils_1.isStandardPage(c) ? Promise.resolve('page') : utils_1.isStaticPage(c) ? Promise.resolve('static-page') : utils_1.isStaticView(c) ? Promise.resolve('view') : Promise.reject(invalidExportsMessage(entry))); return { kind, entry }; })); }; const invalidExportsMessage = (entry) => { const moduleName = `${terminal_1.bold}Pages.${entry.segments.join('.')}${terminal_1.reset}`; const cyan = (str) => `${terminal_1.colors.cyan}${str}${terminal_1.reset}`; return [ `${terminal_1.colors.RED}!${terminal_1.reset} Ran into a problem at ${terminal_1.bold}${terminal_1.colors.yellow}src/Pages/${entry.segments.join('/')}.elm${terminal_1.reset}`, ``, `${terminal_1.bold}elm-spa${terminal_1.reset} expected one of these module definitions:`, ``, ` ${terminal_1.dot} module ${moduleName} exposing (${cyan('view')})`, ` ${terminal_1.dot} module ${moduleName} exposing (${cyan('page')})`, ` ${terminal_1.dot} module ${moduleName} exposing (${cyan('Model')}, ${cyan('Msg')}, ${cyan('page')})`, ``, `Visit ${terminal_1.colors.green}https://elm-spa.dev/guide/03-pages${terminal_1.reset} for more details!` ].join('\n'); }; const createGeneratedFiles = async () => { const entries = await getAllPageEntries(); const segments = entries.map(e => e.segments); const filepathSegments = await getFilepathSegments(entries); const kindForPage = (p) => filepathSegments .filter(item => item.entry.segments.join('.') == p.join('.')) .map(fps => fps.kind)[0] || 'page'; const paramFiles = segments.map(filepath => ({ filepath: ['Gen', 'Params', ...filepath], contents: params_1.default(filepath, utils_1.options(kindForPage)) })); const filesToCreate = [ ...paramFiles, { filepath: ['Page'], contents: page_1.default() }, { filepath: ['Request'], contents: request_1.default() }, { filepath: ['Gen', 'Route'], contents: routes_1.default(segments, utils_1.options(kindForPage)) }, { filepath: ['Gen', 'Pages'], contents: pages_1.default(segments, utils_1.options(kindForPage)) }, { filepath: ['Gen', 'Model'], contents: model_1.default(segments, utils_1.options(kindForPage)) }, { filepath: ['Gen', 'Msg'], contents: msg_1.default(segments, utils_1.options(kindForPage)) } ]; return Promise.all(filesToCreate.map(({ filepath, contents }) => File.create(path_1.default.join(config_1.default.folders.generated, ...filepath) + '.elm', contents))); }; const getAllPageEntries = async () => { const scanPageFilesIn = async (folder) => { const items = await File.scan(folder); return items.map(s => ({ filepath: s, segments: s.substring(folder.length + 1, s.length - '.elm'.length).split(path_1.default.sep) })); }; return Promise.all([ scanPageFilesIn(config_1.default.folders.pages.src), scanPageFilesIn(config_1.default.folders.pages.defaults) ]).then(([left, right]) => left.concat(right)); }; const outputFilepath = path_1.default.join(config_1.default.folders.dist, 'elm.js'); const compileMainElm = (env) => async () => { await ensureElmIsInstalled(env); const start = Date.now(); const elmMake = async () => { const inDevelopment = env === 'development'; const inProduction = env === 'production'; const isSrcMainElmDefined = await File.exists(path_1.default.join(config_1.default.folders.src, 'Main.elm')); const inputFilepath = isSrcMainElmDefined ? path_1.default.join(config_1.default.folders.src, 'Main.elm') : path_1.default.join(config_1.default.folders.defaults.dest, 'Main.elm'); return elm.compileToString(inputFilepath, { output: outputFilepath, report: 'json', debug: inDevelopment, optimize: inProduction, }) .catch((error) => { try { return colorElmError(JSON.parse(error.message.split('\n')[1])); } catch (_a) { const { RED, green } = terminal_1.colors; return Promise.reject([ `${RED}!${terminal_1.reset} elm-spa failed to understand an error`, `Please report the output below to ${green}https://github.com/ryan-haskell/elm-spa/issues${terminal_1.reset}`, `-----`, JSON.stringify(error, null, 2), `-----`, `${RED}!${terminal_1.reset} elm-spa failed to understand an error`, `Please send the output above to ${green}https://github.com/ryan-haskell/elm-spa/issues${terminal_1.reset}`, `` ].join('\n\n')); } }); }; const colorElmError = (output) => { const errors = output.type === 'compile-errors' ? output.errors : [{ path: output.path, problems: [output] }]; const strIf = (str) => (cond) => cond ? str : ''; const boldIf = strIf(terminal_1.bold); const underlineIf = strIf(terminal_1.underline); const repeat = (str, num, min = 3) => [...Array(num < 0 ? min : num)].map(_ => str).join(''); const errorToString = (error) => { const problemToString = (problem) => { const path = error.path.substr(process.cwd().length + 1); return [ `${terminal_1.colors.cyan}-- ${problem.title} ${repeat('-', 63 - problem.title.length - path.length)} ${path}${terminal_1.reset}`, problem.message.map(messageToString).join('') ].join('\n\n'); }; const messageToString = (line) => typeof line === 'string' ? line : [boldIf(line.bold), underlineIf(line.underline), terminal_1.colors[line.color] || '', line.string, terminal_1.reset].join(''); return error.problems.map(problemToString).join('\n\n'); }; return Promise.reject(errors.map(err => errorToString(err)).join('\n\n\n')); }; const success = () => `${terminal_1.check} Build successful! ${terminal_1.dim}(${Date.now() - start}ms)${terminal_1.reset}`; const minify = (rawCode) => terser_1.default.minify(rawCode, { compress: { pure_funcs: `F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9`.split(','), pure_getters: true, keep_fargs: false, unsafe_comps: true, unsafe: true } }) .then(intermediate => terser_1.default.minify(intermediate.code || '', { mangle: true })) .then(minified => File.create(outputFilepath, minified.code || '')); return (env === 'development') ? elmMake() .then(rawJsCode => File.create(outputFilepath, rawJsCode)) .then(_ => success()) .catch(error => error) : elmMake() .then(minify) .then(_ => [success() + '\n']); }; const ensureElmIsInstalled = async (environment) => { await new Promise((resolve, reject) => { child_process_1.default.exec('elm', (err) => { if (err) { if (environment === 'production') { attemptToInstallViaNpm(resolve, reject); } else { offerToInstallForDeveloper(resolve, reject); } } else { resolve(undefined); } }); }); }; const attemptToInstallViaNpm = (resolve, reject) => { process.stdout.write(`\n ${terminal_1.bold}Awesome!${terminal_1.reset} Installing Elm via NPM... `); child_process_1.default.exec(`npm install --global elm@latest-0.19.1`, (err) => { if (err) { console.info(terminal_1.error); reject(` The automatic install didn't work...\n Please visit ${terminal_1.colors.green}https://guide.elm-lang.org/install/elm${terminal_1.reset} to install Elm.\n`); } else { console.info(terminal_1.check); console.info(` Elm is now installed!`); resolve(undefined); } }); }; const offerToInstallForDeveloper = (resolve, reject) => { const rl = readline_1.createInterface({ input: process.stdin, output: process.stdout }); rl.question(`\n${terminal_1.warn} Elm hasn't been installed yet.\n\n May I ${terminal_1.colors.cyan}install${terminal_1.reset} it for you? ${terminal_1.dim}[y/n]${terminal_1.reset} `, answer => { if (answer.toLowerCase() === 'n') { reject(` ${terminal_1.bold}No changes made!${terminal_1.reset}\n Please visit ${terminal_1.colors.green}https://guide.elm-lang.org/install/elm${terminal_1.reset} to install Elm.`); } else { attemptToInstallViaNpm(resolve, reject); } }); }; exports.default = { build: exports.build({ env: 'production', runElmMake: true }), gen: exports.build({ env: 'production', runElmMake: false }) };