Skip to content
Open
117 changes: 70 additions & 47 deletions src/cli/src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,30 @@ import terser from 'terser'
import { bold, underline, colors, reset, check, dim, dot, warn, error } from "../terminal"
import { isStandardPage, isStaticPage, isStaticView, options, PageKind } from "../templates/utils"
import { createMissingAddTemplates } from "./_common"

const elm = require('node-elm-compiler')

export const build = ({ env, runElmMake } : { env : Environment, runElmMake: boolean }) => () =>
export const build = ({ env, runElmMake }: { env: Environment, runElmMake: boolean }) => () =>
Promise.all([
createMissingDefaultFiles(),
createMissingAddTemplates()
createMissingAddTemplates(),
removeUnusedGeneratedFiles(),
removeEmptyDirs()
])
.then(createGeneratedFiles)
.then(runElmMake ? compileMainElm(env): _ => ` ${check} ${bold}elm-spa${reset} generated new files.`)
.then(runElmMake ? compileMainElm(env) : _ => ` ${check} ${bold}elm-spa${reset} generated new files.`)

const createMissingDefaultFiles = async () => {
type Action
= ['DELETE_FROM_DEFAULTS', string[]]
| ['CREATE_IN_DEFAULTS', string[]]
| ['DO_NOTHING', string[]]

const toAction = async (filepath: string[]): Promise<Action> => {
const toAction = async (filepath: string[]): Promise<any> => {
const [inDefaults, inSrc] = await Promise.all([
exists(path.join(config.folders.defaults.dest, ...filepath)),
exists(path.join(config.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.defaults.map(toAction))

const performDefaultFileAction = ([action, relative]: Action): Promise<any> =>
action === 'CREATE_IN_DEFAULTS' ? createDefaultFile(relative)
: action === 'DELETE_FROM_DEFAULTS' ? deleteFromDefaults(relative)
return inSrc && inDefaults ? deleteFromDefaults(filepath)
: !inSrc ? createDefaultFile(filepath)
: Promise.resolve()
}

const createDefaultFile = async (relative: string[]) =>
File.copyFile(
Expand All @@ -62,7 +49,37 @@ const createMissingDefaultFiles = async () => {
const deleteFromDefaults = async (relative: string[]) =>
File.remove(path.join(config.folders.defaults.dest, ...relative))

return Promise.all(actions.map(performDefaultFileAction))
return await Promise.all(config.defaults.map(toAction))

}

const removeUnusedGeneratedFiles = async () => {
const genFilePath = config.folders.pages.generated
const generatedFiles = await relativePagePaths(genFilePath)

const toAction = async (filepath: string): Promise<any> => {
const [inSrc, inDefaults] = await Promise.all([
exists(path.join(config.folders.defaults.src, filepath)),
exists(path.join(genFilePath, filepath))
]);

return !inSrc && !inDefaults ? deleteFromGenerated(filepath) : Promise.resolve()
}

const deleteFromGenerated = async (relative: string) =>
File.remove(path.join(genFilePath, relative))

return await Promise.all(generatedFiles.map(toAction))
}

export const removeEmptyDirs = async (): Promise<void> => {
const scanEmptyPageDirsIn = async (folder: string) =>
File.scanEmptyDirs(folder)

const emptyDirsInGen = await scanEmptyPageDirsIn(config.folders.pages.generated)
if (!emptyDirsInGen.length) return Promise.resolve()
await Promise.all(emptyDirsInGen.map(File.remove))
return Promise.resolve(removeEmptyDirs())
}

type FilepathSegments = {
Expand Down Expand Up @@ -140,10 +157,10 @@ type PageEntry = {
const getAllPageEntries = async (): Promise<PageEntry[]> => {
const scanPageFilesIn = async (folder: string) => {
const items = await File.scan(folder)
return items.map(s => ({
return Promise.resolve(items.map(s => ({
filepath: s,
segments: s.substring(folder.length + 1, s.length - '.elm'.length).split(path.sep)
}))
})))
}

return Promise.all([
Expand All @@ -152,6 +169,12 @@ const getAllPageEntries = async (): Promise<PageEntry[]> => {
]).then(([left, right]) => left.concat(right))
}

const relativePagePaths = async (folder: string) => {
const items = await File.scan(folder)
return Promise.resolve(items.map(s => s.substring(folder.length, s.length)))
}


type Environment = 'production' | 'development'

const outputFilepath = path.join(config.folders.dist, 'elm.js')
Expand All @@ -176,28 +199,28 @@ const compileMainElm = (env: Environment) => async () => {
debug: inDevelopment,
optimize: inProduction,
})
.catch((error: Error) => {
try { return colorElmError(JSON.parse(error.message.split('\n')[1])) }
catch {
const { RED, green } = colors
return Promise.reject([
`${RED}!${reset} elm-spa failed to understand an error`,
`Please report the output below to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
`-----`,
JSON.stringify(error, null, 2),
`-----`,
`${RED}!${reset} elm-spa failed to understand an error`,
`Please send the output above to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
``
].join('\n\n'))
}
})
.catch((error: Error) => {
try { return colorElmError(JSON.parse(error.message.split('\n')[1])) }
catch {
const { RED, green } = colors
return Promise.reject([
`${RED}!${reset} elm-spa failed to understand an error`,
`Please report the output below to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
`-----`,
JSON.stringify(error, null, 2),
`-----`,
`${RED}!${reset} elm-spa failed to understand an error`,
`Please send the output above to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
``
].join('\n\n'))
}
})
}

type ElmError
= ElmCompileError
| ElmJsonError

type ElmCompileError = {
type: 'compile-errors'
errors: ElmProblemError[]
Expand Down Expand Up @@ -225,11 +248,11 @@ const compileMainElm = (env: Environment) => async () => {
string: string
}

const colorElmError = (output : ElmError) => {
const errors : ElmProblemError[] =
const colorElmError = (output: ElmError) => {
const errors: ElmProblemError[] =
output.type === 'compile-errors'
? output.errors
: [ { path: output.path, problems: [output] } ]
: [{ path: output.path, problems: [output] }]

const strIf = (str: string) => (cond: boolean): string => cond ? str : ''
const boldIf = strIf(bold)
Expand Down Expand Up @@ -274,7 +297,7 @@ const compileMainElm = (env: Environment) => async () => {
.then(_ => [success() + '\n'])
}

const ensureElmIsInstalled = async (environment : Environment) => {
const ensureElmIsInstalled = async (environment: Environment) => {
await new Promise((resolve, reject) => {
ChildProcess.exec('elm', (err) => {
if (err) {
Expand Down
3 changes: 2 additions & 1 deletion src/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const config = {
src: path.join(cwd, 'src'),
pages: {
src: path.join(cwd, 'src', 'Pages'),
defaults: path.join(cwd, '.elm-spa', 'defaults', 'Pages')
defaults: path.join(cwd, '.elm-spa', 'defaults', 'Pages'),
generated: path.join(cwd, '.elm-spa', 'generated', 'Gen', 'Params')
},
defaults: {
src: path.join(root, 'src', 'defaults'),
Expand Down
29 changes: 20 additions & 9 deletions src/cli/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import path from "path"
* @param filepath - the absolute path of the file to create
* @param contents - the raw string contents of the file
*/
export const create = async (filepath : string, contents : string) => {
export const create = async (filepath: string, contents: string) => {
await ensureFolderExists(filepath)
return fs.writeFile(filepath, contents, { encoding: 'utf8' })
}
Expand All @@ -23,17 +23,27 @@ export const remove = async (filepath: string) => {
: fs.rmdir(filepath, { recursive: true })
}

export const scanEmptyDirs = async (dir: string): Promise<string[]> => {
const doesExist = await exists(dir)
if (!doesExist) return Promise.resolve([])
const items = await ls(dir)
if (!items.length) return Promise.resolve([dir])
const dirs = await keepFolders(items)
const nestedEmptyDirs = await Promise.all(dirs.map(f => scanEmptyDirs(f)))
return Promise.resolve(nestedEmptyDirs.reduce((a, b) => a.concat(b), []))
}

export const scan = async (dir: string, extension = '.elm'): Promise<string[]> => {
const doesExist = await exists(dir)
if (!doesExist) return []
if (!doesExist) return Promise.resolve([])
const items = await ls(dir)
const [folders, files] = await Promise.all([
keepFolders(items),
items.filter(f => f.endsWith(extension))
Promise.resolve(items.filter(f => f.endsWith(extension)))
])
const listOfFiles = await Promise.all(folders.map(f => scan(f, extension)))
const nestedFiles = listOfFiles.reduce((a, b) => a.concat(b), [])
return files.concat(nestedFiles)
return Promise.resolve(files.concat(nestedFiles))
}

const ls = (dir: string): Promise<string[]> =>
Expand All @@ -56,15 +66,16 @@ export const exists = (filepath: string) =>
.catch(_ => false)



/**
* Copy the file or folder at the given path.
* @param filepath - the path of the file or folder to copy
*/
export const copy = (src : string, dest : string) => {
export const copy = (src: string, dest: string) => {
const exists = oldFs.existsSync(src)
const stats = exists && oldFs.statSync(src)
if (stats && stats.isDirectory()) {
try { oldFs.mkdirSync(dest, { recursive: true }) } catch (_) {}
try { oldFs.mkdirSync(dest, { recursive: true }) } catch (_) { }
oldFs.readdirSync(src).forEach(child =>
copy(path.join(src, child), path.join(dest, child))
)
Expand All @@ -73,18 +84,18 @@ export const copy = (src : string, dest : string) => {
}
}

export const copyFile = async (src : string, dest : string) => {
export const copyFile = async (src: string, dest: string) => {
await ensureFolderExists(dest)
return fs.copyFile(src, dest)
}


const ensureFolderExists = async (filepath : string) => {
const ensureFolderExists = async (filepath: string) => {
const folder = filepath.split(path.sep).slice(0, -1).join(path.sep)
return fs.mkdir(folder, { recursive: true })
}

export const mkdir = (folder : string) : Promise<string> =>
export const mkdir = (folder: string): Promise<string> =>
fs.mkdir(folder, { recursive: true })

export const read = async (path: string) =>
Expand Down
7 changes: 2 additions & 5 deletions src/cli/src/templates/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,8 @@ const pageModelArguments = (path: string[], options : Options) : string => {
const exposes = (value : string) => (str : string) : boolean => {
const regex = new RegExp('^module\\s+[^\\s]+\\s+exposing\\s+\\(((?:\\.\\)|[^)])+)\\)')
const match = (str.match(regex) || [])[1]
if (match) {
return match.split(',').filter(a => a).map(a => a.trim()).includes(value)
} else {
return false
}
return match ? match.split(',').filter(a => a).map(a => a.trim()).includes(value)
: false
}

export const exposesModel = exposes('Model')
Expand Down