handoff-app
Version:
Automated documentation toolchain for building client side documentation from figma
627 lines (626 loc) • 27.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.devApp = exports.watchApp = void 0;
const chokidar_1 = __importDefault(require("chokidar"));
const cross_spawn_1 = __importDefault(require("cross-spawn"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const ws_1 = require("ws");
const config_1 = require("./config");
const pipeline_1 = require("./pipeline");
const builder_1 = __importStar(require("./transformers/preview/component/builder"));
const logger_1 = require("./utils/logger");
/**
* Creates a WebSocket server that broadcasts messages to connected clients.
* Designed for development mode to help with hot-reloading.
*
* @param port - Optional port number for the WebSocket server; defaults to 3001.
* @returns A function that accepts a message string and broadcasts it to all connected clients.
*/
const createWebSocketServer = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (port = 3001) {
const wss = new ws_1.WebSocket.Server({ port });
// Heartbeat function to mark a connection as alive.
const heartbeat = function () {
this.isAlive = true;
};
// Setup a new connection
wss.on('connection', (ws) => {
const extWs = ws;
extWs.isAlive = true;
extWs.send(JSON.stringify({ type: 'WELCOME' }));
extWs.on('error', (error) => logger_1.Logger.error('WebSocket error:', error));
extWs.on('pong', heartbeat);
});
// Periodically ping clients to ensure they are still connected
const pingInterval = setInterval(() => {
wss.clients.forEach((client) => {
const extWs = client;
if (!extWs.isAlive) {
logger_1.Logger.warn('Terminating inactive client');
return client.terminate();
}
extWs.isAlive = false;
client.ping();
});
}, 30000);
// Clean up the interval when the server closes
wss.on('close', () => {
clearInterval(pingInterval);
});
logger_1.Logger.success(`WebSocket server listening on ws://localhost:${port}`);
// Return a function to broadcast a message to all connected clients
return (message) => {
logger_1.Logger.success(`Broadcasting message to ${wss.clients.size} client(s)`);
wss.clients.forEach((client) => {
if (client.readyState === ws_1.WebSocket.OPEN) {
client.send(message);
}
});
};
});
/**
* Gets the working public directory path for a given handoff instance
* Checks for both project-specific and default public directories
*
* @param handoff - The handoff instance containing working path and figma project configuration
* @returns The resolved path to the public directory if it exists, null otherwise
*/
const getWorkingPublicPath = (handoff) => {
const paths = [path_1.default.resolve(handoff.workingPath, `public-${handoff.getProjectId()}`), path_1.default.resolve(handoff.workingPath, `public`)];
for (const path of paths) {
if (fs_extra_1.default.existsSync(path)) {
return path;
}
}
return null;
};
/**
* Gets the application path for a given handoff instance
* @param handoff - The handoff instance containing module path and figma project configuration
* @returns The resolved path to the application directory
*/
const getAppPath = (handoff) => {
return path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
};
/**
* Copy the public dir from the working dir to the module dir
* @param handoff
*/
const syncPublicFiles = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
const appPath = getAppPath(handoff);
const workingPublicPath = getWorkingPublicPath(handoff);
if (workingPublicPath) {
yield fs_extra_1.default.copy(workingPublicPath, path_1.default.resolve(appPath, 'public'), {
overwrite: true,
});
}
});
/**
* Performs cleanup of the application directory by removing the existing app directory if it exists.
* This is typically used before rebuilding the application to ensure a clean state.
*
* @param handoff - The Handoff instance containing configuration and working paths
* @returns Promise that resolves when cleanup is complete
*/
const cleanupAppDirectory = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
const appPath = getAppPath(handoff);
// Clean project app dir
if (fs_extra_1.default.existsSync(appPath)) {
yield fs_extra_1.default.remove(appPath);
}
});
/**
* Publishes the tokens API files to the public directory.
*
* @param handoff - The Handoff instance
*/
const generateTokensApi = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
const apiPath = path_1.default.resolve(path_1.default.join(handoff.workingPath, 'public/api'));
yield fs_extra_1.default.ensureDir(apiPath);
const tokens = yield handoff.getDocumentationObject();
// Early return if no tokens
if (!tokens) {
// Write empty tokens.json for API consistency
yield fs_extra_1.default.writeJson(path_1.default.join(apiPath, 'tokens.json'), {}, { spaces: 2 });
return;
}
yield fs_extra_1.default.writeJson(path_1.default.join(apiPath, 'tokens.json'), tokens, { spaces: 2 });
const tokensDir = path_1.default.join(apiPath, 'tokens');
yield fs_extra_1.default.ensureDir(tokensDir);
// Only iterate if tokens has properties
if (tokens && typeof tokens === 'object') {
const promises = [];
for (const type in tokens) {
if (type === 'timestamp' || !tokens[type] || typeof tokens[type] !== 'object')
continue;
for (const group in tokens[type]) {
if (tokens[type][group]) {
promises.push(fs_extra_1.default.writeJson(path_1.default.join(tokensDir, `${group}.json`), tokens[type][group], { spaces: 2 }));
}
}
}
yield Promise.all(promises);
}
});
/**
* Prepares the project application by copying source files and configuring Next.js.
*
* @param handoff - The Handoff instance
* @returns The path to the prepared application directory
*/
const initializeProjectApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
const srcPath = path_1.default.resolve(handoff.modulePath, 'src', 'app');
const appPath = getAppPath(handoff);
// Publish tokens API
yield generateTokensApi(handoff);
// Prepare project app dir
yield fs_extra_1.default.ensureDir(appPath);
yield fs_extra_1.default.copy(srcPath, appPath, { overwrite: true });
yield syncPublicFiles(handoff);
// Copy custom theme CSS if it exists in the user's project
const customThemePath = path_1.default.resolve(handoff.workingPath, 'theme.css');
if (fs_extra_1.default.existsSync(customThemePath)) {
const destPath = path_1.default.resolve(appPath, 'css', 'theme.css');
yield fs_extra_1.default.copy(customThemePath, destPath, { overwrite: true });
logger_1.Logger.success(`Custom theme.css loaded`);
}
// Prepare project app configuration
// Warning: Regex replacement is fragile and depends on exact formatting in next.config.mjs
const handoffProjectId = handoff.getProjectId();
const handoffAppBasePath = (_a = handoff.config.app.base_path) !== null && _a !== void 0 ? _a : '';
const handoffWorkingPath = path_1.default.resolve(handoff.workingPath);
const handoffModulePath = path_1.default.resolve(handoff.modulePath);
const handoffExportPath = path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId());
const nextConfigPath = path_1.default.resolve(appPath, 'next.config.mjs');
const handoffUseReferences = (_b = handoff.config.useVariables) !== null && _b !== void 0 ? _b : false;
const handoffWebsocketPort = (_d = (_c = handoff.config.app.ports) === null || _c === void 0 ? void 0 : _c.websocket) !== null && _d !== void 0 ? _d : 3001;
const nextConfigContent = (yield fs_extra_1.default.readFile(nextConfigPath, 'utf-8'))
.replace(/basePath:\s+\'\'/g, `basePath: '${handoffAppBasePath}'`)
.replace(/HANDOFF_PROJECT_ID:\s+\'\'/g, `HANDOFF_PROJECT_ID: '${handoffProjectId}'`)
.replace(/HANDOFF_APP_BASE_PATH:\s+\'\'/g, `HANDOFF_APP_BASE_PATH: '${handoffAppBasePath}'`)
.replace(/HANDOFF_WORKING_PATH:\s+\'\'/g, `HANDOFF_WORKING_PATH: '${handoffWorkingPath}'`)
.replace(/HANDOFF_MODULE_PATH:\s+\'\'/g, `HANDOFF_MODULE_PATH: '${handoffModulePath}'`)
.replace(/HANDOFF_EXPORT_PATH:\s+\'\'/g, `HANDOFF_EXPORT_PATH: '${handoffExportPath}'`)
.replace(/HANDOFF_WEBSOCKET_PORT:\s+\'\'/g, `HANDOFF_WEBSOCKET_PORT: '${handoffWebsocketPort}'`)
.replace(/%HANDOFF_MODULE_PATH%/g, handoffModulePath);
yield fs_extra_1.default.writeFile(nextConfigPath, nextConfigContent);
return appPath;
});
/**
* Persists the client config to a JSON file.
*
* @param handoff - The Handoff instance
*/
const persistClientConfig = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
const appPath = getAppPath(handoff);
const destination = path_1.default.resolve(appPath, 'client.config.json');
// Ensure directory exists
yield fs_extra_1.default.ensureDir(appPath);
yield fs_extra_1.default.writeJson(destination, { config: (0, config_1.getClientConfig)(handoff) }, { spaces: 2 });
});
/**
* Watches the working public directory for changes and updates the app.
*
* @param handoff - The Handoff instance
* @param wss - The WebSocket broadcaster
* @param state - The shared watcher state
* @param chokidarConfig - Configuration for chokidar
*/
const watchPublicDirectory = (handoff, wss, state, chokidarConfig) => {
if (fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, 'public'))) {
chokidar_1.default.watch(path_1.default.resolve(handoff.workingPath, 'public'), chokidarConfig).on('all', (event, path) => __awaiter(void 0, void 0, void 0, function* () {
switch (event) {
case 'add':
case 'change':
case 'unlink':
if (!state.debounce) {
state.debounce = true;
try {
logger_1.Logger.warn('Public directory changed. Handoff will ingest the new data...');
yield syncPublicFiles(handoff);
wss(JSON.stringify({ type: 'reload' }));
}
catch (e) {
logger_1.Logger.error('Error syncing public directory:', e);
}
finally {
state.debounce = false;
}
}
break;
}
}));
}
};
/**
* Watches the application source code for changes.
*
* @param handoff - The Handoff instance
*/
const watchAppSource = (handoff) => {
chokidar_1.default
.watch(path_1.default.resolve(handoff.modulePath, 'src', 'app'), {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
ignoreInitial: true,
})
.on('all', (event, path) => __awaiter(void 0, void 0, void 0, function* () {
switch (event) {
case 'add':
case 'change':
case 'unlink':
try {
yield initializeProjectApp(handoff);
}
catch (e) {
logger_1.Logger.error('Error initializing project app:', e);
}
break;
}
}));
};
/**
* Watches the user's pages directory for changes.
*
* @param handoff - The Handoff instance
* @param chokidarConfig - Configuration for chokidar
*/
const watchPages = (handoff, chokidarConfig) => {
if (fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, 'pages'))) {
chokidar_1.default.watch(path_1.default.resolve(handoff.workingPath, 'pages'), chokidarConfig).on('all', (event, path) => __awaiter(void 0, void 0, void 0, function* () {
switch (event) {
case 'add':
case 'change':
case 'unlink':
try {
logger_1.Logger.warn(`Doc page ${event}ed. Please reload browser to see changes...`);
logger_1.Logger.debug(`Path: ${path}`);
}
catch (e) {
logger_1.Logger.error('Error watching pages:', e);
}
break;
}
}));
}
};
/**
* Watches the SCSS entry point for changes.
*
* @param handoff - The Handoff instance
* @param state - The shared watcher state
* @param chokidarConfig - Configuration for chokidar
*/
const watchScss = (handoff, state, chokidarConfig) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
if (((_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.scss) && fs_extra_1.default.existsSync((_d = (_c = handoff.runtimeConfig) === null || _c === void 0 ? void 0 : _c.entries) === null || _d === void 0 ? void 0 : _d.scss)) {
const stat = yield fs_extra_1.default.stat(handoff.runtimeConfig.entries.scss);
chokidar_1.default
.watch(stat.isDirectory() ? handoff.runtimeConfig.entries.scss : path_1.default.dirname(handoff.runtimeConfig.entries.scss), chokidarConfig)
.on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
switch (event) {
case 'add':
case 'change':
case 'unlink':
if (!state.debounce) {
state.debounce = true;
try {
yield handoff.getSharedStyles();
}
catch (e) {
logger_1.Logger.error('Error processing shared styles:', e);
}
finally {
state.debounce = false;
}
}
}
}));
}
});
/**
* Maps configuration entry types to component segments.
*/
const mapEntryTypeToSegment = (type) => {
return {
js: builder_1.ComponentSegment.JavaScript,
scss: builder_1.ComponentSegment.Style,
template: builder_1.ComponentSegment.Previews,
templates: builder_1.ComponentSegment.Previews,
}[type];
};
/**
* Gets the paths of runtime components to watch.
*
* @param handoff - The Handoff instance
* @returns A Map of paths to watch and their entry types
*/
const getRuntimeComponentsPathsToWatch = (handoff) => {
var _a, _b, _c;
const result = new Map();
for (const runtimeComponentId of Object.keys((_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries.components) !== null && _b !== void 0 ? _b : {})) {
const runtimeComponent = handoff.runtimeConfig.entries.components[runtimeComponentId];
for (const [runtimeComponentEntryType, runtimeComponentEntryPath] of Object.entries((_c = runtimeComponent.entries) !== null && _c !== void 0 ? _c : {})) {
const normalizedComponentEntryPath = runtimeComponentEntryPath;
if (fs_extra_1.default.existsSync(normalizedComponentEntryPath)) {
const entryType = runtimeComponentEntryType;
if (fs_extra_1.default.statSync(normalizedComponentEntryPath).isFile()) {
result.set(path_1.default.resolve(normalizedComponentEntryPath), entryType);
}
else {
result.set(normalizedComponentEntryPath, entryType);
}
}
}
}
return result;
};
/**
* Watches runtime components for changes.
*
* @param handoff - The Handoff instance
* @param state - The shared watcher state
* @param runtimeComponentPathsToWatch - Map of paths to watch
*/
const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) => {
if (state.runtimeComponentsWatcher) {
state.runtimeComponentsWatcher.close();
}
if (runtimeComponentPathsToWatch.size > 0) {
const pathsToWatch = Array.from(runtimeComponentPathsToWatch.keys());
state.runtimeComponentsWatcher = chokidar_1.default.watch(pathsToWatch, {
ignoreInitial: true,
});
state.runtimeComponentsWatcher.on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
if (handoff.getConfigFilePaths().includes(file)) {
return;
}
switch (event) {
case 'add':
case 'change':
case 'unlink':
if (!state.debounce) {
state.debounce = true;
try {
const entryType = runtimeComponentPathsToWatch.get(file);
const segmentToUpdate = entryType ? mapEntryTypeToSegment(entryType) : undefined;
const componentDir = path_1.default.basename(path_1.default.dirname(file));
yield (0, builder_1.default)(handoff, componentDir, segmentToUpdate);
}
catch (e) {
logger_1.Logger.error('Error processing component:', e);
}
finally {
state.debounce = false;
}
}
break;
}
}));
}
};
/**
* Watches the runtime configuration for changes.
*
* @param handoff - The Handoff instance
* @param state - The shared watcher state
*/
const watchRuntimeConfiguration = (handoff, state) => {
if (state.runtimeConfigurationWatcher) {
state.runtimeConfigurationWatcher.close();
}
if (handoff.getConfigFilePaths().length > 0) {
state.runtimeConfigurationWatcher = chokidar_1.default.watch(handoff.getConfigFilePaths(), { ignoreInitial: true });
state.runtimeConfigurationWatcher.on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
switch (event) {
case 'add':
case 'change':
case 'unlink':
if (!state.debounce) {
state.debounce = true;
try {
file = path_1.default.dirname(file);
// Reload the Handoff instance to pick up configuration changes
handoff.reload();
// After reloading, persist the updated client configuration
yield persistClientConfig(handoff);
// Restart the runtime components watcher to track potentially updated/added/removed components
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
// Process components based on the updated configuration and file path
yield (0, builder_1.default)(handoff, path_1.default.basename(file));
}
catch (e) {
logger_1.Logger.error('Error reloading runtime configuration:', e);
}
finally {
state.debounce = false;
}
}
break;
}
}));
}
};
/**
* Build the next js application
* @param handoff
* @returns
*/
const buildApp = (handoff, skipComponents) => __awaiter(void 0, void 0, void 0, function* () {
skipComponents = skipComponents !== null && skipComponents !== void 0 ? skipComponents : false;
// Perform cleanup
yield cleanupAppDirectory(handoff);
// Build components
if (!skipComponents) {
yield (0, pipeline_1.buildComponents)(handoff);
}
// Prepare app
const appPath = yield initializeProjectApp(handoff);
yield persistClientConfig(handoff);
// Build app
const buildResult = cross_spawn_1.default.sync('npx', ['next', 'build'], {
cwd: appPath,
stdio: 'inherit',
env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'production' }),
});
if (buildResult.status !== 0) {
let errorMsg = `Next.js build failed with exit code ${buildResult.status}`;
if (buildResult.error) {
errorMsg += `\nSpawn error: ${buildResult.error.message}`;
}
throw new Error(errorMsg);
}
// Ensure output root directory exists
const outputRoot = path_1.default.resolve(handoff.workingPath, handoff.sitesDirectory);
yield fs_extra_1.default.ensureDir(outputRoot);
// Clean the project output directory (if exists)
const output = path_1.default.resolve(outputRoot, handoff.getProjectId());
if (fs_extra_1.default.existsSync(output)) {
yield fs_extra_1.default.remove(output);
}
// Copy the build files into the project output directory
yield fs_extra_1.default.copy(path_1.default.resolve(appPath, 'out'), output);
});
/**
* Watch the next js application.
* Starts a custom dev server with Handoff-specific watchers and hot-reloading.
*
* @param handoff
*/
const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
// Initial processing of the components with caching enabled
// This will skip rebuilding components whose source files haven't changed
yield (0, builder_1.default)(handoff, undefined, undefined, { useCache: true });
const appPath = yield initializeProjectApp(handoff);
// Persist client configuration
yield persistClientConfig(handoff);
// Watch app source
watchAppSource(handoff);
// // does a ts config exist?
// let tsconfigPath = 'tsconfig.json';
// config.typescript = {
// ...config.typescript,
// tsconfigPath,
// };
const dev = true;
const hostname = 'localhost';
const port = (_b = (_a = handoff.config.app.ports) === null || _a === void 0 ? void 0 : _a.app) !== null && _b !== void 0 ? _b : 3000;
// purge out cache
const moduleOutput = path_1.default.resolve(appPath, 'out');
if (fs_extra_1.default.existsSync(moduleOutput)) {
yield fs_extra_1.default.remove(moduleOutput);
// create empty directory
yield fs_extra_1.default.ensureDir(moduleOutput);
}
const nextProcess = (0, cross_spawn_1.default)('npx', ['next', 'dev', '--port', String(port)], {
cwd: appPath,
stdio: 'inherit',
env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'development' }),
});
logger_1.Logger.success(`Ready on http://${hostname}:${port}`);
nextProcess.on('error', (error) => {
logger_1.Logger.error(`Next.js dev process error: ${error}`);
process.exit(1);
});
nextProcess.on('close', (code) => {
logger_1.Logger.success(`Next.js dev process closed with code ${code}`);
process.exit(code);
});
const wss = yield createWebSocketServer((_d = (_c = handoff.config.app.ports) === null || _c === void 0 ? void 0 : _c.websocket) !== null && _d !== void 0 ? _d : 3001);
const chokidarConfig = {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
ignoreInitial: true,
};
const state = {
debounce: false,
runtimeComponentsWatcher: null,
runtimeConfigurationWatcher: null,
};
watchPublicDirectory(handoff, wss, state, chokidarConfig);
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
watchRuntimeConfiguration(handoff, state);
yield watchScss(handoff, state, chokidarConfig);
watchPages(handoff, chokidarConfig);
});
exports.watchApp = watchApp;
/**
* Watch the next js application using the standard Next.js dev server.
* This is useful for debugging the Next.js app itself without the Handoff overlay.
*
* @param handoff
*/
const devApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
// Prepare app
const appPath = yield initializeProjectApp(handoff);
// Purge app cache
const moduleOutput = path_1.default.resolve(appPath, 'out');
if (fs_extra_1.default.existsSync(moduleOutput)) {
yield fs_extra_1.default.remove(moduleOutput);
}
// Persist client configuration
yield persistClientConfig(handoff);
// Run
const devResult = cross_spawn_1.default.sync('npx', ['next', 'dev', '--port', String((_b = (_a = handoff.config.app.ports) === null || _a === void 0 ? void 0 : _a.app) !== null && _b !== void 0 ? _b : 3000)], {
cwd: appPath,
stdio: 'inherit',
env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'development' }),
});
if (devResult.status !== 0) {
let errorMsg = `Next.js dev failed with exit code ${devResult.status}`;
if (devResult.error) {
errorMsg += `\nSpawn error: ${devResult.error.message}`;
}
throw new Error(errorMsg);
}
});
exports.devApp = devApp;
exports.default = buildApp;