@uiengine/core
Version:
Create, document and evolve your design system collaboratively.
241 lines (204 loc) • 8.38 kB
JavaScript
const { dirname, join, relative } = require('path')
const Core = require('./core')
const {
FileUtil: { isDirectory, relativeToCwd },
DebugUtil: { debug3 },
StringUtil: { crossPlatformPath },
MarkdownUtil: { parseString: markdown },
MessageUtil: { reportSuccess, reportInfo, reportError }
} = require('@uiengine/util')
const globPattern = join('**', '*')
const sourceFilesFromConfig = ({ source: { configFile, components, data, pages, templates, additionalWatches }, debug }) => {
const watchesGlob = additionalWatches ? additionalWatches.map(watchPath => isDirectory(watchPath) ? join(watchPath, globPattern) : watchPath) : null
const componentGlobs = components ? components.map(dir => join(dir, '*', globPattern)) : null
const templatesGlob = templates ? join(templates, globPattern) : null
const pagesGlob = pages ? join(pages, globPattern) : null
const dataGlob = data ? join(data, globPattern) : null
const sourceFiles = [configFile, dataGlob, pagesGlob, templatesGlob].concat(componentGlobs).concat(watchesGlob).filter(a => a)
if (debug) {
const uiSrc = dirname(require.resolve('@uiengine/ui'))
const uiGlob = join(uiSrc.replace(join('ui', 'src'), join('ui', '{dist,lib}')), globPattern)
const pluginGlob = join(uiSrc.replace(join('ui', 'src'), join('plugin-*', '{src,ui}')), globPattern)
sourceFiles.push(uiGlob, pluginGlob)
}
return sourceFiles
}
// see https://github.com/paulmillr/chokidar#api
const watchOptions = {
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 200,
pollInterval: 50
}
}
// see https://www.browsersync.io/docs/options/
const browserSyncOptions = {
notify: false,
single: true,
watchOptions
}
const optionWithDefault = (def, value) =>
typeof value !== 'undefined' ? value : def
const requireOptional = (module, option) => {
try {
return require(module)
} catch (err) {
reportError('Missing dependency', new Error(`The optional dependency ${module} failed to install and is required for --${option}. It is likely not supported on your platform.`))
throw err
}
}
const startWatcher = (state, opts, server) => {
const { config } = state
const { watch, info } = opts
const chokidar = requireOptional('chokidar', 'watch')
const sourceFiles = sourceFilesFromConfig(config)
let additionalFiles = []
if (typeof watch === 'string') {
additionalFiles = [watch]
} else if (Array.isArray(watch)) {
additionalFiles = watch
}
const handleFileChange = async (filePath, type) => {
if (Core.isGenerating()) return
try {
if (info) reportInfo('Rebuilding …', { icon: '🚧', transient: true })
const { state, change } = await Core.generateIncrementForFileChange(filePath, type)
if (info) reportInfo(`Rebuilt ${change.type} ${change.item} (${change.action} ${change.file})`, { icon: '✨', transient: false })
if (server) server.sockets.emit('uiengine:state:update', state)
} catch (err) {
reportError(`Rebuild for changed file ${relativeToCwd(filePath)} failed:`, err)
}
}
const watchFiles = sourceFiles.concat(additionalFiles).map(crossPlatformPath)
const watcher = chokidar.watch(watchFiles, watchOptions)
.on('add', filePath => handleFileChange(filePath, 'added'))
.on('addDir', filePath => handleFileChange(filePath, 'added'))
.on('change', filePath => handleFileChange(filePath, 'changed'))
.on('unlink', filePath => handleFileChange(filePath, 'deleted'))
.on('unlinkDir', filePath => handleFileChange(filePath, 'deleted'))
debug3(state, 'Watching files:', JSON.stringify(watchFiles, null, 2))
return watcher
}
const startServer = (state, opts) => {
const { browserSync, target, ui } = state.config
const { watch, info } = opts
const server = requireOptional('browser-sync', 'serve').create('UIengine')
const history = requireOptional('connect-history-api-fallback', 'serve')
const pagesPattern = join('_pages', '**', '*')
const sketchPattern = join('_sketch', '**', '*')
const tokensPattern = join('_tokens', '**', '*')
const variantsPattern = join('_variants', '**', '*')
const webpackPattern = join('_webpack', '**', '*')
const defaults = {
server: {
baseDir: target
},
files: [
{
match: [globPattern],
options: {
cwd: target,
ignored: [
// exclude files with state as they change on every rebuild.
// changes are injected via websockets (see handleFileChange)
'index.html',
'_state.json',
// exclude pages, tokens and variants as the iframes are
// reloaded separately (see server.init callback)
pagesPattern,
sketchPattern,
tokensPattern,
variantsPattern,
webpackPattern
]
}
}
],
middleware: []
}
const options = optionWithDefault(defaults, browserSync)
options.server = optionWithDefault(defaults.server, options.server)
options.server.baseDir = optionWithDefault(defaults.server.baseDir, options.server.baseDir)
options.middleware = optionWithDefault(defaults.middleware, options.middleware)
options.logLevel = optionWithDefault((info ? 'info' : 'silent'), options.logLevel)
options.startPath = optionWithDefault(ui.base, options.startPath)
const basePath = (optionWithDefault('/', ui.base)).replace(/\/$/, '')
options.middleware.push({
route: basePath,
handle: history()
})
options.snippetOptions = {
rule: {
fn: function (snippet, match) {
// browser-sync: append this script to reload pages on change in development.
// we solve this via a manual socket connection to prevent reloading the
// whole iframe host in case just the iframe content changes.
// as the browser-sync client scripts loads asynchronously, we may need to
// retrigger this function a few times.
return match + `\n<!-- UIengine: inject start -->\n${snippet}<script>
let retries = 0;
function setupSocket () {
const socket = window.___browserSync___ && window.___browserSync___.socket;
if (socket) {
socket.on('uiengine:file:change', function(filePath) {
if (window.location.pathname.endsWith(filePath)) {
window.location.reload();
console.debug('[UIengine]', 'Reload on file change', filePath);
}
})
console.debug('[UIengine]', 'Connection to browser-sync socket established.');
} else if (retries <= 10) {
setTimeout(setupSocket, 100);
retries++;
} else {
console.warn('[UIengine]', 'Could not connect to browser-sync socket.');
}
}
setupSocket();
</script>\n<!-- UIengine: inject end -->`
}
}
}
if (watch) {
options.files = optionWithDefault(defaults.files, options.files)
options.watchOptions = optionWithDefault(browserSyncOptions.watchOptions, options.watchOptions)
options.notify = optionWithDefault(browserSyncOptions.notify, options.notify)
}
debug3(state, 'BrowserSync options:', JSON.stringify(options, null, 2))
server.init(options, (err, instance) => {
if (err) reportError('Initializing server failed:', err)
const _paths = filePath => join(target, filePath)
// trigger iframe reloads, see
// https://github.com/BrowserSync/browser-sync/issues/662#issuecomment-110478137
server.watch([
_paths(pagesPattern),
_paths(tokensPattern),
_paths(variantsPattern)
]).on('change', filePath => {
const file = relative(target, filePath)
instance.io.sockets.emit('uiengine:file:change', file)
})
})
return server
}
async function build (options = {}) {
options.info = optionWithDefault(true, options.info)
options.serve = optionWithDefault(false, options.serve)
options.watch = optionWithDefault(false, options.watch)
if (options.info) reportInfo('Building …', { icon: '🚧', transient: true })
try {
const state = await Core.generate(options)
if (options.info) reportSuccess('Build done!')
let server, watcher
if (options.serve) server = startServer(state, options)
if (options.watch) watcher = startWatcher(state, options, server)
return { state, server, watcher }
} catch (err) {
reportError('Build failed!', err)
throw err
}
}
module.exports = {
build,
markdown
}