@w5s/configurator-core 1.0.0-alpha.1

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/src/block.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { type FileOptions, file, fileSync } from './file.js';
2
+
3
+ export interface BlockOptions {
4
+ /**
5
+ * The marker builder function that will take either `markerBegin` or `markerEnd`
6
+ *
7
+ * @default '# ${mark} MANAGED BLOCK'
8
+ */
9
+ marker?: (mark: 'Begin' | 'End') => string;
10
+
11
+ /**
12
+ * File path
13
+ */
14
+ path: string;
15
+
16
+ /**
17
+ * Block content to insert
18
+ */
19
+ block: string;
20
+
21
+ /**
22
+ * Insert position
23
+ */
24
+ insertPosition?: ['before', 'BeginningOfFile' | RegExp] | ['after', 'EndOfFile' | RegExp];
25
+
26
+ /**
27
+ * Block target state
28
+ */
29
+ state?: 'present' | 'absent';
30
+ }
31
+
32
+ const EOF = 'EndOfFile';
33
+ const BOF = 'BeginningOfFile';
34
+ const insertAt = (str: string, index: number, toInsert: string) => str.slice(0, index) + toInsert + str.slice(index);
35
+ const matchLast = (string: string, regexp: RegExp) => {
36
+ const matcher = new RegExp(regexp.source, `${regexp.flags}g`);
37
+ let firstIndex = -1;
38
+ let lastIndex = -1;
39
+ let matches;
40
+
41
+ while (true) {
42
+ matches = matcher.exec(string);
43
+ if (matches == null) {
44
+ break;
45
+ }
46
+ firstIndex = matches.index;
47
+ lastIndex = matcher.lastIndex;
48
+ }
49
+ return { firstIndex, lastIndex };
50
+ };
51
+
52
+ function toFileOptions(options: BlockOptions): FileOptions {
53
+ const {
54
+ marker = (mark) => `# ${mark.toUpperCase()} MANAGED BLOCK`,
55
+ path,
56
+ block: blockName,
57
+ insertPosition = ['after', EOF],
58
+ state = 'present',
59
+ } = options;
60
+
61
+ const EOL = '\n';
62
+ const beginBlock = marker('Begin');
63
+ const endBlock = marker('End');
64
+
65
+ /**
66
+ * @param content
67
+ */
68
+ function findBlock(content: string) {
69
+ const startIndex = content.indexOf(beginBlock);
70
+ const endIndex = content.indexOf(endBlock) + endBlock.length;
71
+
72
+ return {
73
+ endIndex,
74
+ exists: startIndex !== -1 && endIndex >= 0,
75
+ startIndex,
76
+ };
77
+ }
78
+
79
+ function apply(fullContent: string, blockContent: string) {
80
+ const found = findBlock(fullContent);
81
+ const remove = state === 'absent';
82
+ const replaceBlock = remove ? '' : beginBlock + EOL + blockContent + EOL + endBlock;
83
+ const [positionDirection, positionAnchor] = insertPosition;
84
+
85
+ if (found.exists) {
86
+ return fullContent.slice(0, found.startIndex) + replaceBlock + fullContent.slice(found.endIndex);
87
+ }
88
+ if (remove) {
89
+ return fullContent;
90
+ }
91
+ switch (positionDirection) {
92
+ case 'before': {
93
+ if (positionAnchor !== BOF) {
94
+ const { firstIndex } = matchLast(fullContent, positionAnchor);
95
+ if (firstIndex >= 0) {
96
+ return insertAt(fullContent, firstIndex, replaceBlock + EOL);
97
+ }
98
+ }
99
+
100
+ // Beginning of file
101
+ return replaceBlock + EOL + fullContent;
102
+ }
103
+ case 'after': {
104
+ // insert
105
+ if (positionAnchor !== EOF) {
106
+ const { lastIndex } = matchLast(fullContent, positionAnchor);
107
+ if (lastIndex >= 0) {
108
+ return insertAt(fullContent, lastIndex, EOL + replaceBlock);
109
+ }
110
+ }
111
+
112
+ // end of file
113
+ return fullContent + EOL + replaceBlock;
114
+ }
115
+
116
+ default: {
117
+ throw new Error(`Unsupported position ${String(positionDirection)}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ path,
124
+ state: 'present',
125
+ update: (sourceContent) => apply(sourceContent, blockName),
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Replace asynchronously a block in file that follows pattern :
131
+ *
132
+ * marker(markerBegin)
133
+ * ...
134
+ * marker(markerEnd)
135
+ *
136
+ * @param options
137
+ */
138
+ export function block(options: BlockOptions) {
139
+ return file(toFileOptions(options));
140
+ }
141
+
142
+ /**
143
+ * Replace synchronously a block in file that follows pattern :
144
+ *
145
+ * marker(markerBegin)
146
+ * ...
147
+ * marker(markerEnd)
148
+ *
149
+ * @param options
150
+ */
151
+ export function blockSync(options: BlockOptions) {
152
+ return fileSync(toFileOptions(options));
153
+ }
@@ -0,0 +1,91 @@
1
+ import { chmodSync, mkdirSync, rmSync } from 'node:fs';
2
+ import { chmod, mkdir, rm } from 'node:fs/promises';
3
+ import { __exists } from './__exists.js';
4
+ import type { FileMode } from './FileMode.js';
5
+ import { __toMode } from './__toMode.js';
6
+ import { __existsSync } from './__existsSync.js';
7
+
8
+ export interface DirectoryOptions {
9
+ /**
10
+ * Directory path
11
+ */
12
+ readonly path: string;
13
+
14
+ /**
15
+ * Directory target state
16
+ */
17
+ readonly state: 'present' | 'absent';
18
+
19
+ /**
20
+ * File permissions
21
+ */
22
+ readonly mode?: FileMode;
23
+ }
24
+
25
+ /**
26
+ * Ensure directory is present/absent
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * await directory({
31
+ * path: 'foo/bar',
32
+ * state: 'present',
33
+ * mode: {
34
+ * owner: { read: true, write: true, execute: true },
35
+ * group: { read: true, write: true, execute: true },
36
+ * other: { read: true, write: true, execute: true },
37
+ * },
38
+ * })
39
+ * ```
40
+ *
41
+ * @param options
42
+ */
43
+ export async function directory(options: DirectoryOptions): Promise<void> {
44
+ const { path, state, mode } = options;
45
+ const isPresent = await __exists(path);
46
+ if (state === 'present') {
47
+ const newMode = __toMode(mode);
48
+ if (!isPresent) {
49
+ await mkdir(path, { recursive: true, mode: newMode });
50
+ }
51
+ if (newMode != null && isPresent) {
52
+ await chmod(path, newMode);
53
+ }
54
+ } else if (isPresent) {
55
+ await rm(path, { recursive: true });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Ensure directory is present/absent
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * directorySync({
65
+ * path: 'foo/bar',
66
+ * state: 'present',
67
+ * mode: {
68
+ * owner: { read: true, write: true, execute: true },
69
+ * group: { read: true, write: true, execute: true },
70
+ * other: { read: true, write: true, execute: true },
71
+ * },
72
+ * })
73
+ * ```
74
+ *
75
+ * @param options
76
+ */
77
+ export function directorySync(options: DirectoryOptions): void {
78
+ const { path, state, mode } = options;
79
+ const isPresent = __existsSync(path);
80
+ if (state === 'present') {
81
+ const newMode = __toMode(mode);
82
+ if (!isPresent) {
83
+ mkdirSync(path, { recursive: true, mode: newMode });
84
+ }
85
+ if (newMode != null && isPresent) {
86
+ chmodSync(path, newMode);
87
+ }
88
+ } else if (isPresent) {
89
+ rmSync(path, { recursive: true });
90
+ }
91
+ }
package/src/exec.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+
3
+ export interface ExecOptions {
4
+ /**
5
+ * Current working directory
6
+ */
7
+ cwd?: string;
8
+
9
+ /**
10
+ * Stdio options
11
+ */
12
+ stdio?: 'inherit' | 'pipe' | 'ignore';
13
+ }
14
+
15
+ /**
16
+ * Runs a command in a shell and returns a promise that resolves with an object
17
+ * containing the stdout and stderr strings.
18
+ *
19
+ * @param command The command to run
20
+ * @param args The arguments to pass to the command
21
+ * @param options
22
+ * @returns A promise that resolves with an object like `{ stdout: string, stderr: string }`
23
+ */
24
+ export function execSync(
25
+ command: string,
26
+ args: ReadonlyArray<string>,
27
+ options?: ExecOptions,
28
+ ): { stdout: string; stderr: string } {
29
+ const result = spawnSync(command, args, { ...options });
30
+ const encoding = 'utf8';
31
+
32
+ return { stdout: result.stdout.toString(encoding), stderr: result.stderr.toString(encoding) };
33
+ }
34
+
35
+ /**
36
+ * Runs a command in a shell and returns a promise that resolves with an object
37
+ * containing the stdout and stderr strings.
38
+ *
39
+ * @param command The command to run
40
+ * @param args The arguments to pass to the command
41
+ * @param options
42
+ */
43
+ export async function exec(
44
+ command: string,
45
+ args: ReadonlyArray<string>,
46
+ options?: ExecOptions,
47
+ ): Promise<{ stdout: string; stderr: string }> {
48
+ return new Promise((resolve, reject) => {
49
+ const encoding = 'utf8';
50
+ const child = spawn(command, args, { ...options });
51
+ let stdout = '';
52
+ let stderr = '';
53
+
54
+ // Capture the stdout and stderr streams
55
+ if (child.stdout != null) {
56
+ child.stdout.on('data', (data) => {
57
+ stdout += data.toString(encoding);
58
+ });
59
+ }
60
+ if (child.stderr != null) {
61
+ child.stderr.on('data', (data) => {
62
+ stderr += data.toString(encoding);
63
+ });
64
+ }
65
+ // Handle process exit
66
+ child.on('close', (_code) => {
67
+ resolve({ stdout, stderr });
68
+ });
69
+
70
+ // Handle errors
71
+ child.on('error', reject);
72
+ });
73
+ }
package/src/file.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { chmod, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { chmodSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { __exists } from './__exists.js';
4
+ import { __existsSync } from './__existsSync.js';
5
+ import type { FileMode } from './FileMode.js';
6
+ import { __toMode } from './__toMode.js';
7
+
8
+ export interface FileOptions {
9
+ /**
10
+ * File path
11
+ */
12
+ readonly path: string;
13
+
14
+ /**
15
+ * File target state
16
+ */
17
+ readonly state: 'present' | 'absent';
18
+
19
+ /**
20
+ * File content mapping function
21
+ *
22
+ */
23
+ readonly update?: ((content: string) => string | undefined) | undefined;
24
+
25
+ /**
26
+ * File encoding
27
+ */
28
+ readonly encoding?: BufferEncoding;
29
+
30
+ /**
31
+ * File permissions
32
+ */
33
+ readonly mode?: FileMode;
34
+ }
35
+
36
+ /**
37
+ * Ensure file is present/absent with content initialized or modified with `update
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * await file({
42
+ * path: 'foo/bar',
43
+ * state: 'present',
44
+ * update: (content) => content + '_test', // This will append '_test' after current content
45
+ * mode: {
46
+ * owner: { read: true, write: true, execute: true },
47
+ * group: { read: true, write: true, execute: true },
48
+ * other: { read: true, write: true, execute: true },
49
+ * },
50
+ * })
51
+ * ```
52
+ *
53
+ * @param options
54
+ */
55
+ export async function file(options: FileOptions): Promise<void> {
56
+ const { path, state, update, encoding = 'utf8', mode } = options;
57
+ if (state === 'present') {
58
+ const isPresent = await __exists(path);
59
+ const previousContent = isPresent ? await readFile(path, encoding) : '';
60
+ const newContent = update == null ? (isPresent ? undefined : '') : update(previousContent);
61
+ const newMode = __toMode(mode);
62
+ if (newContent != null) {
63
+ await writeFile(path, newContent, { encoding, mode: newMode });
64
+ }
65
+ if (newMode != null && (isPresent || newContent != null)) {
66
+ await chmod(path, newMode);
67
+ }
68
+ } else {
69
+ await rm(path, { force: true });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Ensure file is present/absent with content initialized or modified with `update
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * fileSync({
79
+ * path: 'foo/bar',
80
+ * state: 'present',
81
+ * update: (content) => content + '_test', // This will append '_test' after current content
82
+ * mode: {
83
+ * owner: { read: true, write: true, execute: true },
84
+ * group: { read: true, write: true, execute: true },
85
+ * other: { read: true, write: true, execute: true },
86
+ * },
87
+ * })
88
+ * ```
89
+ *
90
+ * @param options
91
+ */
92
+ export function fileSync(options: FileOptions): void {
93
+ const { path, state, update, encoding = 'utf8', mode } = options;
94
+ if (state === 'present') {
95
+ const isPresent = __existsSync(path);
96
+ const previousContent = isPresent ? readFileSync(path, encoding) : '';
97
+ const newContent = update == null ? (isPresent ? undefined : '') : update(previousContent);
98
+ const newMode = __toMode(mode);
99
+ if (newContent != null) {
100
+ writeFileSync(path, newContent, { encoding, mode: newMode });
101
+ }
102
+ if (newMode != null && (isPresent || newContent != null)) {
103
+ chmodSync(path, newMode);
104
+ }
105
+ } else {
106
+ rmSync(path, { force: true });
107
+ }
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './directory.js';
2
+ export * from './block.js';
3
+ export * from './file.js';
4
+ export * from './json.js';
5
+ export * from './meta.js';
6
+ export * from './yarnConfig.js';
7
+ export * from './yarnVersion.js';
package/src/json.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { type FileOptions, file, fileSync } from './file.js';
2
+
3
+ export type JSONValue = null | number | string | boolean | JSONValue[] | { [key: string]: JSONValue };
4
+
5
+ export interface JSONOption<V = JSONValue> extends Omit<FileOptions, 'update'> {
6
+ /**
7
+ * File content mapping function
8
+ */
9
+ readonly update?: ((content: V | undefined) => V | undefined) | undefined;
10
+ }
11
+
12
+ function toFileOption<Value>({ update, ...otherOptions }: JSONOption<Value>): FileOptions {
13
+ return {
14
+ ...otherOptions,
15
+
16
+ update:
17
+ update == null
18
+ ? update
19
+ : (content) => {
20
+ const jsonValue = content === '' ? undefined : (JSON.parse(content) as Value);
21
+
22
+ return JSON.stringify(update(jsonValue));
23
+ },
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Ensure file is present/absent asynchronously with content value initialized or modified with `update`
29
+ *
30
+ * @param options
31
+ */
32
+ export async function json<Value>(options: JSONOption<Value>): Promise<void> {
33
+ return file(toFileOption(options));
34
+ }
35
+
36
+ /**
37
+ * Ensure file is present/absent synchronously with content value initialized or modified with `update`
38
+ *
39
+ * @param options
40
+ */
41
+ export function jsonSync<Value>(options: JSONOption<Value>): void {
42
+ return fileSync(toFileOption(options));
43
+ }
package/src/meta.ts ADDED
@@ -0,0 +1,8 @@
1
+ export const meta = Object.freeze({
2
+ // @ts-ignore - these variables are injected at build time
3
+ name: (typeof __PACKAGE_NAME__ === 'undefined' ? '' : __PACKAGE_NAME__) as string,
4
+ // @ts-ignore - these variables are injected at build time
5
+ version: (typeof __PACKAGE_VERSION__ === 'undefined' ? '' : __PACKAGE_VERSION__) as string,
6
+ // @ts-ignore - these variables are injected at build time
7
+ buildNumber: 1 as number, // (typeof __PACKAGE_BUILD_NUMBER__ === 'undefined' ? 0 : __PACKAGE_BUILD_NUMBER__) as number,
8
+ });
@@ -0,0 +1,9 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const _dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ let rootPath: string | undefined = undefined;
6
+
7
+ export function getRootPath() {
8
+ return rootPath ?? (rootPath = path.resolve(_dirname, '../../../..'));// we use renovate.json to find root
9
+ }
@@ -0,0 +1,6 @@
1
+ import path from 'node:path';
2
+ import { getRootPath } from './getRootPath.js';
3
+
4
+ export function getTempPath() {
5
+ return path.join(getRootPath(), '.temp');
6
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { afterAll, beforeAll } from 'vitest';
3
+ import { mkdir, rm } from 'node:fs/promises';
4
+ import { getTempPath } from './getTempPath.js';
5
+
6
+ export function getTestPath(prefix: string, remove = true) {
7
+ const returnValue = path.join(getTempPath(), prefix + Math.random().toString(36));
8
+ if (remove) {
9
+ afterAll(async () => {
10
+ try {
11
+ await rm(returnValue, { recursive: true });
12
+ } catch {
13
+ /* empty */
14
+ }
15
+ });
16
+ }
17
+ beforeAll(async () => {
18
+ try {
19
+ await mkdir(returnValue, { recursive: true });
20
+ } catch {
21
+ /* empty */
22
+ }
23
+ });
24
+ return returnValue;
25
+ }
@@ -0,0 +1 @@
1
+ export * from './getTestPath.js';
@@ -0,0 +1,61 @@
1
+ import { exec, execSync } from './exec.js';
2
+
3
+ export interface YarnConfigOptions {
4
+ /**
5
+ * Configuration key
6
+ */
7
+ readonly key: string;
8
+
9
+ /**
10
+ * Option target state
11
+ */
12
+ readonly state: 'present' | 'absent';
13
+
14
+ /**
15
+ * File content mapping function
16
+ *
17
+ */
18
+ readonly update?: ((content: string) => string | undefined) | undefined;
19
+ }
20
+
21
+ /**
22
+ * Synchronous version of {@link yarnConfig}
23
+ *
24
+ * @param options
25
+ * @example
26
+ * yarnConfigSync({
27
+ * key: 'nodeLinker',
28
+ * state: 'present',
29
+ * update: (content) => content.replace('node-modules', 'hoisted'),
30
+ * })
31
+ */
32
+ export function yarnConfigSync(options: YarnConfigOptions) {
33
+ const { key, state, update } = options;
34
+ if (state === 'present') {
35
+ const { stdout } = execSync('yarn', ['config', 'get', String(key)]);
36
+ execSync('yarn', ['config', 'set', String(key), `${update == null ? '' : update(stdout)}`]);
37
+ } else {
38
+ execSync('yarn', ['config', 'unset']);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Set/Unset yarn configuration value
44
+ *
45
+ * @param options
46
+ * @example
47
+ * await yarnConfig({
48
+ * key: 'nodeLinker',
49
+ * state: 'present',
50
+ * update: (content) => content.replace('node-modules', 'hoisted'),
51
+ * })
52
+ */
53
+ export async function yarnConfig(options: YarnConfigOptions): Promise<void> {
54
+ const { key, state, update } = options;
55
+ if (state === 'present') {
56
+ const { stdout } = await exec('yarn', ['config', 'get', String(key)]);
57
+ await exec('yarn', ['config', 'set', String(key), `${update == null ? '' : update(stdout)}`]);
58
+ } else {
59
+ await exec('yarn', ['config', 'unset']);
60
+ }
61
+ }
@@ -0,0 +1,56 @@
1
+ import { exec, execSync } from './exec.js';
2
+
3
+ export type YarnVersionKind = 'berry' | 'classic';
4
+
5
+ export interface YarnVersionOptions {
6
+ /**
7
+ * Option target state
8
+ */
9
+ readonly state: 'present' | 'absent';
10
+
11
+ /**
12
+ * Version mapping function
13
+ *
14
+ */
15
+ readonly update?: (() => YarnVersionKind | undefined) | undefined;
16
+ }
17
+
18
+ /**
19
+ * Synchronous version of {@link yarnVersion}
20
+ *
21
+ * @param options
22
+ * @example
23
+ * yarnVersionSync({
24
+ * state: 'present',
25
+ * update: () => 'berry', // or 'classic'
26
+ * })
27
+ */
28
+ export function yarnVersionSync(options: YarnVersionOptions) {
29
+ const { state, update } = options;
30
+ if (state === 'present') {
31
+ execSync('yarn', ['set', 'version', `${update == null ? 'berry' : update()}`]);
32
+ } else {
33
+ // TODO: remove yarn.lock
34
+ throw new Error('Not implemented');
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Set/Unset yarn configuration value
40
+ *
41
+ * @param options
42
+ * @example
43
+ * await yarnVersion({
44
+ * state: 'present',
45
+ * update: () => 'berry', // or 'classic'
46
+ * })
47
+ */
48
+ export async function yarnVersion(options: YarnVersionOptions): Promise<void> {
49
+ const { state, update } = options;
50
+ if (state === 'present') {
51
+ await exec('yarn', ['set', 'version', `${update == null ? 'berry' : update()}`]);
52
+ } else {
53
+ // TODO: remove yarn.lock
54
+ throw new Error('Not implemented');
55
+ }
56
+ }