saladplate 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 - present Zach Snow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Saladplate
2
+
3
+ _Simple templating; it's smaller than usual._
4
+
5
+ Saladplate is a very simple templating tool, with 3 features:
6
+
7
+ 1. Replace `${{ VAR }}` with the contents of the environment variable `VAR`.
8
+ 2. Replace `$<< filename >>` with the contents of the file `filename`.
9
+ 3. Replace `$(( some command ))` with the output of the command `some command`.
10
+
11
+ If you need more functionality that that, this is not the templating tool for you.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ $ npm -g install saladplate
17
+ $ saladplate --version
18
+ saladplate: version 0.1.0
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ The easiest way to use Saladplate with via the `saladplate` command line tool:
24
+
25
+ ```bash
26
+ $ saladplate --help
27
+
28
+ Usage: saladplate [options] <file>...
29
+ Options:
30
+ --debug enable debug mode
31
+ -v, --version show version information
32
+ -h, --help show this help message
33
+ -d, --directory output directory
34
+ -s, --suffix output suffix; only applies when using --directory
35
+ -o, --output output file; overrides --directory and --suffix
36
+ ```
37
+
38
+ Saladplate generally operates in 2 modes. If you pass `--directory`, then
39
+ each file you pass will be templated independently, and the output written
40
+ into the given directory. Use `--suffix` to change the extension of the output
41
+ file -- for instance, if you template the files `some.template` and `another.txt`
42
+ with `--directory=out` and `--suffix=.html` you will end up with a file `out/some.html`
43
+ and `out/another.html`:
44
+
45
+ ```bash
46
+ $ saladplate --directory=out --suffix=.html *.template
47
+ ```
48
+
49
+ If instead you pass `--output`, then each file will be templated, with the
50
+ results _concatenated_ into the single given output file.
51
+
52
+ ```bash
53
+ $ saladplate --output=index.html *.template
54
+ ```
55
+
56
+ You can also use the exported `template` function to template a string:
57
+
58
+ ```typescript
59
+ import { template } from "saladplate";
60
+ console.info(template("The current PATH is: $PATH"));
61
+ ```
62
+
63
+ ## Development
64
+
65
+ When developing locally, you can test your changes using the *script* `saladplate`,
66
+ which uses `ts-node`. Note the use of `run` below, as well as the use of `--` to
67
+ separate arguments to `npm run` from arguments to `saladplate`:
68
+
69
+ ```bash
70
+ $ npm run saladplate -- --version
71
+
72
+ > saladplate@0.1.0 saladplate
73
+ > ts-node ./bin/saladplate.ts
74
+
75
+ saladplate: version 0.1.0
76
+ ```
77
+
78
+ Tests are located in `tests/` and consist of input templates `tests/*.test` and
79
+ corresponding expected outputs `tests/*.expected`. To run tests:
80
+
81
+ ```bash
82
+ $ npm run test
83
+ ```
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env ts-node
2
+ export {};
@@ -0,0 +1,156 @@
1
+ #! /usr/bin/env node
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import process from "process";
14
+ import util from "util";
15
+ import { BIN, template } from "../src/index";
16
+ const parseArgsConfig = {
17
+ // Exclude the first two arguments: node and the script itself.
18
+ args: process.argv.slice(2),
19
+ options: {
20
+ debug: {
21
+ type: "boolean",
22
+ default: false,
23
+ description: "enable debug mode",
24
+ },
25
+ version: {
26
+ type: "boolean",
27
+ default: false,
28
+ short: "v",
29
+ description: "show version information",
30
+ },
31
+ help: {
32
+ type: "boolean",
33
+ default: false,
34
+ short: "h",
35
+ description: "show this help message",
36
+ },
37
+ directory: {
38
+ type: "string",
39
+ short: "d",
40
+ description: "output directory",
41
+ },
42
+ suffix: {
43
+ type: "string",
44
+ short: "s",
45
+ description: "output suffix; only applies when using --directory",
46
+ },
47
+ output: {
48
+ type: "string",
49
+ short: "o",
50
+ description: "output file; overrides --directory and --suffix",
51
+ },
52
+ },
53
+ allowPositionals: true,
54
+ };
55
+ function help() {
56
+ console.info(`Usage: ${BIN} [options] <file>...`);
57
+ console.info(`Options:`);
58
+ const entries = Object.entries(parseArgsConfig.options);
59
+ const prefixes = {};
60
+ entries.forEach(([name, option]) => {
61
+ const short = "short" in option ? `-${option.short}, ` : "";
62
+ const prefix = ` ${short}--${name}`;
63
+ prefixes[name] = prefix;
64
+ });
65
+ const len = Math.max(...entries.map(([name, option]) => prefixes[name].length)) + 2;
66
+ entries.forEach(([name, option]) => {
67
+ var _a;
68
+ const prefix = prefixes[name];
69
+ console.info(`${prefix}${" ".repeat(len - prefix.length)}${(_a = option.description) !== null && _a !== void 0 ? _a : ""}`);
70
+ });
71
+ console.info("");
72
+ }
73
+ function version() {
74
+ const version = process.env.npm_package_version;
75
+ console.info(`${BIN}: version ${version !== null && version !== void 0 ? version : "unknown"}`);
76
+ }
77
+ const parseArgs = () => {
78
+ const { values: options, positionals: filenames } = util.parseArgs(parseArgsConfig);
79
+ return { options, filenames };
80
+ };
81
+ let output = null;
82
+ function outputForFilename(filename, options) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ if (output) {
85
+ return output;
86
+ }
87
+ if (options.output) {
88
+ output = {
89
+ file: yield fs.promises.open(options.output, "w"),
90
+ filename: options.output,
91
+ };
92
+ return output;
93
+ }
94
+ if (options.directory) {
95
+ const basename = path.basename(filename);
96
+ let outputFilename = path.join(options.directory, basename);
97
+ if (options.suffix) {
98
+ outputFilename = outputFilename.replace(/\.[^.]+$/, options.suffix);
99
+ }
100
+ return {
101
+ file: outputFilename,
102
+ filename: outputFilename,
103
+ };
104
+ }
105
+ output = {
106
+ file: yield fs.promises.open("/dev/stdout", "w"),
107
+ filename: "/dev/stdout",
108
+ };
109
+ return output;
110
+ });
111
+ }
112
+ function main() {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ const { options, filenames } = parseArgs();
115
+ if (options.debug) {
116
+ console.debug(`${BIN}: debug mode enabled`);
117
+ }
118
+ if (options.help) {
119
+ help();
120
+ return;
121
+ }
122
+ if (options.version) {
123
+ version();
124
+ return;
125
+ }
126
+ if (filenames.length === 0) {
127
+ console.error(`${BIN}: no input files; use -- for stdin`);
128
+ help();
129
+ return -1;
130
+ }
131
+ yield Promise.all(filenames.map((filename) => __awaiter(this, void 0, void 0, function* () {
132
+ if (filename === "--") {
133
+ filename = "/dev/stdin";
134
+ }
135
+ options.debug && console.debug(`${BIN}: reading ${filename}...`);
136
+ const content = yield fs.promises.readFile(filename, {
137
+ encoding: "utf-8",
138
+ });
139
+ options.debug && console.debug(`${BIN}: templating ${filename}...`);
140
+ const output = yield template(content, filename, options);
141
+ const { file: outputFile, filename: outputFilename } = yield outputForFilename(filename, options);
142
+ options.debug && console.debug(`${BIN}: writing ${outputFilename}...`);
143
+ yield fs.promises.writeFile(outputFile, output, {
144
+ encoding: "utf-8",
145
+ });
146
+ })));
147
+ });
148
+ }
149
+ main()
150
+ .then((i) => {
151
+ process.exit(i !== null && i !== void 0 ? i : 0);
152
+ })
153
+ .catch((e) => {
154
+ console.error(`${BIN}: unhandled error:`, e);
155
+ process.exit(-1);
156
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The name of the tool; prefixes most output.
3
+ */
4
+ export declare const BIN = "simplate";
5
+ /**
6
+ * Templating options for `template`.
7
+ */
8
+ export type Options = {
9
+ debug?: boolean;
10
+ };
11
+ /**
12
+ * Replace variables, file includes, and command executions in the given `content`, which
13
+ * is assumed to have been read from the given `filename` (or stdin). Collapses newlines
14
+ * at the end of the output so that there's just one.
15
+ */
16
+ export declare const template: (content: string, filename?: string, options?: Options) => Promise<string>;
@@ -0,0 +1,75 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { exec } from "child_process";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import process from "process";
14
+ import util from "util";
15
+ /**
16
+ * The name of the tool; prefixes most output.
17
+ */
18
+ export const BIN = "simplate";
19
+ const cwdForFilename = (filename) => {
20
+ return filename === "/dev/stdin" ? process.cwd() : path.dirname(filename);
21
+ };
22
+ const VAR_RE = /\$\{\{([^\}]+)\}\}/g;
23
+ const FILE_RE = /\$\<\<([^\>]+)\>\>/g;
24
+ const EXEC_RE = /\$\(\(([^\)]+)\)\)/g;
25
+ function replaceAsync(input, regexp, fn) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ const replacements = yield Promise.all(Array.from(input.matchAll(regexp), (match) => fn(match)));
28
+ let i = 0;
29
+ return input.replace(regexp, () => replacements[i++]);
30
+ });
31
+ }
32
+ const replaceVar = (variable, filename, options) => {
33
+ var _a;
34
+ options.debug && console.debug(`${BIN}: ${filename}: evaluating ${variable} for replacement...`);
35
+ return (_a = process.env[variable]) !== null && _a !== void 0 ? _a : "";
36
+ };
37
+ const replaceFile = (includeFilename, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
38
+ // Resolve the include filename relative to the current file; if the current file is stdin
39
+ // then keep the current working directory.
40
+ const cwd = cwdForFilename(filename);
41
+ const include = path.join(cwd, includeFilename);
42
+ options.debug && console.debug(`${BIN}: ${filename}: reading ${include} for replacement...`);
43
+ const content = yield fs.promises.readFile(include, { encoding: "utf-8" });
44
+ return template(content, include, options);
45
+ });
46
+ const replaceExec = (command, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
47
+ // Execute the command in the working directory containing the file we are processing; if
48
+ // the current file is stdin then keep the current working directory.
49
+ const cwd = cwdForFilename(filename);
50
+ options.debug && console.debug(`${BIN}: ${filename}: executing ${command} for replacement...`);
51
+ const { stdout } = yield util.promisify(exec)(command, {
52
+ cwd,
53
+ });
54
+ return stdout;
55
+ });
56
+ /**
57
+ * Replace variables, file includes, and command executions in the given `content`, which
58
+ * is assumed to have been read from the given `filename` (or stdin). Collapses newlines
59
+ * at the end of the output so that there's just one.
60
+ */
61
+ export const template = (content, filename, options) => __awaiter(void 0, void 0, void 0, function* () {
62
+ filename !== null && filename !== void 0 ? filename : (filename = "/dev/stdin");
63
+ options !== null && options !== void 0 ? options : (options = {});
64
+ content = yield replaceAsync(content, VAR_RE, (match) => {
65
+ return replaceVar(match[1].trim(), filename, options);
66
+ });
67
+ content = yield replaceAsync(content, FILE_RE, (match) => {
68
+ return replaceFile(match[1].trim(), filename, options);
69
+ });
70
+ content = yield replaceAsync(content, EXEC_RE, (match) => {
71
+ return replaceExec(match[1].trim(), filename, options);
72
+ });
73
+ content = content.replace(/\n+$/m, "\n");
74
+ return content;
75
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "saladplate",
3
+ "version": "0.1.0",
4
+ "description": "Very simple templating.",
5
+ "keywords": [
6
+ "template",
7
+ "templating"
8
+ ],
9
+ "main": "./dist/src/saladplate.js",
10
+ "bin": {
11
+ "saladplate": "dist/bin/saladplate.js"
12
+ },
13
+ "scripts": {
14
+ "types": "tsc --noEmit",
15
+ "build": "./build.sh",
16
+ "saladplate": "ts-node ./bin/saladplate.ts",
17
+ "test": "./test.sh"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20.12.12",
21
+ "ts-node": "^10.9.2",
22
+ "typescript": "^5.4.5"
23
+ },
24
+ "files": [
25
+ "dist/**/*"
26
+ ],
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/zachsnow/saladplate.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/zachsnow/saladplate/issues"
34
+ }
35
+ }