packaton 0.0.3 → 0.0.7

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.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/TODO.md +1 -0
  3. package/index.d.ts +2 -1
  4. package/index.js +5 -5
  5. package/package.json +1 -1
  6. package/src/app-dev.js +2 -2
  7. package/src/app-prod.js +28 -38
  8. package/src/app.js +1 -1
  9. package/src/config.js +7 -6
  10. package/src/{WatcherDevClient.js → plugins-dev/WatcherDevClient.js} +1 -1
  11. package/src/{HtmlCompiler.js → plugins-prod/HtmlCompiler.js} +5 -3
  12. package/src/plugins-prod/cspNginxMapPlugin.js +9 -0
  13. package/src/{media-remaper.js → plugins-prod/media-remaper.js} +10 -6
  14. package/src/plugins-prod/netiflyAndCloudflareHeadersPlugin.js +19 -0
  15. package/src/{reportSizes.js → plugins-prod/reportSizesPlugin.js} +5 -2
  16. package/src/plugins-prod/sitemapPlugin.js +20 -0
  17. package/src/{app-router.js → router.js} +20 -20
  18. /package/src/{openInBrowser.js → plugins-dev/openInBrowser.js} +0 -0
  19. /package/src/{watcherDev.js → plugins-dev/watcherDev.js} +0 -0
  20. /package/src/{HtmlCompiler.test.js → plugins-prod/HtmlCompiler.test.js} +0 -0
  21. /package/src/{media-remaper.test.js → plugins-prod/media-remaper.test.js} +0 -0
  22. /package/src/{minifyCSS.js → plugins-prod/minifyCSS.js} +0 -0
  23. /package/src/{minifyCSS.test.js → plugins-prod/minifyCSS.test.js} +0 -0
  24. /package/src/{minifyHTML.js → plugins-prod/minifyHTML.js} +0 -0
  25. /package/src/{minifyHTML.test.js → plugins-prod/minifyHTML.test.js} +0 -0
  26. /package/src/{minifyJS.js → plugins-prod/minifyJS.js} +0 -0
  27. /package/src/{fs-utils.js → utils/fs-utils.js} +0 -0
  28. /package/src/{fs-utils.test.js → utils/fs-utils.test.js} +0 -0
  29. /package/src/{http-response.js → utils/http-response.js} +0 -0
  30. /package/src/{mimes.js → utils/mimes.js} +0 -0
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/media-remaper.js) appends a SHA-1 hash
15
+ For long-term caching, [media-remaper.js](src/plugins-prod/media-remaper.js) appends a 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 .media are not automatically copied over,
48
+ - Non-Documents and Files outside /static are not automatically copied over,
49
49
  you need to specify them.
50
- - src/media only files at the top level get hashed-named. But files within subdirs are not (by design).
50
+ - static/media only files at the top level get hashed-named. But files within subdirs are not (by design).
package/TODO.md CHANGED
@@ -6,3 +6,4 @@
6
6
  - remap media in css and js
7
7
  - watch newly added routes
8
8
  - todo test on linux (open in browser too)
