Skip to content

Commit

Permalink
Add JSON comment support (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
qetza authored Mar 10, 2024
1 parent a184b06 commit fd4a9bd
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 84 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
run: npm ci

- name: Check code format
run: npm run format-check
run: npm run format:check

- name: Build
run: npm run build
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:
${{ toJSON(vars) }},
${{ toJSON(secrets) }},
{ "VAR2": "#{inline}#value2", "inline": "inline_" },
${{ toJSON(format('@{0}/tests/data/vars.json', github.workspace)) }},
${{ toJSON(format('@{0}/tests/data/vars.jsonc', github.workspace)) }},
"$ENV_VARS"
]
root: ${{ github.workspace }}/tests
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Changelog

## v1.1.0
- Add support for JSON comments in _variables_ and in JSON variable files and environment variables

## v1.0.0
- Initial Release of the ReplaceTokens action
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ Please refer to the [release page](https://github.com/qetza/replacetokens-action
# Required.
sources: ''

# A JSON serialized object containing the variables values. The object can be:
# A JSON serialized object containing the variables values.
# The object can be:
# - an object: properties will be parsed as key/value pairs
# - a string starting with '@': value is parsed as a path to a JSON file
# - a string starting with '$': value is parsed as an environment variable name
# containing JSON encoded key/value pairs
# - an array: each item must be an object or a string and will be parsed as
# specified previously
#
# Multiple entries are merge into a single list of key/value pairs.
# Multiple entries are merge into a single list of key/value pairs and all JSON
# supports comments.
#
# Example: '[${{ toJSON(vars) }}, ${{ toJSON(secrets) }}]' will pass all defined
# variables and secrets.
Expand Down
6 changes: 4 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ inputs:
required: true
variables:
description: >
A JSON serialized object containing the variables values. The object can be:
A JSON serialized object containing the variables values.
The object can be:
- an object: properties will be parsed as key/value pairs
- a string starting with '@': value is parsed as a path to a JSON file
- a string starting with '$': value is parsed as an environment variable name
containing JSON encoded key/value pairs
- an array: each item must be an object or a string and will be parsed as specified
previously
Multiple entries are merge into a single list of key/value pairs.
Multiple entries are merge into a single list of key/value pairs and all JSON supports
comments.
Example: '[${ toJSON(vars) }}, ${ toJSON(secrets) }}]' will pass all defined variables
and secrets.
Expand Down
130 changes: 125 additions & 5 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43515,10 +43515,14 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.run = void 0;
const core = __importStar(__nccwpck_require__(2186));
const replacetokens_1 = __nccwpck_require__(9633);
const strip_json_comments_1 = __importDefault(__nccwpck_require__(77));
async function run() {
const _debug = console.debug;
const _info = console.info;
Expand Down Expand Up @@ -43618,8 +43622,7 @@ async function run() {
core.setOutput('transforms', result.transforms);
}
catch (error) {
if (error instanceof Error)
core.setFailed(error.message);
core.setFailed(error instanceof Error ? error.message : `${error}`);
}
finally {
// restore console logs
Expand All @@ -43640,16 +43643,16 @@ function getChoiceInput(name, choices, options) {
}
async function parseVariables(input) {
input = input || '{}';
const variables = JSON.parse(input);
const variables = JSON.parse((0, strip_json_comments_1.default)(input));
let load = async (v) => {
if (typeof v === 'string') {
switch (v[0]) {
case '@':
core.debug(`loading variables from file '${v.substring(1)}'`);
return JSON.parse((await (0, replacetokens_1.readTextFile)(v.substring(1))).content || '{}');
return JSON.parse((0, strip_json_comments_1.default)((await (0, replacetokens_1.readTextFile)(v.substring(1))).content || '{}'));
case '$':
core.debug(`loading variables from environment '${v.substring(1)}'`);
return JSON.parse(process.env[v.substring(1)] || '{}');
return JSON.parse((0, strip_json_comments_1.default)(process.env[v.substring(1)] || '{}'));
default:
throw new Error("Unsupported value for: variables\nString values starts with '@' (file path) or '$' (environment variable)");
}
Expand Down Expand Up @@ -43689,6 +43692,123 @@ function parseLogLevel(level) {
}


/***/ }),

/***/ 77:
/***/ ((__unused_webpack_module, exports) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
// source: https://github.com/sindresorhus/strip-json-comments
const singleComment = Symbol('singleComment');
const multiComment = Symbol('multiComment');
const stripWithoutWhitespace = () => '';
const stripWithWhitespace = (string, start, end) => string.slice(start, end).replace(/\S/g, ' ');
const isEscaped = (jsonString, quotePosition) => {
let index = quotePosition - 1;
let backslashCount = 0;
while (jsonString[index] === '\\') {
index -= 1;
backslashCount += 1;
}
return Boolean(backslashCount % 2);
};
function stripJsonComments(jsonString, { whitespace = true, trailingCommas = false } = {}) {
if (typeof jsonString !== 'string') {
throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``);
}
const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
let isInsideString = false;
let isInsideComment = false;
let offset = 0;
let buffer = '';
let result = '';
let commaIndex = -1;
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
// Enter or exit string
const escaped = isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
continue;
}
if (!isInsideComment && currentCharacter + nextCharacter === '//') {
// Enter single-line comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = singleComment;
index++;
}
else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') {
// Exit single-line comment via \r\n
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
continue;
}
else if (isInsideComment === singleComment && currentCharacter === '\n') {
// Exit single-line comment via \n
isInsideComment = false;
buffer += strip(jsonString, offset, index);
offset = index;
}
else if (!isInsideComment && currentCharacter + nextCharacter === '/*') {
// Enter multiline comment
buffer += jsonString.slice(offset, index);
offset = index;
isInsideComment = multiComment;
index++;
continue;
}
else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') {
// Exit multiline comment
index++;
isInsideComment = false;
buffer += strip(jsonString, offset, index + 1);
offset = index + 1;
continue;
}
else if (trailingCommas && !isInsideComment) {
if (commaIndex !== -1) {
if (currentCharacter === '}' || currentCharacter === ']') {
// Strip trailing comma
buffer += jsonString.slice(offset, index);
result += strip(buffer, 0, 1) + buffer.slice(1);
buffer = '';
offset = index;
commaIndex = -1;
}
else if (currentCharacter !== ' ' &&
currentCharacter !== '\t' &&
currentCharacter !== '\r' &&
currentCharacter !== '\n') {
// Hit non-whitespace following a comma; comma is not trailing
buffer += jsonString.slice(offset, index);
offset = index;
commaIndex = -1;
}
}
else if (currentCharacter === ',') {
// Flush buffer prior to this point, and save new comma index
result += buffer + jsonString.slice(offset, index);
buffer = '';
offset = index;
commaIndex = index;
}
}
}
return result + buffer + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset));
}
exports["default"] = stripJsonComments;


