packaton 0.0.24 → 0.0.26

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
@@ -1,6 +1,6 @@
1
1
  # Packaton WIP
2
2
 
3
- Static Site Generator (SSG). Inlines CSS and JS and
3
+ Static Site Generator (SSG). Inlines CSS and JS and
4
4
  creates a header file with their corresponding CSP hashes.
5
5
 
6
6
  ## Usage Example
@@ -32,11 +32,10 @@ If you want to use media files in CSS, create a similar function to
32
32
 
33
33
  ## Production Build
34
34
  It crawls the dev server, and saves each route as static html page.
35
- It saves the pages without the `.html` extension for pretty URLs.
35
+ It saves the pages without the `.html` extension for pretty URLs.
36
36
  See [Pretty routes for static HTML](https://blog.uxtly.com/pretty-routes-for-static-html)
37
37
 
38
38
 
39
-
40
39
  ## Minifiers
41
40
 
42
41
  ```js
package/index.d.ts CHANGED
@@ -3,15 +3,14 @@ export interface Config {
3
3
  srcPath?: string
4
4
  assetsDir?: string
5
5
  ignore?: RegExp
6
-
7
6
 
8
7
  // Dev
9
- host?: string,
10
- port?: number
11
- onReady?: (address: string) => void
12
- hotReload?: boolean // For UI dev purposes only
13
- watchIgnore?: Array<string|RegExp>
14
-
8
+ host?: string
9
+ port?: number
10
+ onReady?: (address: string) => void
11
+ hotReload?: boolean // For UI dev purposes only
12
+ watchIgnore?: Array<string | RegExp>
13
+
15
14
  // Production
16
15
  outputDir?: string
17
16
  outputExtension?: string
@@ -20,5 +19,5 @@ export interface Config {
20
19
  minifyHTML?: (html: string) => Promise<string>
21
20
  sitemapDomain?: string
22
21
  cspMapEnabled?: boolean
22
+ routeHeaders?: [name: string, value: string][]
23
23
  }
24
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "packaton",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "author": "Eric Fortis",
6
6
  "license": "MIT",
package/src/app-prod.js CHANGED
@@ -1,6 +1,6 @@
1
- import { cpSync } from 'node:fs'
2
1
  import { createServer } from 'node:http'
3
2
  import { basename, join } from 'node:path'
3
+ import { cpSync, existsSync } from 'node:fs'
4
4
 
5
5
  import { docs } from './app.js'
6
6
  import { router } from './router.js'
@@ -41,9 +41,25 @@ export async function buildStaticPages(config) {
41
41
  }
42
42
  })
43
43
 
44
+
45
+ const wellKnownDir = join(pSource, '.well-known')
46
+ if (existsSync(wellKnownDir))
47
+ cpSync(wellKnownDir, join(pDist, '.well-known'), {
48
+ recursive: true,
49
+ dereference: true,
50
+ filter(src) {
51
+ return basename(src) !== '.DS_Store'
52
+ }
53
+ })
54
+
44
55
  const pages = await crawlRoutes(server.address(), docs.routes)
45
56
  const mediaHashes = await renameMediaWithHashes(pDist, MEDIA_REL_URL)
46
-
57
+
58
+ const headers = {
59
+ ['/' + MEDIA_REL_URL + '/*']: [
60
+ ['Cache-Control', 'public,max-age=31536000,immutable']
61
+ ]
62
+ }
47
63
  const cspByRoute = []
48
64
  for (const [route, rawHtml] of pages) {
49
65
  const doc = new HtmlCompiler(rawHtml, pSource, {
@@ -52,7 +68,7 @@ export async function buildStaticPages(config) {
52
68
  minifyHTML: config.minifyHTML,
53
69
  mediaRelUrl: MEDIA_REL_URL,
54
70
  mediaHashes
55
-
71
+
56
72
  })
57
73
  await doc.minifyHTML()
58
74
  doc.remapMedia()
@@ -60,13 +76,18 @@ export async function buildStaticPages(config) {
60
76
  await doc.inlineMinifiedCSS()
61
77
  await doc.inlineMinifiedJS()
62
78
  write(join(pDist, route + config.outputExtension), doc.html)
63
- cspByRoute.push([route, doc.csp()])
79
+
80
+ const r = route === '/index' ? '/' : route
81
+ headers[r] ??= []
82
+ headers[r].push(['Content-Security-Policy', doc.csp()])
83
+ for (const h of config.routeHeaders)
84
+ headers[r].push(h)
64
85
  }
65
86
 
66
87
  sitemapPlugin(config, docs.routes)
67
88
  reportSizesPlugin(config, docs.routes)
68
89
  cspNginxMapPlugin(config, cspByRoute)
69
- netiflyAndCloudflareHeadersPlugin(config, cspByRoute, MEDIA_REL_URL)
90
+ write(join(config.outputDir, '_headers'), netiflyAndCloudflareHeadersPlugin(headers))
70
91
  }
71
92
  catch (error) {
72
93
  reject(error)
package/src/config.js CHANGED
@@ -35,6 +35,7 @@ const schema = {
35
35
  minifyHTML: [minifyHTML, optional(Function)],
36
36
  sitemapDomain: ['', optional(String)],
37
37
  cspMapEnabled: [true, optional(Boolean)],
38
+ routeHeaders: [[], Array.isArray]
38
39
  }
39
40
  // TODO watch New Routes?
40
41
 
@@ -1,6 +1,6 @@
1
1
  // We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
2
2
  export async function resolve(specifier, context, nextResolve) {
3
- const result = await nextResolve(specifier, context);
3
+ const result = await nextResolve(specifier, context)
4
4
  if (result.url?.startsWith('file:')) {
5
5
  const url = new URL(result.url)
6
6
  url.searchParams.set('t', performance.now())
@@ -1,17 +1,18 @@
1
1
  const WATCH_API = '/packaton/watch-dev'
2
2
 
3
- let es = null
3
+ let conn = null
4
4
  let timer = null
5
5
 
6
6
  window.addEventListener('beforeunload', teardown)
7
7
  connect()
8
8
  function connect() {
9
- if (es) return
9
+ if (conn)
10
+ return
10
11
 
11
12
  clearTimeout(timer)
12
- es = new EventSource(WATCH_API)
13
+ conn = new EventSource(WATCH_API)
13
14
 
14
- es.onmessage = function (event) {
15
+ conn.onmessage = function (event) {
15
16
  const file = event.data
16
17
  if (file.endsWith('.css'))
17
18
  hotReloadCSS(file)
@@ -19,7 +20,7 @@ function connect() {
19
20
  location.reload()
20
21
  }
21
22
 
22
- es.onerror = function () {
23
+ conn.onerror = function () {
23
24
  console.error('hot reload')
24
25
  teardown()
25
26
  timer = setTimeout(connect, 3000)
@@ -28,8 +29,8 @@ function connect() {
28
29
 
29
30
  function teardown() {
30
31
  clearTimeout(timer)
31
- es?.close()
32
- es = null
32
+ conn?.close()
33
+ conn = null
33
34
  }
34
35
 
35
36
  async function hotReloadCSS(file) {
@@ -72,7 +72,7 @@ export class HtmlCompiler {
72
72
  this.scriptsModuleJs = await Promise.all(scripts
73
73
  .filter(([type]) => type === 'module')
74
74
  .map(([, body]) => this.#minifyJS(body, Boolean('isModule'))))
75
-
75
+
76
76
  this.scriptsNonJs = scripts
77
77
  .filter(([type]) => type !== 'application/javascript' && type !== 'module')
78
78
 
@@ -89,13 +89,13 @@ export class HtmlCompiler {
89
89
 
90
90
  csp() {
91
91
  const cssHash = this.css ? `'${this.hash256(this.css)}'` : '' // TODO maybe self?
92
-
92
+
93
93
  const jsScriptHash = this.scriptsJs ? `'${this.hash256(this.scriptsJs)}'` : '' // TODO maybe self?
94
94
  const jsModulesHashes = this.scriptsModuleJs.map(body => `'${this.hash256(body)}'`).join(' ')
95
95
  const nonJsScriptHashes = this.scriptsNonJs.map(([, body]) => `'${this.hash256(body)}'`).join(' ')
96
96
  const inlineScriptsHashes = this.extractInlineScripts().map(body => `'${this.hash256(body)}'`).join(' ')
97
97
  const externalScriptDomains = this.externalScripts.map(url => `${new URL(url).origin}`).join(' ')
98
-
98
+
99
99
  return [
100
100
  `default-src 'self'`,
101
101
  `img-src 'self' data:`, // data: is for Safari's video player icons and for CSS bg images
@@ -121,7 +121,7 @@ export class HtmlCompiler {
121
121
  this.html = this.html.replace(new RegExp('^.*' + str + '.*\n', 'm'), '')
122
122
  }
123
123
 
124
-
124
+
125
125
  extractStyleSheetHrefs() {
126
126
  const reExtractStyleSheets = /(?<=<link\s.*href=")[^"]+\.css/g
127
127
  return Array.from(this.html.matchAll(reExtractStyleSheets), m => m[0])
@@ -147,14 +147,14 @@ export class HtmlCompiler {
147
147
  ]
148
148
  })
149
149
  }
150
-
150
+
151
151
  extractInlineScripts() {
152
152
  const reExtractInlineScripts = /<script\b([^>]*?)>([\s\S]*?)<\/script>/g
153
153
  return Array.from(this.html.matchAll(reExtractInlineScripts), m => {
154
154
  const attrs = m[1]
155
155
  const body = m[2]
156
- return attrs.includes('src=')
157
- ? null
156
+ return attrs.includes('src=')
157
+ ? null
158
158
  : body
159
159
  }).filter(Boolean)
160
160
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * You can override this minifier in `config.minifyCSS`
3
- *
3
+ *
4
4
  * This program is an oversimplified CSS minifier. It doesn’t
5
5
  * try to minify everything, but only what’s safe and easy to minify.
6
- *
6
+ *
7
7
  * Why?
8
- * When I wrote this program, ~2018, some CSS minifiers reordered rules
8
+ * When I wrote this program, ~2018, some CSS minifiers reordered rules
9
9
  * but that messed up browser-specific prefixes that were used as workarounds.
10
10
  */
11
11
 
@@ -6,15 +6,15 @@
6
6
  * It's based on https://gist.github.com/espretto/1b3cb7e8b01fa7daaaac
7
7
  *
8
8
  * Why?
9
- * When I wrote this program, ~2018, I tried a few libraries but some
9
+ * When I wrote this program, ~2018, I tried a few libraries but some
10
10
  * of them messed up relevant spaces in `<pre>` tags and between tags.
11
11
  *
12
12
  * We don’t remove newlines because for example `<kbd>` and `<a>`
13
13
  * would need special rules to have a space in-place of that newline.
14
14
  *
15
- *
15
+ *
16
16
  * This algorithm basically collects parts that should not be minified and
17
- * replaces them with a known magic string "<preserved>". Then, at the end,
17
+ * replaces them with a known magic string "<preserved>". Then, at the end,
18
18
  * replaces, in order, those magic strings with the original tag and its content.
19
19
  */
20
20
 
@@ -1,28 +1,10 @@
1
- import { join } from 'node:path'
2
- import { write } from '../utils/fs-utils.js'
3
-
4
-
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')
12
-
13
- const cspHeaders = cspByRoute.map(([route, csp]) => {
14
- const r = route === '/index'
15
- ? '/'
16
- : route
17
- return [
18
- r,
19
- ` Content-Security-Policy: ${csp}`,
20
- ` Cache-Control: public,max-age=60`
21
- ].join('\n')
22
- })
23
- cspHeaders.push(`/${relMediaURL}/*`)
24
- cspHeaders.push(' Cache-Control: public,max-age=31536000,immutable')
25
-
26
- write(out, cspHeaders.join('\n'))
1
+ export function netiflyAndCloudflareHeadersPlugin(opts) {
2
+ let result = []
3
+ for (const [route, headers] of Object.entries(opts)) {
4
+ result.push(route)
5
+ for (const [h, v] of headers)
6
+ result.push(` ${h}: ${v}`)
7
+ }
8
+ return result.join('\n')
27
9
  }
28
10
 
@@ -3,18 +3,20 @@ import { write, isFile } from '../utils/fs-utils.js'
3
3
 
4
4
 
5
5
  export function sitemapPlugin(config, routes) {
6
- if (!config.sitemapDomain)
6
+ if (!config.sitemapDomain)
7
7
  return
8
-
8
+
9
9
  const outMap = join(config.outputDir, 'sitemap.txt')
10
10
  const outRobots = join(config.outputDir, 'robots.txt')
11
-
11
+
12
12
  write(outMap, routes
13
13
  .filter(r => r !== '/index')
14
14
  .map(r => `https://${config.sitemapDomain + r}`)
15
15
  .join('\n'))
16
16
 
17
17
  if (!isFile(outRobots))
18
- write(outRobots,
19
- `Sitemap: https://${config.sitemapDomain}/sitemap.txt`)
18
+ write(outRobots, `
19
+ User-agent: *
20
+ Sitemap: https://${config.sitemapDomain}/sitemap.txt
21
+ `.trim())
20
22
  }
@@ -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, renameSync } from 'node:fs'
4
+ import { rmSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
5
 
6
6
 
7
7
  export const read = f => readFileSync(f, 'utf8')
@@ -16,11 +16,11 @@ export function write(fname, data) {
16
16
  writeFileSync(fname, data, 'utf8')
17
17
  }
18
18
 
19
- export function removeDir(dir) {
20
- rmSync(dir, {
21
- recursive: true,
19
+ export function removeDir(dir) {
20
+ rmSync(dir, {
21
+ recursive: true,
22
22
  force: true // allows for removing non-existing directories
23
- })
23
+ })
24
24
  }
25
25
 
26
26
  export const sizeOf = f => lstatSync(f).size
@@ -4,18 +4,18 @@ import { replaceExt } from './fs-utils.js'
4
4
 
5
5
 
6
6
  describe('replaceExt', () => {
7
- test('replaces a simple extension', () =>
7
+ test('replaces a simple extension', () =>
8
8
  equal(replaceExt('file.txt', 'md'), 'file.md'))
9
9
 
10
- test('replaces a multi-part extension', () =>
10
+ test('replaces a multi-part extension', () =>
11
11
  equal(replaceExt('archive.tar.gz', 'zip'), 'archive.tar.zip'))
12
12
 
13
- test('adds extension when none exists', () =>
13
+ test('adds extension when none exists', () =>
14
14
  equal(replaceExt('README', 'md'), 'README.md'))
15
15
 
16
16
  test('handles empty filename', () =>
17
17
  equal(replaceExt('', 'ext'), '.ext'))
18
-
18
+
19
19
  test('handles dot-files', () =>
20
20
  equal(replaceExt('.env', 'txt'), '.env.txt'))
21
21
  })