@eighty4/dank 0.0.4-1 → 0.0.4-3

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/lib/dank.ts CHANGED
@@ -13,6 +13,8 @@ export type DankConfig = {
13
13
  // cdn url rewriting can be simulated with PageMapping
14
14
  pages: Record<`/${string}`, `${string}.html` | PageMapping>
15
15
 
16
+ devPages?: Record<`/__${string}`, `${string}.html` | DevPageMapping>
17
+
16
18
  // port of `dank serve` frontend dev server
17
19
  // used for `dan serve --preview` if previewPort not specified
18
20
  port?: number
@@ -32,6 +34,11 @@ export type PageMapping = {
32
34
  webpage: `${string}.html`
33
35
  }
34
36
 
37
+ export type DevPageMapping = {
38
+ label: string
39
+ webpage: `${string}.html`
40
+ }
41
+
35
42
  export type DevService = {
36
43
  command: string
37
44
  cwd?: string
@@ -66,157 +73,21 @@ export type EsbuildLoader =
66
73
  | 'json'
67
74
  | 'text'
68
75
 
69
- export async function defineConfig(
70
- c: Partial<DankConfig>,
71
- ): Promise<DankConfig> {
72
- if (c.port !== null && typeof c.port !== 'undefined') {
73
- if (typeof c.port !== 'number') {
74
- throw Error('DankConfig.port must be a number')
75
- }
76
- }
77
- if (c.previewPort !== null && typeof c.previewPort !== 'undefined') {
78
- if (typeof c.previewPort !== 'number') {
79
- throw Error('DankConfig.previewPort must be a number')
80
- }
81
- }
82
- validatePages(c.pages)
83
- validateDevServices(c.services)
84
- validateEsbuildConfig(c.esbuild)
85
- normalizePagePaths(c.pages!)
86
- return c as DankConfig
87
- }
88
-
89
- function validateEsbuildConfig(esbuild?: EsbuildConfig) {
90
- if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
91
- if (typeof esbuild.loaders !== 'object') {
92
- throw Error(
93
- 'DankConfig.esbuild.loaders must be a map of extensions to esbuild loaders',
94
- )
95
- } else {
96
- for (const [ext, loader] of Object.entries(esbuild.loaders)) {
97
- if (typeof loader !== 'string') {
98
- throw Error(
99
- `DankConfig.esbuild.loaders['${ext}'] must be a string of a loader name`,
100
- )
101
- }
102
- }
103
- }
104
- }
105
- if (esbuild?.plugins !== null && typeof esbuild?.plugins !== 'undefined') {
106
- if (!Array.isArray(esbuild.plugins)) {
107
- throw Error(
108
- 'DankConfig.esbuild.plugins must be an array of esbuild plugins',
109
- )
110
- }
111
- }
112
- if (esbuild?.port !== null && typeof esbuild?.port !== 'undefined') {
113
- if (typeof esbuild.port !== 'number') {
114
- throw Error('DankConfig.esbuild.port must be a number')
115
- }
116
- }
76
+ // DankConfigFunction arg details about a dank process used when building DankConfig
77
+ export type DankDetails = {
78
+ dev: boolean
79
+ production: boolean
80
+ mode: 'build' | 'serve'
117
81
  }
118
82
 
119
- function validatePages(pages?: DankConfig['pages']) {
120
- if (
121
- pages === null ||
122
- typeof pages === 'undefined' ||
123
- Object.keys(pages).length === 0
124
- ) {
125
- throw Error('DankConfig.pages is required')
126
- }
127
- for (const [urlPath, mapping] of Object.entries(pages)) {
128
- if (typeof mapping === 'string' && mapping.endsWith('.html')) {
129
- continue
130
- }
131
- if (typeof mapping === 'object') {
132
- validatePageMapping(urlPath, mapping)
133
- continue
134
- }
135
- throw Error(
136
- `DankConfig.pages['${urlPath}'] must configure an html file`,
137
- )
138
- }
139
- }
140
-
141
- function validatePageMapping(urlPath: string, mapping: PageMapping) {
142
- if (
143
- mapping.webpage === null ||
144
- typeof mapping.webpage !== 'string' ||
145
- !mapping.webpage.endsWith('.html')
146
- ) {
147
- throw Error(
148
- `DankConfig.pages['${urlPath}'].webpage must configure an html file`,
149
- )
150
- }
151
- if (mapping.pattern === null || typeof mapping.pattern === 'undefined') {
152
- return
153
- }
154
- if (
155
- typeof mapping.pattern === 'object' &&
156
- mapping.pattern.constructor.name === 'RegExp'
157
- ) {
158
- return
159
- }
160
- throw Error(`DankConfig.pages['${urlPath}'].pattern must be a RegExp`)
161
- }
162
-
163
- function validateDevServices(services: DankConfig['services']) {
164
- if (services === null || typeof services === 'undefined') {
165
- return
166
- }
167
- if (!Array.isArray(services)) {
168
- throw Error(`DankConfig.services must be an array`)
169
- }
170
- for (let i = 0; i < services.length; i++) {
171
- const s = services[i]
172
- if (s.command === null || typeof s.command === 'undefined') {
173
- throw Error(`DankConfig.services[${i}].command is required`)
174
- } else if (typeof s.command !== 'string' || s.command.length === 0) {
175
- throw Error(
176
- `DankConfig.services[${i}].command must be a non-empty string`,
177
- )
178
- }
179
- if (s.cwd !== null && typeof s.cwd !== 'undefined') {
180
- if (typeof s.cwd !== 'string' || s.cwd.trim().length === 0) {
181
- throw Error(
182
- `DankConfig.services[${i}].cwd must be a non-empty string`,
183
- )
184
- }
185
- }
186
- if (s.env !== null && typeof s.env !== 'undefined') {
187
- if (typeof s.env !== 'object') {
188
- throw Error(
189
- `DankConfig.services[${i}].env must be an env variable map`,
190
- )
191
- }
192
- for (const [k, v] of Object.entries(s.env)) {
193
- if (typeof v !== 'string') {
194
- throw Error(
195
- `DankConfig.services[${i}].env[${k}] must be a string`,
196
- )
197
- }
198
- }
199
- }
200
- if (s.http !== null && typeof s.http !== 'undefined') {
201
- if (typeof s.http.port !== 'number') {
202
- throw Error(
203
- `DankConfig.services[${i}].http.port must be a number`,
204
- )
205
- }
206
- }
207
- }
208
- }
209
-
210
- function normalizePagePaths(pages: DankConfig['pages']) {
211
- for (const [pageUrl, mapping] of Object.entries(pages)) {
212
- if (typeof mapping === 'string') {
213
- pages[pageUrl as `/${string}`] = normalizePagePath(mapping)
214
- } else {
215
- mapping.webpage = normalizePagePath(mapping.webpage)
216
- }
217
- }
218
- }
83
+ export type DankConfigFunction = (
84
+ dank: DankDetails,
85
+ ) => Partial<DankConfig> | Promise<Partial<DankConfig>>
219
86
 
220
- function normalizePagePath(p: `${string}.html`): `${string}.html` {
221
- return p.replace(/^\.\//, '') as `${string}.html`
87
+ export function defineConfig(config: Partial<DankConfig>): Partial<DankConfig>
88
+ export function defineConfig(config: DankConfigFunction): DankConfigFunction
89
+ export function defineConfig(
90
+ config: Partial<DankConfig> | DankConfigFunction,
91
+ ): Partial<DankConfig> | DankConfigFunction {
92
+ return config
222
93
  }
package/lib/define.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DankBuild } from './flags.ts'
1
+ import type { ResolvedDankConfig } from './config.ts'
2
2
 
3
3
  export type DankGlobal = {
4
4
  IS_DEV: boolean
@@ -9,9 +9,11 @@ type DefineDankGlobalKey = 'dank.IS_DEV' | 'dank.IS_PROD'
9
9
 
10
10
  export type DefineDankGlobal = Record<DefineDankGlobalKey, string>
11
11
 
12
- export function createGlobalDefinitions(build: DankBuild): DefineDankGlobal {
12
+ export function createGlobalDefinitions(
13
+ c: ResolvedDankConfig,
14
+ ): DefineDankGlobal {
13
15
  return {
14
- 'dank.IS_DEV': JSON.stringify(!build.production),
15
- 'dank.IS_PROD': JSON.stringify(build.production),
16
+ 'dank.IS_DEV': JSON.stringify(!c.flags.production),
17
+ 'dank.IS_PROD': JSON.stringify(c.flags.production),
16
18
  }
17
19
  }
@@ -0,0 +1,146 @@
1
+ import { createWriteStream, type WriteStream } from 'node:fs'
2
+ import { mkdir, rm } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import { dirname, resolve } from 'node:path'
5
+ import packageJson from '../package.json' with { type: 'json' }
6
+
7
+ const CONSOLE =
8
+ process.env.DANK_LOG_CONSOLE === '1' ||
9
+ process.env.DANK_LOG_CONSOLE === 'true'
10
+ const FILE = process.env.DANK_LOG_FILE
11
+ const ROLLING =
12
+ process.env.DANK_LOG_ROLLING === '1' ||
13
+ process.env.DANK_LOG_ROLLING === 'true'
14
+
15
+ const logs: Array<string> = []
16
+ let initialized = false
17
+ let preparing: Promise<void>
18
+ let stream: WriteStream
19
+
20
+ export type LogEvent = {
21
+ realm:
22
+ | 'build'
23
+ | 'serve'
24
+ | 'assets'
25
+ | 'config'
26
+ | 'html'
27
+ | 'registry'
28
+ | 'services'
29
+ message: string
30
+ data?: Record<string, LogEventData>
31
+ }
32
+
33
+ type LogEventData =
34
+ | LogEventDatum
35
+ | Array<LogEventDatum>
36
+ | Set<LogEventDatum>
37
+ | Record<string, LogEventDatum>
38
+
39
+ type LogEventDatum = boolean | number | string | null | undefined
40
+
41
+ function toStringLogEvent(logEvent: LogEvent): string {
42
+ const when = new Date().toISOString()
43
+ const message = `[${logEvent.realm}] ${logEvent.message}\n${when}\n`
44
+ if (!logEvent.data) {
45
+ return message
46
+ }
47
+ let data = ''
48
+ for (const k of Object.keys(logEvent.data).sort()) {
49
+ data += `\n ${k} = ${toStringData(logEvent.data[k])}`
50
+ }
51
+ return `${message}${data}\n`
52
+ }
53
+
54
+ function toStringData(datum: LogEventData): string {
55
+ if (datum instanceof Set) {
56
+ datum = Array.from(datum)
57
+ }
58
+ if (
59
+ datum !== null &&
60
+ typeof datum === 'object' &&
61
+ datum.constructor.name === 'Object'
62
+ ) {
63
+ datum = Object.entries(datum).map(([k, v]) => `${k} = ${v}`)
64
+ }
65
+ if (Array.isArray(datum)) {
66
+ if (datum.length === 0) {
67
+ return '[]'
68
+ } else {
69
+ return `[\n ${datum.join('\n ')}\n ]`
70
+ }
71
+ } else {
72
+ return `${datum}`
73
+ }
74
+ }
75
+
76
+ function logToConsoleAndFile(out: string) {
77
+ logToConsole(out)
78
+ logToFile(out)
79
+ }
80
+
81
+ function logToConsole(out: string) {
82
+ console.log('\n' + out)
83
+ }
84
+
85
+ function logToFile(out: string) {
86
+ logs.push(out)
87
+ if (!initialized) {
88
+ initialized = true
89
+ preparing = prepareLogFile().catch(onPrepareLogFileError)
90
+ }
91
+ preparing.then(syncLogs)
92
+ }
93
+
94
+ async function prepareLogFile() {
95
+ const path = resolve(FILE!)
96
+ if (!ROLLING) {
97
+ await rm(path, { force: true })
98
+ }
99
+ await mkdir(dirname(path), { recursive: true })
100
+ stream = createWriteStream(path, { flags: 'a' })
101
+ console.log('debug logging to', FILE)
102
+ logSystemDetails()
103
+ }
104
+
105
+ function logSystemDetails() {
106
+ stream.write(`\
107
+ ---
108
+ os: ${os.type()}
109
+ build: ${os.version()}
110
+ cpu: ${os.arch()}
111
+ cores: ${os.availableParallelism()}
112
+ ${process.versions.bun ? `bun ${process.versions.bun}` : `node ${process.version}`}
113
+ dank: ${packageJson.version}
114
+ \n`)
115
+ }
116
+
117
+ function syncLogs() {
118
+ if (!logs.length) return
119
+ const content = logs.join('\n') + '\n'
120
+ logs.length = 0
121
+ stream.write(content)
122
+ }
123
+
124
+ function onPrepareLogFileError(e: any) {
125
+ console.error(`init log file \`${FILE}\` error: ${e.message}`)
126
+ process.exit(1)
127
+ }
128
+
129
+ function makeLogger(
130
+ logDelegate: (out: string) => void,
131
+ ): (logEvent: LogEvent) => void {
132
+ return logEvent => logDelegate(toStringLogEvent(logEvent))
133
+ }
134
+
135
+ export const LOG = (function resolveLogFn() {
136
+ if (CONSOLE && FILE?.length) {
137
+ return makeLogger(logToConsoleAndFile)
138
+ }
139
+ if (CONSOLE) {
140
+ return makeLogger(logToConsole)
141
+ }
142
+ if (FILE?.length) {
143
+ return makeLogger(logToFile)
144
+ }
145
+ return () => {}
146
+ })()
package/lib/dirs.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { realpath } from 'node:fs/promises'
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
3
+ import { cwd } from 'node:process'
4
+
5
+ export type DankDirectories = {
6
+ buildRoot: string
7
+ // output dir of html during `dank serve`
8
+ buildWatch: string
9
+ buildDist: string
10
+ pages: string
11
+ pagesResolved: string
12
+ projectResolved: string
13
+ projectRootAbs: string
14
+ public: string
15
+ }
16
+
17
+ export async function defaultProjectDirs(
18
+ projectRootAbs: string,
19
+ ): Promise<Readonly<DankDirectories>> {
20
+ if (!projectRootAbs) {
21
+ projectRootAbs = cwd()
22
+ } else if (!isAbsolute(projectRootAbs)) {
23
+ throw Error()
24
+ }
25
+ const projectResolved = await realpath(projectRootAbs)
26
+ const pages = 'pages'
27
+ const pagesResolved = join(projectResolved, pages)
28
+ return Object.freeze({
29
+ buildRoot: 'build',
30
+ buildDist: join('build', 'dist'),
31
+ buildWatch: join('build', 'watch'),
32
+ pages,
33
+ pagesResolved,
34
+ projectResolved,
35
+ projectRootAbs,
36
+ public: 'public',
37
+ })
38
+ }
39
+
40
+ export type ResolveError = 'outofbounds'
41
+
42
+ export class Resolver {
43
+ #dirs: DankDirectories
44
+
45
+ constructor(dirs: DankDirectories) {
46
+ this.#dirs = dirs
47
+ }
48
+
49
+ // cross platform safe absolute path resolution from pages dir
50
+ absPagesPath(...p: Array<string>): string {
51
+ return join(this.#dirs.projectRootAbs, this.#dirs.pages, ...p)
52
+ }
53
+
54
+ // cross platform safe absolute path resolution from project root
55
+ absProjectPath(...p: Array<string>): string {
56
+ return join(this.#dirs.projectRootAbs, ...p)
57
+ }
58
+
59
+ // `p` is expected to be a relative path resolvable from the project dir
60
+ isProjectSubpathInPagesDir(p: string): boolean {
61
+ return resolve(join(this.#dirs.projectResolved, p)).startsWith(
62
+ this.#dirs.pagesResolved,
63
+ )
64
+ }
65
+
66
+ // `p` is expected to be a relative path resolvable from the pages dir
67
+ isPagesSubpathInPagesDir(p: string): boolean {
68
+ return this.isProjectSubpathInPagesDir(join(this.#dirs.pages, p))
69
+ }
70
+
71
+ // resolve a pages subpath from a resource within the pages directory by a relative href
72
+ // `from` is expected to be a pages resource fs path starting with `pages/` and ending with filename
73
+ // the result will be a pages subpath and will not have the pages dir prefix
74
+ // returns 'outofbounds' if the relative path does not resolve to a file within the pages dir
75
+ resolveHrefInPagesDir(from: string, href: string): string | ResolveError {
76
+ const p = join(dirname(from), href)
77
+ if (this.isProjectSubpathInPagesDir(p)) {
78
+ return p
79
+ } else {
80
+ return 'outofbounds'
81
+ }
82
+ }
83
+ }
package/lib/errors.ts ADDED
@@ -0,0 +1,6 @@
1
+ export class DankError extends Error {
2
+ constructor(message: string, cause?: Error) {
3
+ super(message, { cause })
4
+ this.name = 'DankError'
5
+ }
6
+ }
package/lib/esbuild.ts CHANGED
@@ -9,45 +9,39 @@ import esbuild, {
9
9
  type Plugin,
10
10
  type PluginBuild,
11
11
  } from 'esbuild'
12
- import type { EsbuildConfig } from './dank.ts'
13
12
  import type { DefineDankGlobal } from './define.ts'
14
- import type { DankBuild } from './flags.ts'
15
- import type { BuildRegistry, WebsiteRegistry } from './metadata.ts'
13
+ import type { BuildRegistry, WebsiteRegistry } from './registry.ts'
16
14
 
17
15
  export type EntryPoint = { in: string; out: string }
18
16
 
19
17
  export async function esbuildDevContext(
20
- b: DankBuild,
21
18
  r: WebsiteRegistry,
22
19
  define: DefineDankGlobal,
23
20
  entryPoints: Array<EntryPoint>,
24
- c?: EsbuildConfig,
25
21
  ): Promise<BuildContext> {
26
22
  return await esbuild.context({
27
23
  define,
28
24
  entryNames: '[dir]/[name]',
29
25
  entryPoints: mapEntryPointPaths(entryPoints),
30
- outdir: b.dirs.buildWatch,
31
- ...commonBuildOptions(b, r, c),
26
+ outdir: r.config.dirs.buildWatch,
27
+ ...commonBuildOptions(r),
32
28
  splitting: false,
33
29
  write: false,
34
30
  })
35
31
  }
36
32
 
37
33
  export async function esbuildWebpages(
38
- b: DankBuild,
39
34
  r: WebsiteRegistry,
40
35
  define: DefineDankGlobal,
41
36
  entryPoints: Array<EntryPoint>,
42
- c?: EsbuildConfig,
43
37
  ): Promise<void> {
44
38
  try {
45
39
  await esbuild.build({
46
40
  define,
47
41
  entryNames: '[dir]/[name]-[hash]',
48
42
  entryPoints: mapEntryPointPaths(entryPoints),
49
- outdir: b.dirs.buildDist,
50
- ...commonBuildOptions(b, r, c),
43
+ outdir: r.config.dirs.buildDist,
44
+ ...commonBuildOptions(r),
51
45
  })
52
46
  } catch (ignore) {
53
47
  process.exit(1)
@@ -55,19 +49,17 @@ export async function esbuildWebpages(
55
49
  }
56
50
 
57
51
  export async function esbuildWorkers(
58
- b: DankBuild,
59
52
  r: WebsiteRegistry,
60
53
  define: DefineDankGlobal,
61
54
  entryPoints: Array<EntryPoint>,
62
- c?: EsbuildConfig,
63
55
  ): Promise<void> {
64
56
  try {
65
57
  await esbuild.build({
66
58
  define,
67
59
  entryNames: '[dir]/[name]-[hash]',
68
60
  entryPoints: mapEntryPointPaths(entryPoints),
69
- outdir: b.dirs.buildDist,
70
- ...commonBuildOptions(b, r, c),
61
+ outdir: r.config.dirs.buildDist,
62
+ ...commonBuildOptions(r),
71
63
  splitting: false,
72
64
  metafile: true,
73
65
  write: true,
@@ -78,22 +70,19 @@ export async function esbuildWorkers(
78
70
  }
79
71
  }
80
72
 
81
- function commonBuildOptions(
82
- b: DankBuild,
83
- r: WebsiteRegistry,
84
- c?: EsbuildConfig,
85
- ): BuildOptions {
73
+ function commonBuildOptions(r: WebsiteRegistry): BuildOptions {
86
74
  const p = workersPlugin(r.buildRegistry())
87
75
  return {
88
- absWorkingDir: b.dirs.projectRootAbs,
89
76
  assetNames: 'assets/[name]-[hash]',
90
77
  bundle: true,
91
78
  format: 'esm',
92
- loader: c?.loaders || defaultLoaders(),
79
+ loader: r.config.esbuild?.loaders || defaultLoaders(),
93
80
  metafile: true,
94
- minify: b.minify,
81
+ minify: r.config.flags.minify,
95
82
  platform: 'browser',
96
- plugins: c?.plugins?.length ? [p, ...c.plugins] : [p],
83
+ plugins: r.config.esbuild?.plugins?.length
84
+ ? [p, ...r.config.esbuild?.plugins]
85
+ : [p],
97
86
  splitting: true,
98
87
  treeShaking: true,
99
88
  write: true,
@@ -127,11 +116,8 @@ export function workersPlugin(r: BuildRegistry): Plugin {
127
116
  return {
128
117
  name: '@eighty4/dank/esbuild/workers',
129
118
  setup(build: PluginBuild) {
130
- if (!build.initialOptions.absWorkingDir)
131
- throw TypeError('plugin requires absWorkingDir')
132
119
  if (!build.initialOptions.metafile)
133
120
  throw TypeError('plugin requires metafile')
134
- const { absWorkingDir } = build.initialOptions
135
121
 
136
122
  build.onLoad({ filter: /\.(t|m?j)s$/ }, async args => {
137
123
  let contents = await readFile(args.path, 'utf8')
@@ -158,7 +144,7 @@ export function workersPlugin(r: BuildRegistry): Plugin {
158
144
  continue
159
145
  }
160
146
  const clientScript = args.path
161
- .replace(absWorkingDir, '')
147
+ .replace(r.dirs.projectResolved, '')
162
148
  .substring(1)
163
149
  const workerUrl = workerCtorMatch.groups!.url.substring(
164
150
  1,
@@ -182,10 +168,13 @@ export function workersPlugin(r: BuildRegistry): Plugin {
182
168
  )
183
169
  continue
184
170
  }
171
+ const workerCtor = workerCtorMatch.groups!.ctor as
172
+ | 'Worker'
173
+ | 'SharedWorker'
185
174
  const workerUrlPlaceholder = workerEntryPoint
186
175
  .replace(/^pages/, '')
187
176
  .replace(/\.(t|m?j)s$/, '.js')
188
- const workerCtorReplacement = `new ${workerCtorMatch.groups!.ctor}('${workerUrlPlaceholder}'${workerCtorMatch.groups!.end}`
177
+ const workerCtorReplacement = `new ${workerCtor}('${workerUrlPlaceholder}'${workerCtorMatch.groups!.end}`
189
178
  contents =
190
179
  contents.substring(0, workerCtorMatch.index + offset) +
191
180
  workerCtorReplacement +
@@ -199,6 +188,7 @@ export function workersPlugin(r: BuildRegistry): Plugin {
199
188
  r.addWorker({
200
189
  clientScript,
201
190
  workerEntryPoint,
191
+ workerCtor,
202
192
  workerUrl,
203
193
  workerUrlPlaceholder,
204
194
  })