@cluerise/tools 3.0.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/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Cluerise Tools
2
+
3
+ _Tools for maintaining TypeScript projects_
4
+
5
+ ## Tools
6
+
7
+ ### Init
8
+
9
+ ```
10
+ npx @cluerise/tools init [all|name]
11
+ ```
12
+
13
+ Before you run the `init` command, make sure your `package.json` contains
14
+
15
+ - the `"type"` field set to `"module"`,
16
+ - the `"repository"` field set to a valid value (e.g. `"github:org/repo"`).
17
+
18
+ ### Lint
19
+
20
+ ```
21
+ cluerise-tools lint [all|code|staged|commit]
22
+ ```
23
+
24
+ ### Release
25
+
26
+ ```
27
+ cluerise-tools release [create|extract-changelog]
28
+ ```
29
+
30
+ ### Update Node.js versions
31
+
32
+ ```
33
+ cluerise-tools update-node-versions
34
+ ```
35
+
36
+ ### Create Commit Message
37
+
38
+ ```
39
+ cluerise-tools create-commit-message
40
+ ```
41
+
42
+ ### Format Commit Message
43
+
44
+ ```
45
+ cluerise-tools format-commit-message
46
+ ```
47
+
48
+ ### Check Heroku Node.js Version
49
+
50
+ ```
51
+ cluerise-tools check-heroku-node-version
52
+ ```
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ fileArg="$0"
4
+ scriptArg="$1"
5
+ shift
6
+
7
+ filePath=$(readlink -f "$fileArg")
8
+ fileDir=$(dirname "$filePath")
9
+
10
+ node "$fileDir/../scripts/$scriptArg/main.js" "$@"
@@ -0,0 +1,10 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+ max_line_length = 120
@@ -0,0 +1,47 @@
1
+ # All hidden files and directories
2
+ .*
3
+
4
+ # Enable hidden files with extensions
5
+ !.*.*
6
+
7
+ # Enable all hidden directories
8
+ !.*/
9
+
10
+ # Archives
11
+ *.zip
12
+ *.gz
13
+ *.tgz
14
+
15
+ # Build
16
+ build/
17
+ dist/
18
+
19
+ # Dependencies
20
+ node_modules/
21
+
22
+ # Git
23
+ .git/
24
+
25
+ # Git hooks
26
+ hooks/*
27
+ !hooks/*.js
28
+ !hooks/*.ts
29
+
30
+ # Lock files
31
+ package-lock.json
32
+ yarn.lock
33
+
34
+ # Misc
35
+ etc/
36
+
37
+ # Sample of .env and .envrc files
38
+ .env.example
39
+ .env.sample
40
+ .envrc.example
41
+ .envrc.sample
42
+
43
+ # Shell scripts
44
+ *.sh
45
+
46
+ # Tests coverage
47
+ coverage/
@@ -0,0 +1,114 @@
1
+ const importRestrictions = {
2
+ srcDirectory: {
3
+ group: ['src/*'],
4
+ message: '\n Error: Do not import from the src directory'
5
+ },
6
+ nestedDirectories: {
7
+ group: ['./*/*'],
8
+ message: '\n Error: Do not import from nested directories'
9
+ },
10
+ nestedModules: {
11
+ group: Array.from({ length: 10 }, (_, i) => `:/modules${'/*'.repeat(i + 2)}`),
12
+ message: '\n Error: Do not import from nested modules'
13
+ },
14
+ nestedDirectoriesInParentModules: {
15
+ group: Array.from({ length: 10 }, (_, i) => [`${`../`.repeat(i + 1)}*/**`, `!${`../`.repeat(i + 1)}../*`]).flat(),
16
+ message: '\n Error: Do not import from nested directories in parent modules'
17
+ },
18
+ appDirectoryInModules: {
19
+ group: [':/app', ':/app/**'],
20
+ message: '\n Error: Do not import from the app directory in the modules'
21
+ }
22
+ };
23
+
24
+ const commonImportRestrictions = [
25
+ importRestrictions.srcDirectory,
26
+ importRestrictions.nestedDirectories,
27
+ importRestrictions.nestedModules,
28
+ importRestrictions.nestedDirectoriesInParentModules
29
+ ];
30
+
31
+ module.exports = {
32
+ env: {
33
+ es6: true,
34
+ node: true
35
+ },
36
+ extends: [
37
+ 'eslint:recommended',
38
+ 'plugin:@typescript-eslint/recommended',
39
+ 'plugin:prettier/recommended',
40
+ 'plugin:json/recommended-with-comments',
41
+ 'plugin:markdown/recommended-legacy',
42
+ 'plugin:yml/standard'
43
+ ],
44
+ parser: '@typescript-eslint/parser',
45
+ parserOptions: {
46
+ ecmaVersion: 2018,
47
+ sourceType: 'module'
48
+ },
49
+ plugins: ['import', 'simple-import-sort', '@html-eslint'],
50
+ rules: {
51
+ '@typescript-eslint/ban-ts-comment': 'off',
52
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
53
+ '@typescript-eslint/no-unused-vars': 'off',
54
+
55
+ 'import/first': 'error',
56
+ 'import/newline-after-import': 'error',
57
+ 'import/no-amd': 'error',
58
+ 'import/no-commonjs': 'error',
59
+ 'import/no-default-export': 'error',
60
+ 'import/no-duplicates': 'error',
61
+ 'import/no-mutable-exports': 'error',
62
+ 'import/no-unassigned-import': 'error',
63
+ 'import/order': 'off',
64
+
65
+ 'no-restricted-imports': ['error', { patterns: commonImportRestrictions }],
66
+ 'no-undef-init': 'error',
67
+ 'object-shorthand': 'error',
68
+
69
+ 'simple-import-sort/exports': 'error',
70
+ 'simple-import-sort/imports': 'error',
71
+
72
+ 'sort-imports': 'off',
73
+
74
+ 'yml/no-empty-mapping-value': 'off',
75
+ 'yml/quotes': ['error', { prefer: 'single' }]
76
+ },
77
+ overrides: [
78
+ {
79
+ files: ['*.cjs', '*.cts'],
80
+ rules: {
81
+ 'import/no-commonjs': 'off',
82
+ '@typescript-eslint/no-var-requires': 'off'
83
+ }
84
+ },
85
+ {
86
+ files: ['*.config.js', '*.config.ts'],
87
+ rules: {
88
+ 'import/no-default-export': 'off'
89
+ }
90
+ },
91
+ {
92
+ files: ['src/modules/**'],
93
+ rules: {
94
+ 'no-restricted-imports': [
95
+ 'error',
96
+ { patterns: [importRestrictions.appDirectoryInModules, ...commonImportRestrictions] }
97
+ ]
98
+ }
99
+ },
100
+ {
101
+ files: ['*.yaml', '*.yml'],
102
+ parser: 'yaml-eslint-parser'
103
+ },
104
+ {
105
+ files: ['*.html'],
106
+ parser: '@html-eslint/parser',
107
+ extends: ['plugin:@html-eslint/recommended'],
108
+ rules: {
109
+ '@html-eslint/require-title': 'off',
110
+ '@html-eslint/indent': ['error', 2]
111
+ }
112
+ }
113
+ ]
114
+ };
@@ -0,0 +1 @@
1
+ v22.1.0
@@ -0,0 +1,37 @@
1
+ # All hidden files and directories
2
+ .*
3
+
4
+ # Enable hidden files with extensions
5
+ !.*.*
6
+
7
+ # Enable all hidden directories
8
+ !.*/
9
+
10
+ # Build
11
+ build/
12
+ dist/
13
+
14
+ # Dependencies
15
+ node_modules/
16
+
17
+ # Git
18
+ .git/
19
+
20
+ # Git hooks
21
+ hooks/*
22
+ !hooks/*.js
23
+ !hooks/*.ts
24
+
25
+ # Lock files
26
+ package-lock.json
27
+ yarn.lock
28
+
29
+ # Misc
30
+ etc/
31
+
32
+ # Sample of .env file
33
+ .env.example
34
+ .env.sample
35
+
36
+ # Tests coverage
37
+ coverage/
@@ -0,0 +1,23 @@
1
+ # Build
2
+ build/
3
+ dist/
4
+
5
+ # Dependencies
6
+ node_modules/
7
+
8
+ # .env, .envrc
9
+ .env
10
+ .envrc
11
+
12
+ # Lint Staged
13
+ .lint-staged-temp-tsconfig.json
14
+
15
+ # Misc
16
+ .DS_Store
17
+ etc/
18
+
19
+ # Stryker
20
+ .stryker-tmp/
21
+
22
+ # Tests coverage
23
+ coverage/
@@ -0,0 +1 @@
1
+ engine-strict = true
@@ -0,0 +1,31 @@
1
+ import { RuleConfigSeverity, UserConfig } from '@commitlint/types';
2
+
3
+ const commitlintConfig: UserConfig = {
4
+ parserPreset: 'conventional-changelog-conventionalcommits',
5
+ rules: {
6
+ 'body-full-stop': [RuleConfigSeverity.Error, 'always', '.'],
7
+ 'body-leading-blank': [RuleConfigSeverity.Error, 'always'],
8
+ 'body-case': [RuleConfigSeverity.Error, 'always', 'sentence-case'],
9
+
10
+ 'footer-leading-blank': [RuleConfigSeverity.Error, 'always'],
11
+
12
+ 'header-full-stop': [RuleConfigSeverity.Error, 'never', '.'],
13
+ 'header-max-length': [RuleConfigSeverity.Error, 'always', 100],
14
+
15
+ 'scope-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
16
+ 'scope-enum': [RuleConfigSeverity.Error, 'always', ['actions', 'dev', 'lock', 'prod', 'security']],
17
+
18
+ 'subject-case': [RuleConfigSeverity.Error, 'always', 'sentence-case'],
19
+ 'subject-empty': [RuleConfigSeverity.Error, 'never'],
20
+
21
+ 'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
22
+ 'type-enum': [
23
+ RuleConfigSeverity.Error,
24
+ 'always',
25
+ ['chore', 'deps', 'docs', 'feat', 'fix', 'perf', 'refactor', 'release', 'revert', 'style', 'test']
26
+ ],
27
+ 'type-empty': [RuleConfigSeverity.Error, 'never']
28
+ }
29
+ };
30
+
31
+ export default commitlintConfig;
@@ -0,0 +1 @@
1
+ npm run __git-hook:commit-msg --if-present "$@"
@@ -0,0 +1 @@
1
+ npm run __git-hook:post-commit --if-present "$@"
@@ -0,0 +1 @@
1
+ npm run __git-hook:pre-commit --if-present "$@"
@@ -0,0 +1 @@
1
+ npm run __git-hook:prepare-commit-msg --if-present "$@"
@@ -0,0 +1,31 @@
1
+ import FileSystem from 'fs';
2
+ import Path from 'path';
3
+
4
+ const tsconfigPath = '.lint-staged-temp-tsconfig.json';
5
+
6
+ const toRelative = (filename) => Path.relative('.', filename);
7
+ const mapToRelative = (filenames) => filenames.map(toRelative);
8
+
9
+ export default {
10
+ '**': (filenames) => {
11
+ const relativeFilenames = mapToRelative(filenames).join(' ');
12
+
13
+ return [`eslint ${relativeFilenames}`, `prettier --check ${relativeFilenames}`];
14
+ },
15
+ '**/*.ts?(x)': (filenames) => {
16
+ // The lint-staged command runs this function multiple times to prepare
17
+ // console messages. We want to save the temporary tsconfig.json file only
18
+ // when this function is run with filenames (that start with a slash)
19
+ if (filenames.length > 0 && filenames[0].startsWith('/')) {
20
+ const relativeFilenames = mapToRelative(filenames);
21
+ const tsconfig = {
22
+ extends: './tsconfig.json',
23
+ files: relativeFilenames
24
+ };
25
+
26
+ FileSystem.writeFileSync(tsconfigPath, JSON.stringify(tsconfig));
27
+ }
28
+
29
+ return `tsc -p ${tsconfigPath}`;
30
+ }
31
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ printWidth: 120,
3
+ singleQuote: true,
4
+ trailingComma: 'none',
5
+ overrides: [
6
+ {
7
+ files: '*.html',
8
+ options: {
9
+ parser: 'html'
10
+ }
11
+ }
12
+ ]
13
+ };
@@ -0,0 +1,119 @@
1
+ export const defaultReleaseRules = [
2
+ { breaking: true, release: 'major' },
3
+ { type: 'feat', release: 'minor' },
4
+ { type: 'fix', release: 'patch' },
5
+ { type: 'perf', release: 'minor' },
6
+ { type: 'deps', release: false },
7
+ { type: 'deps', scope: 'prod', release: 'patch' },
8
+ { type: 'refactor', release: false },
9
+ { type: 'docs', release: false },
10
+ { type: 'chore', release: false },
11
+ { type: 'release', release: false },
12
+ { type: 'revert', release: false },
13
+ { type: 'style', release: false },
14
+ { type: 'test', release: false }
15
+ ];
16
+
17
+ export const defaultChangelogTypes = [
18
+ { type: 'feat', section: 'Features' },
19
+ { type: 'fix', section: 'Fixes' },
20
+ { type: 'perf', section: 'Performance improvements' },
21
+ { type: 'deps', scope: 'prod', section: 'Dependency updates' }
22
+ ];
23
+
24
+ export const createReleaseConfig = ({
25
+ host,
26
+ owner,
27
+ repository,
28
+ releaseRules = defaultReleaseRules,
29
+ changelogTypes = defaultChangelogTypes
30
+ }) => ({
31
+ branches: ['main'],
32
+ repositoryUrl: `file://${process.cwd()}`,
33
+ preset: 'conventionalcommits',
34
+ dryRun: true,
35
+
36
+ plugins: [
37
+ [
38
+ '@semantic-release/commit-analyzer',
39
+ {
40
+ releaseRules
41
+ }
42
+ ],
43
+ [
44
+ '@semantic-release/release-notes-generator',
45
+ {
46
+ host,
47
+ presetConfig: {
48
+ types: changelogTypes
49
+ },
50
+ writerOpts: {
51
+ commitGroupsSort: ({ title: titleA }, { title: titleB }) => {
52
+ const indexA = changelogTypes.findIndex(({ section }) => section === titleA);
53
+ const indexB = changelogTypes.findIndex(({ section }) => section === titleB);
54
+
55
+ return indexA - indexB;
56
+ },
57
+ finalizeContext: (context) => {
58
+ const commitGroups = context.commitGroups.map((commitGroup) => ({
59
+ ...commitGroup,
60
+ commits: commitGroup.commits.map((commit) => ({
61
+ ...commit,
62
+ subject: commit.subject.replace(
63
+ `${host}/${context.owner}/${context.repository}`,
64
+ `${host}/${owner}/${repository}`
65
+ )
66
+ })),
67
+ collapse: commitGroup.title === 'Dependency updates'
68
+ }));
69
+
70
+ return {
71
+ ...context,
72
+ owner,
73
+ repository,
74
+ commitGroups
75
+ };
76
+ },
77
+ mainTemplate: `{{> header}}
78
+
79
+ {{#if noteGroups}}
80
+ {{#each noteGroups}}
81
+
82
+ ### ⚠ {{title}} !!
83
+
84
+ {{#each notes}}
85
+ * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}}
86
+ {{/each}}
87
+ {{/each}}
88
+ {{/if}}
89
+ {{#each commitGroups}}
90
+
91
+ {{#if title}}
92
+ ### {{title}}
93
+
94
+ {{/if}}
95
+ {{#if collapse}}
96
+ <details>
97
+ <summary>Show all dependency updates</summary>
98
+
99
+ {{/if}}
100
+ {{#each commits}}
101
+ {{> commit root=@root}}
102
+ {{/each}}
103
+ {{#if collapse}}
104
+
105
+ </details>
106
+ {{/if}}
107
+
108
+ {{/each}}`
109
+ }
110
+ }
111
+ ]
112
+ ]
113
+ });
114
+
115
+ export default createReleaseConfig({
116
+ host: 'https://github.com',
117
+ owner: 'cluerise',
118
+ repository: 'tools'
119
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "emitDecoratorMetadata": true,
4
+ "esModuleInterop": true,
5
+ "exactOptionalPropertyTypes": true,
6
+ "experimentalDecorators": true,
7
+ "isolatedModules": true,
8
+ "jsx": "preserve",
9
+ "lib": ["dom", "esnext"],
10
+ "module": "esnext",
11
+ "moduleResolution": "node",
12
+ "noEmit": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "baseUrl": "./",
17
+ "paths": {
18
+ ":/*": ["./src/*/index"]
19
+ },
20
+ "resolveJsonModule": true,
21
+ "skipLibCheck": true,
22
+ "strict": true,
23
+ "target": "esnext",
24
+ "types": ["node", "vite/client"]
25
+ },
26
+ "exclude": ["node_modules"]
27
+ }
@@ -0,0 +1,138 @@
1
+ import SemVer from "semver";
2
+ import FileSystem from "fs/promises";
3
+ import { z } from "zod";
4
+ import { parse } from "smol-toml";
5
+ class ParseGitRepositoryError extends Error {
6
+ }
7
+ const gitProviderOrigins = {
8
+ github: "https://github.com"
9
+ };
10
+ const gitProviderNames = Object.keys(gitProviderOrigins);
11
+ const isGitProviderName = (name) => gitProviderNames.includes(name);
12
+ const parseRepositoryUrl = (urlString) => {
13
+ const urlValue = urlString.includes(":") ? urlString : `https://${urlString}`;
14
+ const url = new URL(urlValue);
15
+ const scheme = url.protocol.slice(0, -1);
16
+ if (isGitProviderName(scheme)) {
17
+ const [owner, repositoryName] = url.pathname.split("/");
18
+ if (!owner || !repositoryName) {
19
+ throw new ParseGitRepositoryError("Unknown owner or repositoryName");
20
+ }
21
+ return {
22
+ origin: gitProviderOrigins[scheme],
23
+ owner,
24
+ repositoryName
25
+ };
26
+ }
27
+ if (scheme === "https") {
28
+ const [, owner, repositoryName] = url.pathname.split("/");
29
+ if (!owner || !repositoryName) {
30
+ throw new ParseGitRepositoryError("Unknown owner or repositoryName");
31
+ }
32
+ return {
33
+ origin: url.origin,
34
+ owner,
35
+ repositoryName: repositoryName.endsWith(".git") ? repositoryName.slice(0, -4) : repositoryName
36
+ };
37
+ }
38
+ throw new ParseGitRepositoryError("Unsupported repository URL");
39
+ };
40
+ const parsePackageJsonRepository = (repository) => {
41
+ if (typeof repository === "string") {
42
+ return parseRepositoryUrl(repository);
43
+ }
44
+ return parseRepositoryUrl(repository.url);
45
+ };
46
+ const enginesSchema = z.object({
47
+ node: z.string()
48
+ });
49
+ const repositoryObjectSchema = z.object({
50
+ type: z.string(),
51
+ url: z.string(),
52
+ directory: z.ostring()
53
+ });
54
+ const repositorySchema = z.union([z.string(), repositoryObjectSchema]);
55
+ const packageJsonSchema = z.object({
56
+ name: z.string(),
57
+ version: z.string(),
58
+ description: z.string(),
59
+ engines: enginesSchema.optional(),
60
+ repository: repositorySchema.optional()
61
+ });
62
+ const readPackageJson = async () => {
63
+ const packageJsonData = await FileSystem.readFile("package.json", { encoding: "utf8" });
64
+ const packageJson = JSON.parse(packageJsonData);
65
+ const parseResult = packageJsonSchema.safeParse(packageJson);
66
+ if (!parseResult.success) {
67
+ throw parseResult.error;
68
+ }
69
+ return packageJson;
70
+ };
71
+ const CoreCommands = {
72
+ readPackageJson,
73
+ parsePackageJsonRepository
74
+ };
75
+ const runMain = (main2) => {
76
+ main2(process.argv.slice(2)).then((exitCode) => {
77
+ process.exit(exitCode);
78
+ }).catch((error) => {
79
+ console.error(error);
80
+ process.exit(1);
81
+ });
82
+ };
83
+ const herokuNodeReleasesUrl = "https://raw.githubusercontent.com/heroku/heroku-buildpack-nodejs/latest/inventory/node.toml";
84
+ const herokuNodeReleaseSchema = z.object({
85
+ version: z.string(),
86
+ channel: z.string(),
87
+ arch: z.string(),
88
+ url: z.string(),
89
+ etag: z.string()
90
+ });
91
+ const herokuNodeReleasesSchema = z.object({
92
+ name: z.string(),
93
+ releases: z.array(herokuNodeReleaseSchema)
94
+ });
95
+ const fetchNodeReleases = async () => {
96
+ const response = await fetch(herokuNodeReleasesUrl);
97
+ const data = await response.text();
98
+ const releasesData = parse(data);
99
+ const { releases } = herokuNodeReleasesSchema.parse(releasesData);
100
+ return releases;
101
+ };
102
+ const HerokuCommands = {
103
+ fetchNodeReleases
104
+ };
105
+ const getResultMessage = (nodeVersion, supported) => {
106
+ const nodeVersionIsRange = SemVer.valid(nodeVersion) === null;
107
+ const name = `Node.js version ${nodeVersionIsRange ? `range "${nodeVersion}"` : nodeVersion}`;
108
+ return `${name} is ${supported ? "" : "not "}supported on Heroku`;
109
+ };
110
+ const main = async () => {
111
+ var _a;
112
+ try {
113
+ const packageJson = await CoreCommands.readPackageJson();
114
+ const nodeVersion = (_a = packageJson.engines) == null ? void 0 : _a.node;
115
+ if (!nodeVersion) {
116
+ console.error("Error: Node.js version is not specified in package.json");
117
+ return 1;
118
+ }
119
+ const nodeVersionIsValid = SemVer.validRange(nodeVersion) !== null;
120
+ if (!nodeVersionIsValid) {
121
+ console.error(`Error: Node.js version "${nodeVersion}" is not valid semver version or range`);
122
+ return 1;
123
+ }
124
+ const releases = await HerokuCommands.fetchNodeReleases();
125
+ const nodeVersionIsSupported = releases.some((release) => SemVer.satisfies(release.version, nodeVersion));
126
+ if (!nodeVersionIsSupported) {
127
+ console.error(`Error: ${getResultMessage(nodeVersion, false)}`);
128
+ return 1;
129
+ }
130
+ console.log(`Info: ${getResultMessage(nodeVersion, true)}`);
131
+ return 0;
132
+ } catch (error) {
133
+ console.error("Error: Cannot check Node.js version on Heroku");
134
+ console.error(error);
135
+ return 1;
136
+ }
137
+ };
138
+ runMain(main);