flores
Advanced tools
Comparing version 0.1.0 to 1.0.0-beta.0
{ | ||
"name": "flores", | ||
"version": "0.1.0", | ||
"version": "1.0.0-beta.0", | ||
"description": "Minimalist static site generator.", | ||
@@ -23,3 +23,8 @@ "keywords": [ | ||
}, | ||
"scripts": {}, | ||
"scripts": { | ||
"lint": "eslint ./", | ||
"lint-fix": "eslint ./ --fix", | ||
"prepublishOnly": "npm run lint && npm run test", | ||
"test": "jest" | ||
}, | ||
"dependencies": { | ||
@@ -47,5 +52,15 @@ "chokidar": "^2.0.4", | ||
"postcss-preset-env": "^6.5.0", | ||
"sitemap": "^2.1.0" | ||
"sitemap": "^2.1.0", | ||
"socket.io": "^2.2.0" | ||
}, | ||
"devDependencies": {}, | ||
"devDependencies": { | ||
"eslint": "^5.10.0", | ||
"eslint-config-airbnb-base": "^13.1.0", | ||
"eslint-config-prettier": "^3.3.0", | ||
"eslint-plugin-import": "^2.14.0", | ||
"eslint-plugin-prettier": "^3.0.0", | ||
"jest": "^23.6.0", | ||
"jsdom": "^13.1.0", | ||
"prettier": "^1.15.3" | ||
}, | ||
"engines": { | ||
@@ -52,0 +67,0 @@ "node": ">=8.0.0" |
@@ -1,3 +0,6 @@ | ||
# Flores [WIP] | ||
# Flores | ||
[![Build Status](https://badgen.net/travis/risan/flores)](https://travis-ci.org/risan/flores) | ||
[![Test Covarage](https://badgen.net/codecov/c/github/risan/flores)](https://codecov.io/gh/risan/flores) | ||
[![Greenkeeper](https://badges.greenkeeper.io/risan/flores.svg)](https://greenkeeper.io) | ||
[![Latest Version](https://badgen.net/npm/v/flores)](https://www.npmjs.com/package/flores) | ||
@@ -19,3 +22,3 @@ | ||
(async () => { | ||
await flores.build("/path/to/source"); | ||
await flores.build(); | ||
})(); | ||
@@ -28,6 +31,6 @@ ``` | ||
Build the site for production. | ||
Generate the website. | ||
```js | ||
flores.build([basePath]) | ||
flores.build([options]) | ||
``` | ||
@@ -37,10 +40,10 @@ | ||
* `basePath` (`String`): The site source path, default to the current working directory. | ||
* `options` (`Object`): The [configuration options](#configuration-options). | ||
### `flores.serve` | ||
Build the site and start the development server. | ||
Generate the website and start the development server. | ||
```js | ||
flores.serve([basePath]) | ||
flores.serve([options]) | ||
``` | ||
@@ -50,10 +53,10 @@ | ||
* `basePath` (`String`): The site source path, default to the current working directory. | ||
* `options` (`Object`): The [configuration options](#configuration-options). | ||
### `flores.watch` | ||
Start the development server and the file watcher. | ||
Start the development server and watch for the file changes. It will automatically refresh the browser on file changes. | ||
```js | ||
flores.watch([basePath]) | ||
flores.watch([options]) | ||
``` | ||
@@ -63,6 +66,34 @@ | ||
* `basePath` (`String`): The site source path, default to the current working directory. | ||
* `options` (`Object`): The [configuration options](#configuration-options). | ||
### Configuration Options | ||
Configuration options is an optional `Object` that you can pass to `build`, `serve`, or `watch` methods. | ||
* **`env`** (`String`): The environment name, default to `process.env.NODE_ENV`. If the `NODE_ENV` environment variable is not set, `production` will be set. Note that for `serve` and `watch` methods, the `env` value will always be set to `development`. | ||
* **`url`** (`String`): The website URL, default to `http://localhost:4000`. | ||
* **`basePath`** (`String`): The base path of your website project directory, default to `process.cwd()`. | ||
* **`sourceDir`** (`String`): The directory for the website source relative to the `basePath`, default to `src`. | ||
* **`outputDir`** (`String`): The directory where the generated website will be stored relative to the `basePath`, default to `public`, | ||
* **`templatesDir`** (`String`): The templates directory relative to the `sourceDir`, default to `templates`. | ||
* **`assetsDir`** (`String`): The CSS assets directory relative to the `sourceDir`, default to `assets`. | ||
* **`defaultTemplate`** (`String`): The default template name for the markdown post, default to `post.njk`. You can override the template for individual post by providing the `template` field on the post's front matter. | ||
* **`defaultCollectionTemplate`** (`String`): The default template name for the markdown post collection page, default to `collection.njk`. You can override the template for individual post collection page by providing the `template` field on the page's front matter. | ||
* **`copyFiles`** (`Array`): List of files or file patterns to copy, default to: | ||
```js | ||
["images/**", "robot.txt", "**/*.html"]` | ||
``` | ||
* `postcssPresetEnv` (`Object`): [PostCSS Preset Env options](https://github.com/csstools/postcss-preset-env#options), default to: | ||
```js | ||
{ | ||
stage: 3, | ||
preserve: false | ||
} | ||
``` | ||
## License | ||
[MIT](https://github.com/risan/flores/blob/master/LICENSE) © [Risan Bagja Pradana](https://bagja.net) |
@@ -1,3 +0,2 @@ | ||
const path = require("path"); | ||
/* eslint no-console: "off" */ | ||
const fs = require("fs-extra"); | ||
@@ -9,3 +8,2 @@ | ||
const processCssFiles = require("./process-css-files"); | ||
const processCollectionPages = require("./process-collection-pages"); | ||
const processMarkdownFiles = require("./process-markdown-files"); | ||
@@ -16,13 +14,13 @@ const Renderer = require("./renderer"); | ||
* Build the site. | ||
* @param {String} basePath - The project base path. | ||
* @param {Object} options - The site configuration options. | ||
* @return {Object} | ||
*/ | ||
const build = async (basePath = process.cwd()) => { | ||
const config = new Config(basePath); | ||
const build = async (options = {}) => { | ||
const config = new Config(options); | ||
console.log(`⏳ Generating website: ${config.outputDir}`); | ||
console.log(`⏳ Generating website to: ${config.outputPath}`); | ||
const renderer = new Renderer(config); | ||
await fs.remove(config.outputDir); | ||
await fs.remove(config.outputPath); | ||
@@ -35,10 +33,10 @@ const assets = await processCssFiles(config); | ||
const pages = await processMarkdownFiles({ config, renderer }); | ||
const { posts, collectionPages } = await processMarkdownFiles({ | ||
config, | ||
renderer | ||
}); | ||
const { | ||
posts, collectionPages | ||
} = await processCollectionPages(pages, { config, renderer }); | ||
console.log(`✅ ${posts.length} markdown posts are converted.`); | ||
console.log(`✅ ${collectionPages.length} collection pages are generated.`); | ||
console.log(`✅ ${pages.length} markdown files are converted.`); | ||
const files = await copyStaticFiles(config); | ||
@@ -45,0 +43,0 @@ |
const path = require("path"); | ||
const { URL } = require("url"); | ||
const PRODUCTION = "production"; | ||
const PROTOCOL = /^http[s]?:\/\//i; | ||
const LEADING_SLASH = /^\//; | ||
const TRAILING_SLASH = /\/$/; | ||
const LEADING_AND_TRAILING_SLASHES = /(^\/|\/$)/g; | ||
@@ -11,14 +13,12 @@ | ||
* Create new config instance. | ||
* @param {String} basePath - The site base path. | ||
* @param {Object} options - The site configuration options. | ||
*/ | ||
constructor(basePath) { | ||
this.basePath = basePath; | ||
constructor(options = {}) { | ||
this.options = { ...Config.defaultOptions, ...options }; | ||
this.data = this.formatData( | ||
this.loadDataFromFile() | ||
); | ||
this.parseOptions(this.options); | ||
return new Proxy(this, { | ||
get(config, prop) { | ||
return prop in config ? config[prop] : config.data[prop]; | ||
return prop in config ? config[prop] : config.options[prop]; | ||
} | ||
@@ -29,2 +29,35 @@ }); | ||
/** | ||
* Parse the config options. | ||
* @param {Object} options - The config options. | ||
* @return {Void} | ||
*/ | ||
parseOptions(options) { | ||
this.basePath = path.resolve(options.basePath); | ||
this.sourcePath = path.resolve(this.basePath, options.sourceDir); | ||
this.outputPath = path.resolve(this.basePath, options.outputDir); | ||
this.templatesPath = path.resolve(this.sourcePath, options.templatesDir); | ||
this.assetsPath = path.resolve(this.sourcePath, options.assetsDir); | ||
this.url = PROTOCOL.test(options.url) | ||
? options.url | ||
: `http://${options.url}`; | ||
const urlObj = new URL(this.url); | ||
this.port = urlObj.port ? parseInt(urlObj.port, 10) : 4000; | ||
if (this.isProduction()) { | ||
this.origin = urlObj.origin; | ||
const pathname = urlObj.pathname.replace( | ||
LEADING_AND_TRAILING_SLASHES, | ||
"" | ||
); | ||
this.pathname = pathname ? `/${pathname}` : ""; | ||
} else { | ||
this.origin = `http://localhost:${this.port}`; | ||
this.pathname = ""; | ||
} | ||
} | ||
/** | ||
* Get url for the given path. | ||
@@ -40,4 +73,4 @@ * @param {String} p - The url path to generate. | ||
return cleanRelativeUrl | ||
? `${this.data.url}/${cleanRelativeUrl}` | ||
: this.data.url; | ||
? `${this.origin}/${cleanRelativeUrl}` | ||
: this.origin; | ||
} | ||
@@ -51,7 +84,7 @@ | ||
getRelativeUrl(p = "/") { | ||
const baseUrl = this.data.pathPrefix ? `${this.data.pathPrefix}/` : "/"; | ||
const pathPrefix = this.pathname ? `${this.pathname}/` : "/"; | ||
const cleanPath = p.replace(LEADING_SLASH, ""); | ||
return cleanPath ? baseUrl + cleanPath : baseUrl; | ||
return cleanPath ? pathPrefix + cleanPath : pathPrefix; | ||
} | ||
@@ -64,82 +97,15 @@ | ||
isProduction() { | ||
return this.data.env.toLowerCase() === "production"; | ||
return this.options.env.toLowerCase() === "production"; | ||
} | ||
/** | ||
* Format the config data. | ||
* @param {Object} data - The config data. | ||
* @return {Object} The formatted config data. | ||
*/ | ||
formatData(data) { | ||
// Directories. | ||
data.sourceDir = this.resolvePath(data.sourceDir); | ||
data.outputDir = this.resolvePath(data.outputDir); | ||
data.templatesDir = path.resolve(data.sourceDir, data.templatesDir); | ||
data.assetsDir = path.resolve(data.sourceDir, data.assetsDir); | ||
if (data.env.toLowerCase() === "production") { | ||
// url. | ||
if (!PROTOCOL.test(data.url)) { | ||
data.url = `http://${data.url}`; | ||
} | ||
data.url = data.url.replace(TRAILING_SLASH, ""); | ||
// Path prefix. | ||
data.pathPrefix = data.pathPrefix.replace( | ||
LEADING_AND_TRAILING_SLASHES, "" | ||
); | ||
if (data.pathPrefix) { | ||
data.pathPrefix = `/${data.pathPrefix}`; | ||
} | ||
} else { | ||
data.url = `${Config.defaultData.url}:${Config.defaultData.port}`; | ||
data.pathPrefix = ""; | ||
} | ||
return data; | ||
} | ||
/** | ||
* Resolve the given path. | ||
* @param {String} p - The path to resolve. | ||
* @return {String} The resolved path. | ||
*/ | ||
resolvePath(p) { | ||
if (path.isAbsolute(p)) { | ||
return p; | ||
} | ||
return path.resolve(this.basePath, p); | ||
} | ||
/** | ||
* Load config data from file. | ||
* @return {Object} The config data. | ||
*/ | ||
loadDataFromFile() { | ||
let data = {}; | ||
try { | ||
const configFile = path.resolve(this.basePath, "site.config.js"); | ||
data = require(configFile); | ||
} catch(error) { | ||
// | ||
} | ||
return { ...Config.defaultData, ...data }; | ||
} | ||
/** | ||
* Default config data. | ||
* Get the default config options. | ||
* @return {Object} | ||
*/ | ||
static get defaultData() { | ||
static get defaultOptions() { | ||
return { | ||
env: process.env.NODE_ENV ? process.env.NODE_ENV : "production", | ||
url: "http://localhost", | ||
port: 4000, | ||
pathPrefix: "", | ||
env: process.env.NODE_ENV ? process.env.NODE_ENV : PRODUCTION, | ||
watch: false, | ||
url: "http://localhost:4000", | ||
basePath: process.cwd(), | ||
sourceDir: "src", | ||
@@ -149,9 +115,5 @@ outputDir: "public", | ||
assetsDir: "assets", | ||
defaultTemplate: "post.html", | ||
defaultCollectionTemplate: "collection.html", | ||
copyFiles: [ | ||
"images/**", | ||
"robot.txt", | ||
"**/*.html" | ||
], | ||
defaultTemplate: "post.njk", | ||
defaultCollectionTemplate: "collection.njk", | ||
copyFiles: ["images/**", "robot.txt", "**/*.html"], | ||
postcssPresetEnv: { | ||
@@ -158,0 +120,0 @@ stage: 3, |
@@ -13,9 +13,13 @@ const path = require("path"); | ||
const files = await globby(config.copyFiles, { | ||
cwd: config.sourceDir | ||
cwd: config.sourcePath | ||
}); | ||
await Promise.all(files.map(file => fs.copy( | ||
path.join(config.sourceDir, file), | ||
path.join(config.outputDir, file) | ||
))); | ||
await Promise.all( | ||
files.map(file => | ||
fs.copy( | ||
path.join(config.sourcePath, file), | ||
path.join(config.outputPath, file) | ||
) | ||
) | ||
); | ||
@@ -22,0 +26,0 @@ return files; |
@@ -19,6 +19,6 @@ const path = require("path"); | ||
let obj = { | ||
const obj = { | ||
url: page.url, | ||
...defaultValue, | ||
...get(page, "frontMatter.sitemap", {}), | ||
...get(page, "frontMatter.sitemap", {}) | ||
}; | ||
@@ -28,3 +28,3 @@ | ||
obj.lastmodISO = page.frontMatter.modifiedAt.toISOString(); | ||
} else if(has(page, "frontMatter.date")) { | ||
} else if (has(page, "frontMatter.date")) { | ||
obj.lastmodISO = page.frontMatter.date.toISOString(); | ||
@@ -45,4 +45,4 @@ } | ||
const postUrls = posts.map(post => formatSiteMapItem(post)); | ||
const collectionPageUrls = collectionPages.map( | ||
page => formatSiteMapItem(page, { | ||
const collectionPageUrls = collectionPages.map(page => | ||
formatSiteMapItem(page, { | ||
changefreq: "daily" | ||
@@ -54,13 +54,10 @@ }) | ||
hostname: config.url, | ||
urls: [ | ||
...collectionPageUrls.filter(Boolean), | ||
...postUrls.filter(Boolean) | ||
] | ||
urls: [...collectionPageUrls.filter(Boolean), ...postUrls.filter(Boolean)] | ||
}); | ||
const outputPath = path.join(config.outputDir, "sitemap.xml"); | ||
const sitemapPath = path.join(config.outputPath, "sitemap.xml"); | ||
return await fs.outputFile(outputPath, sitemap.toString()); | ||
return fs.outputFile(sitemapPath, sitemap.toString()); | ||
}; | ||
module.exports = generateSitemap; |
@@ -7,14 +7,16 @@ const fm = require("front-matter"); | ||
md.use(mdAnchor, { permalink: true, permalinkBefore: true }) | ||
.use(mdToc, { | ||
containerHeaderHtml: "<h2>Table of Contents</h2>", | ||
includeLevel: [2, 3, 4, 5] | ||
}); | ||
const getMarkdownOutputInfo = require("./get-markdown-output-info"); | ||
md.use(mdAnchor, { permalink: true, permalinkBefore: true }).use(mdToc, { | ||
containerHeaderHtml: "<h2>Table of Contents</h2>", | ||
includeLevel: [2, 3, 4, 5] | ||
}); | ||
/** | ||
* Parse markdown file. | ||
* @param {String} path - The markdown file path to parse. | ||
* @return {Object} Return the frontMatter and the rendered HTML. | ||
* @param {Object} config - The Config instance. | ||
* @return {Object} Return the frontMatter, the rendered HTML, and output info. | ||
*/ | ||
const parseMarkdownFile = async path => { | ||
const parseMarkdownFile = async (path, config) => { | ||
const source = await fs.readFile(path, "utf8"); | ||
@@ -24,5 +26,8 @@ | ||
const outputInfo = getMarkdownOutputInfo(path, config); | ||
return { | ||
frontMatter: attributes, | ||
html: md.render(body) | ||
content: md.render(body), | ||
...outputInfo | ||
}; | ||
@@ -29,0 +34,0 @@ }; |
@@ -19,7 +19,7 @@ const crypto = require("crypto"); | ||
const relativePath = path.relative(config.sourceDir, file); | ||
let outputPath = path.join(config.outputDir, relativePath); | ||
const relativePath = path.relative(config.sourcePath, file); | ||
let outputPath = path.join(config.outputPath, relativePath); | ||
const processor = postcss([ | ||
atImport, | ||
atImport({ path: [config.assetsPath] }), | ||
presetEnv(config.postcssPresetEnv) | ||
@@ -33,4 +33,3 @@ ]); | ||
const result = await processor.process(css, { | ||
from: path.relative(config.basePath, file), | ||
to: path.relative(config.basePath, outputPath), | ||
from: file, | ||
map: !config.isProduction() | ||
@@ -42,3 +41,3 @@ }); | ||
hash.update(css); | ||
hash.update(result.css); | ||
@@ -45,0 +44,0 @@ const outputHash = hash.digest("hex").substring(0, 10); |
@@ -15,3 +15,3 @@ const path = require("path"); | ||
const sourceFiles = await globby("**/[^_]*.css", { | ||
cwd: config.assetsDir, | ||
cwd: config.assetsPath, | ||
absolute: true | ||
@@ -24,8 +24,8 @@ }); | ||
const sourceNames = sourceFiles.map( | ||
file => path.relative(config.assetsDir, file) | ||
const sourceNames = sourceFiles.map(file => | ||
path.relative(config.assetsPath, file) | ||
); | ||
const outputRelativeUrls = outputFiles.map( | ||
file => config.getRelativeUrl(path.relative(config.outputDir, file)) | ||
const outputRelativeUrls = outputFiles.map(file => | ||
config.getRelativeUrl(path.relative(config.outputPath, file)) | ||
); | ||
@@ -32,0 +32,0 @@ |
@@ -1,20 +0,56 @@ | ||
const globby = require("globby"); | ||
const processMarkdownFile = require("./process-markdown-file"); | ||
const parseMarkdownFiles = require("./parse-markdown-files"); | ||
/** | ||
* Write the page. | ||
* @param {Object} page - The page data. | ||
* @param {Config} options.config - The Config instance. | ||
* @param {Renderer} options.renderer - The Renderer instance. | ||
* @param {Object} options.data - All website pages data. | ||
* @return {Promise} | ||
*/ | ||
const writePage = async (page, { config, renderer, data }) => { | ||
const defaultTemplate = page.frontMatter.collection | ||
? config.defaultCollectionTemplate | ||
: config.defaultTemplate; | ||
const template = page.frontMatter.template | ||
? page.frontMatter.template | ||
: defaultTemplate; | ||
return renderer.writeHtml(page.outputPath, template, { | ||
...data, | ||
...page, | ||
collection: data.collections[page.collectionName] | ||
}); | ||
}; | ||
/** | ||
* Process all markdown files. | ||
* @param {Config} options.config - The Config instance. | ||
* @param {Renderer} options.renderer - The Renderer instance. | ||
* @return {Array} | ||
* @return {Object} | ||
*/ | ||
const processMarkdownFiles = async ({ config, renderer }) => { | ||
const files = await globby("**/*.(md|markdown)", { | ||
cwd: config.sourceDir, | ||
absolute: true | ||
}); | ||
const data = await parseMarkdownFiles(config); | ||
const data = await Promise.all( | ||
files.map(file => processMarkdownFile(file, { config, renderer })) | ||
await Promise.all( | ||
data.posts.map(page => | ||
writePage(page, { | ||
config, | ||
renderer, | ||
data | ||
}) | ||
) | ||
); | ||
await Promise.all( | ||
data.collectionPages.map(page => | ||
writePage(page, { | ||
config, | ||
renderer, | ||
data | ||
}) | ||
) | ||
); | ||
return data; | ||
@@ -21,0 +57,0 @@ }; |
@@ -13,5 +13,3 @@ const fs = require("fs-extra"); | ||
this.config = config; | ||
this.env = nunjucks.configure(config.templatesDir, { | ||
noCache: !config.isProduction() | ||
}); | ||
this.env = nunjucks.configure(config.templatesPath); | ||
@@ -36,7 +34,27 @@ this.env.addGlobal("config", this.config); | ||
render(template, data = {}) { | ||
return this.env.render(template, data); | ||
let str = this.env.render(template, data); | ||
if (this.config.isProduction()) { | ||
str = minifier.minify(str, { | ||
collapseWhitespace: true, | ||
removeComments: true | ||
}); | ||
} | ||
if (this.config.watch) { | ||
str = str.replace( | ||
/(<\/body[\s]*>)/i, | ||
` | ||
<script src="/socket.io/socket.io.js"></script> | ||
<script src="/flores/socket-client.js"></script> | ||
$1 | ||
` | ||
); | ||
} | ||
return str; | ||
} | ||
/** | ||
* Render the template and write to file. | ||
* Render the template and write it to a file. | ||
* @param {String} outputPath - The path to save the file. | ||
@@ -48,11 +66,4 @@ * @param {String} template - The template to render. | ||
async writeHtml(outputPath, template, data = {}) { | ||
let str = this.render(template, data); | ||
const str = this.render(template, data); | ||
if (this.config.isProduction()) { | ||
str = minifier.minify(str, { | ||
collapseWhitespace: true, | ||
removeComments: true | ||
}); | ||
} | ||
await fs.outputFile(outputPath, str); | ||
@@ -62,4 +73,14 @@ | ||
} | ||
/** | ||
* Clear the compiled template cache. | ||
* @return {Void} | ||
*/ | ||
clearCache() { | ||
for (let i = 0; i < this.env.loaders.length; i += 1) { | ||
this.env.loaders[i].cache = {}; | ||
} | ||
} | ||
} | ||
module.exports = Renderer; |
@@ -0,10 +1,13 @@ | ||
/* eslint no-console: "off" */ | ||
const express = require("express"); | ||
const SocketIo = require("socket.io"); | ||
/** | ||
* Run the server. | ||
* @param {String} options.publicDir - The directory to serve. | ||
* @param {Number} options.port - The port to listen to. | ||
* @param {String} options.publicDir - The directory to serve. | ||
* @param {Number} options.port - The port to listen to. | ||
* @param {Boolean} options.watch - Set to true to run watcher. | ||
* @return {Promise} | ||
*/ | ||
const runServer = ({ publicDir, port }) => { | ||
const runServer = ({ publicDir, port, watch = false }) => { | ||
const app = express(); | ||
@@ -14,7 +17,21 @@ | ||
return new Promise(resolve => | ||
app.listen(port, () => resolve(app, { publicDir, port })) | ||
); | ||
return new Promise(resolve => { | ||
const server = app.listen(port, () => { | ||
console.log(`⚡️ Server is running: http://localhost:${port}`); | ||
if (watch) { | ||
app.get("/flores/socket-client.js", (req, res) => | ||
res.sendFile(`${__dirname}/socket-client.js`) | ||
); | ||
const socketIo = new SocketIo(server); | ||
resolve({ app, server, socketIo }); | ||
} else { | ||
resolve({ app, server }); | ||
} | ||
}); | ||
}); | ||
}; | ||
module.exports = runServer; |
@@ -6,16 +6,14 @@ const build = require("./build"); | ||
* Serve the generated site. | ||
* @param {String} basePath - The project base path. | ||
* @param {Object} options - The configuration data. | ||
* @return {Promise} | ||
*/ | ||
const serve = async (basePath = process.cwd()) => { | ||
const { config } = await build(basePath); | ||
const serve = async (options = {}) => { | ||
const { config } = await build({ ...options, env: "development" }); | ||
await runServer({ | ||
publicDir: config.outputDir, | ||
runServer({ | ||
publicDir: config.outputPath, | ||
port: config.port | ||
}); | ||
console.log(`⚡️ Server is running: http://localhost:${config.port}`); | ||
}; | ||
module.exports = serve; |
174
src/watch.js
@@ -0,1 +1,2 @@ | ||
/* eslint no-console: "off" */ | ||
const path = require("path"); | ||
@@ -11,8 +12,5 @@ | ||
const processCssFiles = require("./process-css-files"); | ||
const processCollectionPages = require("./process-collection-pages"); | ||
const processMarkdownFiles = require("./process-markdown-files"); | ||
const runServer = require("./run-server"); | ||
const EVENTS_TO_CATCH = ["change", "add", "unlink"]; | ||
const CSS_FILE = "css"; | ||
@@ -25,21 +23,33 @@ const MARKDOWN_FILE = "markdown"; | ||
const MARKDOWN_EXTENSIONS = [".md", ".markdown"]; | ||
const TEMPLATE_EXTENSIONS = [".html", ".njs"]; | ||
const TEMPLATE_EXTENSIONS = [".html", ".njk"]; | ||
/** | ||
* Run the file watcher. | ||
* @param {String} basePath - The project base path. | ||
* @param {Object} options - The configuration data. | ||
* @return {Promise} | ||
*/ | ||
const watch = async (basePath = process.cwd()) => { | ||
const { config, renderer } = await build(basePath); | ||
const watch = async (options = {}) => { | ||
const { config, renderer } = await build({ | ||
...options, | ||
env: "development", | ||
watch: true | ||
}); | ||
await runServer({ | ||
publicDir: config.outputDir, | ||
port: config.port | ||
const { socketIo } = await runServer({ | ||
publicDir: config.outputPath, | ||
port: config.port, | ||
watch: true | ||
}); | ||
console.log(`⚡️ Server is running: http://localhost:${config.port}`); | ||
/** | ||
* Reload the browser. | ||
* @return {Void} | ||
*/ | ||
const reloadBrowser = () => socketIo.emit("flores.reloadBrowser"); | ||
const assetsPath = path.relative(config.sourceDir, config.assetsDir) + "/"; | ||
const templatesPath = path.relative(config.sourceDir, config.templatesDir) + "/"; | ||
const assetsDir = `${path.relative(config.sourcePath, config.assetsPath)}/`; | ||
const templatesDir = `${path.relative( | ||
config.sourcePath, | ||
config.templatesPath | ||
)}/`; | ||
@@ -56,3 +66,3 @@ /** | ||
if (extension === CSS_EXTENSION && p.startsWith(assetsPath)) { | ||
if (extension === CSS_EXTENSION && p.startsWith(assetsDir)) { | ||
return CSS_FILE; | ||
@@ -65,3 +75,3 @@ } | ||
if (TEMPLATE_EXTENSIONS.includes(extension) && p.startsWith(templatesPath)) { | ||
if (TEMPLATE_EXTENSIONS.includes(extension) && p.startsWith(templatesDir)) { | ||
return TEMPLATE_FILE; | ||
@@ -78,24 +88,32 @@ } | ||
/** | ||
* Get event message for logging. | ||
* @param {String} options.event - The event type. | ||
* @param {String} options.p - The file path. | ||
* @param {String} options.fileType - The file type. | ||
* @return {String} | ||
* Process markdown files. | ||
* @param {Boolean} options.clearCache - Set to true to clear the compiled template caches. | ||
* @return {Promise} | ||
*/ | ||
const getEventMessage = ({ event, p, fileType }) => { | ||
switch (event) { | ||
case "change": | ||
return `✏️ ${fileType} file is updated: ${p}`; | ||
case "add": | ||
return `✨ New ${fileType} file: ${p}`; | ||
case "unlink": | ||
return `🔥 ${fileType} is deleted: ${p}`; | ||
}; | ||
const processMarkdown = async ({ clearCache = false } = {}) => { | ||
if (clearCache) { | ||
renderer.clearCache(); | ||
} | ||
const { posts, collectionPages } = await processMarkdownFiles({ | ||
config, | ||
renderer | ||
}); | ||
console.log(`✅ ${posts.length} markdown posts are converted.`); | ||
console.log(`✅ ${collectionPages.length} collection pages are generated.`); | ||
await generateSitemap({ config, posts, collectionPages }); | ||
console.log("✅ Sitemap is generated."); | ||
reloadBrowser(); | ||
}; | ||
/** | ||
* Handle css file change. | ||
* Process css files. | ||
* @param {Boolean} options.reprocessMarkdown - Set to true to reprocess the markdown files to. | ||
* @return {Promise} | ||
*/ | ||
const handleCssChange = async () => { | ||
const processCss = async ({ reprocessMarkdown = false } = {}) => { | ||
const assets = await processCssFiles(config); | ||
@@ -107,47 +125,31 @@ | ||
await handleMarkdownOrTemplateChange(); | ||
if (reprocessMarkdown) { | ||
await processMarkdown(); | ||
} else { | ||
reloadBrowser(); | ||
} | ||
}; | ||
/** | ||
* Handle markdown or template change. | ||
* Copy file to output directory. | ||
* @param {String} p - The file path to copy. | ||
* @return {Promise} | ||
*/ | ||
const handleMarkdownOrTemplateChange = async () => { | ||
const pages = await processMarkdownFiles({ config, renderer }); | ||
const copyFile = async p => | ||
fs.copy(path.join(config.sourcePath, p), path.join(config.outputPath, p)); | ||
const { | ||
posts, collectionPages | ||
} = await processCollectionPages(pages, { config, renderer }); | ||
console.log(`✅ ${pages.length} markdown files are converted.`); | ||
await generateSitemap({ config, posts, collectionPages }); | ||
console.log("✅ Sitemap is generated."); | ||
}; | ||
/** | ||
* Handle static file change. | ||
* @param {String} options.event - The event type. | ||
* @param {String} options.p - The file path. | ||
* Remove file from the output directory. | ||
* @param {String} p - The file path to remove. | ||
* @return {Promise} | ||
*/ | ||
const handleStaticFileChange = async ({ event, p }) => { | ||
if (["change", "add"].includes(event)) { | ||
await fs.copy( | ||
path.join(config.sourceDir, p), | ||
path.join(config.outputDir, p) | ||
); | ||
} else if (event === "unlink") { | ||
await fs.remove(path.join(config.outputDir, p)); | ||
} | ||
}; | ||
const removeFile = async p => fs.remove(path.join(config.outputPath, p)); | ||
const handleCssChangeDebounced = debounce(handleCssChange, 1000); | ||
const handleMarkdownOrTemplateChangeDebounced = debounce(handleMarkdownOrTemplateChange, 1000); | ||
const processCssDebounced = debounce(processCss, 500); | ||
const processMarkdownDebounced = debounce(processMarkdown, 500); | ||
const watcher = chokidar.watch(".", { | ||
ignored: /(^|[\/\\])\../, | ||
ignored: /(^|[/\\])\../, | ||
ignoreInitial: true, | ||
cwd: config.sourceDir | ||
cwd: config.sourcePath | ||
}); | ||
@@ -157,7 +159,23 @@ | ||
watcher.on("all", async (event, p) => { | ||
if (!EVENTS_TO_CATCH.includes(event)) { | ||
watcher.on("change", async p => { | ||
const fileType = getFileType(p); | ||
if (fileType === null) { | ||
return; | ||
} | ||
console.log(`✏️ ${fileType} file is updated: ${p}`); | ||
if (fileType === CSS_FILE) { | ||
processCssDebounced(); | ||
} else if (fileType === MARKDOWN_FILE) { | ||
await processMarkdown(); | ||
} else if (fileType === TEMPLATE_FILE) { | ||
processMarkdownDebounced({ clearCache: true }); | ||
} else { | ||
await copyFile(p); | ||
} | ||
}); | ||
watcher.on("add", async p => { | ||
const fileType = getFileType(p); | ||
@@ -169,14 +187,32 @@ | ||
console.log(getEventMessage({ event, p, fileType })); | ||
console.log(`✨ New ${fileType} file: ${p}`); | ||
if (fileType === CSS_FILE) { | ||
await handleCssChangeDebounced(); | ||
processCssDebounced({ reprocessMarkdown: true }); | ||
} else if ([MARKDOWN_FILE, TEMPLATE_FILE].includes(fileType)) { | ||
await handleMarkdownOrTemplateChangeDebounced(); | ||
processMarkdownDebounced(); | ||
} else { | ||
await handleStaticFileChange({ event, p }); | ||
await copyFile(p); | ||
} | ||
}); | ||
watcher.on("unlink", async p => { | ||
const fileType = getFileType(p); | ||
if (fileType === null) { | ||
return; | ||
} | ||
console.log(`🔥 ${fileType} is deleted: ${p}`); | ||
if (fileType === CSS_FILE) { | ||
processCssDebounced({ reprocessMarkdown: true }); | ||
} else if ([MARKDOWN_FILE, TEMPLATE_FILE].includes(fileType)) { | ||
processMarkdownDebounced(); | ||
} else { | ||
await removeFile(p); | ||
} | ||
}); | ||
}; | ||
module.exports = watch; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
28351
708
95
9
23
8
19
1
+ Addedsocket.io@^2.2.0
+ Addedafter@0.8.2(transitive)
+ Addedarraybuffer.slice@0.0.7(transitive)
+ Addedbacko2@1.0.2(transitive)
+ Addedbase64-arraybuffer@0.1.4(transitive)
+ Addedbase64id@2.0.0(transitive)
+ Addedblob@0.0.5(transitive)
+ Addedbrowserslist@4.23.1(transitive)
+ Addedcomponent-bind@1.0.0(transitive)
+ Addedcomponent-emitter@1.2.1(transitive)
+ Addedcomponent-inherit@0.0.3(transitive)
+ Addedcookie@0.4.2(transitive)
+ Addeddebug@3.1.04.1.1(transitive)
+ Addedengine.io@3.6.1(transitive)
+ Addedengine.io-client@3.5.3(transitive)
+ Addedengine.io-parser@2.2.1(transitive)
+ Addedhas-binary2@1.0.3(transitive)
+ Addedhas-cors@1.1.0(transitive)
+ Addedindexof@0.0.1(transitive)
+ Addedisarray@2.0.1(transitive)
+ Addedparseqs@0.0.6(transitive)
+ Addedparseuri@0.0.6(transitive)
+ Addedsocket.io@2.5.0(transitive)
+ Addedsocket.io-adapter@1.1.2(transitive)
+ Addedsocket.io-client@2.5.0(transitive)
+ Addedsocket.io-parser@3.3.33.4.3(transitive)
+ Addedto-array@0.1.4(transitive)
+ Addedws@7.4.6(transitive)
+ Addedxmlhttprequest-ssl@1.6.3(transitive)
+ Addedyeast@0.1.2(transitive)
- Removedbrowserslist@4.23.0(transitive)
- Removedcaniuse-lite@1.0.30001629(transitive)
- Removedelectron-to-chromium@1.4.790(transitive)