UNPKG

@swell/cli

Version:

Swell's command line interface/utility

272 lines (271 loc) 9.69 kB
import { createHash as createBlake3Hash } from 'blake3-wasm'; import { pluralize, titleize } from 'inflection'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { toAppId } from '../create/index.js'; import { AppConfig } from './app-config.js'; export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js'; export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js'; export const PUSH_CONCURRENCY = 3; export var SwellJsonFields; (function (SwellJsonFields) { SwellJsonFields["DESCRIPTION"] = "description"; SwellJsonFields["EXTENSIONS"] = "extensions"; SwellJsonFields["ID"] = "id"; SwellJsonFields["KIND"] = "kind"; SwellJsonFields["NAME"] = "name"; SwellJsonFields["PERMISSIONS"] = "permissions"; SwellJsonFields["STOREFRONT"] = "storefront"; SwellJsonFields["THEME"] = "theme"; SwellJsonFields["TYPE"] = "type"; SwellJsonFields["VERSION"] = "version"; })(SwellJsonFields || (SwellJsonFields = {})); export var ConfigType; (function (ConfigType) { ConfigType["ASSET"] = "asset"; ConfigType["CONTENT"] = "content"; ConfigType["FILE"] = "file"; ConfigType["FRONTEND"] = "frontend"; ConfigType["FUNCTION"] = "function"; ConfigType["MODEL"] = "model"; ConfigType["NOTIFICATION"] = "notification"; ConfigType["SETTING"] = "setting"; ConfigType["THEME"] = "theme"; ConfigType["WEBHOOK"] = "webhook"; })(ConfigType || (ConfigType = {})); // Models and content should come first in order const ConfigTypeBatchOrder = [ ConfigType.MODEL, ConfigType.CONTENT, ConfigType.ASSET, ConfigType.FUNCTION, ConfigType.NOTIFICATION, ConfigType.SETTING, ConfigType.WEBHOOK, ConfigType.THEME, ConfigType.FRONTEND, ConfigType.FILE, ]; // Configs available for different app types const StandardConfigTypes = [ 'MODEL', 'CONTENT', 'ASSET', 'FUNCTION', 'FRONTEND', 'NOTIFICATION', 'SETTING', 'WEBHOOK', 'THEME', ]; export const StorefrontConfigTypes = [ 'FRONTEND', 'ASSET', 'SETTING', ]; export const ThemeConfigTypes = ['THEME', 'ASSET']; const getConfigPaths = (types) => // eslint-disable-next-line unicorn/no-array-reduce Object.keys(ConfigType).reduce((acc, type) => { if (types.includes(type)) { // a bit of a hack to make the path singular for content configs // need to refactor this to a function if we add more exceptions acc[type] = type === 'CONTENT' || type === 'FRONTEND' || type === 'THEME' ? type.toLowerCase() : pluralize(type.toLowerCase()); } return acc; }, {}); export const ConfigPaths = { ...getConfigPaths(StandardConfigTypes), }; export const StorefrontConfigPaths = { ...getConfigPaths(StorefrontConfigTypes), }; export const ThemeConfigPaths = { ...getConfigPaths(ThemeConfigTypes), }; export var ConfigInputFields; (function (ConfigInputFields) { ConfigInputFields["FILE"] = "file"; ConfigInputFields["NAME"] = "name"; ConfigInputFields["TYPE"] = "type"; ConfigInputFields["VALUES"] = "values"; ConfigInputFields["VERSION"] = "version"; })(ConfigInputFields || (ConfigInputFields = {})); export const FrontendProjectTypes = [ { buildCommand: 'npx @cloudflare/next-on-pages', configFileName: 'next.config', deployPath: '.vercel/output/static', devCommand: 'npx next dev --port ${PORT}', installCommand: 'npx create-next-app@latest frontend --typescript --use-npm --src-dir --app --eslint --import-alias "@/*" --tailwind', name: 'Next.js', slug: 'nextjs', }, { buildCommand: 'npx astro build', configFileName: 'astro.config', deployPath: 'dist', devCommand: 'npx astro dev --port ${PORT}', installCommand: 'npm create astro@latest frontend -- --install --no-git --yes --skip-houston --typescript strict', name: 'Astro', slug: 'astro', }, ]; const frontendProjectConfigExtensions = ['.mjs', '.js', '.ts', '.cjs']; export function getAppSlugId(app) { return toAppId(app.private_id) || app.public_id || app.id; } export function getFrontendProjectType(appPath) { for (const projectType of FrontendProjectTypes) { for (const extension of frontendProjectConfigExtensions) { const configFile = path.join(appPath, 'frontend', `${projectType.configFileName}${extension}`); if (filePathExists(configFile)) { return projectType; } } } } export function getConfigTypeFromPath(path) { for (const type in ConfigPaths) { if (ConfigPaths[type] === path) { const configType = ConfigType[type]; return configType; } } return undefined; } export function getConfigTypeKeyFromValue(value) { return Object.keys(ConfigType).find((key) => ConfigType[key] === value); } export function filePathExists(filePath) { try { // eslint-disable-next-line no-bitwise fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK); return true; } catch (error) { if (error.code !== 'ENOENT') { throw error; } return false; } } export async function filePathExistsAsync(filePath) { try { // eslint-disable-next-line no-bitwise await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.W_OK); return true; } catch (error) { if (error.code !== 'ENOENT') { throw error; } return false; } } export async function writeFile(file, data) { // Convert from base64 if needed let contents; try { const decoded = Buffer.from(data, 'base64'); const isBase64 = decoded.toString('base64') === data; contents = isBase64 ? decoded : data; } catch { contents = data; } await fs.promises.mkdir(path.dirname(file), { recursive: true }); await fs.promises.writeFile(file, contents); } export function deleteFile(file) { return fs.promises.unlink(file); } export async function writeJsonFile(file, data = {}) { return writeFile(file, `${JSON.stringify(data, null, 2)}\n`); } // Hash method adapted from wrangler-sdk export function hashFile(filePath, fileContents, relativePath) { const contents = fileContents ?? fs.readFileSync(filePath); // Include relative file path because we typically cache by hash including file metadata // This allows de-duplication of cache entries for the same file contents return hashString(contents, relativePath || ''); } export function hashString(...contents) { const hash = createBlake3Hash(); for (const content of contents) { hash.update(content); } return hash.digest('hex', { length: 16 }); } export function appAssetImage(appPath, fileName) { const assetsDirPath = path.join(appPath, ConfigPaths['ASSET']); if (!filePathExists(assetsDirPath)) { return; } for (const configFile of fs.readdirSync(assetsDirPath)) { if (path.parse(configFile).name === fileName) { return path.join(assetsDirPath, configFile); } } } export function appConfigFromFile(filePath, configType, appPath) { const { name } = path.parse(filePath); const config = AppConfig.create({ name, filePath, appPath, type: configType, file_path: filePath, }); // Do not allow files larger than 10MB if (config.mbSize && config.mbSize > 10) { throw new Error(`App file size too large: ${config.filePath} (${config.mbSize} MB)`); } return config; } export function findAppConfig(app, filePath, configType) { const { name } = path.parse(filePath); const config = app?.configs?.find((c) => c && c.type === configType && c.name === name); return config; } /** * Get files to push in batches prioritized by dependencies * * @param {AppConfig[]} configs - List of app configs * @returns {BatchAppFiles[]} Batches of files to push */ export function batchAppFilesByType(configs) { const modelsWithExtends = configs.filter((config) => config.type === 'model' && config.values.extends); const modelsWithoutExtends = configs.filter((config) => config.type === 'model' && !config.values.extends); const contentTypesWithCollection = configs.filter((config) => config.type === 'content' && config.values.collection); const contentTypesWithoutCollection = configs.filter((config) => config.type === 'content' && !config.values.collection); const prioritized = new Set([ ...modelsWithExtends, ...modelsWithoutExtends, ...contentTypesWithCollection, ...contentTypesWithoutCollection, ]); const sorted = [ ...prioritized.values(), ...configs.filter((config) => !prioritized.has(config)), ]; const batches = []; for (const configType of ConfigTypeBatchOrder) { const configsByType = sorted.filter((config) => config.type === configType); const label = configType === ConfigType.FILE ? 'App files' : titleize(configType); const concurrency = configType === ConfigType.NOTIFICATION || configType === ConfigType.ASSET ? 1 : PUSH_CONCURRENCY; batches.push({ configs: configsByType, type: configType, label, concurrency, }); } // Add base files if config types are not specified return batches; }