@changke/staticnext-build 0.26.1 → 0.27.0

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/AGENTS.md ADDED
@@ -0,0 +1,195 @@
1
+ # AGENTS.md
2
+
3
+ Guidelines for AI agents working in the `@changke/staticnext-build` repository.
4
+
5
+ ## Project Overview
6
+
7
+ Node.js CLI build tool (`sn-build`) that orchestrates static asset builds: cleaning,
8
+ copying, Markdown-to-HTML, HTML prototyping (Nunjucks or jstmpl), JS/TS bundling,
9
+ and CSS bundling via esbuild. Pure JavaScript, ESM-only, no TypeScript compiler,
10
+ no React/Next.js framework.
11
+
12
+ ## Build / Lint / Test Commands
13
+
14
+ ```bash
15
+ # Lint (ESLint v9 flat config with @stylistic)
16
+ npm run lint
17
+
18
+ # Run all tests (Node.js built-in test runner, requires Node >= 22.14.0)
19
+ npm test
20
+
21
+ # Run a single test file
22
+ node --test test/util.test.mjs
23
+
24
+ # Run tests matching a name pattern
25
+ node --test --test-name-pattern="getTargetPathString" test/util.test.mjs
26
+
27
+ # Lint + test (runs automatically before npm publish)
28
+ npm run prepublishOnly
29
+ ```
30
+
31
+ There is no build/compile step -- the source is plain `.mjs` files executed directly.
32
+
33
+ ## Module System
34
+
35
+ - **ESM only.** Package has `"type": "module"`. All files use `.mjs` extension.
36
+ - No CommonJS (`require`, `module.exports`) anywhere.
37
+
38
+ ## Code Style
39
+
40
+ Formatting is enforced entirely by ESLint with `@stylistic/eslint-plugin` -- there
41
+ is no Prettier. Run `npm run lint` to check.
42
+
43
+ ### Formatting Rules
44
+
45
+ | Rule | Setting |
46
+ |------|---------|
47
+ | Indentation | 2 spaces |
48
+ | Quotes | Single quotes |
49
+ | Semicolons | Always required |
50
+ | Trailing commas | Never (`comma-dangle: never`) |
51
+ | Object braces | No spaces (`{foo}` not `{ foo }`) |
52
+ | Block spacing | No spaces (`{return x}` not `{ return x }`) |
53
+ | Brace style | 1TBS (opening brace on same line) |
54
+ | Arrow parens | As-needed (omit for single param) |
55
+ | Named function parens | No space before: `function foo()` |
56
+ | Async arrow | Space required: `async () =>` |
57
+ | Line endings | LF only |
58
+ | Trailing whitespace | Trimmed (except in `.md` files) |
59
+ | Final newline | Always insert |
60
+
61
+ ### Import Conventions
62
+
63
+ ```js
64
+ // Node built-ins: always use `node:` prefix
65
+ import {readFile} from 'node:fs/promises';
66
+ import path from 'node:path';
67
+ import {env} from 'node:process';
68
+
69
+ // Third-party: bare specifiers
70
+ import {build} from 'esbuild';
71
+ import {marked} from 'marked';
72
+
73
+ // Local: relative paths with explicit .mjs extension
74
+ import {consoleLogColored} from '../utils.mjs';
75
+ import Config from './config.mjs';
76
+ ```
77
+
78
+ - Prefer named imports over namespace imports.
79
+ - Default imports are used for classes/singletons (e.g., `Config`, `path`).
80
+ - Group imports: Node built-ins first, then third-party, then local (separated by blank lines).
81
+
82
+ ### Export Conventions
83
+
84
+ Modules typically provide both named and default exports:
85
+
86
+ ```js
87
+ const clean = async dirs => { /* ... */ };
88
+ export {clean};
89
+ export default clean;
90
+ ```
91
+
92
+ ### Naming Conventions
93
+
94
+ | Entity | Convention | Examples |
95
+ |--------|-----------|----------|
96
+ | Variables, functions, params | camelCase | `getTargetPathString`, `srcPaths` |
97
+ | Classes | PascalCase | `Config` |
98
+ | Constants | camelCase (not SCREAMING_SNAKE) | `taskName`, `defaultConfig` |
99
+ | Files | kebab-case with `.mjs` | `set-env-dev.mjs`, `is-prod.mjs` |
100
+ | Test files | `*.test.mjs` | `util.test.mjs`, `config.test.mjs` |
101
+
102
+ ### Types
103
+
104
+ No TypeScript compiler is used. Types are defined via **JSDoc** in `lib/typedefs.mjs`:
105
+
106
+ ```js
107
+ /**
108
+ * @typedef {Object} PathPart
109
+ * @property {string} assets Path for general static assets
110
+ * @property {string=} css Path for CSS files
111
+ */
112
+ ```
113
+
114
+ Use `@param`, `@returns`, `@typedef`, and `@type` annotations for documenting
115
+ function signatures and inline type hints:
116
+
117
+ ```js
118
+ /** @type string[] */
119
+ const negativePatterns = [];
120
+ ```
121
+
122
+ ### Error Handling
123
+
124
+ - Use `try/catch` in async functions.
125
+ - Log errors with `console.error()`.
126
+ - Top-level entry point (`build.mjs`) catches with `.catch()` and calls `process.exit(1)`.
127
+ - Inner functions log errors and allow graceful continuation where appropriate.
128
+
129
+ ### Async Patterns
130
+
131
+ - `async/await` is the primary pattern.
132
+ - `Promise.all()` for parallel operations (e.g., running styles + scripts + markdown concurrently).
133
+ - `.then()` chaining for sequential task pipelines:
134
+ ```js
135
+ setEnvDev().then(tasks.clean).then(tasks.copy).then(paraDev);
136
+ ```
137
+ - Arrow functions are the dominant function style.
138
+
139
+ ## Testing
140
+
141
+ Tests use the **Node.js built-in test runner** (`node:test` module), not Jest or Vitest.
142
+
143
+ ### Test Structure
144
+
145
+ ```js
146
+ import {suite, test} from 'node:test';
147
+ import assert from 'node:assert/strict';
148
+
149
+ void suite('SuiteName', () => {
150
+ test('descriptive test name', () => {
151
+ assert.strictEqual(actual, expected);
152
+ });
153
+
154
+ test('async test', async () => {
155
+ const result = await someAsyncFunc();
156
+ assert.strictEqual(result, expected);
157
+ });
158
+ });
159
+ ```
160
+
161
+ Key patterns:
162
+ - Suites are wrapped with `void suite(...)` (the `void` suppresses the return value).
163
+ - Use `node:assert/strict` -- always strict assertions.
164
+ - Tests clean up after themselves (create/remove temp directories in `before`/`after`).
165
+ - Test fixtures live in `test/fixtures/`.
166
+
167
+ ## Project Structure
168
+
169
+ ```
170
+ build.mjs CLI entry point (bin: sn-build)
171
+ sn-config.mjs Default project configuration
172
+ lib/
173
+ config.mjs Config loading, merging, path helpers
174
+ is-prod.mjs NODE_ENV check helper
175
+ typedefs.mjs JSDoc type definitions
176
+ utils.mjs File I/O, glob helpers, console coloring
177
+ vars.mjs Default configuration values
178
+ tasks/
179
+ clean.mjs Remove target directories
180
+ copy.mjs Copy static assets via glob
181
+ markdown.mjs Convert .md -> .html (marked + nunjucks/jstmpl)
182
+ prototype.mjs Generate static HTML pages from templates
183
+ scripts.mjs Bundle JS/TS via esbuild
184
+ set-env-dev.mjs Set NODE_ENV=development
185
+ set-env-prod.mjs Set NODE_ENV=production
186
+ styles.mjs Bundle CSS via esbuild
187
+ test/
188
+ *.test.mjs Test files
189
+ fixtures/ Test fixture data
190
+ ```
191
+
192
+ ## Changelog & Versioning
193
+
194
+ The project follows [Semantic Versioning](https://semver.org/) and maintains a
195
+ `CHANGELOG.md` in [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.27.0
8
+ 2026-02-12
9
+ ### Changed
10
+ - Updated dependencies
11
+ - Various code changes after AI (Opus 4.6) code review
12
+
7
13
  ## 0.26.0
8
14
  2025-12-21
9
15
  ### Removed
package/build.mjs CHANGED
@@ -3,7 +3,7 @@ import {argv} from 'node:process';
3
3
  import path from 'node:path';
4
4
 
5
5
  import './lib/typedefs.mjs';
6
- import Config from './lib/config.mjs';
6
+ import config from './lib/config.mjs';
7
7
 
8
8
  import clean from './lib/tasks/clean.mjs';
9
9
  import copy from './lib/tasks/copy.mjs';
@@ -24,14 +24,14 @@ if (debug) {
24
24
  }
25
25
 
26
26
  // load config data
27
- const conf = await Config.loadConfig();
27
+ const conf = await config.loadConfig();
28
28
 
29
29
  const taskEnded = taskName => {
30
30
  console.timeEnd(taskName);
31
31
  };
32
32
 
33
- const srcPaths = Config.getSourcePaths(conf.sourcePath, path.normalize(conf.sourceRoot));
34
- const tgtPaths = Config.getTargetPaths(conf.targetPath, path.normalize(conf.targetRoot), conf.targetAssetsRoot);
33
+ const srcPaths = config.getSourcePaths(conf.sourcePath, path.normalize(conf.sourceRoot));
34
+ const tgtPaths = config.getTargetPaths(conf.targetPath, path.normalize(conf.targetRoot), conf.targetAssetsRoot);
35
35
 
36
36
  // Set parameters of each task
37
37
  const tasks = {
@@ -47,7 +47,7 @@ const tasks = {
47
47
  // Markdown file assets
48
48
  // src/main/webapp/md/google-pixel-9/assets/cover.webp -> public/posts/google-pixel-9/assets/cover.webp
49
49
  {sources: `${srcPaths.markdown}/**/assets/**/*.*`, target: tgtPaths.markdown, opts: {levelRemoval: 4}}
50
- ].concat(Config.getCopyPairs(conf.copyPairs, conf.sourceRoot, tgtPaths.assets))),
50
+ ].concat(config.getCopyPairs(conf.copyPairs, conf.sourceRoot, tgtPaths.assets))),
51
51
  markdown: () => markdown(
52
52
  srcPaths.markdown,
53
53
  [srcPaths.prototype, srcPaths.modules],
@@ -70,7 +70,7 @@ const tasks = {
70
70
  conf.njkGlobals,
71
71
  tgtPaths.prototype
72
72
  ),
73
- scripts: () => scripts(Config.getScriptEntries(conf.moduleEntries, conf.mainEntryFile, srcPaths), tgtPaths.assets),
73
+ scripts: () => scripts(config.getScriptEntries(conf.moduleEntries, conf.mainEntryFile, srcPaths), tgtPaths.assets),
74
74
  styles: () => styles([
75
75
  `${srcPaths.css}/brands/*.css`,
76
76
  `${srcPaths.css}/index.css`,
@@ -120,6 +120,11 @@ const taskMap = {
120
120
 
121
121
  const task = taskMap[taskName];
122
122
 
123
+ if (typeof task !== 'function') {
124
+ console.warn(`Task "${taskName}" not found or is not a function!`);
125
+ process.exit(1);
126
+ }
127
+
123
128
  consoleLogColored('+++ SN Build +++', ['bold', 'blue']);
124
129
  console.time(taskName);
125
130
  console.log(`Task "${taskName}" started...`);
package/lib/config.mjs CHANGED
@@ -14,7 +14,7 @@ class Config {
14
14
  const cf = configFile || `${cwd()}${this.customConfigFile}`;
15
15
  const mod = await import(cf);
16
16
  const customConf = mod.default;
17
- return this.deepMergeConfObj(defaultConf, customConf);
17
+ return this.mergeConfObj(defaultConf, customConf);
18
18
  } catch (e) {
19
19
  // file not exist
20
20
  console.error('Error loading file: ', e);
@@ -22,9 +22,9 @@ class Config {
22
22
  }
23
23
  }
24
24
 
25
- // keep nested obj values while merging
25
+ // keep nested obj values while merging (only merges one level deep)
26
26
  // e.g. {foo:{a:1,b:2}} merged with {foo:{b:3,c:4}} will be {foo:{a:1,b:3,c:4}}
27
- deepMergeConfObj(defaultConf, customConf) {
27
+ mergeConfObj(defaultConf, customConf) {
28
28
  const conf = Object.assign({}, defaultConf);
29
29
  for (const [k, v] of Object.entries(customConf)) {
30
30
  if (this.objsToMerge.includes(k)) {
@@ -89,12 +89,13 @@ class Config {
89
89
  */
90
90
  getCopyPairs = (copyPairs, sourceBase, targetBase, appendSourcePath = false) => {
91
91
  if (copyPairs && Array.isArray(copyPairs) && copyPairs.length > 0) {
92
- return copyPairs.map(pr => {
92
+ return copyPairs.map(pair => {
93
+ const pc = {...pair};
93
94
  if (appendSourcePath) {
94
- pr.sources = `${appendSourcePath}${pr.sources}`;
95
+ pc.sources = `${sourceBase}${pc.sources}`;
95
96
  }
96
- pr.target = `${targetBase}${pr.target}`;
97
- return pr;
97
+ pc.target = `${targetBase}${pc.target}`;
98
+ return pc;
98
99
  });
99
100
  } else {
100
101
  return [];
package/lib/is-prod.mjs CHANGED
@@ -1,3 +1,5 @@
1
- export default function() {
2
- return (process.env.NODE_ENV === 'production');
3
- }
1
+ import {env} from 'node:process';
2
+
3
+ const isProd = () => env.NODE_ENV === 'production';
4
+
5
+ export default isProd;
@@ -27,13 +27,10 @@ const globCopy = async (globPatterns, targetRoot, opts, verbose = false) => {
27
27
  /** @type string[] */
28
28
  const sources = await glob2Array(globPatterns);
29
29
 
30
- let targetPath = '';
31
30
  return Promise.all(sources.map(async src => {
32
- if (opts.flat) {
33
- targetPath = path.join(targetRoot, path.basename(src));
34
- } else {
35
- targetPath = path.join(targetRoot, genTargetPath(src, opts.levelRemoval));
36
- }
31
+ const targetPath = opts.flat
32
+ ? path.join(targetRoot, path.basename(src))
33
+ : path.join(targetRoot, genTargetPath(src, opts.levelRemoval));
37
34
  if (verbose) {
38
35
  consoleLogColored(`copying ${src} -> ${targetPath}`, 'dim');
39
36
  }
@@ -7,18 +7,16 @@ import njk from 'nunjucks';
7
7
  import {renderPageToString} from '@changke/jstmpl';
8
8
  import {createAndWriteToFile, getTargetPathString, glob2Array, consoleLogColored, isDebugMode} from '../utils.mjs';
9
9
 
10
- marked.use({
11
- headerIds: false,
12
- mangle: false
13
- },
14
- markedXhtml(),
15
- markedHighlight({
16
- langPrefix: 'hljs language-',
17
- highlight: (code, lang) => {
18
- const language = hljs.getLanguage(lang) ? lang : 'plaintext';
19
- return hljs.highlight(code, {language}).value;
20
- }
21
- }));
10
+ marked.use(
11
+ markedXhtml(),
12
+ markedHighlight({
13
+ langPrefix: 'hljs language-',
14
+ highlight: (code, lang) => {
15
+ const language = hljs.getLanguage(lang) ? lang : 'plaintext';
16
+ return hljs.highlight(code, {language}).value;
17
+ }
18
+ })
19
+ );
22
20
 
23
21
  const {Environment, FileSystemLoader} = njk;
24
22
 
@@ -1,8 +1,5 @@
1
- const setEnvDev = () => {
2
- return new Promise(resolve => {
3
- process.env.NODE_ENV = 'development';
4
- resolve();
5
- });
1
+ const setEnvDev = async () => {
2
+ process.env.NODE_ENV = 'development';
6
3
  };
7
4
 
8
5
  export default setEnvDev;
@@ -1,8 +1,5 @@
1
- const setEnvProd = () => {
2
- return new Promise(resolve => {
3
- process.env.NODE_ENV = 'production';
4
- resolve();
5
- });
1
+ const setEnvProd = async () => {
2
+ process.env.NODE_ENV = 'production';
6
3
  };
7
4
 
8
5
  export default setEnvProd;
@@ -20,7 +20,7 @@ const styles = (sources, targetPath, external = []) => {
20
20
  outdir: targetPath,
21
21
  sourcemap: !isEnvProd(),
22
22
  minify: isEnvProd(),
23
- external: external
23
+ external
24
24
  });
25
25
  });
26
26
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@changke/staticnext-build",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "Build scripts extracted from StaticNext seed project",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "lint": "eslint . lib test",
8
- "test": "node --test",
8
+ "test": "node --test 'test/*.test.mjs'",
9
9
  "prepublishOnly": "npm run lint && npm run test"
10
10
  },
11
11
  "engines": {
@@ -20,15 +20,15 @@
20
20
  "type": "module",
21
21
  "devDependencies": {
22
22
  "@eslint/js": "^9.39.2",
23
- "@stylistic/eslint-plugin": "^5.6.1",
23
+ "@stylistic/eslint-plugin": "^5.8.0",
24
24
  "eslint": "^9.39.2",
25
25
  "globals": "^16.5.0"
26
26
  },
27
27
  "dependencies": {
28
28
  "@changke/jstmpl": "^0.0.1",
29
- "esbuild": "^0.27.2",
29
+ "esbuild": "^0.27.3",
30
30
  "highlight.js": "^11.11.1",
31
- "marked": "^17.0.1",
31
+ "marked": "^17.0.2",
32
32
  "marked-highlight": "^2.2.3",
33
33
  "marked-xhtml": "^1.0.14",
34
34
  "nunjucks": "^3.2.4"