@uiengine/core
Version:
Create, document and evolve your design system collaboratively.
386 lines (300 loc) • 12.4 kB
JavaScript
const { join, relative, resolve } = require('path')
const R = require('ramda')
const Interface = require('./interface')
const Connector = require('./connector')
const {
UiengineInputError,
PageUtil: { isTokensPage, pageIdToPath },
FileUtil: { copy, write },
MessageUtil: { markSample },
StringUtil: { dasherize, replaceTemplateComments },
DebugUtil: { debug2, debug3, debug4 }
} = require('@uiengine/util')
const copyPageFile = (targetPath, sourcePath, source) => {
const filePath = relative(sourcePath, source)
const target = resolve(targetPath, filePath)
return copy(source, target)
}
const withThemes = async (themes, task, includingAll = false) => {
const tasks = themes.map(({ id: themeId }) => task(themeId))
if (includingAll) tasks.push(task('_all'))
return Promise.all(tasks)
}
const pruneStateForView = state => {
// remove components[id].variants[].themes form state,
// because it contains the rendered html which is huge.
const components = state.components || {}
const viewComponents = R.map(component => {
const variants = component.variants || []
const viewVariants = R.map(variant => R.dissoc('themes', variant), variants)
return R.assoc('variants', viewVariants, component)
}, components)
return R.assoc('components', viewComponents, state)
}
async function render (state, template, data, themeId, identifier) {
debug4(state, `Builder.render(${template}, ${themeId}, ${identifier}):start`)
const { templates } = state.config.source
if (!templates) throw new UiengineInputError('Templates source directory must be defined!')
const templatePath = join(templates, template)
let rendered
try {
rendered = await Connector.render(state, templatePath, data, themeId, identifier)
} catch (err) {
const message = [`${identifier} could not be generated!`]
if (state.config.debug) message.push(markSample(JSON.stringify(data, null, 2)))
throw new UiengineInputError(message, err)
}
debug4(state, `Builder.render(${template}, ${themeId}, ${identifier}):end`)
return rendered
}
async function generatePage (state, pageId) {
return Promise.all([
generatePageWithTemplate(state, pageId),
generatePageWithTokens(state, pageId),
generatePageFiles(state, pageId)
])
}
async function generatePageFiles (state, pageId) {
debug4(state, `Builder.generatePageFiles(${pageId}):start`)
const { pages, config } = state
const page = pages[pageId]
if (!page.files) return
const targetPagePath = page.path
const sourcePagePath = pageIdToPath(page.id)
const targetPath = join(config.target, targetPagePath)
const sourcePath = join(config.source.pages, sourcePagePath)
const copyFile = R.partial(copyPageFile, [targetPath, sourcePath])
const copyFiles = R.map(copyFile, page.files)
await Promise.all(copyFiles)
debug4(state, `Builder.generatePageFiles(${pageId}):end`)
}
async function generatePageWithTemplate (state, pageId) {
debug2(state, `Builder.generatePageWithTemplate(${pageId}):start`)
const { pages, config } = state
const { name, target, themes, version } = config
const page = pages[pageId]
if (!page) {
throw new UiengineInputError(`Page "${pageId}" does not exist or has not been fetched yet.`)
}
if (page.template || page.fragment) {
const template = page.template || config.template
const { id, context, fragment } = page
await withThemes(themes, async themeId => {
let { rendered, foot } = await render(state, template, context, themeId, pageId)
const content = fragment
? (await render(state, fragment, context, themeId, pageId)).rendered
: rendered
rendered = replaceTemplateComments(rendered, {
class: `uie-page uie-page--${dasherize(id)}`,
title: `${page.title} • ${name} (${version})`,
theme: themeId,
content,
foot
})
// write file
const filePath = resolve(target, '_pages', themeId, `${id}.html`)
return write(filePath, rendered)
})
}
debug2(state, `Builder.generatePageWithTemplate(${pageId}):end`)
}
async function generatePagesWithTemplate (state, template) {
debug3(state, `Builder.generatePagesWithTemplate(${template}):start`)
const affectedPages = R.filter(page => [page.template, page.fragment].includes(template), state.pages)
const pageIds = Object.keys(affectedPages)
const build = R.partial(generatePageWithTemplate, [state])
const builds = R.map(build, pageIds)
await Promise.all(builds)
debug3(state, `Builder.generatePagesWithTemplate(${template}):end`)
}
async function generatePageWithTokens (state, pageId) {
debug2(state, `Builder.generatePageWithTokens(${pageId}):start`)
const { pages, config } = state
const { name, target, themes, version } = config
const page = pages[pageId]
if (!page) {
throw new UiengineInputError(`Page "${pageId}" does not exist or has not been fetched yet.`)
}
if (isTokensPage(page.type)) {
// render tokens with context, in preview layout
const { id, title } = page
const data = page
const template = page.template || config.template
await withThemes(themes, async themeId => {
let { rendered, foot } = await render(state, template, data, themeId, pageId)
const content = await Interface.render(state, 'tokens', page, themeId)
rendered = replaceTemplateComments(rendered, {
class: `uie-tokens uie-tokens--${dasherize(id)}`,
title: `${title} • ${name} (${version})`,
theme: themeId,
content,
foot
})
// write file
const filePath = resolve(target, '_tokens', themeId, `${id}.html`)
await write(filePath, rendered)
}, true)
}
debug2(state, `Builder.generatePageWithTokens(${pageId}):end`)
}
async function generateVariant (state, variant) {
const { id, componentId } = variant
debug2(state, `Builder.generateVariant(${id}):start`)
const { config, components } = state
const { target, themes, name, version } = config
const component = components[componentId]
// render variant preview, with layout
const data = { state }
const template = variant.template || component.template || config.template
await withThemes(themes, async themeId => {
let { rendered } = await render(state, template, data, themeId, id)
const { rendered: content, foot } = variant.themes[themeId]
rendered = replaceTemplateComments(rendered, {
class: `uie-variant uie-variant--${dasherize(componentId)} uie-variant--${dasherize(id)}`,
title: `${component.title}: ${variant.title} • ${name} (${version})`,
theme: themeId,
content,
foot
})
// write file
const filePath = resolve(target, '_variants', themeId, `${id}.html`)
return write(filePath, rendered)
})
debug2(state, `Builder.generateVariant(${id}):end`)
}
async function generateComponentVariants (state, componentId) {
debug3(state, `Builder.generateComponentVariants(${componentId}):start`)
const component = state.components[componentId]
const variants = component.variants || []
const build = R.partial(generateVariant, [state])
const builds = R.map(build, variants)
await Promise.all(builds)
debug3(state, `Builder.generateComponentVariants(${componentId}):end`)
}
async function generateVariantsWithTemplate (state, template) {
debug3(state, `Builder.generateVariantsWithTemplate(${template}):start`)
const components = Object.values(state.components)
const isPreviewTemplate = template === state.config.template
const affectedVariants = R.reduce((list, component) => {
return R.concat(list, R.filter(variant => {
// the variant must be regenerated in two cases:
// 1.) the template is the general preview template and
// the variant does not use a custom template
// 2.) the template matches the variant template
return (isPreviewTemplate && variant.template === undefined) ||
variant.template === template
}, component.variants))
}, [], components)
const build = R.partial(generateVariant, [state])
const builds = R.map(build, affectedVariants)
await Promise.all(builds)
debug3(state, `Builder.generateVariantsWithTemplate(${template}):end`)
}
async function generateTokensWithTemplate (state, template) {
debug3(state, `Builder.generateTokensWithTemplate(${template}):start`)
const isPreviewTemplate = template === state.config.template
const affectedPages = R.filter(page => {
// the page must be regenerated in two cases:
// 1.) the template is the general preview template and
// the variant does not use a custom template
// 2.) the template matches the page template
return isTokensPage(page.type) &&
(
(isPreviewTemplate && page.template === undefined) ||
page.template === template
)
}, state.pages)
const pageIds = Object.keys(affectedPages)
const build = R.partial(generatePageWithTokens, [state])
const builds = R.map(build, pageIds)
await Promise.all(builds)
debug3(state, `Builder.generateTokensWithTemplate(${template}):end`)
}
async function generateStateHTML (state) {
debug3(state, 'Builder.generateStateHTML():start')
const data = { state }
const rendered = await Interface.render(state, 'index', data)
const filePath = resolve(state.config.target, 'index.html')
await write(filePath, rendered)
debug3(state, 'Builder.generateStateHTML():end')
}
async function generateStateJSON (state) {
debug3(state, 'Builder.generateStateJSON():start')
const json = JSON.stringify(state, null, 2)
const filePath = resolve(state.config.target, '_state.json')
await write(filePath, json)
debug3(state, 'Builder.generateStateJSON():end')
}
async function generateState (state) {
debug2(state, 'Builder.generateState():start')
const viewState = pruneStateForView(state)
await Promise.all([
generateStateHTML(viewState),
generateStateJSON(viewState)
])
debug2(state, 'Builder.generateState():end')
}
// generateIncrement is a better name for the public function,
// whereas generateState describes it better internally
const generateIncrement = generateState
async function generateSketch (state) {
debug2(state, 'Builder.generateSketch():start')
const { config: { name, target, template, version, themes, source: { templates } } } = state
if (templates && template) {
// render variant preview, with layout
const data = { state }
await withThemes(themes, async themeId => {
const identifier = 'HTML Sketchapp Export'
let { rendered, foot } = await render(state, template, data, themeId, identifier)
const content = await Interface.render(state, 'sketch', data, themeId)
rendered = replaceTemplateComments(rendered, {
class: 'uie-html-sketchapp',
title: `HTML Sketchapp Export ${themeId} • ${name} (${version})`,
theme: themeId,
content,
foot
})
// write file
const filePath = resolve(target, '_sketch', `${themeId}.html`)
return write(filePath, rendered)
})
}
debug2(state, 'Builder.generateSketch():end')
}
async function generate (state) {
debug2(state, 'Builder.generate():start')
const pageIds = Object.keys(state.pages)
const variants = R.reduce((list, component) => {
return R.concat(list, component.variants)
}, [], Object.values(state.components))
const templateBuild = R.partial(generatePageWithTemplate, [state])
const templateBuilds = R.map(templateBuild, pageIds)
const tokenBuild = R.partial(generatePageWithTokens, [state])
const tokenBuilds = R.map(tokenBuild, pageIds)
const fileBuild = R.partial(generatePageFiles, [state])
const fileBuilds = R.map(fileBuild, pageIds)
const variantBuild = R.partial(generateVariant, [state])
const variantBuilds = R.map(variantBuild, variants)
await Promise.all([
...fileBuilds,
...tokenBuilds,
...variantBuilds,
...templateBuilds,
generateSketch(state),
generateState(state)
])
debug2(state, 'Builder.generate():end')
}
module.exports = {
generate,
generateIncrement,
generateComponentVariants,
generatePage,
generatePageFiles,
generatePageWithTokens,
generatePageWithTemplate,
generatePagesWithTemplate,
generateVariant,
generateVariantsWithTemplate,
generateTokensWithTemplate
}