redput 4.5.0 → 4.7.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 CHANGED
@@ -1,3 +1,17 @@
1
+ 2026.08.30, v4.7.0
2
+
3
+ feature:
4
+ - 8748c6b redput: insert-rule-link: add
5
+
6
+ 2026.08.29, v4.6.0
7
+
8
+ fix:
9
+ - fb923e2 write-readme: code blocks
10
+ - fa39c1f redput: write: write-readme: insert-rule-section: multiline fixtures
11
+
12
+ feature:
13
+ - bb59d23 insert-rule-section: add
14
+
1
15
  2026.06.26, v4.5.0
2
16
 
3
17
  feature:
@@ -0,0 +1,56 @@
1
+ import {putout} from 'putout';
2
+ import * as pluginConvertEsmToCommonjs from '@putout/plugin-nodejs/convert-esm-to-commonjs';
3
+ import * as pluginOptionalChaining from '@putout/plugin-optional-chaining';
4
+ import * as pluginPutout from '@putout/plugin-putout';
5
+ import * as pluginDeclare from '@putout/plugin-declare';
6
+ import * as pluginTypes from '@putout/plugin-types';
7
+ import * as pluginDeclareBeforeReference from '@putout/plugin-declare-before-reference';
8
+ import * as pluginNodejs from '@putout/plugin-nodejs';
9
+ import * as pluginDestructuring from '@putout/plugin-destructuring';
10
+ import * as pluginMaybe from '@putout/plugin-maybe';
11
+ import * as pluginVariables from '@putout/plugin-variables';
12
+ import * as pluginConditions from '@putout/plugin-conditions';
13
+ import protectFromLoops from './protect-from-loops/protect-from-loops.js';
14
+
15
+ export const compileRule = (code, globals = {}) => {
16
+ const exports = {};
17
+ const module = {
18
+ exports,
19
+ };
20
+
21
+ const keys = [
22
+ 'module',
23
+ 'exports',
24
+ ...Object.keys(globals),
25
+ ];
26
+
27
+ const values = [
28
+ module,
29
+ exports,
30
+ ...Object.values(globals),
31
+ ];
32
+
33
+ const {code: compiled} = putout(code, {
34
+ fixCount: 10,
35
+ plugins: [
36
+ ['declare', pluginDeclare],
37
+ ['declare-before-reference', pluginDeclareBeforeReference],
38
+ ['destructuring/merge-properties', pluginDestructuring.rules['merge-properties']],
39
+ ['variables/extract-keywords', pluginVariables.rules['extract-keywords']],
40
+ ['putout', pluginPutout],
41
+ ['maybe', pluginMaybe],
42
+ ['types', pluginTypes],
43
+ ['conditions', pluginConditions],
44
+ ['variables/convert-const-to-let', pluginVariables.rules['convert-const-to-let']],
45
+ ['optional-chaining', pluginOptionalChaining],
46
+ ['nodejs/declare-after-require', pluginNodejs.rules['declare-after-require']],
47
+ ['nodejs/convert-esm-to-commonjs', pluginConvertEsmToCommonjs],
48
+ ],
49
+ });
50
+
51
+ const safe = protectFromLoops(compiled);
52
+
53
+ new Function(keys.join(), safe).apply(exports, values);
54
+
55
+ return module.exports;
56
+ };
@@ -0,0 +1,19 @@
1
+ import halts, {loopProtect} from 'halting-problem';
2
+
3
+ const TIMEOUT = 10 * 1000;
4
+
5
+ export default function protectFromLoops(jsCode) {
6
+ halts(jsCode);
7
+
8
+ const start = Date.now();
9
+
10
+ jsCode = loopProtect(jsCode, [
11
+ '(function (line) {',
12
+ 'if (Date.now() > ' + (start + TIMEOUT) + ') {',
13
+ ' throw new Error("Infinite loop detected on line " + line);',
14
+ '}',
15
+ '})',
16
+ ].join(''));
17
+
18
+ return jsCode;
19
+ }
package/lib/redput.js CHANGED
@@ -40,6 +40,7 @@ export const redput = async (link, {token}) => {
40
40
  rule,
41
41
  fixture,
42
42
  report,
43
+ link,
43
44
  });
44
45
 
