elm-spa
Version:
single page apps made easy
256 lines (255 loc) • 13 kB
JavaScript
;
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 })
};