9
+ - refactor to plugins/prod and plugins/dev
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface Config {
2
2
  mode?: 'development' | 'production';
3
3
  srcPath?: string
4
+ staticDir?: string
4
5
  ignore?: RegExp
5
6
 
6
7
 
@@ -11,7 +12,7 @@ export interface Config {
11
12
  hotReload?: boolean // For UI dev purposes only
12
13
 
13
14
  // Production
14
- outputPath?: string
15
+ outputDir?: string
15
16
  outputExtension?: string
16
17
  minifyJS?: (js: string) => Promise<string>
17
18
  minifyCSS?: (css: string) => Promise<string>
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Packaton } from './src/app.js'
2
- export { HtmlCompiler } from './src/HtmlCompiler.js'
3
- export { minifyHTML } from './src/minifyHTML.js'
4
- export { minifyCSS } from './src/minifyCSS.js'
5
- export { minifyJS } from './src/minifyJS.js'
6
- export { reportSizes } from './src/reportSizes.js'
2
+ export { HtmlCompiler } from './src/plugins-prod/HtmlCompiler.js'
3
+ export { minifyHTML } from './src/plugins-prod/minifyHTML.js'
4
+ export { minifyCSS } from './src/plugins-prod/minifyCSS.js'
5
+ export { minifyJS } from './src/plugins-prod/minifyJS.js'
6
+ export { reportSizesPlugin } from './src/plugins-prod/reportSizesPlugin.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "packaton",
3
- "version": "0.0.3",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "author": "Eric Fortis",
6
6
  "license": "MIT",
package/src/app-dev.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import http from 'node:http'
2
2
 
3
- import { router } from './app-router.js'
4
- import { watchDev } from './WatcherDevClient.js'
3
+ import { router } from './router.js'
4
+ import { watchDev } from './plugins-dev/WatcherDevClient.js'
5
5
 
6
6
 
7
7
  /**
package/src/app-prod.js CHANGED
@@ -1,13 +1,16 @@
1
- import http from 'node:http'
2
1
  import { cpSync } from 'node:fs'
2
+ import { createServer } from 'node:http'
3
3
  import { basename, join, dirname } from 'node:path'
4
4
 
5
5
  import { docs } from './app.js'
6
- import { router } from './app-router.js'
7
- import { reportSizes } from './reportSizes.js'
8
- import { HtmlCompiler } from './HtmlCompiler.js'
9
- import { write, removeDir } from './fs-utils.js'
10
- import { renameMediaWithHashes } from './media-remaper.js'
6
+ import { router } from './router.js'
7
+ import { HtmlCompiler } from './plugins-prod/HtmlCompiler.js'
8
+ import { sitemapPlugin } from './plugins-prod/sitemapPlugin.js'
9
+ import { reportSizesPlugin } from './plugins-prod/reportSizesPlugin.js'
10
+ import { write, removeDir, } from './utils/fs-utils.js'
11
+ import { cspNginxMapPlugin } from './plugins-prod/cspNginxMapPlugin.js'
12
+ import { renameMediaWithHashes } from './plugins-prod/media-remaper.js'
13
+ import { netiflyAndCloudflareHeadersPlugin } from './plugins-prod/netiflyAndCloudflareHeadersPlugin.js'
11
14
 
12
15
 
13
16
  /**
@@ -16,24 +19,20 @@ import { renameMediaWithHashes } from './media-remaper.js'
16
19
  */
17
20
  export async function buildStaticPages(config) {
18
21
  return new Promise((resolve, reject) => {
22
+ const MEDIA_REL_URL = join(config.staticDir, 'media')
23
+
19
24
  const pSource = config.srcPath
20
- const pDist = config.outputPath
21
- const pDistMedia = join(config.outputPath, 'media')
22
- const pDistSitemap = join(pDist, 'sitemap.txt')
23
- const pDistCspNginxMap = join(pDist, '.csp-map.nginx')
24
- const pSizesReport = 'packed-sizes.json'
25
+ const pDist = config.outputDir
26
+ const pDistStatic = join(config.outputDir, config.staticDir)
27
+ const pDistMedia = join(pDist, MEDIA_REL_URL)
25
28
 
26
- const server = http.createServer(router(config))
27
- server.listen(0, '127.0.0.1', async error => {
29
+ const server = createServer(router(config))
30
+ server.on('error', reject)
31
+ server.listen(0, '127.0.0.1', async () => {
28
32
  docs.init(config.srcPath, config.ignore)
29
33
  try {
30
- if (error) {
31
- reject(error)
32
- return
33
- }
34
-
35
34
  removeDir(pDist)
36
- cpSync(join(pSource, 'media'), pDistMedia, {
35
+ cpSync(join(pSource, config.staticDir), pDistStatic, {
37
36
  recursive: true,
38
37
  dereference: true,
39
38
  filter(src) {
@@ -48,7 +47,12 @@ export async function buildStaticPages(config) {
48
47
 
49
48
  const cspByRoute = []
50
49
  for (const [route, rawHtml] of pages) {
51
- const doc = new HtmlCompiler(rawHtml, join(pSource, dirname(route)), config)
50
+ const doc = new HtmlCompiler(rawHtml, join(pSource, dirname(route)), {
51
+ minifyJS: config.minifyJS,
52
+ minifyCSS: config.minifyCSS,
53
+ minifyHTML: config.minifyHTML,
54
+ mediaRelUrl: MEDIA_REL_URL
55
+ })
52
56
  await doc.minifyHTML()
53
57
  doc.remapMedia(mediaHashes)
54
58
  // TODO remap media in css and js
@@ -58,24 +62,10 @@ export async function buildStaticPages(config) {
58
62
  cspByRoute.push([route, doc.csp()])
59
63
  }
60
64
 
61
- if (config.sitemapDomain)
62
- write(pDistSitemap, docs.routes
63
- .filter(r => r !== '/index')
64
- .map(r => `https://${config.sitemapDomain + r}`)
65
- .join('\n'))
66
-
67
- if (config.cspMapEnabled) {
68
- write(pDistCspNginxMap, cspByRoute.map(([route, csp]) =>
69
- `${route} "${csp}";`).join('\n'))
70
-
71
- // cloudflare
72
- write(pDistMedia + '/_headers', cspByRoute.map(([route, csp]) => {
73
- const r = route === '/index' ? '/' : route
74
- return `${r}\n Content-Security-Policy: ${csp}`
75
- }).join('\n'))
76
- }
77
-
78
- reportSizes(pSizesReport, pDist, docs.routes.map(f => f + config.outputExtension))
65
+ sitemapPlugin(config, docs.routes)
66
+ reportSizesPlugin(config, docs.routes)
67
+ cspNginxMapPlugin(config, cspByRoute)
68
+ netiflyAndCloudflareHeadersPlugin(config, cspByRoute, MEDIA_REL_URL)
79
69
  }
80
70
  catch (error) {
81
71
  reject(error)
package/src/app.js CHANGED
@@ -2,7 +2,7 @@ import { readdirSync } from 'node:fs'
2
2
  import { basename, join, dirname } from 'node:path'
3
3
 
4
4
  import { setup } from './config.js'
5
- import { isFile } from './fs-utils.js'
5
+ import { isFile } from './utils/fs-utils.js'
6
6
  import { devStaticPages } from './app-dev.js'
7
7
  import { buildStaticPages } from './app-prod.js'
8
8
 
package/src/config.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { resolve } from 'node:path'
2
2
 
3
- import { isDirectory } from './fs-utils.js'
4
- import { openInBrowser } from './openInBrowser.js'
3
+ import { isDirectory } from './utils/fs-utils.js'
4
+ import { openInBrowser } from './plugins-dev/openInBrowser.js'
5
5
 
6
- import { minifyJS } from './minifyJS.js'
7
- import { minifyCSS } from './minifyCSS.js'
8
- import { minifyHTML } from './minifyHTML.js'
6
+ import { minifyJS } from './plugins-prod/minifyJS.js'
7
+ import { minifyCSS } from './plugins-prod/minifyCSS.js'
8
+ import { minifyHTML } from './plugins-prod/minifyHTML.js'
9
9
 
10
10
 
11
11
  /** @type {{
@@ -17,6 +17,7 @@ import { minifyHTML } from './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
21
  ignore: [/^_/, optional(RegExp)],
21
22
 
22
23
  // Development
@@ -27,7 +28,7 @@ const schema = {
27
28
 
28
29
  // Production
29
30
  outputExtension: ['.html', optional(String)],
30
- outputPath: ['dist', optional(String)], // TODO resolve
31
+ outputDir: ['dist', optional(String)], // TODO resolve
31
32
  minifyJS: [minifyJS, optional(Function)],
32
33
  minifyCSS: [minifyCSS, optional(Function)],
33
34
  minifyHTML: [minifyHTML, optional(Function)],
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { watch } from 'node:fs'
3
3
  import { EventEmitter } from 'node:events'
4
- import { docs } from './app.js'
4
+ import { docs } from '../app.js'
5
5
 
6
6
 
7
7
  export const devClientWatcher = new class extends EventEmitter {
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { createHash } from 'node:crypto'
3
3
 
4
- import { read } from './fs-utils.js'
4
+ import { read } from '../utils/fs-utils.js'
5
5
  import { remapMediaInHTML } from './media-remaper.js'
6
6
 
7
7
 
@@ -10,6 +10,7 @@ export class HtmlCompiler {
10
10
  pSource = ''
11
11
  css = ''
12
12
  scriptsJs = ''
13
+ mediaRelUrl = ''
13
14
  scriptsNonJs = ''
14
15
  externalScripts = []
15
16
  externalCSS = []
@@ -17,12 +18,13 @@ export class HtmlCompiler {
17
18
  #minifyCSS = a => a
18
19
  #minifyHTML = a => a
19
20
 
20
- constructor(html, pSource = '', { minifyJS, minifyCSS, minifyHTML }) {
21
+ constructor(html, pSource = '', { minifyJS, minifyCSS, minifyHTML, mediaRelUrl }) {
21
22
  this.html = html
22
23
  this.pSource = pSource
23
24
  this.#minifyJS = minifyJS
24
25
  this.#minifyCSS = minifyCSS
25
26
  this.#minifyHTML = minifyHTML
27
+ this.mediaRelUrl = mediaRelUrl
26
28
  }
27
29
 
28
30
  // Removes comments and format multi-line tags (needed for `removeLineContaining`)
@@ -31,7 +33,7 @@ export class HtmlCompiler {
31
33
  }
32
34
 
33
35
  remapMedia(mediaHashes) {
34
- this.html = remapMediaInHTML(mediaHashes, this.html)
36
+ this.html = remapMediaInHTML(mediaHashes, this.html, this.mediaRelUrl)
35
37
  }
36
38
 
37
39
  async inlineMinifiedCSS() {
@@ -0,0 +1,9 @@
1
+ import { write } from '../utils/fs-utils.js'
2
+ import { join } from 'node:path'
3
+
4
+
5
+ export function cspNginxMapPlugin(config, cspByRoute) {
6
+ const out = join(config.outputDir, '.csp-map.nginx')
7
+ write(out, cspByRoute.map(([route, csp]) =>
8
+ `${route} "${csp}";`).join('\n'))
9
+ }
@@ -1,6 +1,6 @@
1
1
  import { join, parse } from 'node:path'
2
2
  import { renameSync } from 'node:fs'
3
- import { sha1, listFiles } from './fs-utils.js'
3
+ import { sha1, listFiles } from '../utils/fs-utils.js'
4
4
 
5
5
 
6
6
  /**
@@ -21,7 +21,6 @@ export async function renameMediaWithHashes(dir) {
21
21
  return mediaHashes
22
22
  }
23
23
 
24
- // TODO if media is made configurable, we'd need to espace the regex for example .media -> \.media
25
24
  // Having one dir is kinda nice for nginx headers, but that's not an excuse nor solves nested dirs with same filename
26
25
 
27
26
  // TODO for (b of base) find and replace base with new hash
@@ -35,17 +34,22 @@ export async function renameMediaWithHashes(dir) {
35
34
  * If you want to handle CSS files, edit the regex so
36
35
  * instead of checking `="` (e.g. src="img.png") also checks for `url(`
37
36
  **/
38
- export function remapMediaInHTML(mediaHashes, html) {
39
- const reFindMedia = new RegExp('(="media/.*?)"', 'g')
40
- const reFindMediaKey = new RegExp('="media/')
37
+ 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
41
 
42
42
  for (const [, url] of html.matchAll(reFindMedia)) {
43
43
  const hashedName = mediaHashes.get(url.replace(reFindMediaKey, ''))
44
44
  if (!hashedName)
45
45
  throw `ERROR: Missing ${url}\n`
46
- html = html.replace(url, `="media/${hashedName}`)
46
+ html = html.replace(url, `="${mediaRelUrl}/${hashedName}`)
47
47
  }
48
48
  return html
49
49
  }
50
50
 
51
51
 
52
+ function escapeForRegex(literal) {
53
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
54
+ }
55
+
@@ -0,0 +1,19 @@
1
+ import { join } from 'node:path'
2
+ import { write } from '../utils/fs-utils.js'
3
+
4
+
5
+ export function netiflyAndCloudflareHeadersPlugin(config, cspByRoute, MEDIA_URL) {
6
+ const out = join(join(config.outputDir, config.staticDir), '_headers')
7
+
8
+ const cspHeaders = cspByRoute.map(([route, csp]) => {
9
+ const r = route === '/index'
10
+ ? '/'
11
+ : route
12
+ return `${r}\n Content-Security-Policy: ${csp}`
13
+ })
14
+ cspHeaders.push(`${MEDIA_URL}/*`)
15
+ cspHeaders.push(' Cache-Control: public,max-age=31536000,immutable')
16
+
17
+ write(out, cspHeaders.join('\n'))
18
+ }
19
+
@@ -1,8 +1,11 @@
1
1
  import { join } from 'node:path'
2
- import { read, sizeOf, sha1, saveAsJSON, isFile } from './fs-utils.js'
2
+ import { read, sizeOf, sha1, saveAsJSON, isFile } from '../utils/fs-utils.js'
3
3
 
4
4
 
5
- export function reportSizes(reportFilename, baseDir, files) {
5
+ export function reportSizesPlugin(config, routes) {
6
+ const files = routes.map(f => f + config.outputExtension)
7
+ const baseDir = config.outputDir
8
+ const reportFilename = 'packed-sizes.json'
6
9
  const oldReport = isFile(reportFilename)
7
10
  ? JSON.parse(read(reportFilename))
8
11
  : {}
@@ -0,0 +1,20 @@
1
+ import { join } from 'node:path'
2
+ import { write, isFile } from '../utils/fs-utils.js'
3
+
4
+
5
+ export function sitemapPlugin(config, routes) {
6
+ if (!config.sitemapDomain)
7
+ return
8
+
9
+ const outMap = join(config.outputDir, 'sitemap.txt')
10
+ const outRobots = join(config.outputDir, 'robots.txt')
11
+
12
+ write(outMap, routes
13
+ .filter(r => r !== '/index')
14
+ .map(r => `https://${config.sitemapDomain + r}`)
15
+ .join('\n'))
16
+
17
+ if (!isFile(outRobots))
18
+ write(outRobots,
19
+ `Sitemap: https://${config.sitemapDomain}/sitemap.txt`)
20
+ }
@@ -2,43 +2,43 @@ import { join } from 'node:path'
2
2
  import { readFile } from 'node:fs/promises'
3
3
 
4
4
  import { docs } from './app.js'
5
- import { mimeFor } from './mimes.js'
6
- import { devClientWatcher } from './WatcherDevClient.js'
7
- import { sendError, sendJSON, servePartialContent, serveStaticAsset } from './http-response.js'
5
+ import { mimeFor } from './utils/mimes.js'
6
+ import { devClientWatcher } from './plugins-dev/WatcherDevClient.js'
7
+ import { sendError, sendJSON, servePartialContent, serveStaticAsset } from './utils/http-response.js'
8
8
 
9
9
 
10
+ const WATCHER_DEV = '/plugins-dev/watcherDev.js'
11
+
10
12
  const API = {
11
13
  watchDev: '/packaton/watch-dev'
12
14
  }
13
15
 
14
- const WATCHER_DEV = '/watcherDev.js'
15
16
 
16
17
 
17
18
  /** @param {Config} config */
18
19
  export function router({ srcPath, ignore, mode }) {
19
20
  docs.init(srcPath, ignore)
21
+ const isDev = mode === 'development'
20
22
  return async function (req, response) {
21
23
  let url = new URL(req.url, 'http://_').pathname
22
24
  try {
23
- if (url === API.watchDev) {
25
+ if (url === API.watchDev)
24
26
  longPollDevHotReload(req, response)
25
- return
26
- }
27
- if (url === WATCHER_DEV) {
27
+
28
+ else if (url === WATCHER_DEV)
28
29
  serveStaticAsset(response, join(import.meta.dirname, url))
29
- return
30
- }
31
-
32
- if (url === '/')
33
- url = '/index'
34
-
35
- const file = join(srcPath, url)
36
- if (docs.hasRoute(url))
37
- await serveDocument(response, docs.fileFor(url), mode === 'development')
30
+
31
+ else if (docs.hasRoute(url))
32
+ await serveDocument(response, docs.fileFor(url), isDev)
33
+
34
+ else if (docs.hasRoute(join(url, 'index')))
35
+ await serveDocument(response, docs.fileFor(join(url, 'index')), isDev)
36
+
38
37
  else if (req.headers.range)
39
- await servePartialContent(response, req.headers, file)
38
+ await servePartialContent(response, req.headers, join(srcPath, url))
39
+
40
40
  else
41
- serveStaticAsset(response, file)
41
+ serveStaticAsset(response, join(srcPath, url))
42
42
  }
43
43
  catch (error) {
44
44
  sendError(response, error)
@@ -51,7 +51,7 @@ async function serveDocument(response, file, isDev) {
51
51
  ? await readFile(file, 'utf8')
52
52
  : (await import(file + '?' + Date.now())).default()
53
53
  if (isDev)
54
- html += `<script type="module" src="${WATCHER_DEV}"></script>`
54
+ html += `<script type="module" src="${WATCHER_DEV}"></script>`
55
55
  response.setHeader('Content-Type', mimeFor('html'))
56
56
  response.end(html)
57
57
  }
File without changes
File without changes
File without changes
File without changes