45
46
  if (errorWrite)
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ import {putout} from 'putout';
3
+ import {compileRule} from '../compile-rule/compile-rule.js';
4
+
5
+ const noop = () => '';
6
+
7
+ export const runRule = (ruleSource, source) => {
8
+ const plugin = compileRule(ruleSource, {
9
+ require: (name) => {
10
+ if (name === 'path' || name === 'node:path')
11
+ return path;
12
+
13
+ return putout;
14
+ },
15
+ });
16
+
17
+ plugin.report = plugin.report ?? noop;
18
+
19
+ const {code} = putout(source, {
20
+ cache: false,
21
+ fixCount: 1,
22
+ plugins: [
23
+ ['run-rule', plugin],
24
+ ],
25
+ });
26
+
27
+ return code;
28
+ };
@@ -1,6 +1,8 @@
1
1
  import {access} from 'node:fs/promises';
2
2
  import {tryToCatch} from 'try-to-catch';
3
3
  import {isTSCode} from './is-ts/index.js';
4
+ import {runRule} from '../run-rule/run-rule.js';
5
+ import {writeReadme} from './nested/write-readme/write-readme.js';
4
6
  import {
5
7
  updateNestedIndex,
6
8
  updateOverallNestedFixtures,
@@ -15,7 +17,7 @@ import {
15
17
  writeTests,
16
18
  } from './simple.js';
17
19
 
18
- export const writePlugin = async (name, {rule, fixture, report, options}) => {
20
+ export const writePlugin = async (name, {rule, fixture, report, options, link}) => {
19
21
  const [isNested] = await tryToCatch(access, './package.json');
20
22
 
21
23
  if (isNested)
@@ -24,6 +26,7 @@ export const writePlugin = async (name, {rule, fixture, report, options}) => {
24
26
  fixture,
25
27
  report,
26
28
  options,
29
+ link,
27
30
  });
28
31
 
29
32
  await writeSimple(name, {
@@ -33,25 +36,29 @@ export const writePlugin = async (name, {rule, fixture, report, options}) => {
33
36
  });
34
37
  };
35
38
 
36
- export const writeNested = async (name, {rule, fixture, report, options}) => {
39
+ export const writeNested = async (name, {rule, fixture, report, options, link}) => {
37
40
  const isTS = isTSCode(fixture);
38
41
  const ext = isTS ? 'ts' : 'js';
42
+ const correct = runRule(rule, fixture);
39
43
 
40
44
  await writeNestedRule(name, rule, options);
41
- await writeNestedFixtures(name, fixture, ext);
45
+ await writeNestedFixtures(name, fixture, ext, correct);
42
46
  await writeNestedTests(name, report);
43
47
 
44
48
  await tryToCatch(writeNestedOptional, name, {
45
49
  options,
46
50
  fixture,
47
51
  ext,
52
+ correct,
53
+ link,
48
54
  });
49
55
  };
50
56
 
51
- export const writeNestedOptional = async (name, {options, fixture, ext}) => {
57
+ export const writeNestedOptional = async (name, {options, fixture, ext, correct, link}) => {
52
58
  await updateNestedIndex(name, options);
53
- await updateOverallNestedFixtures(name, fixture, ext);
59
+ await updateOverallNestedFixtures(name, fixture, ext, correct);
54
60
  await updateOverallNestedTest(name);
61
+ await writeReadme(name, link, correct, fixture);
55
62
  };
56
63
 
57
64
  export const writeSimple = async (name, {rule, fixture, report}) => {
@@ -26,13 +26,18 @@ export const writeNestedRule = async (name, data) => {
26
26
  await writeFile(`./${name}/index.js`, code);
27
27
  };
28
28
 
29
- export const writeNestedFixtures = async (name, data, ext) => {
30
- await mkdir(`./${name}/fixture`, {
29
+ export const writeNestedFixtures = async (name, data, ext, correct, overrides = {}) => {
30
+ const {
31
+ write = writeFile,
32
+ mkdir: makeDir = mkdir,
33
+ } = overrides;
34
+
35
+ await makeDir(`./${name}/fixture`, {
31
36
  recursive: true,
32
37
  });
33
38
 
34
- await writeFile(`./${name}/fixture/${name}.${ext}`, data);
35
- await writeFile(`./${name}/fixture/${name}-fix.${ext}`, data);
39
+ await write(`./${name}/fixture/${name}.${ext}`, data);
40
+ await write(`./${name}/fixture/${name}-fix.${ext}`, correct ?? data);
36
41
  };
37
42
 
38
43
  export const writeNestedTests = async (name, report) => {
@@ -86,7 +91,9 @@ export const updateOverallNestedTest = async (name) => {
86
91
  await writeFile(`../test/${plugin}.js`, code);
87
92
  };
88
93
 
89
- export const updateOverallNestedFixtures = async (name, data, ext) => {
90
- await writeFile(`../test/fixture/${name}.${ext}`, data);
91
- await writeFile(`../test/fixture/${name}-fix.${ext}`, data);
94
+ export const updateOverallNestedFixtures = async (name, data, ext, correct, overrides = {}) => {
95
+ const {write = writeFile} = overrides;
96
+
97
+ await write(`../test/fixture/${name}.${ext}`, data);
98
+ await write(`../test/fixture/${name}-fix.${ext}`, correct ?? data);
92
99
  };
@@ -0,0 +1,58 @@
1
+ import {template, operator} from 'putout';
2
+
3
+ const {
4
+ compare,
5
+ __markdown,
6
+ insertAfter,
7
+ } = operator;
8
+
9
+ export const report = ({name}) => `insert '#${name}' link to Rules`;
10
+
11
+ export const fix = ({name, rules}) => {
12
+ const nodeLink = template.ast(`ul(li('✅ ', link('${name}', '#${name}')))`);
13
+ insertAfter(rules, nodeLink);
14
+ };
15
+
16
+ export const traverse = ({options, push}) => {
17
+ const {name = 'hello'} = options;
18
+
19
+ return {
20
+ [__markdown]: (path) => {
21
+ const elements = path.get('arguments.0.elements');
22
+ const {rules, link} = parseElements(elements, name);
23
+
24
+ if (link)
25
+ return;
26
+
27
+ if (!rules)
28
+ return;
29
+
30
+ push({
31
+ path,
32
+ name,
33
+ rules,
34
+ });
35
+ },
36
+ };
37
+ };
38
+
39
+ function parseElements(elements, name) {
40
+ let link;
41
+ let rules;
42
+
43
+ for (const element of elements) {
44
+ if (compare(element, `ul(li('✅ ', link('${name}', '#${name}')))`)) {
45
+ link = element;
46
+ continue;
47
+ }
48
+
49
+ if (compare(element, `heading(2, 'Rules')`))
50
+ rules = element;
51
+ }
52
+
53
+ return {
54
+ link,
55
+ rules,
56
+ };
57
+ }
58
+
@@ -0,0 +1,87 @@
1
+ import {template, operator} from 'putout';
2
+
3
+ const {
4
+ compare,
5
+ __markdown,
6
+ remove,
7
+ } = operator;
8
+
9
+ export const report = ({name}) => `Insert '${name}' to 'README.md'`;
10
+
11
+ export const fix = ({name, link, license, correct, incorrect}) => {
12
+ const {elements} = license.parentPath.node;
13
+
14
+ const heading = template.ast(`heading(2, '${name}')`);
15
+ const paragraph = template.ast(`paragraph('Checkout in 🐊', link(bold('Putout Editor'), '${link}'), '.')`);
16
+ const headingIncorrect = template.ast(`heading(3, '❌ Example of incorrect code')`);
17
+ const codeIncorrect = template.ast(`codeblock('js', \`${incorrect}\`)`);
18
+ const headingCorrect = template.ast(`heading(3, '✅ Example of correct code')`);
19
+ const codeCorrect = template.ast(`codeblock('js', \`${correct}\`)`);
20
+
21
+ const licenseType = license.getNextSibling();
22
+
23
+ elements.push(...[
24
+ heading,
25
+ paragraph,
26
+ headingIncorrect,
27
+ codeIncorrect,
28
+ headingCorrect,
29
+ codeCorrect,
30
+ license.node,
31
+ licenseType.node,
32
+ ]);
33
+
34
+ remove(licenseType);
35
+ remove(license);
36
+ };
37
+
38
+ export const traverse = ({options, push}) => {
39
+ const {
40
+ name,
41
+ link,
42
+ correct = '',
43
+ incorrect = '',
44
+ } = options;
45
+
46
+ return {
47
+ [__markdown]: (path) => {
48
+ const elements = path.get('arguments.0.elements');
49
+ const {license, ruleSection} = parseElements(elements, name);
50
+
51
+ if (ruleSection)
52
+ return;
53
+
54
+ if (!license)
55
+ return;
56
+
57
+ push({
58
+ path,
59
+ name,
60
+ link,
61
+ license,
62
+ correct,
63
+ incorrect,
64
+ });
65
+ },
66
+ };
67
+ };
68
+
69
+ function parseElements(elements, name) {
70
+ let license;
71
+ let ruleSection;
72
+
73
+ for (const element of elements) {
74
+ if (compare(element, 'heading(2, "License")')) {
75
+ license = element;
76
+ continue;
77
+ }
78
+
79
+ if (compare(element, `heading(2, '${name}')`))
80
+ ruleSection = element;
81
+ }
82
+
83
+ return {
84
+ license,
85
+ ruleSection,
86
+ };
87
+ }
@@ -0,0 +1,55 @@
1
+ import {readFile, writeFile} from 'node:fs/promises';
2
+ import {join} from 'node:path';
3
+ import process from 'node:process';
4
+ import {putout} from 'putout';
5
+ import {branch, merge} from '@putout/processor-markdown';
6
+ import * as insertRuleSection from './insert-rule-section/index.js';
7
+ import * as insertRuleLink from './insert-rule-link/index.js';
8
+
9
+ const README = 'README.md';
10
+ const getSource = (a) => a.source;
11
+
12
+ export const writeReadme = async (name, link, correct, incorrect, overrides = {}) => {
13
+ const {
14
+ read = readFile,
15
+ write = writeFile,
16
+ cwd = process.cwd(),
17
+ } = overrides;
18
+
19
+ const readmePath = join(cwd, '..', README);
20
+ const readme = await read(readmePath, 'utf8');
21
+
22
+ const list = await branch(readme);
23
+ const [{source}] = list;
24
+
25
+ const {code} = putout(source, {
26
+ rules: {
27
+ 'insert-rule-section': ['on', {
28
+ name,
29
+ link,
30
+ correct,
31
+ incorrect,
32
+ }],
33
+ 'insert-rule-link': ['on', {
34
+ name,
35
+ }],
36
+ },
37
+ plugins: [
38
+ ['insert-rule-section', insertRuleSection],
39
+ ['insert-rule-link', insertRuleLink],
40
+ ],
41
+ });
42
+
43
+ const newList = [
44
+ code,
45
+ ...list
46
+ .slice(1)
47
+ .map(getSource),
48
+ incorrect,
49
+ correct,
50
+ ];
51
+
52
+ const result = await merge(source, newList);
53
+
54
+ await write(readmePath, result);
55
+ };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "redput",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "CLI tool to convert source from 🐊Putout Editor to files",
5
5
  "main": "lib/redput.js",
6
6
  "bin": {
7
7
  "redput": "bin/redput.js"
8
8
  },
9
9
  "exports": {
10
- "./plugin/get-report": "./lib/get-report/plugin-get-report/index.js"
10
+ "./plugin/get-report": "./lib/get-report/plugin-get-report/index.js",
11
+ "./compile-rule": "./lib/compile-rule/compile-rule.js"
11
12
  },
12
13
  "type": "module",
13
14
  "scripts": {
@@ -55,10 +56,17 @@
55
56
  "dependencies": {
56
57
  "@putout/plugin-declare": "^8.0.0",
57
58
  "@putout/plugin-declare-before-reference": "^10.1.0",
59
+ "@putout/plugin-conditions": "^9.2.1",
60
+ "@putout/plugin-destructuring": "^2.1.1",
58
61
  "@putout/plugin-esm": "^10.0.0",
62
+ "@putout/plugin-maybe": "^5.1.0",
59
63
  "@putout/plugin-nodejs": "^21.0.0",
64
+ "@putout/plugin-optional-chaining": "^3.0.1",
60
65
  "@putout/plugin-putout": "^29.0.0",
66
+ "@putout/plugin-types": "^10.6.0",
61
67
  "@putout/plugin-variables": "^2.2.0",
68
+ "@putout/processor-markdown": "^14.0.5",
69
+ "halting-problem": "^1.1.0",
62
70
  "node-fetch": "^3.3.2",
63
71
  "octokit": "^5.0.3",
64
72
  "putout": "^42.0.5",