@eighty4/dank 0.0.5-2 → 0.0.5-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/build.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
- import { createBuildTag } from './build_tag.ts'
4
3
  import { loadConfig, type ResolvedDankConfig } from './config.ts'
5
4
  import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
6
5
  import type { DankDirectories } from './dirs.ts'
@@ -14,7 +13,7 @@ export async function buildWebsite(
14
13
  if (!c) {
15
14
  c = await loadConfig('build', process.cwd())
16
15
  }
17
- const buildTag = await createBuildTag(c.flags)
16
+ const buildTag = await c.buildTag()
18
17
  console.log(
19
18
  c.flags.minify
20
19
  ? c.flags.production
package/lib/build_tag.ts CHANGED
@@ -1,25 +1,129 @@
1
1
  import { exec } from 'node:child_process'
2
+ import type { DankConfig } from './dank.ts'
2
3
  import type { DankFlags } from './flags.ts'
3
4
 
4
- export async function createBuildTag(flags: DankFlags): Promise<string> {
5
+ export async function createBuildTag(
6
+ projectDir: string,
7
+ flags: DankFlags,
8
+ buildTagSource?: DankConfig['buildTag'],
9
+ ): Promise<string> {
10
+ if (typeof buildTagSource === 'function') {
11
+ buildTagSource = await buildTagSource({ production: flags.production })
12
+ }
13
+ if (typeof buildTagSource === 'undefined' || buildTagSource === null) {
14
+ buildTagSource = await resolveExpressionDefault(projectDir)
15
+ }
16
+ if (typeof buildTagSource !== 'string') {
17
+ throw TypeError(
18
+ 'DankConfig.buildTag must resolve to a string expession',
19
+ )
20
+ }
21
+ const params: BuildTagParams = {}
5
22
  const now = new Date()
23
+ const paramPattern = new RegExp(/{{\s*(?<name>[a-z][A-Za-z]+)\s*}}/g)
24
+ let paramMatch: RegExpExecArray | null
25
+ let buildTag = buildTagSource
26
+ let offset = 0
27
+ while ((paramMatch = paramPattern.exec(buildTagSource)) != null) {
28
+ const paramName = paramMatch.groups!.name.trim() as keyof BuildTagParams
29
+ let paramValue: string
30
+ if (params[paramName]) {
31
+ paramValue = params[paramName]
32
+ } else {
33
+ paramValue = params[paramName] = await getParamValue(
34
+ projectDir,
35
+ paramName,
36
+ now,
37
+ buildTagSource,
38
+ )
39
+ }
40
+ buildTag =
41
+ buildTag.substring(0, paramMatch.index + offset) +
42
+ paramValue +
43
+ buildTag.substring(paramMatch.index + paramMatch[0].length + offset)
44
+ offset += paramValue.length - paramMatch[0].length
45
+ }
46
+ const validate = /^[A-Za-z\d][A-Za-z\d-_\.]+$/
47
+ if (!validate.test(buildTag)) {
48
+ throw Error(
49
+ `build tag ${buildTag} does not pass pattern ${validate.source} validation`,
50
+ )
51
+ }
52
+ return buildTag
53
+ }
54
+
55
+ async function resolveExpressionDefault(projectDir: string): Promise<string> {
56
+ const base = '{{ date }}-{{ timeMS }}'
57
+ const isGitRepo = await new Promise(res =>
58
+ exec('git rev-parse --is-inside-work-tree', { cwd: projectDir }, err =>
59
+ res(!err),
60
+ ),
61
+ )
62
+ return isGitRepo ? base + '-{{ gitHash }}' : base
63
+ }
64
+
65
+ type BuildTagParams = {
66
+ date?: string
67
+ gitHash?: string
68
+ timeMS?: string
69
+ }
70
+
71
+ async function getParamValue(
72
+ projectDir: string,
73
+ name: keyof BuildTagParams,
74
+ now: Date,
75
+ buildTagSource: string,
76
+ ): Promise<string> {
77
+ switch (name) {
78
+ case 'date':
79
+ return getDate(now)
80
+ case 'gitHash':
81
+ try {
82
+ return await getGitHash(projectDir)
83
+ } catch (e) {
84
+ if (e === 'not-repo') {
85
+ throw Error(
86
+ `buildTag cannot use \`gitHash\` in \`${buildTagSource}\` outside of a git repository`,
87
+ )
88
+ } else {
89
+ throw e
90
+ }
91
+ }
92
+ case 'timeMS':
93
+ return getTimeMS(now)
94
+ default:
95
+ throw Error(name + ' is not a supported build tag param')
96
+ }
97
+ }
98
+
99
+ function getDate(now: Date): string {
100
+ return now.toISOString().substring(0, 10)
101
+ }
102
+
103
+ async function getGitHash(projectDir: string): Promise<string> {
104
+ return await new Promise((res, rej) =>
105
+ exec(
106
+ 'git rev-parse --short HEAD',
107
+ { cwd: projectDir },
108
+ (err, stdout, stderr) => {
109
+ if (err) {
110
+ if (stderr.includes('not a git repository')) {
111
+ rej('not-repo')
112
+ } else {
113
+ rej(err)
114
+ }
115
+ }
116
+ res(stdout.trim())
117
+ },
118
+ ),
119
+ )
120
+ }
121
+
122
+ function getTimeMS(now: Date): string {
6
123
  const ms =
7
124
  now.getUTCMilliseconds() +
8
125
  now.getUTCSeconds() * 1000 +
9
126
  now.getUTCMinutes() * 1000 * 60 +
10
127
  now.getUTCHours() * 1000 * 60 * 60
11
- const date = now.toISOString().substring(0, 10)
12
- const time = String(ms).padStart(8, '0')
13
- const when = `${date}-${time}`
14
- if (flags.production) {
15
- const gitHash = await new Promise((res, rej) =>
16
- exec('git rev-parse --short HEAD', (err, stdout) => {
17
- if (err) rej(err)
18
- res(stdout.trim())
19
- }),
20
- )
21
- return `${when}-${gitHash}`
22
- } else {
23
- return when
24
- }
128
+ return String(ms).padStart(8, '0')
25
129
  }
package/lib/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, resolve } from 'node:path'
2
+ import { createBuildTag } from './build_tag.ts'
2
3
  import type {
3
4
  DankConfig,
4
5
  DankDetails,
@@ -34,6 +35,8 @@ export type ResolvedDankConfig = {
34
35
  get devPages(): Readonly<DankConfig['devPages']>
35
36
  get services(): Readonly<DankConfig['services']>
36
37
 
38
+ buildTag(): Promise<string>
39
+
37
40
  reload(): Promise<void>
38
41
  }
39
42
 
@@ -60,6 +63,7 @@ export async function loadConfig(
60
63
  }
61
64
 
62
65
  class DankConfigInternal implements ResolvedDankConfig {
66
+ #buildTag: DankConfig['buildTag']
63
67
  #dirs: Readonly<DankDirectories>
64
68
  #flags: Readonly<DankFlags>
65
69
  #mode: 'build' | 'serve'
@@ -119,11 +123,20 @@ class DankConfigInternal implements ResolvedDankConfig {
119
123
  return this.#services
120
124
  }
121
125
 
126
+ async buildTag(): Promise<string> {
127
+ return await createBuildTag(
128
+ this.#dirs.projectRootAbs,
129
+ this.#flags,
130
+ this.#buildTag,
131
+ )
132
+ }
133
+
122
134
  async reload() {
123
135
  const userConfig = await resolveConfig(
124
136
  this.#modulePath,
125
137
  resolveDankDetails(this.#mode, this.#flags),
126
138
  )
139
+ this.#buildTag = userConfig.buildTag
127
140
  this.#dankPort = resolveDankPort(this.#flags, userConfig)
128
141
  this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
129
142
  this.#esbuild = Object.freeze(userConfig.esbuild)
@@ -173,6 +186,7 @@ function resolveDankDetails(
173
186
  function validateDankConfig(c: Partial<DankConfig>) {
174
187
  try {
175
188
  validatePorts(c)
189
+ validateBuildTag(c.buildTag)
176
190
  validatePages(c.pages)
177
191
  validateDevPages(c.devPages)
178
192
  validateDevServices(c.services)
@@ -202,6 +216,20 @@ function validatePorts(c: Partial<DankConfig>) {
202
216
  }
203
217
  }
204
218
 
219
+ function validateBuildTag(buildTag: DankConfig['buildTag']) {
220
+ if (buildTag === null) {
221
+ return
222
+ }
223
+ switch (typeof buildTag) {
224
+ case 'undefined':
225
+ case 'string':
226
+ case 'function':
227
+ return
228
+ default:
229
+ throw Error('DankConfig.buildTag must be a string or function')
230
+ }
231
+ }
232
+
205
233
  function validateEsbuildConfig(esbuild?: EsbuildConfig) {
206
234
  if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
207
235
  if (typeof esbuild.loaders !== 'object') {
package/lib/dank.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Plugin as EsbuildPlugin } from 'esbuild'
2
2
 
3
3
  export type DankConfig = {
4
- // used for releases and service worker caching
5
- // buildTag?: (() => Promise<string> | string) | string
4
+ // used for service worker caching
5
+ buildTag?: string | BuildTagBuilder
6
6
 
7
7
  // customize esbuild configs
8
8
  esbuild?: EsbuildConfig
@@ -26,6 +26,14 @@ export type DankConfig = {
26
26
  services?: Array<DevService>
27
27
  }
28
28
 
29
+ export type BuildTagParams = {
30
+ production: boolean
31
+ }
32
+
33
+ export type BuildTagBuilder = (
34
+ build: BuildTagParams,
35
+ ) => Promise<string> | string
36
+
29
37
  // extend an html entrypoint with url rewriting similar to cdn configurations
30
38
  // after trying all webpage, bundle and asset paths, mapping patterns
31
39
  // will be tested in the alphabetical order of the webpage paths
package/lib_js/build.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { createBuildTag } from "./build_tag.js";
4
3
  import { loadConfig } from "./config.js";
5
4
  import { createGlobalDefinitions } from "./define.js";
6
5
  import { esbuildWebpages, esbuildWorkers } from "./esbuild.js";
@@ -10,7 +9,7 @@ async function buildWebsite(c) {
10
9
  if (!c) {
11
10
  c = await loadConfig("build", process.cwd());
12
11
  }
13
- const buildTag = await createBuildTag(c.flags);
12
+ const buildTag = await c.buildTag();
14
13
  console.log(c.flags.minify ? c.flags.production ? "minified production" : "minified" : "unminified", "build", buildTag, "building in ./build/dist");
15
14
  await rm(c.dirs.buildRoot, { recursive: true, force: true });
16
15
  await mkdir(c.dirs.buildDist, { recursive: true });
@@ -1,21 +1,81 @@
1
1
  import { exec } from "node:child_process";
2
- async function createBuildTag(flags) {
2
+ async function createBuildTag(projectDir, flags, buildTagSource) {
3
+ if (typeof buildTagSource === "function") {
4
+ buildTagSource = await buildTagSource({ production: flags.production });
5
+ }
6
+ if (typeof buildTagSource === "undefined" || buildTagSource === null) {
7
+ buildTagSource = await resolveExpressionDefault(projectDir);
8
+ }
9
+ if (typeof buildTagSource !== "string") {
10
+ throw TypeError("DankConfig.buildTag must resolve to a string expession");
11
+ }
12
+ const params = {};
3
13
  const now = /* @__PURE__ */ new Date();
4
- const ms = now.getUTCMilliseconds() + now.getUTCSeconds() * 1e3 + now.getUTCMinutes() * 1e3 * 60 + now.getUTCHours() * 1e3 * 60 * 60;
5
- const date = now.toISOString().substring(0, 10);
6
- const time = String(ms).padStart(8, "0");
7
- const when = `${date}-${time}`;
8
- if (flags.production) {
9
- const gitHash = await new Promise((res, rej) => exec("git rev-parse --short HEAD", (err, stdout) => {
10
- if (err)
11
- rej(err);
12
- res(stdout.trim());
13
- }));
14
- return `${when}-${gitHash}`;
15
- } else {
16
- return when;
14
+ const paramPattern = new RegExp(/{{\s*(?<name>[a-z][A-Za-z]+)\s*}}/g);
15
+ let paramMatch;
16
+ let buildTag = buildTagSource;
17
+ let offset = 0;
18
+ while ((paramMatch = paramPattern.exec(buildTagSource)) != null) {
19
+ const paramName = paramMatch.groups.name.trim();
20
+ let paramValue;
21
+ if (params[paramName]) {
22
+ paramValue = params[paramName];
23
+ } else {
24
+ paramValue = params[paramName] = await getParamValue(projectDir, paramName, now, buildTagSource);
25
+ }
26
+ buildTag = buildTag.substring(0, paramMatch.index + offset) + paramValue + buildTag.substring(paramMatch.index + paramMatch[0].length + offset);
27
+ offset += paramValue.length - paramMatch[0].length;
28
+ }
29
+ const validate = /^[A-Za-z\d][A-Za-z\d-_\.]+$/;
30
+ if (!validate.test(buildTag)) {
31
+ throw Error(`build tag ${buildTag} does not pass pattern ${validate.source} validation`);
32
+ }
33
+ return buildTag;
34
+ }
35
+ async function resolveExpressionDefault(projectDir) {
36
+ const base = "{{ date }}-{{ timeMS }}";
37
+ const isGitRepo = await new Promise((res) => exec("git rev-parse --is-inside-work-tree", { cwd: projectDir }, (err) => res(!err)));
38
+ return isGitRepo ? base + "-{{ gitHash }}" : base;
39
+ }
40
+ async function getParamValue(projectDir, name, now, buildTagSource) {
41
+ switch (name) {
42
+ case "date":
43
+ return getDate(now);
44
+ case "gitHash":
45
+ try {
46
+ return await getGitHash(projectDir);
47
+ } catch (e) {
48
+ if (e === "not-repo") {
49
+ throw Error(`buildTag cannot use \`gitHash\` in \`${buildTagSource}\` outside of a git repository`);
50
+ } else {
51
+ throw e;
52
+ }
53
+ }
54
+ case "timeMS":
55
+ return getTimeMS(now);
56
+ default:
57
+ throw Error(name + " is not a supported build tag param");
17
58
  }
18
59
  }
60
+ function getDate(now) {
61
+ return now.toISOString().substring(0, 10);
62
+ }
63
+ async function getGitHash(projectDir) {
64
+ return await new Promise((res, rej) => exec("git rev-parse --short HEAD", { cwd: projectDir }, (err, stdout, stderr) => {
65
+ if (err) {
66
+ if (stderr.includes("not a git repository")) {
67
+ rej("not-repo");
68
+ } else {
69
+ rej(err);
70
+ }
71
+ }
72
+ res(stdout.trim());
73
+ }));
74
+ }
75
+ function getTimeMS(now) {
76
+ const ms = now.getUTCMilliseconds() + now.getUTCSeconds() * 1e3 + now.getUTCMinutes() * 1e3 * 60 + now.getUTCHours() * 1e3 * 60 * 60;
77
+ return String(ms).padStart(8, "0");
78
+ }
19
79
  export {
20
80
  createBuildTag
21
81
  };
package/lib_js/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, resolve } from "node:path";
2
+ import { createBuildTag } from "./build_tag.js";
2
3
  import { defaultProjectDirs } from "./dirs.js";
3
4
  import { resolveFlags as lookupDankFlags } from "./flags.js";
4
5
  var __rewriteRelativeImportExtension = function(path, preserveJsx) {
@@ -24,6 +25,7 @@ async function loadConfig(mode, projectRootAbs) {
24
25
  return c;
25
26
  }
26
27
  class DankConfigInternal {
28
+ #buildTag;
27
29
  #dirs;
28
30
  #flags;
29
31
  #mode;
@@ -67,8 +69,12 @@ class DankConfigInternal {
67
69
  get services() {
68
70
  return this.#services;
69
71
  }
72
+ async buildTag() {
73
+ return await createBuildTag(this.#dirs.projectRootAbs, this.#flags, this.#buildTag);
74
+ }
70
75
  async reload() {
71
76
  const userConfig = await resolveConfig(this.#modulePath, resolveDankDetails(this.#mode, this.#flags));
77
+ this.#buildTag = userConfig.buildTag;
72
78
  this.#dankPort = resolveDankPort(this.#flags, userConfig);
73
79
  this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig);
74
80
  this.#esbuild = Object.freeze(userConfig.esbuild);
@@ -99,6 +105,7 @@ function resolveDankDetails(mode, flags) {
99
105
  function validateDankConfig(c) {
100
106
  try {
101
107
  validatePorts(c);
108
+ validateBuildTag(c.buildTag);
102
109
  validatePages(c.pages);
103
110
  validateDevPages(c.devPages);
104
111
  validateDevServices(c.services);
@@ -119,6 +126,19 @@ function validatePorts(c) {
119
126
  }
120
127
  }
121
128
  }
129
+ function validateBuildTag(buildTag) {
130
+ if (buildTag === null) {
131
+ return;
132
+ }
133
+ switch (typeof buildTag) {
134
+ case "undefined":
135
+ case "string":
136
+ case "function":
137
+ return;
138
+ default:
139
+ throw Error("DankConfig.buildTag must be a string or function");
140
+ }
141
+ }
122
142
  function validateEsbuildConfig(esbuild) {
123
143
  if (esbuild?.loaders !== null && typeof esbuild?.loaders !== "undefined") {
124
144
  if (typeof esbuild.loaders !== "object") {
@@ -1,5 +1,6 @@
1
1
  import type { Plugin as EsbuildPlugin } from 'esbuild';
2
2
  export type DankConfig = {
3
+ buildTag?: string | BuildTagBuilder;
3
4
  esbuild?: EsbuildConfig;
4
5
  pages: Record<`/${string}`, `${string}.html` | PageMapping>;
5
6
  devPages?: Record<`/__${string}`, `${string}.html` | DevPageMapping>;
@@ -7,6 +8,10 @@ export type DankConfig = {
7
8
  previewPort?: number;
8
9
  services?: Array<DevService>;
9
10
  };
11
+ export type BuildTagParams = {
12
+ production: boolean;
13
+ };
14
+ export type BuildTagBuilder = (build: BuildTagParams) => Promise<string> | string;
10
15
  export type PageMapping = {
11
16
  pattern?: RegExp;
12
17
  webpage: `${string}.html`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eighty4/dank",
3
- "version": "0.0.5-2",
3
+ "version": "0.0.5-3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Adam McKee Bennett <adam.be.g84d@gmail.com>",