@changke/staticnext-build 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.1.0
8
+ 2023-06-23
9
+ ### Added
10
+ - Initial release
11
+ - Extracted the build code and related npm packages from `@changke/staticnext-seed` project into this standalone package
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # StaticNext Build
2
+
3
+ A wrapper that invokes various packages to build static assets, extracted from `@changke/staticnext-seed` project.
package/build.mjs ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {argv} from 'node:process';
4
+
5
+ import config from './lib/config.mjs';
6
+
7
+ import clean from './lib/tasks/clean.mjs';
8
+ import copy from './lib/tasks/copy.mjs';
9
+ import markdown from './lib/tasks/markdown.mjs';
10
+ import prototype from './lib/tasks/prototype.mjs';
11
+ import scripts from './lib/tasks/scripts.mjs';
12
+ import setEnvDev from './lib/tasks/set-env-dev.mjs';
13
+ import setEnvProd from './lib/tasks/set-env-prod.mjs';
14
+ import styles from './lib/tasks/styles.mjs';
15
+
16
+ const taskName = argv[2]?.toLowerCase() || 'dev';
17
+
18
+ // load config data
19
+ const conf = await config.loadConfig();
20
+
21
+ const taskEnded = taskName => {
22
+ console.timeEnd(taskName);
23
+ };
24
+
25
+ // Set parameters of each task
26
+ const tasks = {
27
+ clean: () => clean([`${conf.targetRoot}/*`]),
28
+ copy: () => copy([
29
+ {sources: [`${conf.sourceRoot}/*`, `!${conf.sourceRoot}/*.d.ts`], target: conf.targetRoot},
30
+ {sources: `${conf.sourcePath.assets}/**/*`, target: conf.targetPath.assets},
31
+ {sources: `${conf.sourcePath.modules}/**/assets/**/*`, target: conf.targetPath.moduleAssets}
32
+ ]),
33
+ markdown: () => markdown(
34
+ conf.sourcePath.markdown,
35
+ [conf.sourcePath.prototype, conf.sourcePath.modules],
36
+ conf.njkGlobals,
37
+ conf.targetPath.markdown
38
+ ),
39
+ prototype: () => prototype(
40
+ conf.sourcePath.prototype,
41
+ [conf.sourcePath.modules],
42
+ conf.njkGlobals,
43
+ conf.targetPath.prototype
44
+ ),
45
+ scripts: () => scripts(conf.scriptEntries(conf.moduleEntries), conf.targetPath.assets),
46
+ styles: () => styles([
47
+ `${conf.sourcePath.css}/brands/*.css`,
48
+ `${conf.sourcePath.css}/index.css`,
49
+ `${conf.sourcePath.modules}/**/*.css`,
50
+ `!${conf.sourcePath.modules}/**/_*.css`
51
+ ], conf.targetPath.assets)
52
+ };
53
+
54
+ const paraDev = () => {
55
+ return Promise.all([
56
+ tasks.markdown(),
57
+ tasks.prototype(),
58
+ tasks.styles(),
59
+ tasks.scripts()
60
+ ]);
61
+ };
62
+
63
+ const paraProd = () => {
64
+ return Promise.all([
65
+ tasks.markdown(),
66
+ tasks.styles(),
67
+ tasks.scripts()
68
+ ]);
69
+ };
70
+
71
+ const dev = () => {
72
+ return setEnvDev().then(tasks.clean).then(tasks.copy).then(paraDev);
73
+ };
74
+
75
+ const prod = () => {
76
+ return setEnvProd().then(tasks.clean).then(tasks.copy).then(paraProd);
77
+ };
78
+
79
+ const taskMap = {
80
+ 'clean': tasks.clean,
81
+ 'copy': tasks.copy,
82
+ 'markdown': tasks.markdown,
83
+ 'prototype': tasks.prototype,
84
+ 'scripts': tasks.scripts,
85
+ 'setenvdev': setEnvDev,
86
+ 'setenvprod': setEnvProd,
87
+ 'styles': tasks.styles,
88
+ 'dev': dev,
89
+ 'prod': prod
90
+ };
91
+
92
+ const task = taskMap[taskName];
93
+
94
+ console.log('+++ SN Build +++');
95
+ console.time(taskName);
96
+ console.log(`Task "${taskName}" started...`);
97
+ task().then(() => {
98
+ taskEnded(taskName);
99
+ }).catch(err => {
100
+ console.error(err);
101
+ process.exit(1);
102
+ });
package/lib/config.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import {cwd} from 'node:process';
2
+
3
+ import vars from './vars.mjs';
4
+
5
+ class Config {
6
+ customConfigFile = '/sn-config.mjs';
7
+
8
+ async loadConfig(configFile) {
9
+ const defaultConf = vars;
10
+ try {
11
+ // try loading custom config file
12
+ const cf = configFile || `${cwd()}${this.customConfigFile}`;
13
+ const mod = await import(cf);
14
+ const customConf = mod.default;
15
+ return Object.assign({}, defaultConf, customConf);
16
+ } catch (e) {
17
+ // file not exist
18
+ return defaultConf;
19
+ }
20
+ }
21
+ }
22
+
23
+ const config = new Config();
24
+
25
+ export default config;
@@ -0,0 +1,3 @@
1
+ export default function() {
2
+ return (process.env.NODE_ENV === 'production');
3
+ }
@@ -0,0 +1,15 @@
1
+ import {deleteAsync} from 'del';
2
+
3
+ /**
4
+ * Clean the target directory
5
+ *
6
+ * @param {(string|string[])} targetPath
7
+ * @returns {Promise<string[]>}
8
+ */
9
+ const clean = (targetPath) => {
10
+ console.log('=> clean');
11
+ return deleteAsync(targetPath);
12
+ };
13
+
14
+ export {clean};
15
+ export default clean;
@@ -0,0 +1,21 @@
1
+ import cpy from 'cpy';
2
+
3
+ /**
4
+ * @typedef {Object} STPair
5
+ * @property {(string|string[])} sources
6
+ * @property {string} target
7
+ */
8
+
9
+ /**
10
+ * Copy (different) assets
11
+ *
12
+ * @param {STPair[]} sourceTargetPairs
13
+ * @return {Promise<Awaited<unknown>[]>}
14
+ */
15
+ const copy = (sourceTargetPairs) => {
16
+ console.log('=> copy');
17
+ return Promise.all(sourceTargetPairs.map(pair => cpy(pair.sources, pair.target)));
18
+ };
19
+
20
+ export {copy};
21
+ export default copy;
@@ -0,0 +1,69 @@
1
+ import {globby} from 'globby';
2
+ import {readFile} from 'node:fs/promises';
3
+ import {marked} from 'marked';
4
+ import {markedXhtml} from 'marked-xhtml';
5
+ import njk from 'nunjucks';
6
+
7
+ import {createAndWriteToFile, getTargetPathString} from '../utils.mjs';
8
+
9
+ marked.use({
10
+ headerIds: false,
11
+ mangle: false
12
+ }, markedXhtml());
13
+
14
+ const {Environment, FileSystemLoader} = njk;
15
+
16
+ const njkWrapper = {
17
+ top: '{% extends "templates/markdown.njk" %}{% block title %}{{ pageTitle }}{% endblock %}{% block content %}',
18
+ bottom: '{% endblock %}'
19
+ };
20
+
21
+ const getPageTitle = content => {
22
+ let t = 'Untitled';
23
+ if (content.length > 1) {
24
+ const firstLine = content.split('\n')[0];
25
+ if (firstLine) {
26
+ t = firstLine.replace('# ', ''); // First line MUST be a level 1 heading!
27
+ }
28
+ }
29
+ return t;
30
+ };
31
+
32
+ /**
33
+ * Generate docs using markdown and Nunjucks
34
+ *
35
+ * @param {string} mdSourcePathRoot
36
+ * @param {string[]} njkPaths
37
+ * @param {Object} njkGlobals
38
+ * @param {string} mdTargetPath
39
+ * @return {Promise<string[]>}
40
+ */
41
+ const markdown = (mdSourcePathRoot, njkPaths, njkGlobals, mdTargetPath) => {
42
+ console.log('=> markdown');
43
+ return globby(`${mdSourcePathRoot}/**/*.md`).then(docs => {
44
+ const njkEnv = new Environment(new FileSystemLoader(njkPaths, {}));
45
+ for (const [k, v] of Object.entries(njkGlobals)) {
46
+ njkEnv.addGlobal(k, v);
47
+ }
48
+ docs.forEach(async (doc) => {
49
+ const mdContent = await readFile(doc, {encoding: 'utf8'});
50
+ const html = marked.parse(mdContent);
51
+ const pageTitle = getPageTitle(mdContent);
52
+ const njkContent = njkWrapper.top + html + njkWrapper.bottom;
53
+ const res = njkEnv.renderString(njkContent, {
54
+ 'pageTitle': pageTitle
55
+ });
56
+ const target = getTargetPathString(
57
+ doc,
58
+ mdSourcePathRoot,
59
+ mdTargetPath,
60
+ 'md',
61
+ 'html'
62
+ );
63
+ await createAndWriteToFile(target, res);
64
+ });
65
+ });
66
+ };
67
+
68
+ export {markdown};
69
+ export default markdown;
@@ -0,0 +1,41 @@
1
+ import njk from 'nunjucks';
2
+ import {globby} from 'globby';
3
+ import {readFile} from 'node:fs/promises';
4
+
5
+ import {getTargetPathString, createAndWriteToFile} from '../utils.mjs';
6
+
7
+ const {Environment, FileSystemLoader} = njk;
8
+
9
+ /**
10
+ * Generate static pages with Nunjucks
11
+ *
12
+ * @param {string} prototypeSourcePathRoot
13
+ * @param {string[]} njkPaths
14
+ * @param {Object} njkGlobals
15
+ * @param {string} prototypeTargetPath
16
+ * @return {Promise<string[]>}
17
+ */
18
+ const prototype = (prototypeSourcePathRoot, njkPaths, njkGlobals, prototypeTargetPath) => {
19
+ console.log('=> prototype');
20
+ return globby(`${prototypeSourcePathRoot}/pages/**/*.njk`).then(pages => {
21
+ const njkEnv = new Environment(new FileSystemLoader([prototypeSourcePathRoot].concat(njkPaths), {}));
22
+ for (const [k, v] of Object.entries(njkGlobals)) {
23
+ njkEnv.addGlobal(k, v);
24
+ }
25
+ pages.forEach(async (page) => {
26
+ const pageContent = await readFile(page, {encoding: 'utf8'});
27
+ const res = njkEnv.renderString(pageContent);
28
+ const targetFile = getTargetPathString(
29
+ page,
30
+ `${prototypeSourcePathRoot}/pages`,
31
+ prototypeTargetPath,
32
+ 'njk',
33
+ 'html'
34
+ );
35
+ await createAndWriteToFile(targetFile, res);
36
+ });
37
+ });
38
+ };
39
+
40
+ export {prototype};
41
+ export default prototype;
@@ -0,0 +1,27 @@
1
+ import {build} from 'esbuild';
2
+
3
+ import isEnvProd from '../is-prod.mjs';
4
+
5
+ /**
6
+ * Transpile / bundle JS files using esbuild
7
+ *
8
+ * @param {string[]} entries
9
+ * @param {string} targetPath
10
+ * @return {Promise}
11
+ */
12
+ const esm = (entries, targetPath) => {
13
+ console.log('=> scripts');
14
+ return build({
15
+ entryPoints: entries,
16
+ bundle: true,
17
+ format: 'esm',
18
+ target: 'es2021',
19
+ splitting: true,
20
+ outdir: targetPath,
21
+ minify: isEnvProd(), // only minify in prod build
22
+ sourcemap: !isEnvProd() // only create sourcemaps in dev build
23
+ });
24
+ };
25
+
26
+ export {esm};
27
+ export default esm;
@@ -0,0 +1,8 @@
1
+ const setEnvDev = () => {
2
+ return new Promise((resolve) => {
3
+ process.env.NODE_ENV = 'development';
4
+ resolve();
5
+ });
6
+ };
7
+
8
+ export default setEnvDev;
@@ -0,0 +1,8 @@
1
+ const setEnvProd = () => {
2
+ return new Promise((resolve) => {
3
+ process.env.NODE_ENV = 'production';
4
+ resolve();
5
+ });
6
+ };
7
+
8
+ export default setEnvProd;
@@ -0,0 +1,28 @@
1
+ import {build} from 'esbuild';
2
+ import {globby} from 'globby';
3
+
4
+ import isEnvProd from '../is-prod.mjs';
5
+
6
+ /**
7
+ * Use esbuild to bundle CSS files
8
+ *
9
+ * @param {string[]} sources
10
+ * @param {string} targetPath
11
+ * @return {Promise}
12
+ */
13
+ const styles = (sources, targetPath) => {
14
+ console.log('=> styles');
15
+ return globby(sources).then(entries => {
16
+ return build({
17
+ entryPoints: entries,
18
+ bundle: true,
19
+ target: 'es2021',
20
+ outdir: targetPath,
21
+ sourcemap: !isEnvProd(),
22
+ minify: isEnvProd()
23
+ });
24
+ });
25
+ };
26
+
27
+ export {styles};
28
+ export default styles;
package/lib/utils.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import {access, mkdir, writeFile} from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ const getTargetPathString = (sourcePath, sourceRoot, targetRoot, srcExt, tgtExt) => {
5
+ const baseName = path.basename(sourcePath, `.${srcExt}`);
6
+ const dirName = path.dirname(sourcePath).replace(sourceRoot, '');
7
+ return path.join(targetRoot, dirName, `${baseName}.${tgtExt}`);
8
+ };
9
+
10
+ const createAndWriteToFile = async (filePath, fileContent) => {
11
+ const tp = path.dirname(filePath);
12
+ try {
13
+ await access(tp);
14
+ } catch {
15
+ await mkdir(tp, {recursive: true});
16
+ }
17
+ await writeFile(filePath, fileContent, {flag: 'w+'});
18
+ };
19
+
20
+ export {
21
+ getTargetPathString,
22
+ createAndWriteToFile
23
+ };
package/lib/vars.mjs ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @typedef {Object} PathPart
3
+ * @property {string} assets Path for general static assets
4
+ * @property {string=} css Path for CSS files
5
+ * @property {string=} js Path for JS files
6
+ * @property {string=} modules Path for module directories / files
7
+ * @property {string=} moduleAssets Path for assets of individual modules
8
+ * @property {string=} prototype Path for prototype (HTML) files
9
+ * @property {string=} test Path for unit-test related files
10
+ * @property {string=} markdown Path for markdown files
11
+ */
12
+
13
+ /**
14
+ * @typedef {Function} ScriptEntryFunc
15
+ * @param {string[]} modEntries
16
+ * @param {string=} mainEntry
17
+ * @return {string[]}
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} SNConfig
22
+ * @property {string} sourceRoot Root dir of source
23
+ * @property {string} targetRoot Root dir for output (target)
24
+ * @property {PathPart} sourcePath Paths of different types of source files
25
+ * @property {PathPart} targetPath Paths for different output files
26
+ * @property {string[]} tenants Theme-names
27
+ * @property {string[]} moduleEntries List of input files of modules e.g. 'mod-foo/foo.ts'
28
+ * @property {string} mainEntryFile The entry point of all JS e.g. main.ts
29
+ * @property {ScriptEntryFunc} scriptEntries Inputs of source files for esbuild
30
+ * @property {string} assetsUrlPath Path of assets used in URL for resource loading etc.
31
+ * @property {boolean} markdownEnabled Enable markdown feature or not
32
+ * @property {Record<string, string>} njkGlobals Global variables assigned to Nunjucks compiler
33
+ */
34
+
35
+ // default config values
36
+ const moduleEntries = [];
37
+
38
+ const sourceRoot = './src/main/webapp';
39
+ const targetRoot = './tgt/static';
40
+
41
+ /** @type {PathPart} */
42
+ const sourcePath = {
43
+ assets: `${sourceRoot}/assets`,
44
+ css: `${sourceRoot}/css`,
45
+ js: `${sourceRoot}/js`,
46
+ modules: `${sourceRoot}/modules`,
47
+ prototype: `${sourceRoot}/prototype`,
48
+ test: `${sourceRoot}/test`,
49
+ markdown: `${sourceRoot}/md`
50
+ };
51
+
52
+ const targetAssetsRoot = `${targetRoot}/assets/_sn_`;
53
+ const targetPath = {
54
+ assets: targetAssetsRoot,
55
+ css: `${targetAssetsRoot}/css`,
56
+ js: `${targetAssetsRoot}/js`,
57
+ moduleAssets: `${targetAssetsRoot}/modules`,
58
+ prototype: `${targetRoot}/prototype`,
59
+ markdown: `${targetRoot}/docs`
60
+ };
61
+
62
+ // "themes", not in use, replaced with brands directory
63
+ const tenants = ['one', 'two', 'three'];
64
+
65
+ const mainEntryFile = `${sourcePath.js}/main.ts`;
66
+
67
+ // also add the main entry file
68
+ /** @type {ScriptEntryFunc} */
69
+ const scriptEntries = (modEntries, mainEntry = mainEntryFile) => {
70
+ // For code splitting, as input config for esbuild
71
+ const moduleEntryFiles = modEntries.map(f => {
72
+ return `${sourcePath.modules}/${f}`;
73
+ });
74
+ return [mainEntry].concat(moduleEntryFiles);
75
+ };
76
+
77
+ // To get relative path of assets
78
+ const assetsUrlPath = '/assets/_sn_';
79
+
80
+ // Markdown enabled?
81
+ const markdownEnabled = true;
82
+
83
+ // globals for Nunjucks
84
+ const njkGlobals = {
85
+ 'assetsUrlPath': assetsUrlPath
86
+ };
87
+
88
+ /** @type {SNConfig} */
89
+ const config = {
90
+ sourceRoot,
91
+ targetRoot,
92
+ sourcePath,
93
+ targetPath,
94
+ tenants,
95
+ moduleEntries,
96
+ mainEntryFile,
97
+ scriptEntries,
98
+ assetsUrlPath,
99
+ markdownEnabled,
100
+ njkGlobals
101
+ };
102
+
103
+ export default config;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@changke/staticnext-build",
3
+ "version": "0.1.0",
4
+ "description": "Build scripts extracted from StaticNext seed project",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "lint": "eslint --ext .mjs . lib test",
8
+ "test": "mocha",
9
+ "prepublishOnly": "npm run lint && npm run test"
10
+ },
11
+ "engines": {
12
+ "node": ">=16"
13
+ },
14
+ "bin": {
15
+ "sn-build": "./build.mjs"
16
+ },
17
+ "keywords": [],
18
+ "author": "@changke",
19
+ "license": "ISC",
20
+ "type": "module",
21
+ "devDependencies": {
22
+ "eslint": "^8.43.0",
23
+ "mocha": "^10.2.0"
24
+ },
25
+ "dependencies": {
26
+ "cpy": "^10.1.0",
27
+ "del": "^7.0.0",
28
+ "esbuild": "^0.18.6",
29
+ "globby": "^13.2.0",
30
+ "marked": "^5.1.0",
31
+ "marked-xhtml": "^1.0.1",
32
+ "nunjucks": "^3.2.4"
33
+ }
34
+ }