@markuplint/create-rule 4.0.0-dev.3823

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/bin/create-rule.mjs +9 -0
  4. package/lib/cli.d.ts +1 -0
  5. package/lib/cli.js +79 -0
  6. package/lib/create-rule-helper-error.d.ts +3 -0
  7. package/lib/create-rule-helper-error.js +6 -0
  8. package/lib/create-rule-helper.d.ts +2 -0
  9. package/lib/create-rule-helper.js +16 -0
  10. package/lib/create-rule-package.d.ts +2 -0
  11. package/lib/create-rule-package.js +18 -0
  12. package/lib/create-rule-to-core.d.ts +3 -0
  13. package/lib/create-rule-to-core.js +36 -0
  14. package/lib/create-rule-to-project.d.ts +2 -0
  15. package/lib/create-rule-to-project.js +16 -0
  16. package/lib/fs-exists.d.ts +1 -0
  17. package/lib/fs-exists.js +10 -0
  18. package/lib/glob.d.ts +1 -0
  19. package/lib/glob.js +6 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +3 -0
  22. package/lib/install-scaffold.d.ts +4 -0
  23. package/lib/install-scaffold.js +61 -0
  24. package/lib/is-markuplint-repo.d.ts +1 -0
  25. package/lib/is-markuplint-repo.js +5 -0
  26. package/lib/read-package-json.d.ts +1 -0
  27. package/lib/read-package-json.js +13 -0
  28. package/lib/search-core-repository.d.ts +1 -0
  29. package/lib/search-core-repository.js +17 -0
  30. package/lib/transfer.d.ts +8 -0
  31. package/lib/transfer.js +112 -0
  32. package/lib/types.d.ts +30 -0
  33. package/lib/types.js +1 -0
  34. package/package.json +40 -0
  35. package/scaffold/.eslintrc +5 -0
  36. package/scaffold/core/README.ja.md +26 -0
  37. package/scaffold/core/README.md +22 -0
  38. package/scaffold/core/index.spec.ts +18 -0
  39. package/scaffold/core/index.ts +31 -0
  40. package/scaffold/core/schema.json +47 -0
  41. package/scaffold/package/README.md +60 -0
  42. package/scaffold/package/src/index.ts +15 -0
  43. package/scaffold/package/src/rules/__ruleName__.ts +94 -0
  44. package/scaffold/package/tsconfig.json +20 -0
  45. package/scaffold/project/index.ts +15 -0
  46. package/scaffold/project/rules/__ruleName__.spec.ts +32 -0
  47. package/scaffold/project/rules/__ruleName__.ts +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017-2023 Yusuke Hirao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @markuplint/create-rule