/***/ }),

/***/ 9491:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "replacetokens-action",
"version": "1.0.0",
"version": "1.1.0",
"description": "An action to replace tokens with variables and/or secrets.",
"private": true,
"author": "Guillaume ROUCHON",
Expand All @@ -21,15 +21,15 @@
"url": "https://github.com/qetza/replacetokens-action"
},
"exports": {
".": "dist/index.js"
".": "./dist/index.js"
},
"engines": {
"node": ">=16"
},
"scripts": {
"build": "npm run format && npm run package",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"format:check": "prettier --check **/*.ts",
"package": "ncc build src/index.ts --license licenses.txt",
"test": "jest"
},
Expand Down
9 changes: 5 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Options,
TokenPatterns
} from '@qetza/replacetokens';
import stripJsonComments from './strip-json-comments';

export async function run(): Promise<void> {
const _debug = console.debug;
Expand Down Expand Up @@ -116,7 +117,7 @@ export async function run(): Promise<void> {
core.setOutput('tokens', result.tokens);
core.setOutput('transforms', result.transforms);
} catch (error) {
if (error instanceof Error) core.setFailed(error.message);
core.setFailed(error instanceof Error ? error.message : `${error}`);
} finally {
// restore console logs
console.debug = _debug;
Expand All @@ -138,19 +139,19 @@ function getChoiceInput(name: string, choices: string[], options?: core.InputOpt

async function parseVariables(input: string): Promise<{ [key: string]: any }> {
input = input || '{}';
const variables = JSON.parse(input);
const variables = JSON.parse(stripJsonComments(input));

let load = async (v: any) => {
if (typeof v === 'string') {
switch (v[0]) {
case '@':
core.debug(`loading variables from file '${v.substring(1)}'`);

return JSON.parse((await readTextFile(v.substring(1))).content || '{}');
return JSON.parse(stripJsonComments((await readTextFile(v.substring(1))).content || '{}'));
case '$':
core.debug(`loading variables from environment '${v.substring(1)}'`);

return JSON.parse(process.env[v.substring(1)] || '{}');
return JSON.parse(stripJsonComments(process.env[v.substring(1)] || '{}'));
default:
throw new Error(
"Unsupported value for: variables\nString values starts with '@' (file path) or '$' (environment variable)"
Expand Down
Loading

0 comments on commit fd4a9bd

Please sign in to comment.