packaton 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ computes their corresponding CSP nonce and injects it as well.
12
12
 
13
13
 
14
14
  ## Images and Videos (immutable naming)
15
- For long-term caching, [media-remaper.js](src/plugins-prod/media-remaper.js) appends a SHA-1 hash
15
+ For long-term caching, [media-remaper.js](src/plugins-prod/media-remaper.js) appends an SHA-1 hash
16
16
  to the filenames and takes care of rewriting their `src` in HTML (**only in HTML**).
17
17
 
18
18
  If you want to use media files in CSS, create a similar function to
@@ -45,6 +45,6 @@ To avoid minifying, you can pass `a=>a`
45
45
  - can't write inline scripts or css (all must be in an external file, packaton inlines them)
46
46
  - must have an index
47
47
  - Ignored Documents start with `_`, so you can't have routes that begin with _
48
- - Non-Documents and Files outside /static are not automatically copied over,
48
+ - Non-Documents and Files outside config.assetsDir are not automatically copied over,
49
49
  you need to specify them.
50
- - static/media only files at the top level get hashed-named. But files within subdirs are not (by design).
50
+ - assets/media only files at the top level get hashed-named. But files within subdirs are not (by design).
package/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export interface Config {
2
2
  mode?: 'development' | 'production';
3
3
  srcPath?: string
4
- staticDir?: string
4
+ assetsDir?: string
5
5
  ignore?: RegExp
6
6
 
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "packaton",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "author": "Eric Fortis",
6
6
  "license": "MIT",
package/src/app-prod.js CHANGED
@@ -19,12 +19,11 @@ import { netiflyAndCloudflareHeadersPlugin } from './plugins-prod/netiflyAndClou
19
19
  */
20
20
  export async function buildStaticPages(config) {
21
21
  return new Promise((resolve, reject) => {
22
- const MEDIA_REL_URL = join(config.staticDir, 'media')
22
+ const MEDIA_REL_URL = join(config.assetsDir, 'media')
23
23
 
24
24
  const pSource = config.srcPath
25
25
  const pDist = config.outputDir
26
- const pDistStatic = join(config.outputDir, config.staticDir)
27
- const pDistMedia = join(pDist, MEDIA_REL_URL)
26
+ const pDistAssets = join(pDist, config.assetsDir)
28
27
 
29
28
  const server = createServer(router(config))
30
29
  server.on('error', reject)
@@ -32,7 +31,7 @@ export async function buildStaticPages(config) {
32
31
  docs.init(config.srcPath, config.ignore)
33
32
  try {
34
33
  removeDir(pDist)
35
- cpSync(join(pSource, config.staticDir), pDistStatic, {
34
+ cpSync(join(pSource, config.assetsDir), pDistAssets, {
36
35
  recursive: true,
37
36
  dereference: true,
38
37
  filter(src) {
@@ -42,19 +41,21 @@ export async function buildStaticPages(config) {
42
41
  }
43
42
  })
44
43
 
45
- const mediaHashes = await renameMediaWithHashes(pDistMedia) // only on top dir
46
44
  const pages = await crawlRoutes(server.address(), docs.routes)
47
-
45
+ const mediaHashes = await renameMediaWithHashes(pDist, MEDIA_REL_URL)
46
+
48
47
  const cspByRoute = []
49
48
  for (const [route, rawHtml] of pages) {
50
49
  const doc = new HtmlCompiler(rawHtml, join(pSource, dirname(route)), {
51
50
  minifyJS: config.minifyJS,
52
51
  minifyCSS: config.minifyCSS,
53
52
  minifyHTML: config.minifyHTML,
54
- mediaRelUrl: MEDIA_REL_URL
53
+ mediaRelUrl: MEDIA_REL_URL,
54
+ mediaHashes
55
+
55
56
  })
56
57
  await doc.minifyHTML()
57
- doc.remapMedia(mediaHashes)
58
+ doc.remapMedia()
58
59
  // TODO remap media in css and js
59
60
  await doc.inlineMinifiedCSS()
60
61
  await doc.inlineMinifiedJS()
package/src/config.js CHANGED
@@ -17,7 +17,7 @@ import { minifyHTML } from './plugins-prod/minifyHTML.js'
17
17
  const schema = {
18
18
  mode: ['development', val => ['development', 'production'].includes(val)],
19
19
  srcPath: [resolve('src'), isDirectory],
20
- staticDir: ['static', optional(String)],
20
+ assetsDir: ['assets', optional(String)],
21
21
  ignore: [/^_/, optional(RegExp)],
22
22
 
23
23
  // Development
@@ -11,6 +11,7 @@ export class HtmlCompiler {
11
11
  css = ''
12
12
  scriptsJs = ''
13
13
  mediaRelUrl = ''
14
+ mediaHashes = new Map()
14
15
  scriptsNonJs = ''
15
16
  externalScripts = []
16
17
  externalCSS = []
@@ -18,13 +19,14 @@ export class HtmlCompiler {
18
19
  #minifyCSS = a => a
19
20
  #minifyHTML = a => a
20
21
 
21
- constructor(html, pSource = '', { minifyJS, minifyCSS, minifyHTML, mediaRelUrl }) {
22
+ constructor(html, pSource = '', { minifyJS, minifyCSS, minifyHTML, mediaRelUrl, mediaHashes }) {
22
23
  this.html = html
23
24
  this.pSource = pSource
24
25
  this.#minifyJS = minifyJS
25
26
  this.#minifyCSS = minifyCSS
26
27
  this.#minifyHTML = minifyHTML
27
28
  this.mediaRelUrl = mediaRelUrl
29
+ this.mediaHashes = mediaHashes
28
30
  }
29
31
 
30
32
  // Removes comments and format multi-line tags (needed for `removeLineContaining`)
@@ -32,8 +34,8 @@ export class HtmlCompiler {
32
34
  this.html = await this.#minifyHTML(this.html)
33
35
  }
34
36
 
35
- remapMedia(mediaHashes) {
36
- this.html = remapMediaInHTML(mediaHashes, this.html, this.mediaRelUrl)
37
+ remapMedia() {
38
+ this.html = remapMediaInHTML(this.mediaHashes, this.html, this.mediaRelUrl)
37
39
  }
38
40
 
39
41
  async inlineMinifiedCSS() {
@@ -1,5 +1,5 @@
1
- import { join, parse } from 'node:path'
2
1
  import { renameSync } from 'node:fs'
2
+ import { join, parse, relative } from 'node:path'
3
3
  import { sha1, listFiles } from '../utils/fs-utils.js'
4
4
 
5
5
 
@@ -7,49 +7,33 @@ import { sha1, listFiles } from '../utils/fs-utils.js'
7
7
  * Subdirectories are ignored
8
8
  * foo.avif -> foo-<sha1>.avif
9
9
  */
10
- export async function renameMediaWithHashes(dir) {
11
- const mediaHashes = new Map()
12
-
13
- for (const file of await listFiles(dir)) {
14
- const { name, base, ext } = parse(file)
15
- const newFileName = name + '-' + sha1(file) + ext
16
- const newFile = join(dir, newFileName)
17
- mediaHashes.set(base, newFileName)
10
+ export async function renameMediaWithHashes(distDir, mediaDir) {
11
+ const mDir = join(distDir, mediaDir)
12
+ const map = new Map()
13
+
14
+ for (const file of await listFiles(mDir)) {
15
+ const { dir, name, ext } = parse(file)
16
+ const newFile = join(dir, name + '-' + sha1(file) + ext)
18
17
  renameSync(file, newFile)
18
+ map.set(relative(distDir, file), relative(distDir, newFile))
19
19
  }
20
20
 
21
- return mediaHashes
21
+ return map
22
22
  }
23
23
 
24
- // Having one dir is kinda nice for nginx headers, but that's not an excuse nor solves nested dirs with same filename
25
-
26
- // TODO for (b of base) find and replace base with new hash
27
- // so it works in dirs outside media/
28
- // mm currently a limitation is that the dictionary doesn't have the path, just the name, so
29
- // filenames need to be unique, regardless of being in a subfolder
30
- /**
31
- * Edit the media source links in the HTML, so they have the new SHA-1 hashed
32
- * filenames. Assumes that all the files are in "media/" (not ../media, ./media)
33
- *
34
- * If you want to handle CSS files, edit the regex so
35
- * instead of checking `="` (e.g. src="img.png") also checks for `url(`
36
- **/
37
24
  export function remapMediaInHTML(mediaHashes, html, mediaRelUrl) {
38
- const mURL = escapeForRegex(mediaRelUrl)
39
- const reFindMedia = new RegExp(`(="${mURL}/.*?)"`, 'g')
40
- const reFindMediaKey = new RegExp(`="${mURL}/`)
41
-
42
- for (const [, url] of html.matchAll(reFindMedia)) {
43
- const hashedName = mediaHashes.get(url.replace(reFindMediaKey, ''))
25
+ const mURL = escapeRegex(mediaRelUrl)
26
+ const reFindMedia = new RegExp(`="(/?)(${mURL}/[^"]*)"`, 'g')
27
+ return html.replace(reFindMedia, (_, optLeadingSlash, url) => {
28
+ const hashedName = mediaHashes.get(url)
44
29
  if (!hashedName)
45
- throw `ERROR: Missing ${url}\n`
46
- html = html.replace(url, `="${mediaRelUrl}/${hashedName}`)
47
- }
48
- return html
30
+ throw new Error(`ERROR: Missing ${url}`)
31
+ return `="${optLeadingSlash}${hashedName}"`
32
+ })
49
33
  }
50
34
 
51
35
 
52
- function escapeForRegex(literal) {
36
+ function escapeRegex(literal) {
53
37
  return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
54
38
  }
55
39
 
@@ -2,8 +2,13 @@ import { join } from 'node:path'
2
2
  import { write } from '../utils/fs-utils.js'
3
3
 
4
4
 
5
- export function netiflyAndCloudflareHeadersPlugin(config, cspByRoute, MEDIA_URL) {
6
- const out = join(join(config.outputDir, config.staticDir), '_headers')
5
+ /**
6
+ * @param {Config} config
7
+ * @param {string} cspByRoute
8
+ * @param {string} relMediaURL
9
+ */
10
+ export function netiflyAndCloudflareHeadersPlugin(config, cspByRoute, relMediaURL) {
11
+ const out = join(join(config.outputDir, config.assetsDir), '_headers')
7
12
 
8
13
  const cspHeaders = cspByRoute.map(([route, csp]) => {
9
14
  const r = route === '/index'
@@ -15,7 +20,7 @@ export function netiflyAndCloudflareHeadersPlugin(config, cspByRoute, MEDIA_URL)
15
20
  ` Cache-Control: public,max-age=60`
16
21
  ].join('\n')
17
22
  })
18
- cspHeaders.push(`${MEDIA_URL}/*`)
23
+ cspHeaders.push(`/${relMediaURL}/*`)
19
24
  cspHeaders.push(' Cache-Control: public,max-age=31536000,immutable')
20
25
 
21
26
  write(out, cspHeaders.join('\n'))
package/src/router.js CHANGED
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
4
4
  import { docs } from './app.js'
5
5
  import { mimeFor } from './utils/mimes.js'
6
6
  import { devClientWatcher } from './plugins-dev/WatcherDevClient.js'
7
- import { sendError, sendJSON, servePartialContent, serveStaticAsset } from './utils/http-response.js'
7
+ import { sendError, sendJSON, servePartialContent, serveAsset } from './utils/http-response.js'
8
8
 
9
9
 
10
10
  const WATCHER_DEV = '/plugins-dev/watcherDev.js'
@@ -26,7 +26,7 @@ export function router({ srcPath, ignore, mode }) {
26
26
  longPollDevHotReload(req, response)
27
27
 
28
28
  else if (url === WATCHER_DEV)
29
- serveStaticAsset(response, join(import.meta.dirname, url))
29
+ serveAsset(response, join(import.meta.dirname, url))
30
30
 
31
31
  else if (docs.hasRoute(url))
32
32
  await serveDocument(response, docs.fileFor(url), isDev)
@@ -38,7 +38,7 @@ export function router({ srcPath, ignore, mode }) {
38
38
  await servePartialContent(response, req.headers, join(srcPath, url))
39
39
 
40
40
  else
41
- serveStaticAsset(response, join(srcPath, url))
41
+ serveAsset(response, join(srcPath, url))
42
42
  }
43
43
  catch (error) {
44
44
  sendError(response, error)
@@ -1,7 +1,7 @@
1
1
  import { readdir } from 'node:fs/promises'
2
2
  import { createHash } from 'node:crypto'
3
3
  import { join, dirname } from 'node:path'
4
- import { rmSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
4
+ import { rmSync, lstatSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
5
5
 
6
6
 
7
7
  export const read = f => readFileSync(f, 'utf8')
@@ -29,7 +29,7 @@ export const sha1 = f => createHash('sha1').update(readFileSync(f)).digest('base
29
29
  export async function listFiles(dir) {
30
30
  return (await readdir(dir, {
31
31
  withFileTypes: true,
32
- recursive: false
32
+ recursive: true
33
33
  }))
34
34
  .filter(e => e.isFile())
35
35
  .map(e => join(e.parentPath, e.name))
@@ -15,7 +15,7 @@ export function sendJSON(response, payload) {
15
15
  response.end(JSON.stringify(payload))
16
16
  }
17
17
 
18
- export function serveStaticAsset(response, file) {
18
+ export function serveAsset(response, file) {
19
19
  response.setHeader('Content-Type', mimeFor(file))
20
20
  const reader = fs.createReadStream(file)
21
21
  reader.on('open', function () { this.pipe(response) })