@dittowords/cli 3.0.0 → 3.2.0-alpha
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/bin/ditto.js +25 -9
- package/bin/ditto.js.map +1 -1
- package/bin/generate-suggestions.js +71 -0
- package/bin/generate-suggestions.js.map +1 -0
- package/bin/http/fetchComponents.js +13 -0
- package/bin/http/fetchComponents.js.map +1 -0
- package/bin/replace.js +107 -0
- package/bin/replace.js.map +1 -0
- package/lib/ditto.ts +45 -10
- package/lib/generate-suggestions.test.ts +65 -0
- package/lib/generate-suggestions.ts +107 -0
- package/lib/http/fetchComponents.ts +14 -0
- package/lib/replace.test.ts +101 -0
- package/lib/replace.ts +106 -0
- package/package.json +9 -2
package/bin/ditto.js
CHANGED
|
@@ -13,6 +13,8 @@ const pull_1 = require("./pull");
|
|
|
13
13
|
const quit_1 = require("./utils/quit");
|
|
14
14
|
const add_project_1 = __importDefault(require("./add-project"));
|
|
15
15
|
const remove_project_1 = __importDefault(require("./remove-project"));
|
|
16
|
+
const replace_1 = require("./replace");
|
|
17
|
+
const generate_suggestions_1 = require("./generate-suggestions");
|
|
16
18
|
const processMetaOption_1 = __importDefault(require("./utils/processMetaOption"));
|
|
17
19
|
const COMMANDS = [
|
|
18
20
|
{
|
|
@@ -33,20 +35,28 @@ const COMMANDS = [
|
|
|
33
35
|
},
|
|
34
36
|
],
|
|
35
37
|
},
|
|
38
|
+
{
|
|
39
|
+
name: "generate-suggestions",
|
|
40
|
+
description: "Find text that can be potentially replaced with Ditto text",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "replace",
|
|
44
|
+
description: "Find and replace Ditto text with code",
|
|
45
|
+
},
|
|
36
46
|
];
|
|
37
47
|
const setupCommands = () => {
|
|
38
48
|
commander_1.program.name("ditto-cli");
|
|
39
|
-
COMMANDS.forEach((
|
|
49
|
+
COMMANDS.forEach((commandConfig) => {
|
|
40
50
|
const cmd = commander_1.program
|
|
41
|
-
.command(
|
|
42
|
-
.description(
|
|
43
|
-
.action(() => executeCommand(
|
|
44
|
-
if ("commands" in
|
|
45
|
-
|
|
51
|
+
.command(commandConfig.name)
|
|
52
|
+
.description(commandConfig.description)
|
|
53
|
+
.action((str, options) => executeCommand(commandConfig.name, options));
|
|
54
|
+
if ("commands" in commandConfig) {
|
|
55
|
+
commandConfig.commands.forEach((nestedCommand) => {
|
|
46
56
|
cmd
|
|
47
57
|
.command(nestedCommand.name)
|
|
48
58
|
.description(nestedCommand.description)
|
|
49
|
-
.action(() => executeCommand(`${
|
|
59
|
+
.action((str, options) => executeCommand(`${commandConfig.name} ${nestedCommand.name}`, options));
|
|
50
60
|
});
|
|
51
61
|
}
|
|
52
62
|
});
|
|
@@ -54,7 +64,7 @@ const setupCommands = () => {
|
|
|
54
64
|
const setupOptions = () => {
|
|
55
65
|
commander_1.program.option("-m, --meta <data...>", "Include arbitrary data in requests to the Ditto API. Ex: -m githubActionRequest:true trigger:manual");
|
|
56
66
|
};
|
|
57
|
-
const executeCommand = async (command) => {
|
|
67
|
+
const executeCommand = async (command, options) => {
|
|
58
68
|
const needsInitialization = (0, init_1.needsTokenOrSource)();
|
|
59
69
|
if (needsInitialization) {
|
|
60
70
|
try {
|
|
@@ -83,6 +93,12 @@ const executeCommand = async (command) => {
|
|
|
83
93
|
case "project remove": {
|
|
84
94
|
return (0, remove_project_1.default)();
|
|
85
95
|
}
|
|
96
|
+
case "generate-suggestions": {
|
|
97
|
+
return (0, generate_suggestions_1.generateSuggestions)();
|
|
98
|
+
}
|
|
99
|
+
case "replace": {
|
|
100
|
+
return (0, replace_1.replace)(options);
|
|
101
|
+
}
|
|
86
102
|
default: {
|
|
87
103
|
(0, quit_1.quit)("Exiting Ditto CLI...");
|
|
88
104
|
return;
|
|
@@ -93,7 +109,7 @@ const main = async () => {
|
|
|
93
109
|
setupCommands();
|
|
94
110
|
setupOptions();
|
|
95
111
|
if (process.argv.length <= 2 && process.argv[1].includes("ditto-cli")) {
|
|
96
|
-
await executeCommand("none");
|
|
112
|
+
await executeCommand("none", []);
|
|
97
113
|
return;
|
|
98
114
|
}
|
|
99
115
|
commander_1.program.parse(process.argv);
|
package/bin/ditto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ditto.js","sourceRoot":"","sources":["../lib/ditto.ts"],"names":[],"mappings":";;;;;;AACA,0DAA0D;AAC1D,yCAAoC;AACpC,wDAAwD;AACxD,4BAA0B;AAE1B,sCAAuD;AACvD,iCAA8B;AAC9B,uCAAoC;AACpC,gEAAuC;AACvC,sEAA6C;
|
|
1
|
+
{"version":3,"file":"ditto.js","sourceRoot":"","sources":["../lib/ditto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateSuggestions = exports.findTextInJSXFiles = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const glob_1 = __importDefault(require("glob"));
|
|
9
|
+
const parser_1 = require("@babel/parser");
|
|
10
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
11
|
+
const fetchComponents_1 = require("./http/fetchComponents");
|
|
12
|
+
async function generateSuggestions() {
|
|
13
|
+
const components = await (0, fetchComponents_1.fetchComponents)();
|
|
14
|
+
const results = {};
|
|
15
|
+
for (const [compApiId, component] of Object.entries(components)) {
|
|
16
|
+
if (!results[compApiId]) {
|
|
17
|
+
results[compApiId] = [];
|
|
18
|
+
}
|
|
19
|
+
const result = await findTextInJSXFiles(".", component.text);
|
|
20
|
+
results[compApiId] = [...results[compApiId], ...result];
|
|
21
|
+
}
|
|
22
|
+
// Display results to user
|
|
23
|
+
console.log(JSON.stringify(results, null, 2));
|
|
24
|
+
}
|
|
25
|
+
exports.generateSuggestions = generateSuggestions;
|
|
26
|
+
async function findTextInJSXFiles(path, searchString) {
|
|
27
|
+
const result = [];
|
|
28
|
+
const files = glob_1.default.sync(`${path}/**/*.+(jsx|tsx)`, {
|
|
29
|
+
ignore: "**/node_modules/**",
|
|
30
|
+
});
|
|
31
|
+
const promises = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
promises.push(fs_extra_1.default.readFile(file, "utf-8").then((code) => {
|
|
34
|
+
const ast = (0, parser_1.parse)(code, {
|
|
35
|
+
sourceType: "module",
|
|
36
|
+
plugins: ["jsx", "typescript"],
|
|
37
|
+
});
|
|
38
|
+
const occurrences = [];
|
|
39
|
+
(0, traverse_1.default)(ast, {
|
|
40
|
+
JSXText(path) {
|
|
41
|
+
if (path.node.value.includes(searchString)) {
|
|
42
|
+
const regex = new RegExp(searchString, "g");
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = regex.exec(path.node.value)) !== null) {
|
|
45
|
+
const lines = path.node.value.slice(0, match.index).split("\n");
|
|
46
|
+
if (!path.node.loc) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const lineNumber = path.node.loc.start.line + lines.length - 1;
|
|
50
|
+
const codeLines = code.split("\n");
|
|
51
|
+
const line = codeLines[lineNumber - 1];
|
|
52
|
+
const preview = replaceAt(line, match.index, searchString, `{{${searchString}}}`);
|
|
53
|
+
occurrences.push({ lineNumber, preview });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (occurrences.length > 0) {
|
|
59
|
+
result.push({ file, occurrences });
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
await Promise.all(promises);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
exports.findTextInJSXFiles = findTextInJSXFiles;
|
|
67
|
+
function replaceAt(str, index, searchString, replacement) {
|
|
68
|
+
return (str.substring(0, index) +
|
|
69
|
+
str.substring(index, str.length).replace(searchString, replacement));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=generate-suggestions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-suggestions.js","sourceRoot":"","sources":["../lib/generate-suggestions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fetchComponents = void 0;
|
|
7
|
+
const api_1 = __importDefault(require("../api"));
|
|
8
|
+
async function fetchComponents() {
|
|
9
|
+
const { data } = await api_1.default.get("/components", {});
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
exports.fetchComponents = fetchComponents;
|
|
13
|
+
//# sourceMappingURL=fetchComponents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchComponents.js","sourceRoot":"","sources":["../../lib/http/fetchComponents.ts"],"names":[],"mappings":";;;;;;AAAA,iDAAyB;AAElB,KAAK,UAAU,eAAe;IACnC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,aAAG,CAAC,GAAG,CAO3B,aAAa,EAAE,EAAE,CAAC,CAAC;IAEtB,OAAO,IAAI,CAAC;AACd,CAAC;AAXD,0CAWC"}
|
package/bin/replace.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.replaceJSXTextInFile = exports.parseOptions = exports.replace = void 0;
|
|
30
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
31
|
+
const parser_1 = require("@babel/parser");
|
|
32
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
33
|
+
const t = __importStar(require("@babel/types"));
|
|
34
|
+
const core_1 = require("@babel/core");
|
|
35
|
+
async function replaceJSXTextInFile(filePath, replacement) {
|
|
36
|
+
const code = await fs_extra_1.default.readFile(filePath, "utf-8");
|
|
37
|
+
const ast = (0, parser_1.parse)(code, {
|
|
38
|
+
sourceType: "module",
|
|
39
|
+
plugins: ["jsx", "typescript"],
|
|
40
|
+
});
|
|
41
|
+
(0, traverse_1.default)(ast, {
|
|
42
|
+
JSXText(path) {
|
|
43
|
+
const { searchString, replaceWith } = replacement;
|
|
44
|
+
const regex = new RegExp(searchString, "gi");
|
|
45
|
+
if (regex.test(path.node.value)) {
|
|
46
|
+
const splitValues = splitByCaseInsensitive(path.node.value, searchString);
|
|
47
|
+
const nodes = [];
|
|
48
|
+
splitValues.forEach((splitValue) => {
|
|
49
|
+
if (splitValue.toLowerCase() === searchString.toLowerCase()) {
|
|
50
|
+
const identifier = t.jsxIdentifier("DittoComponent");
|
|
51
|
+
const componentId = t.jsxAttribute(t.jsxIdentifier("componentId"), t.stringLiteral(replaceWith));
|
|
52
|
+
const o = t.jsxOpeningElement(identifier, [componentId], true);
|
|
53
|
+
const jsxElement = t.jsxElement(o, undefined, [], true);
|
|
54
|
+
nodes.push(jsxElement);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
nodes.push(t.jsxText(splitValue));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
path.replaceWithMultiple(nodes);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// transfromFromAst types are wrong?
|
|
65
|
+
/* @ts-ignore */
|
|
66
|
+
const { code: transformedCode } = (0, core_1.transformFromAst)(ast, code, {
|
|
67
|
+
// Don't let this codebase's Babel config affect the code we're transforming.
|
|
68
|
+
configFile: false,
|
|
69
|
+
});
|
|
70
|
+
fs_extra_1.default.writeFile(filePath, transformedCode);
|
|
71
|
+
}
|
|
72
|
+
exports.replaceJSXTextInFile = replaceJSXTextInFile;
|
|
73
|
+
function splitByCaseInsensitive(str, delimiter) {
|
|
74
|
+
return str.split(new RegExp(`(${delimiter})`, "gi")).filter((s) => s !== "");
|
|
75
|
+
}
|
|
76
|
+
function replace(options) {
|
|
77
|
+
let filePath;
|
|
78
|
+
let searchString;
|
|
79
|
+
let replaceWith;
|
|
80
|
+
try {
|
|
81
|
+
const parsedOptions = parseOptions(options);
|
|
82
|
+
filePath = parsedOptions.filePath;
|
|
83
|
+
searchString = parsedOptions.searchString;
|
|
84
|
+
replaceWith = parsedOptions.replaceWith;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error(e);
|
|
88
|
+
console.error("Usage for replace: ditto-cli replace <file path> <search string> <replace with>");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
replaceJSXTextInFile(filePath, { searchString, replaceWith });
|
|
92
|
+
}
|
|
93
|
+
exports.replace = replace;
|
|
94
|
+
function parseOptions(options) {
|
|
95
|
+
if (options.length !== 3) {
|
|
96
|
+
throw new Error("The options array must contain <file path> <search string> <replace with>.");
|
|
97
|
+
}
|
|
98
|
+
const filePath = options[0];
|
|
99
|
+
// Check if the file path exists and points to a regular file (not a directory or other file system object).
|
|
100
|
+
const isFilePathValid = fs_extra_1.default.existsSync(filePath) && fs_extra_1.default.lstatSync(filePath).isFile();
|
|
101
|
+
if (!isFilePathValid) {
|
|
102
|
+
throw new Error(`${filePath} is not a valid file path.`);
|
|
103
|
+
}
|
|
104
|
+
return { filePath, searchString: options[1], replaceWith: options[2] };
|
|
105
|
+
}
|
|
106
|
+
exports.parseOptions = parseOptions;
|
|
107
|
+
//# sourceMappingURL=replace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace.js","sourceRoot":"","sources":["../lib/replace.ts"],"names":[],"mappings":"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"}
|
package/lib/ditto.ts
CHANGED
|
@@ -4,15 +4,27 @@ import { program } from "commander";
|
|
|
4
4
|
// to use V8's code cache to speed up instantiation time
|
|
5
5
|
import "v8-compile-cache";
|
|
6
6
|
|
|
7
|
+
import packageJson from "../package.json";
|
|
8
|
+
|
|
7
9
|
import { init, needsTokenOrSource } from "./init/init";
|
|
8
10
|
import { pull } from "./pull";
|
|
9
11
|
import { quit } from "./utils/quit";
|
|
10
12
|
import addProject from "./add-project";
|
|
11
13
|
import removeProject from "./remove-project";
|
|
14
|
+
import { replace } from "./replace";
|
|
15
|
+
import { generateSuggestions } from "./generate-suggestions";
|
|
12
16
|
|
|
13
17
|
import processMetaOption from "./utils/processMetaOption";
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
const VERSION = packageJson.version;
|
|
20
|
+
|
|
21
|
+
type Command =
|
|
22
|
+
| "pull"
|
|
23
|
+
| "project"
|
|
24
|
+
| "project add"
|
|
25
|
+
| "project remove"
|
|
26
|
+
| "generate-suggestions"
|
|
27
|
+
| "replace";
|
|
16
28
|
|
|
17
29
|
const COMMANDS = [
|
|
18
30
|
{
|
|
@@ -33,23 +45,36 @@ const COMMANDS = [
|
|
|
33
45
|
},
|
|
34
46
|
],
|
|
35
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: "generate-suggestions",
|
|
50
|
+
description: "Find text that can be potentially replaced with Ditto text",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "replace",
|
|
54
|
+
description: "Find and replace Ditto text with code",
|
|
55
|
+
},
|
|
36
56
|
] as const;
|
|
37
57
|
|
|
38
58
|
const setupCommands = () => {
|
|
39
59
|
program.name("ditto-cli");
|
|
40
60
|
|
|
41
|
-
COMMANDS.forEach((
|
|
61
|
+
COMMANDS.forEach((commandConfig) => {
|
|
42
62
|
const cmd = program
|
|
43
|
-
.command(
|
|
44
|
-
.description(
|
|
45
|
-
.action(() => executeCommand(
|
|
63
|
+
.command(commandConfig.name)
|
|
64
|
+
.description(commandConfig.description)
|
|
65
|
+
.action((str, options) => executeCommand(commandConfig.name, options));
|
|
46
66
|
|
|
47
|
-
if ("commands" in
|
|
48
|
-
|
|
67
|
+
if ("commands" in commandConfig) {
|
|
68
|
+
commandConfig.commands.forEach((nestedCommand) => {
|
|
49
69
|
cmd
|
|
50
70
|
.command(nestedCommand.name)
|
|
51
71
|
.description(nestedCommand.description)
|
|
52
|
-
.action(() =>
|
|
72
|
+
.action((str, options) =>
|
|
73
|
+
executeCommand(
|
|
74
|
+
`${commandConfig.name} ${nestedCommand.name}`,
|
|
75
|
+
options
|
|
76
|
+
)
|
|
77
|
+
);
|
|
53
78
|
});
|
|
54
79
|
}
|
|
55
80
|
});
|
|
@@ -60,9 +85,13 @@ const setupOptions = () => {
|
|
|
60
85
|
"-m, --meta <data...>",
|
|
61
86
|
"Include arbitrary data in requests to the Ditto API. Ex: -m githubActionRequest:true trigger:manual"
|
|
62
87
|
);
|
|
88
|
+
program.version(VERSION, "-v, --version", "Output the current version");
|
|
63
89
|
};
|
|
64
90
|
|
|
65
|
-
const executeCommand = async (
|
|
91
|
+
const executeCommand = async (
|
|
92
|
+
command: Command | "none",
|
|
93
|
+
options: string[]
|
|
94
|
+
): Promise<void> => {
|
|
66
95
|
const needsInitialization = needsTokenOrSource();
|
|
67
96
|
if (needsInitialization) {
|
|
68
97
|
try {
|
|
@@ -91,6 +120,12 @@ const executeCommand = async (command: Command | "none"): Promise<void> => {
|
|
|
91
120
|
case "project remove": {
|
|
92
121
|
return removeProject();
|
|
93
122
|
}
|
|
123
|
+
case "generate-suggestions": {
|
|
124
|
+
return generateSuggestions();
|
|
125
|
+
}
|
|
126
|
+
case "replace": {
|
|
127
|
+
return replace(options);
|
|
128
|
+
}
|
|
94
129
|
default: {
|
|
95
130
|
quit("Exiting Ditto CLI...");
|
|
96
131
|
return;
|
|
@@ -103,7 +138,7 @@ const main = async () => {
|
|
|
103
138
|
setupOptions();
|
|
104
139
|
|
|
105
140
|
if (process.argv.length <= 2 && process.argv[1].includes("ditto-cli")) {
|
|
106
|
-
await executeCommand("none");
|
|
141
|
+
await executeCommand("none", []);
|
|
107
142
|
return;
|
|
108
143
|
}
|
|
109
144
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { findTextInJSXFiles } from "./generate-suggestions";
|
|
4
|
+
|
|
5
|
+
describe("findTextInJSXFiles", () => {
|
|
6
|
+
async function createTempFile(filename, content) {
|
|
7
|
+
const filePath = path.join(".", filename);
|
|
8
|
+
await fs.writeFile(filePath, content);
|
|
9
|
+
return filePath;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function deleteTempFile(filename) {
|
|
13
|
+
const filePath = path.join(".", filename);
|
|
14
|
+
await fs.unlink(filePath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it("should return an empty array when no files are found", async () => {
|
|
18
|
+
const result = await findTextInJSXFiles(".", "searchString");
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return an empty array when searchString is not found in any file", async () => {
|
|
24
|
+
const file1 = await createTempFile("file1.jsx", "<div>No match</div>");
|
|
25
|
+
const file2 = await createTempFile("file2.tsx", "<div>No match</div>");
|
|
26
|
+
|
|
27
|
+
const result = await findTextInJSXFiles(".", "searchString");
|
|
28
|
+
|
|
29
|
+
expect(result).toEqual([]);
|
|
30
|
+
|
|
31
|
+
await deleteTempFile(file1);
|
|
32
|
+
await deleteTempFile(file2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return an array with correct occurrences when searchString is found", async () => {
|
|
36
|
+
const file1 = await createTempFile(
|
|
37
|
+
"file1.jsx",
|
|
38
|
+
`<div>Test searchString and another searchString</div>`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const expectedResult = [
|
|
42
|
+
{
|
|
43
|
+
file: file1,
|
|
44
|
+
occurrences: [
|
|
45
|
+
{
|
|
46
|
+
lineNumber: 1,
|
|
47
|
+
preview:
|
|
48
|
+
"<div>Test {{searchString}} and another searchString</div>",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
lineNumber: 1,
|
|
52
|
+
preview:
|
|
53
|
+
"<div>Test searchString and another {{searchString}}</div>",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const result = await findTextInJSXFiles(".", "searchString");
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual(expectedResult);
|
|
62
|
+
|
|
63
|
+
await deleteTempFile(file1);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import glob from "glob";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import traverse from "@babel/traverse";
|
|
5
|
+
|
|
6
|
+
import { fetchComponents } from "./http/fetchComponents";
|
|
7
|
+
|
|
8
|
+
async function generateSuggestions() {
|
|
9
|
+
const components = await fetchComponents();
|
|
10
|
+
const results: {
|
|
11
|
+
[compApiId: string]: FindResults;
|
|
12
|
+
} = {};
|
|
13
|
+
|
|
14
|
+
for (const [compApiId, component] of Object.entries(components)) {
|
|
15
|
+
if (!results[compApiId]) {
|
|
16
|
+
results[compApiId] = [];
|
|
17
|
+
}
|
|
18
|
+
const result = await findTextInJSXFiles(".", component.text);
|
|
19
|
+
results[compApiId] = [...results[compApiId], ...result];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Display results to user
|
|
23
|
+
console.log(JSON.stringify(results, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Occurence {
|
|
27
|
+
lineNumber: number;
|
|
28
|
+
preview: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type FindResults = { file: string; occurrences: Occurence[] }[];
|
|
32
|
+
|
|
33
|
+
async function findTextInJSXFiles(
|
|
34
|
+
path: string,
|
|
35
|
+
searchString: string
|
|
36
|
+
): Promise<FindResults> {
|
|
37
|
+
const result: { file: string; occurrences: Occurence[] }[] = [];
|
|
38
|
+
const files = glob.sync(`${path}/**/*.+(jsx|tsx)`, {
|
|
39
|
+
ignore: "**/node_modules/**",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const promises: Promise<any>[] = [];
|
|
43
|
+
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
promises.push(
|
|
46
|
+
fs.readFile(file, "utf-8").then((code) => {
|
|
47
|
+
const ast = parse(code, {
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
plugins: ["jsx", "typescript"],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const occurrences: Occurence[] = [];
|
|
53
|
+
|
|
54
|
+
traverse(ast, {
|
|
55
|
+
JSXText(path) {
|
|
56
|
+
if (path.node.value.includes(searchString)) {
|
|
57
|
+
const regex = new RegExp(searchString, "g");
|
|
58
|
+
let match;
|
|
59
|
+
while ((match = regex.exec(path.node.value)) !== null) {
|
|
60
|
+
const lines = path.node.value.slice(0, match.index).split("\n");
|
|
61
|
+
|
|
62
|
+
if (!path.node.loc) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lineNumber = path.node.loc.start.line + lines.length - 1;
|
|
67
|
+
|
|
68
|
+
const codeLines = code.split("\n");
|
|
69
|
+
const line = codeLines[lineNumber - 1];
|
|
70
|
+
const preview = replaceAt(
|
|
71
|
+
line,
|
|
72
|
+
match.index,
|
|
73
|
+
searchString,
|
|
74
|
+
`{{${searchString}}}`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
occurrences.push({ lineNumber, preview });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (occurrences.length > 0) {
|
|
84
|
+
result.push({ file, occurrences });
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await Promise.all(promises);
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function replaceAt(
|
|
96
|
+
str: string,
|
|
97
|
+
index: number,
|
|
98
|
+
searchString: string,
|
|
99
|
+
replacement: string
|
|
100
|
+
) {
|
|
101
|
+
return (
|
|
102
|
+
str.substring(0, index) +
|
|
103
|
+
str.substring(index, str.length).replace(searchString, replacement)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { findTextInJSXFiles, generateSuggestions };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import api from "../api";
|
|
2
|
+
|
|
3
|
+
export async function fetchComponents() {
|
|
4
|
+
const { data } = await api.get<{
|
|
5
|
+
[compApiId: string]: {
|
|
6
|
+
name: string;
|
|
7
|
+
text: string;
|
|
8
|
+
status: "NONE" | "WIP" | "REVIEW" | "FINAL";
|
|
9
|
+
folder: "string" | null;
|
|
10
|
+
};
|
|
11
|
+
}>("/components", {});
|
|
12
|
+
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { parseOptions, replaceJSXTextInFile } from "./replace"; // Assuming the function is exported in a separate file
|
|
3
|
+
|
|
4
|
+
// Helper function to create a temporary file
|
|
5
|
+
async function createTempJSXFile(content: string): Promise<string> {
|
|
6
|
+
const tempFile = "tempFile.jsx";
|
|
7
|
+
await fs.writeFile(tempFile, content);
|
|
8
|
+
return tempFile;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Helper function to delete the temporary file
|
|
12
|
+
async function deleteTempFile(filePath: string): Promise<void> {
|
|
13
|
+
await fs.unlink(filePath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("parseOptions", () => {
|
|
17
|
+
test("should pass with valid input", async () => {
|
|
18
|
+
const tempFile = await createTempJSXFile("<div>Hello, world!</div>");
|
|
19
|
+
expect(() =>
|
|
20
|
+
parseOptions([tempFile, "secondString", "thirdString"])
|
|
21
|
+
).not.toThrow();
|
|
22
|
+
|
|
23
|
+
const result = parseOptions([tempFile, "secondString", "thirdString"]);
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
filePath: tempFile,
|
|
26
|
+
searchString: "secondString",
|
|
27
|
+
replaceWith: "thirdString",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
deleteTempFile(tempFile);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("should throw error when options array does not have exactly three strings", () => {
|
|
34
|
+
expect(() => parseOptions(["oneString"])).toThrow(
|
|
35
|
+
"The options array must contain <file path> <search string> <replace with>."
|
|
36
|
+
);
|
|
37
|
+
expect(() => parseOptions(["one", "two"])).toThrow(
|
|
38
|
+
"The options array must contain <file path> <search string> <replace with>."
|
|
39
|
+
);
|
|
40
|
+
expect(() => parseOptions(["one", "two", "three", "four"])).toThrow(
|
|
41
|
+
"The options array must contain <file path> <search string> <replace with>."
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("should throw error when the first string is not a valid file path", () => {
|
|
46
|
+
const invalidFilePath = "/path/to/invalid/file.txt";
|
|
47
|
+
expect(() =>
|
|
48
|
+
parseOptions([invalidFilePath, "secondString", "thirdString"])
|
|
49
|
+
).toThrow(`${invalidFilePath} is not a valid file path.`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("should throw error when the first string is a directory", () => {
|
|
53
|
+
const directoryPath = ".";
|
|
54
|
+
expect(() =>
|
|
55
|
+
parseOptions([directoryPath, "secondString", "thirdString"])
|
|
56
|
+
).toThrow(`${directoryPath} is not a valid file path.`);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("replaceJSXTextInFile", () => {
|
|
61
|
+
afterEach(async () => {
|
|
62
|
+
await deleteTempFile("tempFile.jsx");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("should replace JSX text with a DittoComponent", async () => {
|
|
66
|
+
const tempFile = await createTempJSXFile("<div>Hello, world</div>");
|
|
67
|
+
const searchString = "world";
|
|
68
|
+
const replaceWith = "some-id";
|
|
69
|
+
|
|
70
|
+
await replaceJSXTextInFile(tempFile, { searchString, replaceWith });
|
|
71
|
+
|
|
72
|
+
const transformedCode = await fs.readFile(tempFile, "utf-8");
|
|
73
|
+
expect(transformedCode).toContain(
|
|
74
|
+
`<div>Hello, <DittoComponent componentId="${replaceWith}" /></div>`
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("should handle case-insensitive search", async () => {
|
|
79
|
+
const tempFile = await createTempJSXFile("<div>HeLLo, WoRlD</div>");
|
|
80
|
+
const searchString = "world";
|
|
81
|
+
const replaceWith = "some-id";
|
|
82
|
+
|
|
83
|
+
await replaceJSXTextInFile(tempFile, { searchString, replaceWith });
|
|
84
|
+
|
|
85
|
+
const transformedCode = await fs.readFile(tempFile, "utf-8");
|
|
86
|
+
expect(transformedCode).toContain(
|
|
87
|
+
`<div>HeLLo, <DittoComponent componentId="${replaceWith}" /></div>`
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should not replace JSX text if searchString is not found", async () => {
|
|
92
|
+
const tempFile = await createTempJSXFile("<div>Hello, world!</div>");
|
|
93
|
+
const searchString = "foobar";
|
|
94
|
+
const replaceWith = "some-id";
|
|
95
|
+
|
|
96
|
+
await replaceJSXTextInFile(tempFile, { searchString, replaceWith });
|
|
97
|
+
|
|
98
|
+
const transformedCode = await fs.readFile(tempFile, "utf-8");
|
|
99
|
+
expect(transformedCode).toContain("<div>Hello, world!</div>");
|
|
100
|
+
});
|
|
101
|
+
});
|
package/lib/replace.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { parse } from "@babel/parser";
|
|
3
|
+
import traverse from "@babel/traverse";
|
|
4
|
+
import * as t from "@babel/types";
|
|
5
|
+
import { transformFromAst } from "@babel/core";
|
|
6
|
+
|
|
7
|
+
async function replaceJSXTextInFile(
|
|
8
|
+
filePath: string,
|
|
9
|
+
replacement: { searchString: string; replaceWith: string }
|
|
10
|
+
) {
|
|
11
|
+
const code = await fs.readFile(filePath, "utf-8");
|
|
12
|
+
const ast = parse(code, {
|
|
13
|
+
sourceType: "module",
|
|
14
|
+
plugins: ["jsx", "typescript"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
traverse(ast, {
|
|
18
|
+
JSXText(path) {
|
|
19
|
+
const { searchString, replaceWith } = replacement;
|
|
20
|
+
|
|
21
|
+
const regex = new RegExp(searchString, "gi");
|
|
22
|
+
if (regex.test(path.node.value)) {
|
|
23
|
+
const splitValues = splitByCaseInsensitive(
|
|
24
|
+
path.node.value,
|
|
25
|
+
searchString
|
|
26
|
+
);
|
|
27
|
+
const nodes: (t.JSXElement | t.JSXText)[] = [];
|
|
28
|
+
|
|
29
|
+
splitValues.forEach((splitValue) => {
|
|
30
|
+
if (splitValue.toLowerCase() === searchString.toLowerCase()) {
|
|
31
|
+
const identifier = t.jsxIdentifier("DittoComponent");
|
|
32
|
+
const componentId = t.jsxAttribute(
|
|
33
|
+
t.jsxIdentifier("componentId"),
|
|
34
|
+
t.stringLiteral(replaceWith)
|
|
35
|
+
);
|
|
36
|
+
const o = t.jsxOpeningElement(identifier, [componentId], true);
|
|
37
|
+
const jsxElement = t.jsxElement(o, undefined, [], true);
|
|
38
|
+
nodes.push(jsxElement);
|
|
39
|
+
} else {
|
|
40
|
+
nodes.push(t.jsxText(splitValue));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
path.replaceWithMultiple(nodes);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// transfromFromAst types are wrong?
|
|
50
|
+
/* @ts-ignore */
|
|
51
|
+
const { code: transformedCode } = transformFromAst(ast, code, {
|
|
52
|
+
// Don't let this codebase's Babel config affect the code we're transforming.
|
|
53
|
+
configFile: false,
|
|
54
|
+
});
|
|
55
|
+
fs.writeFile(filePath, transformedCode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitByCaseInsensitive(str: string, delimiter: string) {
|
|
59
|
+
return str.split(new RegExp(`(${delimiter})`, "gi")).filter((s) => s !== "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function replace(options: string[]) {
|
|
63
|
+
let filePath: string;
|
|
64
|
+
let searchString: string;
|
|
65
|
+
let replaceWith: string;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const parsedOptions = parseOptions(options);
|
|
69
|
+
filePath = parsedOptions.filePath;
|
|
70
|
+
searchString = parsedOptions.searchString;
|
|
71
|
+
replaceWith = parsedOptions.replaceWith;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(e);
|
|
74
|
+
console.error(
|
|
75
|
+
"Usage for replace: ditto-cli replace <file path> <search string> <replace with>"
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
replaceJSXTextInFile(filePath, { searchString, replaceWith });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseOptions(options: string[]): {
|
|
84
|
+
filePath: string;
|
|
85
|
+
searchString: string;
|
|
86
|
+
replaceWith: string;
|
|
87
|
+
} {
|
|
88
|
+
if (options.length !== 3) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"The options array must contain <file path> <search string> <replace with>."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const filePath = options[0];
|
|
95
|
+
// Check if the file path exists and points to a regular file (not a directory or other file system object).
|
|
96
|
+
const isFilePathValid =
|
|
97
|
+
fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
|
|
98
|
+
|
|
99
|
+
if (!isFilePathValid) {
|
|
100
|
+
throw new Error(`${filePath} is not a valid file path.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { filePath, searchString: options[1], replaceWith: options[2] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { replace, parseOptions, replaceJSXTextInFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dittowords/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0-alpha",
|
|
4
4
|
"description": "Command Line Interface for Ditto (dittowords.com).",
|
|
5
5
|
"main": "bin/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"ditto-cli": "bin/ditto.js"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@babel/core": "^7.11.4",
|
|
37
36
|
"@babel/preset-env": "^7.20.2",
|
|
38
37
|
"@babel/preset-typescript": "^7.18.6",
|
|
39
38
|
"@tsconfig/node16": "^1.0.3",
|
|
@@ -53,12 +52,20 @@
|
|
|
53
52
|
"typescript": "^4.7.4"
|
|
54
53
|
},
|
|
55
54
|
"dependencies": {
|
|
55
|
+
"@babel/core": "^7.11.4",
|
|
56
|
+
"@babel/parser": "^7.21.4",
|
|
57
|
+
"@babel/traverse": "^7.21.4",
|
|
58
|
+
"@babel/types": "^7.21.4",
|
|
59
|
+
"@types/babel-traverse": "^6.25.7",
|
|
60
|
+
"@types/fs-extra": "^11.0.1",
|
|
56
61
|
"axios": "^0.27.2",
|
|
57
62
|
"boxen": "^5.1.2",
|
|
58
63
|
"chalk": "^4.1.0",
|
|
59
64
|
"commander": "^6.1.0",
|
|
60
65
|
"enquirer": "^2.3.6",
|
|
61
66
|
"faker": "^5.1.0",
|
|
67
|
+
"fs-extra": "^11.1.1",
|
|
68
|
+
"glob": "^9.3.4",
|
|
62
69
|
"js-yaml": "^4.1.0",
|
|
63
70
|
"ora": "^5.0.0",
|
|
64
71
|
"v8-compile-cache": "^2.1.1"
|