@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 +195 -0
- package/CHANGELOG.md +6 -0
- package/build.mjs +11 -6
- package/lib/config.mjs +8 -7
- package/lib/is-prod.mjs +5 -3
- package/lib/tasks/copy.mjs +3 -6
- package/lib/tasks/markdown.mjs +10 -12
- package/lib/tasks/set-env-dev.mjs +2 -5
- package/lib/tasks/set-env-prod.mjs +2 -5
- package/lib/tasks/styles.mjs +1 -1
- package/package.json +5 -5
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
|
|
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
|
|
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 =
|
|
34
|
-
const tgtPaths =
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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(
|
|
92
|
+
return copyPairs.map(pair => {
|
|
93
|
+
const pc = {...pair};
|
|
93
94
|
if (appendSourcePath) {
|
|
94
|
-
|
|
95
|
+
pc.sources = `${sourceBase}${pc.sources}`;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
return
|
|
97
|
+
pc.target = `${targetBase}${pc.target}`;
|
|
98
|
+
return pc;
|
|
98
99
|
});
|
|
99
100
|
} else {
|
|
100
101
|
return [];
|
package/lib/is-prod.mjs
CHANGED
package/lib/tasks/copy.mjs
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|
package/lib/tasks/markdown.mjs
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
package/lib/tasks/styles.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@changke/staticnext-build",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
29
|
+
"esbuild": "^0.27.3",
|
|
30
30
|
"highlight.js": "^11.11.1",
|
|
31
|
-
"marked": "^17.0.
|
|
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"
|