2
+
3
+ [![npm version](https://badge.fury.io/js/%40markuplint%2Fcreate-rule.svg)](https://www.npmjs.com/package/@markuplint/create-rule)
4
+
5
+ ## Usage
6
+
7
+ ```shell
8
+ $ npx @markuplint/create-rule <rule-name>
9
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRule } from '../lib/cli.js';
4
+
5
+ await createRule().catch(error => {
6
+ process.stderr.write(error + '\n');
7
+ process.exit(1);
8
+ });
9
+ process.exit(0);
package/lib/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function createRule(): Promise<void>;
package/lib/cli.js ADDED
@@ -0,0 +1,79 @@
1
+ import { resolve } from 'node:path';
2
+ import { input, installModule, select, confirm, font, header } from '@markuplint/cli-utils';
3
+ import { createRuleHelper } from './create-rule-helper.js';
4
+ import { isMarkuplintRepo } from './is-markuplint-repo.js';
5
+ const icons = {
6
+ README: '📝',
7
+ index: '📜',
8
+ schema: '⚙️ ',
9
+ package: '🎁',
10
+ tsconfig: '💎',
11
+ };
12
+ export async function createRule() {
13
+ process.stdout.write(header('Create a rule'));
14
+ process.stdout.write('\n');
15
+ process.stdout.write('\n');
16
+ const firstChoices = [
17
+ { name: 'Add the rule to this project', value: 'ADD_TO_PROJECT' },
18
+ { name: 'Create the rule and publish it as a package', value: 'PUBLISH_AS_PACKAGE' },
19
+ ];
20
+ if (await isMarkuplintRepo()) {
21
+ firstChoices.push({ name: 'Contribute the new rule to markuplint core rules', value: 'CONTRIBUTE_TO_CORE' });
22
+ }
23
+ const purpose = await select({
24
+ message: 'What purpose do you create the rule for?',
25
+ choices: firstChoices,
26
+ });
27
+ const dirQuestion = purpose === 'ADD_TO_PROJECT' ? 'What is the directory name?' : 'What is the plugin name?';
28
+ const pluginName = purpose === 'CONTRIBUTE_TO_CORE' ? '' : await input(dirQuestion, /^[a-z][\da-z]*(?:-[a-z][\da-z]*)*$/i);
29
+ const ruleName = await input('What is the rule name?', /^[a-z][\da-z]*(?:-[a-z][\da-z]*)*$/i);
30
+ const core = purpose === 'CONTRIBUTE_TO_CORE'
31
+ ? {
32
+ description: await input('Description:'),
33
+ category: await select({
34
+ message: 'Category:',
35
+ choices: [
36
+ { name: 'Conformance checking', value: 'validation' },
37
+ { name: 'Accessibility', value: 'a11y' },
38
+ { name: 'Naming Convention', value: 'naming-convention' },
39
+ { name: 'Maintainability', value: 'maintainability' },
40
+ { name: 'Style', value: 'style' },
41
+ ],
42
+ }),
43
+ severity: await select({
44
+ message: 'Severity:',
45
+ choices: [
46
+ { name: 'error', value: 'error' },
47
+ { name: 'warning', value: 'warning' },
48
+ ],
49
+ }),
50
+ }
51
+ : undefined;
52
+ const lang = purpose === 'CONTRIBUTE_TO_CORE'
53
+ ? 'TYPESCRIPT'
54
+ : await select({
55
+ message: 'Which language will you implement?',
56
+ choices: [
57
+ { name: 'TypeScript', value: 'TYPESCRIPT' },
58
+ { name: 'JavaScript', value: 'JAVASCRIPT' },
59
+ ],
60
+ });
61
+ const needTest = purpose === 'CONTRIBUTE_TO_CORE' ? true : await confirm('Do you need the test?', { initial: true });
62
+ const result = await createRuleHelper({ purpose, pluginName, ruleName, lang, needTest, core });
63
+ for (const file of result.files) {
64
+ output(pluginName || 'core', file.test ? '🖍 ' : icons[file.name] ?? '🛡 ', file.fileName, resolve(file.destDir, file.fileName + file.ext));
65
+ }
66
+ if (result.dependencies.length > 0) {
67
+ await installModule(result.dependencies);
68
+ }
69
+ if (result.devDependencies.length > 0) {
70
+ await installModule(result.devDependencies, true);
71
+ }
72
+ }
73
+ function output(name, icon, title, path) {
74
+ const _marker = font.xterm(39)('✔') + ' ';
75
+ const _title = (icon, title) => `${icon} ` + font.bold(`${name}/${title}`);
76
+ const _file = (path) => ' ' + font.cyanBright(path);
77
+ process.stdout.write(_marker + _title(icon, title) + _file(path));
78
+ process.stdout.write('\n');
79
+ }
@@ -0,0 +1,3 @@
1
+ export declare class CreateRuleHelperError extends Error {
2
+ name: string;
3
+ }
@@ -0,0 +1,6 @@
1
+ export class CreateRuleHelperError extends Error {
2
+ constructor() {
3
+ super(...arguments);
4
+ this.name = 'CreateRuleHelperError';
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { CreateRuleHelperParams, CreateRuleHelperResult } from './types.js';
2
+ export declare function createRuleHelper(params: CreateRuleHelperParams): Promise<CreateRuleHelperResult>;
@@ -0,0 +1,16 @@
1
+ import { createRulePackage } from './create-rule-package.js';
2
+ import { createRuleToCore } from './create-rule-to-core.js';
3
+ import { createRuleToProject } from './create-rule-to-project.js';
4
+ export async function createRuleHelper(params) {
5
+ switch (params.purpose) {
6
+ case 'ADD_TO_PROJECT': {
7
+ return await createRuleToProject(params);
8
+ }
9
+ case 'PUBLISH_AS_PACKAGE': {
10
+ return await createRulePackage(params);
11
+ }
12
+ case 'CONTRIBUTE_TO_CORE': {
13
+ return await createRuleToCore(params);
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
2
+ export declare function createRulePackage({ pluginName, ruleName, lang, needTest, }: CreateRuleCreatorParams): Promise<CreateRuleHelperResult>;
@@ -0,0 +1,18 @@
1
+ import path from 'node:path';
2
+ import { CreateRuleHelperError } from './create-rule-helper-error.js';
3
+ import { glob } from './glob.js';
4
+ import { installScaffold } from './install-scaffold.js';
5
+ export async function createRulePackage({ pluginName, ruleName, lang, needTest, }) {
6
+ const newRuleDir = path.resolve(process.cwd(), '*');
7
+ const files = await glob(newRuleDir);
8
+ if (files.length > 0) {
9
+ throw new CreateRuleHelperError('The directory is not empty');
10
+ }
11
+ return await installScaffold('package', process.cwd(), {
12
+ pluginName,
13
+ ruleName,
14
+ lang,
15
+ needTest,
16
+ packageJson: true,
17
+ });
18
+ }
@@ -0,0 +1,3 @@
1
+ import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
2
+ export declare function createRuleToCore({ ruleName, core }: CreateRuleCreatorParams): Promise<CreateRuleHelperResult>;
3
+ export declare function getRulesDir(): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { CreateRuleHelperError } from './create-rule-helper-error.js';
3
+ import { fsExists } from './fs-exists.js';
4
+ import { installScaffold } from './install-scaffold.js';
5
+ import { searchCoreRepository } from './search-core-repository.js';
6
+ const rulesRelDir = ['packages', '@markuplint', 'rules', 'src'];
7
+ export async function createRuleToCore({ ruleName, core }) {
8
+ if (!core) {
9
+ throw new CreateRuleHelperError('Core options are not defined');
10
+ }
11
+ const rulesDir = await getRulesDir();
12
+ const newRuleDir = path.resolve(rulesDir, ruleName);
13
+ const exists = await fsExists(newRuleDir);
14
+ if (exists) {
15
+ throw new CreateRuleHelperError(`A new rule "${ruleName}" already exists`);
16
+ }
17
+ return await installScaffold('core', newRuleDir, {
18
+ pluginName: '',
19
+ ruleName,
20
+ lang: 'TYPESCRIPT',
21
+ needTest: true,
22
+ core,
23
+ });
24
+ }
25
+ export async function getRulesDir() {
26
+ const rootDir = await searchCoreRepository();
27
+ if (!rootDir) {
28
+ throw new CreateRuleHelperError('The repository of markuplint is not found');
29
+ }
30
+ const rulesDir = path.resolve(rootDir, ...rulesRelDir);
31
+ const exists = await fsExists(rulesDir);
32
+ if (!exists) {
33
+ throw new CreateRuleHelperError(`Core rules directory (${rulesDir}) is not found`);
34
+ }
35
+ return rulesDir;
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
2
+ export declare function createRuleToProject({ pluginName, ruleName, lang, needTest, }: CreateRuleCreatorParams): Promise<CreateRuleHelperResult>;
@@ -0,0 +1,16 @@
1
+ import { resolve } from 'node:path';
2
+ import { CreateRuleHelperError } from './create-rule-helper-error.js';
3
+ import { fsExists } from './fs-exists.js';
4
+ import { installScaffold } from './install-scaffold.js';
5
+ export async function createRuleToProject({ pluginName, ruleName, lang, needTest, }) {
6
+ const pluginDir = resolve(process.cwd(), pluginName);
7
+ if (await fsExists(pluginDir)) {
8
+ throw new CreateRuleHelperError(`The directory exists: ${pluginDir}`);
9
+ }
10
+ return await installScaffold('project', pluginDir, {
11
+ pluginName,
12
+ ruleName,
13
+ lang,
14
+ needTest,
15
+ });
16
+ }
@@ -0,0 +1 @@
1
+ export declare function fsExists(path: string): Promise<boolean>;
@@ -0,0 +1,10 @@
1
+ import { stat } from 'node:fs/promises';
2
+ export async function fsExists(path) {
3
+ const res = await stat(path).catch(error => {
4
+ if (error?.code === 'ENOENT') {
5
+ return null;
6
+ }
7
+ throw error;
8
+ });
9
+ return !!res;
10
+ }
package/lib/glob.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const glob: (pattern: string) => Promise<string[]>;
package/lib/glob.js ADDED
@@ -0,0 +1,6 @@
1
+ import { sep } from 'node:path';
2
+ import { glob as origin } from 'glob';
3
+ export const glob = async (pattern) => {
4
+ const normalized = pattern.split(sep).join('/');
5
+ return await origin(normalized);
6
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './create-rule-helper.js';
2
+ export * from './types.js';
3
+ export { isMarkuplintRepo } from './is-markuplint-repo.js';
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './create-rule-helper.js';
2
+ export * from './types.js';
3
+ export { isMarkuplintRepo } from './is-markuplint-repo.js';
@@ -0,0 +1,4 @@
1
+ import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
2
+ export declare function installScaffold(scaffoldType: 'core' | 'project' | 'package', dest: string, params: CreateRuleCreatorParams & {
3
+ readonly packageJson?: boolean;
4
+ }): Promise<CreateRuleHelperResult>;
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs/promises';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { fsExists } from './fs-exists.js';
5
+ import { transfer } from './transfer.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export async function installScaffold(scaffoldType, dest, params) {
9
+ const exists = await fsExists(dest);
10
+ if (!exists) {
11
+ await fs.mkdir(dest);
12
+ }
13
+ const scaffoldDir = resolve(__dirname, '..', 'scaffold', scaffoldType);
14
+ const transferred = await transfer(scaffoldType, scaffoldDir, dest, {
15
+ transpile: params.lang === 'JAVASCRIPT',
16
+ test: params.needTest,
17
+ replacer: {
18
+ pluginName: params.pluginName,
19
+ ruleName: params.ruleName,
20
+ description: params.core?.description,
21
+ category: params.core?.category,
22
+ severity: params.core?.severity,
23
+ },
24
+ });
25
+ const packageJson = params.packageJson ? resolve(dest, 'package.json') : null;
26
+ const dependencies = [];
27
+ const devDependencies = [];
28
+ if (packageJson) {
29
+ // const ext = params.lang === 'JAVASCRIPT' ? 'js' : 'ts';
30
+ const packageContent = {
31
+ name: params.ruleName,
32
+ scripts: {},
33
+ };
34
+ if (params.lang === 'TYPESCRIPT') {
35
+ packageContent.scripts.build = 'tsc';
36
+ }
37
+ dependencies.push('@markuplint/ml-core');
38
+ devDependencies.push('markuplint');
39
+ if (params.needTest) {
40
+ packageContent.scripts.test = 'vitest';
41
+ devDependencies.push('vitest');
42
+ }
43
+ if (params.lang === 'TYPESCRIPT') {
44
+ devDependencies.push('typescript');
45
+ }
46
+ await fs.writeFile(packageJson, JSON.stringify(packageContent, null, 2), { encoding: 'utf8' });
47
+ transferred.push({
48
+ ext: '.json',
49
+ name: 'package',
50
+ fileName: 'package',
51
+ test: false,
52
+ destDir: dest,
53
+ filePath: packageJson,
54
+ });
55
+ }
56
+ return {
57
+ files: transferred,
58
+ dependencies,
59
+ devDependencies,
60
+ };
61
+ }
@@ -0,0 +1 @@
1
+ export declare function isMarkuplintRepo(): Promise<boolean>;
@@ -0,0 +1,5 @@
1
+ import { searchCoreRepository } from './search-core-repository.js';
2
+ export async function isMarkuplintRepo() {
3
+ const rootDir = await searchCoreRepository();
4
+ return !!rootDir;
5
+ }
@@ -0,0 +1 @@
1
+ export declare function readPackageJson(dir: string): Promise<string | null>;
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function readPackageJson(dir) {
4
+ const filePath = path.resolve(dir, 'package.json');
5
+ try {
6
+ const json = await fs.readFile(filePath, { encoding: 'utf8' });
7
+ const data = JSON.parse(json);
8
+ return data?.name ?? null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ export declare function searchCoreRepository(): Promise<string | null>;
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import { readPackageJson } from './read-package-json.js';
3
+ export async function searchCoreRepository() {
4
+ const paths = path.resolve(process.cwd()).split(path.sep);
5
+ // eslint-disable-next-line no-constant-condition
6
+ while (true) {
7
+ const currentDir = paths.join(path.sep);
8
+ const name = await readPackageJson(currentDir);
9
+ if (name === 'markuplint-packages') {
10
+ return currentDir;
11
+ }
12
+ const dir = paths.pop();
13
+ if (!dir) {
14
+ return null;
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { File } from './types.js';
2
+ type TransferOptions = {
3
+ readonly transpile?: boolean;
4
+ readonly test?: boolean;
5
+ readonly replacer?: Readonly<Record<string, string | void>>;
6
+ };
7
+ export declare function transfer(scaffoldType: 'core' | 'project' | 'package', baseDir: string, destDir: string, options?: TransferOptions): Promise<File[]>;
8
+ export {};
@@ -0,0 +1,112 @@
1
+ import { statSync } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import { resolve, extname, basename, relative, dirname, sep } from 'node:path';
4
+ import { format } from 'prettier';
5
+ import tsc from 'typescript';
6
+ import { fsExists } from './fs-exists.js';
7
+ import { glob } from './glob.js';
8
+ // eslint-disable-next-line import/no-named-as-default-member
9
+ const { transpile, ScriptTarget } = tsc;
10
+ export async function transfer(scaffoldType, baseDir, destDir, options) {
11
+ const files = await scan(baseDir, destDir);
12
+ const results = [];
13
+ for (const file of files) {
14
+ const result = await transferFile(scaffoldType, file, options);
15
+ if (result) {
16
+ results.push(result);
17
+ }
18
+ }
19
+ return results;
20
+ }
21
+ async function transferFile(scaffoldType, file, options) {
22
+ if (!(await fsExists(file.filePath))) {
23
+ return null;
24
+ }
25
+ if (file.test && !options?.test) {
26
+ return null;
27
+ }
28
+ let contents = await fs.readFile(file.filePath, { encoding: 'utf8' });
29
+ if (options?.replacer) {
30
+ for (const [before, after] of Object.entries(options?.replacer)) {
31
+ if (!after) {
32
+ continue;
33
+ }
34
+ // Hyphenation to camel-case for variables
35
+ // `rule-name` => `ruleName`
36
+ contents = contents.replaceAll(new RegExp(`__${before}__c`, 'g'),
37
+ // Camelize
38
+ after.replaceAll(/-+([a-z])/gi, (_, $1) => $1.toUpperCase()).replace(/^[a-z]/, $0 => $0.toLowerCase()));
39
+ contents = contents.replaceAll(new RegExp(`__${before}__`, 'g'), after);
40
+ }
41
+ }
42
+ // Remove prettier ignore comment
43
+ contents = contents.replace(/\n\s*\/\/ prettier-ignore/, '');
44
+ contents = contents.replace(/\n\s*<!-- prettier-ignore(?:-(?:start|end))? -->/, '');
45
+ const newFile = { ...file };
46
+ if (scaffoldType === 'core' && file.test) {
47
+ const name = options?.replacer?.ruleName;
48
+ if (!name) {
49
+ throw new Error('Rule name is empty');
50
+ }
51
+ newFile.destDir = newFile.destDir.replace(`${sep}rules${sep}src${sep}`, `${sep}rules${sep}test${sep}`);
52
+ contents = contents.replace("require('./').default", `require('../../lib/${name}').default`);
53
+ }
54
+ // TypeScript transpiles to JS
55
+ if (newFile.ext === '.ts' && options?.transpile) {
56
+ newFile.ext = '.js';
57
+ contents = transpile(contents, {
58
+ target: ScriptTarget.ESNext,
59
+ }, newFile.filePath);
60
+ // Insert new line before comments and the export keyword
61
+ contents = contents.replaceAll(/(\n)(\s+\/\*\*|export)/g, '$1\n$2');
62
+ }
63
+ const candidateName = options?.replacer?.[newFile.name.replaceAll('_', '')];
64
+ if (candidateName) {
65
+ newFile.name = candidateName;
66
+ newFile.fileName = candidateName + (newFile.test ? '.spec' : '');
67
+ }
68
+ const dest = resolve(newFile.destDir, newFile.fileName + newFile.ext);
69
+ // Prettier
70
+ const parser = newFile.ext === '.md'
71
+ ? 'markdown'
72
+ : newFile.ext === '.json'
73
+ ? 'json'
74
+ : newFile.ext === '.ts'
75
+ ? options?.transpile
76
+ ? 'babel'
77
+ : 'typescript'
78
+ : undefined;
79
+ contents = await format(contents, { parser, filepath: dest });
80
+ if (!(await fsExists(newFile.destDir))) {
81
+ await fs.mkdir(newFile.destDir, { recursive: true });
82
+ }
83
+ await fs.writeFile(dest, contents, { encoding: 'utf8' });
84
+ return newFile;
85
+ }
86
+ async function scan(baseDir, destDir) {
87
+ const fileList = await glob(resolve(baseDir, '**', '*'));
88
+ const destList = fileList
89
+ .map(filePath => {
90
+ const stat = statSync(filePath);
91
+ if (!stat.isFile()) {
92
+ return null;
93
+ }
94
+ const relPath = relative(baseDir, filePath);
95
+ const destPath = resolve(destDir, relPath);
96
+ const ext = extname(destPath);
97
+ const fileName = basename(destPath, ext);
98
+ const test = extname(fileName) === '.spec';
99
+ const name = basename(fileName, '.spec');
100
+ const destFileDir = dirname(destPath);
101
+ return {
102
+ ext,
103
+ fileName,
104
+ name,
105
+ test,
106
+ destDir: destFileDir,
107
+ filePath,
108
+ };
109
+ })
110
+ .filter((f) => !!f);
111
+ return destList;
112
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ export type CreateRuleHelperParams = CreateRuleCreatorParams & {
2
+ readonly purpose: CreateRulePurpose;
3
+ };
4
+ export type CreateRuleCreatorParams = {
5
+ readonly pluginName: string;
6
+ readonly ruleName: string;
7
+ readonly lang: CreateRuleLanguage;
8
+ readonly needTest: boolean;
9
+ readonly core?: CreateRuleCreatorCoreParams;
10
+ };
11
+ export type CreateRuleCreatorCoreParams = {
12
+ readonly description: string;
13
+ readonly category: string;
14
+ readonly severity: string;
15
+ };
16
+ export type CreateRuleHelperResult = {
17
+ readonly files: readonly File[];
18
+ readonly dependencies: readonly string[];
19
+ readonly devDependencies: readonly string[];
20
+ };
21
+ export type CreateRuleLanguage = 'JAVASCRIPT' | 'TYPESCRIPT';
22
+ export type CreateRulePurpose = 'ADD_TO_PROJECT' | 'PUBLISH_AS_PACKAGE' | 'CONTRIBUTE_TO_CORE';
23
+ export type File = {
24
+ readonly ext: string;
25
+ readonly name: string;
26
+ readonly fileName: string;
27
+ readonly test: boolean;
28
+ readonly destDir: string;
29
+ readonly filePath: string;
30
+ };
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@markuplint/create-rule",
3
+ "version": "4.0.0-dev.3823+b28398ab",
4
+ "description": "Rule generator for markuplint",
5
+ "repository": "git@github.com:markuplint/markuplint.git",
6
+ "author": "Yusuke Hirao <yusukehirao@me.com>",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./lib/index.js"
13
+ }
14
+ },
15
+ "types": "./lib/index.d.ts",
16
+ "bin": {
17
+ "create-rule": "./bin/create-rule.mjs"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc --build --watch",
25
+ "clean": "tsc --build --clean"
26
+ },
27
+ "dependencies": {
28
+ "@markuplint/cli-utils": "4.0.0-dev.3823+b28398ab",
29
+ "@markuplint/ml-core": "4.0.0-dev.10+b28398ab",
30
+ "glob": "^10.3.6",
31
+ "prettier": "^3.1.1",
32
+ "ts-node": "^10.9.2",
33
+ "typescript": "^5.3.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/fs-extra": "^11.0.4",
37
+ "fs-extra": "^11.2.0"
38
+ },
39
+ "gitHead": "b28398ab9c8f0ad790f2915ad5da8f3a80e9b8d6"
40
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "rules": {
3
+ "import/no-default-export": 0
4
+ }
5
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: TODO/翻訳 __description__
3
+ ---
4
+
5
+ # `__ruleName__`
6
+
7
+ <!-- textlint-disable ja-technical-writing/ja-no-mixed-period -->
8
+
9
+ <!-- prettier-ignore-start -->
10
+ TODO: 翻訳
11
+ __description__
12
+ <!-- prettier-ignore-end -->
13
+
14
+ ❌ 間違ったコード例
15
+
16
+ ```html
17
+ <todo>Write incorrect codes</todo>
18
+ ```
19
+
20
+ ✅ 正しいコード例
21
+
22
+ ```html
23
+ <todo>Write correct codes</todo>
24
+ ```
25
+
26
+ <!-- textlint-enable ja-technical-writing/ja-no-mixed-period -->
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: __ruleName__
3
+ description: __description__
4
+ ---
5
+
6
+ # `__ruleName__`
7
+
8
+ <!-- prettier-ignore-start -->
9
+ __description__
10
+ <!-- prettier-ignore-end -->
11
+
12
+ ❌ Examples of **incorrect** code for this rule
13
+
14
+ ```html
15
+ <todo>Write incorrect codes</todo>
16
+ ```
17
+
18
+ ✅ Examples of **correct** code for this rule
19
+
20
+ ```html
21
+ <todo>Write correct codes</todo>
22
+ ```
@@ -0,0 +1,18 @@
1
+ import { mlRuleTest } from 'markuplint';
2
+ import { test, expect } from 'vitest';
3
+
4
+ import rule from './index.js';
5
+
6
+ test('It is test', async () => {
7
+ const { violations } = await mlRuleTest(rule, '<x-foo></x-foo>');
8
+ expect(violations.length).toBe(1);
9
+ // expect(violations).toStrictEqual([
10
+ // {
11
+ // severity: 'error',
12
+ // line: 1,
13
+ // col: 1,
14
+ // raw: '',
15
+ // message: 'It is issue',
16
+ // },
17
+ // ]);
18
+ });
@@ -0,0 +1,31 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+
3
+ export default createRule<boolean, null>({
4
+ defaultValue: true,
5
+ defaultOptions: null,
6
+ async verify({ document, report, t }) {
7
+ // Element
8
+ await document.walkOn('Element', el => {
9
+ const raw = el.raw.trim();
10
+ if (/./.test(raw)) {
11
+ report({
12
+ scope: el,
13
+ message: t('It is {0}', 'issue'),
14
+ });
15
+ }
16
+ });
17
+
18
+ // Attribute
19
+ await document.walkOn('Attr', attr => {
20
+ if (/./.test(attr.name)) {
21
+ report({
22
+ scope: attr,
23
+ line: attr.nameNode?.startLine,
24
+ col: attr.nameNode?.startCol,
25
+ raw: attr.nameNode?.raw,
26
+ message: t('It is {0}', 'issue'),
27
+ });
28
+ }
29
+ });
30
+ },
31
+ });
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "_category": "__category__",
4
+ "definitions": {
5
+ "value": {
6
+ "type": "boolean",
7
+ "description": "__VALUE_DESCRIPTION__",
8
+ "description:ja": "__VALUE_DESCRIPTION_IN_JAPANESE__"
9
+ },
10
+ "options": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "properties": {
14
+ "__OPTIONS_PROP_NAME__": {
15
+ "type": "boolean",
16
+ "default": "true",
17
+ "description": "__OPTIONS_PROP_DESCRIPTION__",
18
+ "description:ja": "__OPTIONS_PROP_DESCRIPTION_IN_JAPANESE__"
19
+ }
20
+ }
21
+ }
22
+ },
23
+ "oneOf": [
24
+ {
25
+ "type": "boolean"
26
+ },
27
+ {
28
+ "$ref": "#/definitions/value"
29
+ },
30
+ {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "value": { "$ref": "#/definitions/value" },
35
+ "options": { "$ref": "#/definitions/options" },
36
+ "option": { "$ref": "#/definitions/options", "deprecated": true },
37
+ "severity": {
38
+ "$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/ml-config/schema.json#/definitions/severity",
39
+ "default": "__severity__"
40
+ },
41
+ "reason": {
42
+ "type": "string"
43
+ }
44
+ }
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,60 @@
1
+ # The `__pluginName__` rule
2
+
3
+ TODO: Write a description
4
+
5
+ ## Install
6
+
7
+ ```shell
8
+ npm install --save-dev {{ name }}
9
+ ```
10
+
11
+ ## Applying rules
12
+
13
+ ```json
14
+ {
15
+ "plugins": ["__pluginName__"],
16
+ "rules": {
17
+ "__pluginName__/__ruleName__": {
18
+ "value": "__MAIN_VALUE__",
19
+ "options": {
20
+ "foo": "__OPTIONAL_VALUE__",
21
+ "bar": [123, 456, 789]
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## Rule Details
29
+
30
+ 👎 Examples of **incorrect** code for this rule
31
+
32
+ ```html
33
+ <todo>Write incorrect codes</todo>
34
+ ```
35
+
36
+ 👍 Examples of **correct** code for this rule
37
+
38
+ ```html
39
+ <todo>Write correct codes</todo>
40
+ ```
41
+
42
+ ### Interface
43
+
44
+ - Type: `string`
45
+ - Default Value: `"__DEFAULT_MAIN_VALUE__"`
46
+
47
+ ### Options
48
+
49
+ TODO: Write a description
50
+
51
+ #### Interface
52
+
53
+ | Property | Type | Optional | Default Value | Description |
54
+ | -------- | ---------- | -------- | ------------- | ------------------------- |
55
+ | `foo` | `string` | ✔ | `undefined` | TODO: Write a description |
56
+ | `bar` | `number[]` | ✔ | `undefined` | TODO: Write a description |
57
+
58
+ ### Default severity
59
+
60
+ TODO: Choose `error` or `warning`
@@ -0,0 +1,15 @@
1
+ import { createPlugin } from '@markuplint/ml-core';
2
+
3
+ import { __ruleName__c } from './rules/__ruleName__.js';
4
+
5
+ export default createPlugin({
6
+ name: '__pluginName__',
7
+ create(settings) {
8
+ return {
9
+ rules: {
10
+ // prettier-ignore
11
+ '__ruleName__': __ruleName__c(settings),
12
+ },
13
+ };
14
+ },
15
+ });
@@ -0,0 +1,94 @@
1
+ import type { CreatePluginSettings } from '@markuplint/ml-core';
2
+
3
+ import { createRule } from '@markuplint/ml-core';
4
+
5
+ /**
6
+ * Step 0-1. Define the type of principal value
7
+ *
8
+ * You can define it as either `string` or `number` or `boolean` or `Array`.
9
+ * If you define a complex structure, you should define `boolean` to this
10
+ * and define that structure to the options.
11
+ */
12
+ type MainValue = string;
13
+
14
+ /**
15
+ * Step 0-2. Define the type of options
16
+ *
17
+ * You can define it as an _Object_. Set `null` if it is empty.
18
+ */
19
+ type Options = {
20
+ foo?: string;
21
+ bar?: number[];
22
+ };
23
+
24
+ export const __ruleName__c = (settings: CreatePluginSettings) =>
25
+ createRule<MainValue, Options>({
26
+ /**
27
+ * Step 1-1. Choose the severity from `error` or `warning`
28
+ *
29
+ * Default is `error`
30
+ */
31
+ defaultSeverity: 'error',
32
+
33
+ /**
34
+ * Step 1-2. Set the default principal value
35
+ *
36
+ * It adopts this value in the evaluation
37
+ * if it sets `true` or undefined (in other words, it doesn't set)
38
+ * to the configuration.
39
+ */
40
+ defaultValue: '__DEFAULT_MAIN_VALUE__',
41
+
42
+ /**
43
+ * Step 1-3. Set the default options
44
+ *
45
+ * It adopts this value in the evaluation
46
+ * if it doesn't set to the configuration.
47
+ */
48
+ defaultOptions: {},
49
+
50
+ /**
51
+ * Step 2. Write a process.
52
+ *
53
+ * @param context
54
+ */
55
+ async verify({ document, report, t }) {
56
+ /**
57
+ * Example: Use `walkOn` method to traverse the node tree
58
+ */
59
+ await document.walkOn('Comment', comment => {
60
+ /**
61
+ * Example: Access the property of the node to get needed data
62
+ */
63
+ const commentText = comment.raw.trim();
64
+
65
+ /**
66
+ * Example: Compare data according to your design to report the violation
67
+ */
68
+ if (/^<!--\s*todo:/i.test(commentText)) {
69
+ /**
70
+ * Example: It delivers the violation to the linter engine
71
+ *
72
+ * This `report` method can call many times.
73
+ */
74
+ report({
75
+ /**
76
+ * Example: Define the scope.
77
+ *
78
+ * Set the _node_ that gives
79
+ * the location (line number and column number)
80
+ * to the linter engine.
81
+ */
82
+ scope: comment,
83
+
84
+ /**
85
+ * Example: The message that is output to a user.
86
+ *
87
+ * You can set just strings without through the translator.
88
+ */
89
+ message: t('It is {0}', 'TODO'),
90
+ });
91
+ }
92
+ });
93
+ },
94
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2019",
5
+ "strict": true,
6
+ "strictNullChecks": true,
7
+ "strictPropertyInitialization": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "experimentalDecorators": true,
10
+ "esModuleInterop": true,
11
+ "noImplicitAny": true,
12
+ "declaration": true,
13
+ "lib": ["dom", "es2015", "es2016", "es2017", "es2018", "es2019", "esnext"],
14
+ "skipLibCheck": true,
15
+ "outDir": "./lib",
16
+ "rootDir": "./src"
17
+ },
18
+ "include": ["./src/**/*"],
19
+ "exclude": ["node_modules", "lib", "./src/**/*.spec.ts"]
20
+ }
@@ -0,0 +1,15 @@
1
+ import { createPlugin } from '@markuplint/ml-core';
2
+
3
+ import { __ruleName__c } from './rules/__ruleName__.js';
4
+
5
+ export default createPlugin({
6
+ name: '__pluginName__',
7
+ create(setting) {
8
+ return {
9
+ rules: {
10
+ // prettier-ignore
11
+ '__ruleName__': __ruleName__c(setting),
12
+ },
13
+ };
14
+ },
15
+ });
@@ -0,0 +1,32 @@
1
+ import { mlRuleTest } from 'markuplint';
2
+ import { test, expect } from 'vitest';
3
+
4
+ import { __ruleName__c } from './__ruleName__.js';
5
+
6
+ /**
7
+ * Example: Write tests
8
+ */
9
+ test('It is test', async () => {
10
+ const { violations } = await mlRuleTest(
11
+ __ruleName__c({
12
+ /* Plugin settings */
13
+ }),
14
+ /**
15
+ * Example: The target HTML that is evaluated
16
+ */
17
+ '<div><!-- TODO: I will do something --></div>',
18
+ );
19
+
20
+ /**
21
+ * Example: Set expected results.
22
+ */
23
+ expect(violations).toStrictEqual([
24
+ {
25
+ severity: 'error',
26
+ line: 1,
27
+ col: 6,
28
+ raw: '<!-- TODO: I will do something -->',
29
+ message: 'It is TODO',
30
+ },
31
+ ]);
32
+ });
@@ -0,0 +1,94 @@
1
+ import type { CreatePluginSettings } from '@markuplint/ml-core';
2
+
3
+ import { createRule } from '@markuplint/ml-core';
4
+
5
+ /**
6
+ * Step 0-1. Define the type of principal value
7
+ *
8
+ * You can define it as either `string` or `number` or `boolean` or `Array`.
9
+ * If you define a complex structure, you should define `boolean` to this
10
+ * and define that structure to the options.
11
+ */
12
+ type MainValue = string;
13
+
14
+ /**
15
+ * Step 0-2. Define the type of options
16
+ *
17
+ * You can define it as an _Object_. Set `null` if it is empty.
18
+ */
19
+ type Options = {
20
+ foo?: string;
21
+ bar?: number[];
22
+ };
23
+
24
+ export const __ruleName__c = (settings: CreatePluginSettings = {}) =>
25
+ createRule<MainValue, Options>({
26
+ /**
27
+ * Step 1-1. Choose the severity from `error` or `warning`
28
+ *
29
+ * Default is `error`
30
+ */
31
+ defaultSeverity: 'error',
32
+
33
+ /**
34
+ * Step 1-2. Set the default principal value
35
+ *
36
+ * It adopts this value in the evaluation
37
+ * if it sets `true` or undefined (in other words, it doesn't set)
38
+ * to the configuration.
39
+ */
40
+ defaultValue: '__DEFAULT_MAIN_VALUE__',
41
+
42
+ /**
43
+ * Step 1-3. Set the default options
44
+ *
45
+ * It adopts this value in the evaluation
46
+ * if it doesn't set to the configuration.
47
+ */
48
+ defaultOptions: {},
49
+
50
+ /**
51
+ * Step 2. Write a process.
52
+ *
53
+ * @param context
54
+ */
55
+ async verify({ document, report, t }) {
56
+ /**
57
+ * Example: Use `walkOn` method to traverse the node tree
58
+ */
59
+ await document.walkOn('Comment', comment => {
60
+ /**
61
+ * Example: Access the property of the node to get needed data
62
+ */
63
+ const commentText = comment.raw.trim();
64
+
65
+ /**
66
+ * Example: Compare data according to your design to report the violation
67
+ */
68
+ if (/^<!--\s*todo:/i.test(commentText)) {
69
+ /**
70
+ * Example: It delivers the violation to the linter engine
71
+ *
72
+ * This `report` method can call many times.
73
+ */
74
+ report({
75
+ /**
76
+ * Example: Define the scope.
77
+ *
78
+ * Set the _node_ that gives
79
+ * the location (line number and column number)
80
+ * to the linter engine.
81
+ */
82
+ scope: comment,
83
+
84
+ /**
85
+ * Example: The message that is output to a user.
86
+ *
87
+ * You can set just strings without through the translator.
88
+ */
89
+ message: t('It is {0}', 'TODO'),
90
+ });
91
+ }
92
+ });
93
+ },
94
+ });