diff --git a/bin/bake.js b/bin/bake.js index be68d703..e85342ab 100755 --- a/bin/bake.js +++ b/bin/bake.js @@ -7,8 +7,6 @@ import { resolve } from 'path'; // packages import debug from 'debug'; import mri from 'mri'; -import { rollup } from 'rollup'; -import requireFromString from 'require-from-string'; // local import { Baker } from '../lib/index.js'; @@ -17,7 +15,6 @@ import { logErrorMessage } from '../lib/utils.js'; const logger = debug('baker:cli'); const OUTPUT_DIR = '_dist'; -const SCREENSHOT_DIR = '_screenshot'; const defaultConfigFile = 'baker.config.js'; @@ -28,6 +25,7 @@ const defaultConfig = { domain: undefined, embeds: 'embeds', entrypoints: 'scripts/app.js', + imageSrcSizes: undefined, input: process.cwd(), layouts: '_layouts', nunjucksVariables: undefined, @@ -41,68 +39,39 @@ const defaultConfig = { crosswalkPath: undefined, }; -function getDefaultFromConfig(module) { +function getDefaultFromModule(module) { return module.__esModule ? module.default : module; } -async function compileAndLoadConfig(pathToConfig) { - const bundle = await rollup({ - external: () => true, - input: pathToConfig, - treeshake: false, - }); - - const { - output: [{ code }], - } = await bundle.generate({ - exports: 'named', - format: 'cjs', - interop: 'auto', - }); - const loadedConfig = requireFromString(code, pathToConfig); - - return getDefaultFromConfig(loadedConfig); -} +async function betterPrepareConfig(flags) { + console.log('betterPrepareConfig:flags'); + console.log(JSON.stringify(flags, null, 3)); -async function prepareConfig(inputOptions) { - // the input directory everything is relative to - const input = inputOptions.input; + const { input, config } = flags; + console.log('betterPrepareConfig:input'); + console.log(input); + console.log('betterPrepareConfig:config'); + console.log(config); - // a config parameter was passed - if (inputOptions.config) { - // we check to see if it was passed as a boolean and use our default path to the config, otherwise we use what was given - const pathToConfig = resolve( - input, - inputOptions.config === true ? defaultConfigFile : inputOptions.config - ); + const projectConfigFilePath = resolve(input, !!config ? config : defaultConfigFile); + const projectConfigFile = await import(projectConfigFilePath); - inputOptions = await compileAndLoadConfig(pathToConfig); - } + console.log('betterPrepareConfig:projectConfigFile'); + console.log(projectConfigFile); + + const projectConfig = getDefaultFromModule(projectConfigFile); + console.log('betterPrepareConfig:projectConfig'); + console.log(projectConfig.default); - // prep a helper function to resolve paths against input - const resolver = (key) => inputOptions[key] || defaultConfig[key]; - - const options = {}; - - options.assets = resolver('assets'); - options.createPages = resolver('createPages'); - options.data = resolver('data'); - options.domain = resolver('domain'); - options.embeds = resolver('embeds'); - options.entrypoints = resolver('entrypoints'); - options.input = resolver('input'); - options.layouts = resolver('layouts'); - options.nunjucksVariables = resolver('nunjucksVariables'); - options.nunjucksFilters = resolver('nunjucksFilters'); - options.nunjucksTags = resolver('nunjucksTags'); - options.minifyOptions = resolver('minifyOptions'); - options.output = resolver('output'); - options.pathPrefix = resolver('pathPrefix'); - options.staticRoot = resolver('staticRoot'); - options.svelteCompilerOptions = resolver('svelteCompilerOptions'); - options.crosswalkPath = resolver('crosswalkPath'); - - return options; + const mergedConfig = { + ...defaultConfig, + ...projectConfig.default, + }; + + console.log('betterPrepareConfig:mergedConfig'); + console.log(JSON.stringify(mergedConfig, null, 3)); + + return mergedConfig; } const mriConfig = { @@ -131,7 +100,7 @@ async function run(args) { const { _, ...flags } = mri(args, mriConfig); const command = _[0]; - const config = await prepareConfig(flags); + const config = await betterPrepareConfig(flags); logger('command:', command); logger('resolved input flags:', config); @@ -154,12 +123,8 @@ async function run(args) { } break; case 'screenshot': - // Change a few config options for taking screenshots - const screenshotConfig = { ...config, output: SCREENSHOT_DIR }; - const screenshotBaker = new Baker(screenshotConfig); - try { - await screenshotBaker.screenshot(); + await baker.screenshots(); console.log(green(bold('The screenshot was a success!'))); } catch (err) { diff --git a/lib/engines/base.js b/lib/engines/base.js index 2003ee8b..21fbf687 100644 --- a/lib/engines/base.js +++ b/lib/engines/base.js @@ -6,7 +6,6 @@ import chokidar from 'chokidar'; import glob from 'fast-glob'; // local -import { getBasePath } from '../env.js'; import { noop, outputFile } from '../utils.js'; /** @@ -14,7 +13,6 @@ import { noop, outputFile } from '../utils.js'; * handle the majority of tasks. */ export class BaseEngine { - subManifests = []; /** * @param {object} options @@ -84,10 +82,6 @@ export class BaseEngine { return this.dependencies.add(file); } - getDependencies() { - return Array.from(this.dependencies); - } - invalidate() { this.manifest = {}; this.dependencies.clear(); diff --git a/lib/engines/rollup.js b/lib/engines/rollup.js index 7eb9a91f..1c7adfdf 100644 --- a/lib/engines/rollup.js +++ b/lib/engines/rollup.js @@ -23,11 +23,11 @@ import { polyfillsDynamicImport } from '../paths.js'; import * as svelteConfig from '../../svelte.config.js'; import { createRequire } from 'module'; +import { yellow } from 'colorette'; const require = createRequire(import.meta.url); export class RollupEngine extends BaseEngine { - subManifests = ['modern', 'css', 'legacy']; constructor({ entrypoints, svelteCompilerOptions = {}, ...args }) { super(args); @@ -99,7 +99,7 @@ export class RollupEngine extends BaseEngine { 'mini-sync/client' )}";\nimport "${polyfillsDynamicImport}";\n`; - const config = { + return { input, plugins: [ !nomodule && prependEntry({ content }), @@ -179,10 +179,8 @@ export class RollupEngine extends BaseEngine { ].filter(Boolean), inlineDynamicImports, preserveEntrySignatures: false, - onwarn, + onwarn: RollupEngine.onWarn, }; - - return config; } generateOutputOptions({ dir, nomodule = false }) { @@ -287,13 +285,18 @@ export class RollupEngine extends BaseEngine { const entrypoints = await this.findFiles(); // if there are no entrypoints no need to continue - if (!entrypoints.length) return; + if (!entrypoints.length) { + console.log( + yellow(`No files found for specified entrypoints: ${entrypoints}\n`) + ); + return; + } // use our list of entrypoints to create the Rollup input const modernInput = this.generateModernInput(entrypoints); // get our current environment for passing into the input bundle - const { stringified: replaceValues } = getEnvironment(this.pathPrefix); + const { replaceValues } = getEnvironment(this.pathPrefix); // create the modern bundle const { @@ -341,7 +344,7 @@ export class RollupEngine extends BaseEngine { const input = this.generateModernInput(entrypoints); // get our current environment for passing into the input bundle - const { stringified: replaceValues } = getEnvironment(this.pathPrefix); + const { replaceValues } = getEnvironment(this.pathPrefix); // generate the Rollup inputOptions const inputOptions = this.generateInputOptions(input, replaceValues); @@ -395,15 +398,15 @@ export class RollupEngine extends BaseEngine { } }); } -} -export function onwarn(warning, onwarn) { - if ( - warning.code === 'CIRCULAR_DEPENDENCY' && - /[/\\]d3-\w+[/\\]/.test(warning.message) - ) { - return; - } + static onWarn(warning, onwarn) { + if ( + warning.code === 'CIRCULAR_DEPENDENCY' && + /[/\\]d3-\w+[/\\]/.test(warning.message) + ) { + return; + } - onwarn(warning); + onwarn(warning); + } } diff --git a/lib/env.js b/lib/env.js index a6db6d78..704e17b1 100644 --- a/lib/env.js +++ b/lib/env.js @@ -12,11 +12,7 @@ export const isProductionEnv = nodeEnv === 'production'; export const inDebugMode = Boolean(DEBUG); -/** - * Regex for grabbing any environmental variables that may start with "BAKER_". - * @type {RegExp} - */ -const BAKER_REGEX = /^BAKER_/i; +const BAKER_PREFIX = 'BAKER_'; /** * @param {string} pathPrefix @@ -26,10 +22,10 @@ export function getEnvironment(pathPrefix) { const keys = Object.keys(process.env); // find any of them that match our regex for BAKER_ exclusive ones - const bakerKeys = keys.filter((key) => BAKER_REGEX.test(key)); + const bakerKeys = keys.filter((key) => key.startsWith(BAKER_PREFIX)); // build the object of environment variables - const raw = bakerKeys.reduce( + const vars = bakerKeys.reduce( (env, key) => { env[key] = process.env[key]; return env; @@ -43,12 +39,12 @@ export function getEnvironment(pathPrefix) { ); // Stringify all values so we can pass it directly to rollup-plugin-replace - const stringified = Object.keys(raw).reduce((env, key) => { - env[`process.env.${key}`] = JSON.stringify(raw[key]); + const replaceValues = Object.keys(vars).reduce((env, key) => { + env[`process.env.${key}`] = JSON.stringify(vars[key]); return env; }, {}); - return { raw, stringified }; + return { vars, replaceValues }; } export function getBasePath(domain, pathPrefix) { diff --git a/lib/filters/srcSets.js b/lib/filters/srcSets.js new file mode 100644 index 00000000..99bf8882 --- /dev/null +++ b/lib/filters/srcSets.js @@ -0,0 +1,23 @@ +import { Baker } from '../index.js'; + +function maxstache(str, ctx) { + return str + .split(/\{|\}/) + .map((t, i) => (!(i % 2) ? t : ctx[t])) + .join(''); +} + +/** + * @param {number[]} sizes + * @param {Baker} instance + **/ +export function createSrcSetFilter(sizes, instance) { + return function createSrcSet(source) { + return sizes + .map((size) => { + const url = instance.getStaticPath(`${maxstache(source, { size })}`); + return `${url} ${size}w`; + }) + .join(', '); + }; +} diff --git a/lib/index.js b/lib/index.js index 8c2cd788..4a436ab2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,7 +5,7 @@ import path, { join, resolve } from 'path'; // packages import chokidar from 'chokidar'; -import { green, yellow } from 'colorette'; +import { red, yellow } from 'colorette'; import debounce from 'lodash.debounce'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; @@ -39,6 +39,7 @@ import { AssetsEngine } from './engines/assets.js'; import { NunjucksEngine } from './engines/nunjucks.js'; import { RollupEngine } from './engines/rollup.js'; import { SassEngine } from './engines/sass.js'; +import { createSrcSetFilter } from './filters/srcSets.js'; const CROSSWALK_ALLOWED_ASSET_TYPES = [ 'img', @@ -52,7 +53,7 @@ const CROSSWALK_ALLOWED_EXTS = { vid: validVideoExtensions.map((ext) => ext.replace('.', '')), }; -const FIXED_FALLBACK_SCREENSHOT_WIDTH = 375; +const SCREENSHOT_FIXED_FALLBACK_WIDTH = 375; /** * @typedef {Object} BakerOptions @@ -61,6 +62,7 @@ const FIXED_FALLBACK_SCREENSHOT_WIDTH = 375; * @property {string} data * @property {string} [domain] * @property {string} entrypoints + * @Property {{[key: string]: number[]}} imageSrcSizes * @property {string} input * @property {string} layouts * @property {{[key: string]: (...args) => unknown}} [nunjucksVariables] @@ -84,6 +86,7 @@ export class Baker extends EventEmitter { domain, embeds, entrypoints, + imageSrcSizes, input, layouts, nunjucksVariables, @@ -209,6 +212,17 @@ export class Baker extends EventEmitter { this.nunjucks.addCustomFilter('log', logFilter); this.nunjucks.addCustomFilter('jsonScript', jsonScriptFilter); + // Add image size src set filters + if (imageSrcSizes) { + Object.keys(imageSrcSizes).forEach((size) => { + const srcSetFilterName = size.charAt(0).toUpperCase() + size.slice(1); + this.nunjucks.addCustomFilter( + srcSetFilterName, + createSrcSetFilter(size, this) + ); + }); + } + // if an object of custom nunjucks filters was provided, add them now if (nunjucksFilters) { this.nunjucks.addCustomFilters(nunjucksFilters); @@ -341,8 +355,10 @@ export class Baker extends EventEmitter { ); } - if (!preppedData.assets[normalizedAssetType]) + if (!preppedData.assets[normalizedAssetType]) { preppedData.assets[normalizedAssetType] = {}; + } + preppedData.assets[normalizedAssetType][normalizedAssetName] = {}; Object.entries(assetSources).forEach(([ext, path]) => { @@ -362,21 +378,53 @@ export class Baker extends EventEmitter { }, preppedData); } + async bake() { + // emit event that a bake has begun + this.emit('bake:start'); + + // build the distribution + // remove the output directory to make sure it's clean + await premove(this.output); + + // prep the data + const data = await this.getData(); + + // pass the data context to rollup and nunjucks + this.rollup.context = data; + this.nunjucks.context = data; + + // wait for all the assets to prepare first + await this.assets.build(); + + // compile the rest of the assets + await Promise.all([this.sass.build(), this.rollup.build()]); + + // build the HTML + await this.nunjucks.build(); + + if (isProductionEnv) { + console.log(yellow('Taking screenshots for fallback PNGs...')); + await this.screenshots(); + console.log('Done taking screenshots'); + } + + // emit event that a bake has completed + this.emit('bake:end'); + } + /** * Generates fallback images for web-component embeds. - * @param {string} baseUrl The local server's base URL * @returns {Promise} */ - async buildEmbedFallbacks(baseUrl) { - const distDir = this.output.split('/').slice(0, -1).join('/'); - const embedFilePattern = path.join(distDir, '_dist', 'embeds'); + async screenshots() { + const embedsDirectory = path.join(this.output, 'embeds'); /** * An array of file paths representing embed files. * @type {string[]} */ - const embedFiles = await new Promise((resolve, reject) => { - readdir(embedFilePattern, (err, files) => { + const embedFilepaths = await new Promise((resolve, reject) => { + readdir(embedsDirectory, (err, files) => { //handling error if (err) { reject('Unable to scan directory: ' + err); @@ -386,21 +434,52 @@ export class Baker extends EventEmitter { }); }); - if (!embedFiles.length) return; + if (!embedFilepaths.length) { + console.log(red(`No embeds found in directory ${embedsDirectory}`)); + return; + } + + console.log('Embed filepaths found in output directory:', embedFilepaths); + + // we only load mini-sync if it is being used + const { create } = await import('mini-sync'); + + // prep the server instance + const server = create({ + dir: [this.output, this.input], + port: 3000, + }); + // Start the server + console.log(yellow('Starting screenshot server...')); + const { local: baseUrl, network: external } = await server.start(); + + // screenshot the embeds + await this.takeScreenshots(baseUrl, embedFilepaths); + + console.log(yellow('Closing server...')); + await server.close(); + } + + /** + * An array of file paths representing embed files. + * @type {string[]} + */ + async takeScreenshots(baseUrl, embedFilepaths) { + // Start puppeteer const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); - for (const embedName of embedFiles) { + for (const embedFilepath of embedFilepaths) { try { - const embedPath = `embeds/${embedName}/index.html`; - const screenshotLocalUrl = `${baseUrl}/${embedPath}`; - console.log(`Taking screenshot of: ${screenshotLocalUrl}`); + const embedPath = `embeds/${embedFilepath}/index.html`; + const embedUrl = `${baseUrl}/embeds/${embedPath}`; + console.log(`Taking screenshot of: ${embedUrl}`); const page = await browser.newPage(); - await page.goto(screenshotLocalUrl, { + await page.goto(embedUrl, { waitUntil: 'networkidle0', }); @@ -415,8 +494,9 @@ export class Baker extends EventEmitter { document.documentElement.clientHeight ); }); + await page.setViewport({ - width: FIXED_FALLBACK_SCREENSHOT_WIDTH, + width: SCREENSHOT_FIXED_FALLBACK_WIDTH, height: contentHeight, deviceScaleFactor: 2, }); @@ -424,18 +504,13 @@ export class Baker extends EventEmitter { await page.waitForNetworkIdle(); // store the fallback image in the _dist directory - const screenshotEmbedDir = join(this.output, embedPath) - .split('/') - .slice(0, -1) - .join('/') - .replace('_screenshot', '_dist'); - const screenshotStoragePath = join(screenshotEmbedDir, 'fallback.png'); - console.log(`Storing the fallback image at: ${screenshotStoragePath}.`); - - await page.screenshot({ path: screenshotStoragePath, fullPage: true }); + const screenshotFilepath = `${this.output}/${embedFilepath}/fallback.png`; + console.log(`Storing the fallback image at: ${screenshotFilepath}.`); + + await page.screenshot({ path: screenshotFilepath, fullPage: true }); await page.close(); } catch (err) { - console.error(`Failed to process ${embedName}: ${err.message}`); + console.error(`Failed to process ${embedFilepath}: ${err.message}`); } } @@ -462,7 +537,7 @@ export class Baker extends EventEmitter { let dataError = null; let assetsError = null; - clearConsole(); + //clearConsole(); console.log(yellow('Starting initial serve...')); let data; @@ -476,70 +551,32 @@ export class Baker extends EventEmitter { try { await this.assets.build(); } catch (err) { - assetsError = err; + onError('Assets', err); } // we need an initial run to populate the manifest try { await this.sass.build(); } catch (err) { - stylesError = err; + onError('Styles', err); } try { this.rollup.context = data; await this.rollup.build(); } catch (err) { - scriptsError = err; + onError('Rollup', err); } try { this.nunjucks.context = data; await this.nunjucks.build(); } catch (err) { - templatesError = err; + onError('Templates', err); } const { local, network: external } = await server.start(); - - const logStatus = () => { - clearConsole(); - - let hadError = false; - - if (templatesError) { - hadError = true; - onError('Templates', templatesError); - } - - if (stylesError) { - hadError = true; - onError('Styles', stylesError); - } - - if (scriptsError) { - hadError = true; - onError('Scripts', scriptsError); - } - - if (dataError) { - hadError = true; - onError('Data', dataError); - } - - if (assetsError) { - hadError = true; - onError('Assets', assetsError); - } - - if (!hadError) { - console.log(green('Project compiled successfully!')); - printInstructions({ external, local }); - } - }; - - // our initial status log - logStatus(); + printInstructions({ external, local }); // set up the watcher this.sass.watch((err, outputs) => { @@ -636,59 +673,41 @@ export class Baker extends EventEmitter { }); } - async buildDistribution() { - // remove the output directory to make sure it's clean - await premove(this.output); - - // prep the data - const data = await this.getData(); - - // pass the data context to rollup and nunjucks - this.rollup.context = data; - this.nunjucks.context = data; - - // wait for all the assets to prepare first - await this.assets.build(); - - // compile the rest of the assets - await Promise.all([this.sass.build(), this.rollup.build()]); - - // build the HTML - await this.nunjucks.build(); - } - - async bake() { - // emit event that a bake has begun - this.emit('bake:start'); - - // build the distribution - await this.buildDistribution(); - - // emit event that a bake has completed - this.emit('bake:end'); - } - - async screenshot() { - // build the _screenshot directory - await this.buildDistribution(); - - // we only load mini-sync if it is being used - const { create } = await import('mini-sync'); - - // prep the server instance - const server = create({ - dir: [this.output, this.input], - port: 3000, - }); - - // Start the server - console.log(yellow('Starting screenshot server...')); - const { local: baseUrl, network: external } = await server.start(); - - // screenshot the embeds - await this.buildEmbedFallbacks(baseUrl); - - console.log(yellow('Closing server...')); - await server.close(); - } + watch() {} + + // static logStatus() { + // //clearConsole(); + // + // let hadError = false; + // + // if (templatesError) { + // hadError = true; + // onError('Templates', templatesError); + // } + // + // if (stylesError) { + // hadError = true; + // onError('Styles', stylesError); + // } + // + // if (scriptsError) { + // hadError = true; + // onError('Scripts', scriptsError); + // } + // + // if (dataError) { + // hadError = true; + // onError('Data', dataError); + // } + // + // if (assetsError) { + // hadError = true; + // onError('Assets', assetsError); + // } + // + // if (!hadError) { + // console.log(green('Project compiled successfully!')); + // printInstructions({ external, local }); + // } + // } } diff --git a/lib/utils.js b/lib/utils.js index 05750ab8..1ad7f5c2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -66,7 +66,7 @@ export function logErrorMessage(err) { } export function onError(type, err) { - console.log(red(`${type} failed to compile.\n`)); + console.error(red(`${type} failed to compile.\n`)); logErrorMessage(err); } diff --git a/package-lock.json b/package-lock.json index 34d48a40..7ccb92cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7059,11 +7059,6 @@ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==" }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", diff --git a/package.json b/package.json index ded73c9a..57878061 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "premove": "^4.0.0", "puppeteer": "^13.7.0", "quaff": "^5.0.0", - "require-from-string": "^2.0.2", "rev-path": "^3.0.0", "rollup": "^2.79.1", "rollup-plugin-svelte": "^7.1.6",