crdncli 1.0.3

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/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Creiden
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # crdncli
2
+
3
+ CLI tools by **Creiden** for scaffolding parts of a CRDN-style Next.js app.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g crdncli
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `crdn add-feature [name]`
14
+
15
+ Creates a new feature in `src/features/<name>` based on the template shipped with this package.
16
+
17
+ If your app has tests, it also copies feature unit tests to:
18
+
19
+ - `tests/unit/features/<name>`
20
+
21
+ Examples:
22
+
23
+ ```bash
24
+ crdn add-feature auth
25
+ crdn add-feature billing
26
+ ```
27
+
28
+ ### `crdn add-lang [code]`
29
+
30
+ Adds a new language file to `/messages` based on the existing JSON structure (values are created empty),
31
+ and tries to register the locale in `src/lib/i18n/routing.ts` if present.
32
+
33
+ Examples:
34
+
35
+ ```bash
36
+ crdn add-lang fr
37
+ crdn add-lang ar
38
+ ```
39
+
40
+ ### `crdn delete-lang [code]`
41
+
42
+ Deletes `/messages/<code>.json` (if it exists) and removes the locale from `src/lib/i18n/routing.ts` (if it exists).
43
+ You’ll be prompted to confirm with **y/N** before anything is deleted.
44
+
45
+ Examples:
46
+
47
+ ```bash
48
+ crdn delete-lang fr
49
+ crdn delete-lang ar
50
+ ```
51
+
52
+ ## Help
53
+
54
+ ```bash
55
+ crdn --help
56
+ crdn --version
57
+ ```
package/bin/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { createProgram } = require("../lib/cli");
4
+
5
+ createProgram().parse(process.argv);
@@ -0,0 +1,194 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+ const readline = require("readline");
5
+
6
+ /**
7
+ * crdn add-feature <name>
8
+ *
9
+ * - Must be run from the root of a CRDN Next.js app (the app created by `create-crdn-app`)
10
+ * - Copies the template feature from `crdn/templates/feature` into `src/features/<name>`
11
+ * - Performs simple text replacements so you can base new features on the auth example
12
+ */
13
+
14
+ function run(featureName) {
15
+ const appRoot = process.cwd();
16
+
17
+ const srcDir = path.join(appRoot, "src");
18
+ if (!fs.existsSync(srcDir)) {
19
+ console.error(
20
+ chalk.red(
21
+ "This command must be run from the root of a CRDN Next.js app (missing `src/`)."
22
+ )
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ const targetFeatureDir = path.join(appRoot, "src", "features", featureName);
28
+
29
+ if (fs.existsSync(targetFeatureDir)) {
30
+ console.error(
31
+ chalk.red(
32
+ `Feature \`${featureName}\` already exists at src/features/${featureName}.`
33
+ )
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ // Resolve the template directory inside the installed `crdn` package.
39
+ // When installed globally or locally, `__dirname` will be `<packageRoot>/commands`.
40
+ const templateDir = path.join(__dirname, "..", "templates", "feature");
41
+
42
+ if (!fs.existsSync(templateDir)) {
43
+ console.error(
44
+ chalk.red("Could not find feature templates inside the `crdn` package.`")
45
+ );
46
+ process.exit(1);
47
+ }
48
+
49
+ // Copy the template directory into the feature folder (excluding tests)
50
+ fs.copySync(templateDir, targetFeatureDir, {
51
+ filter: (src) => !src.includes(`${path.sep}tests${path.sep}`),
52
+ });
53
+
54
+ // After copying, apply simple content/name replacements so the template
55
+ // can be based on `auth` but work for any feature name.
56
+ const featureNameCamel =
57
+ featureName.charAt(0).toLowerCase() + featureName.slice(1);
58
+ const featureNamePascal =
59
+ featureName.charAt(0).toUpperCase() + featureName.slice(1);
60
+
61
+ const replaceInFile = (filePath) => {
62
+ if (!fs.existsSync(filePath)) return;
63
+
64
+ let content = fs.readFileSync(filePath, "utf8");
65
+
66
+ // Fix up template-relative import paths that depend on renamed files
67
+ content = content
68
+ .replace(/\.\/components\/feature\b/g, `./components/${featureNamePascal}`)
69
+ .replace(/\.\/hooks\/useFeature\b/g, `./hooks/use${featureNamePascal}`)
70
+ .replace(/\.\/api\/featureApi\b/g, `./api/${featureNameCamel}Api`)
71
+ .replace(/\.\/types\/feature\.types\b/g, `./types/${featureNameCamel}.types`)
72
+ .replace(/\.\.\/api\/featureApi\b/g, `../api/${featureNameCamel}Api`)
73
+ .replace(/\.\.\/hooks\/useFeature\b/g, `../hooks/use${featureNamePascal}`)
74
+ .replace(/\.\.\/types\/feature\.types\b/g, `../types/${featureNameCamel}.types`);
75
+
76
+ // Basic convention: templates may use these placeholders.
77
+ content = content
78
+ .replace(/__FEATURE_PASCAL__/g, featureNamePascal)
79
+ .replace(/__FEATURE_CAMEL__/g, featureNameCamel)
80
+ .replace(/__FEATURE_RAW__/g, featureName);
81
+
82
+ // Also, if the template is auth/feature-based, rename obvious identifiers:
83
+ content = content
84
+ // old auth-based names
85
+ .replace(/\bauthKeys\b/g, `${featureNameCamel}Keys`)
86
+ .replace(/\bauthApi\b/g, `${featureNameCamel}Api`)
87
+ .replace(/\bAuth\b/g, featureNamePascal)
88
+ .replace(/\bauth\b/g, featureNameCamel)
89
+ // new generic feature-based names
90
+ .replace(/\bfeatureKeys\b/g, `${featureNameCamel}Keys`)
91
+ .replace(/\bfeatureApi\b/g, `${featureNameCamel}Api`)
92
+ .replace(/\buseFeature\b/g, `use${featureNamePascal}`);
93
+
94
+ fs.writeFileSync(filePath, content, "utf8");
95
+ };
96
+
97
+ const walk = (dir) => {
98
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
99
+ for (const entry of entries) {
100
+ let fullPath = path.join(dir, entry.name);
101
+
102
+ if (entry.isDirectory()) {
103
+ walk(fullPath);
104
+ } else {
105
+ // Rename template files to match the new feature name
106
+ let newName = entry.name;
107
+
108
+ if (newName === "featureApi.ts") {
109
+ newName = `${featureNameCamel}Api.ts`;
110
+ } else if (newName === "useFeature.ts") {
111
+ newName = `use${featureNamePascal}.ts`;
112
+ } else if (newName === "feature.types.ts") {
113
+ newName = `${featureNameCamel}.types.ts`;
114
+ } else if (newName === "feature.tsx") {
115
+ // main UI component file
116
+ newName = `${featureNamePascal}.tsx`;
117
+ } else if (newName === "featureApi.smoke.test.ts") {
118
+ newName = `${featureNameCamel}Api.smoke.test.ts`;
119
+ } else if (newName === "featureKeys.test.ts") {
120
+ newName = `${featureNameCamel}Keys.test.ts`;
121
+ } else if (newName === "feature.barrel.test.ts") {
122
+ newName = `${featureNameCamel}.barrel.test.ts`;
123
+ }
124
+
125
+ if (newName !== entry.name) {
126
+ const newPath = path.join(dir, newName);
127
+ fs.renameSync(fullPath, newPath);
128
+ fullPath = newPath;
129
+ }
130
+
131
+ replaceInFile(fullPath);
132
+ }
133
+ }
134
+ };
135
+
136
+ walk(targetFeatureDir);
137
+
138
+ // Copy tests (following the Next.js template convention)
139
+ const appTestsFeatureDir = path.join(
140
+ appRoot,
141
+ "tests",
142
+ "unit",
143
+ "features",
144
+ featureNameCamel
145
+ );
146
+ const templateTestsFeatureDir = path.join(
147
+ templateDir,
148
+ "tests",
149
+ "unit",
150
+ "features",
151
+ "feature"
152
+ );
153
+
154
+ if (
155
+ fs.existsSync(path.join(appRoot, "tests")) &&
156
+ fs.existsSync(templateTestsFeatureDir)
157
+ ) {
158
+ fs.ensureDirSync(appTestsFeatureDir);
159
+ fs.copySync(templateTestsFeatureDir, appTestsFeatureDir);
160
+
161
+ // Apply replacements + renames inside the copied tests folder
162
+ walk(appTestsFeatureDir);
163
+ }
164
+
165
+ console.log(
166
+ chalk.green(
167
+ `Feature \`${featureName}\` created at src/features/${featureName}.`
168
+ )
169
+ );
170
+ }
171
+
172
+ module.exports = function addFeature(featureName) {
173
+ if (featureName) {
174
+ run(featureName);
175
+ return;
176
+ }
177
+
178
+ const rl = readline.createInterface({
179
+ input: process.stdin,
180
+ output: process.stdout,
181
+ });
182
+
183
+ rl.question(chalk.cyan("Enter feature name: "), (answer) => {
184
+ rl.close();
185
+
186
+ const trimmed = answer.trim();
187
+ if (!trimmed) {
188
+ console.error(chalk.red("Feature name is required."));
189
+ process.exit(1);
190
+ }
191
+
192
+ run(trimmed);
193
+ });
194
+ };
@@ -0,0 +1,162 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+ const readline = require("readline");
5
+
6
+ function askForCode(onDone) {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+
12
+ rl.question(chalk.cyan("Enter language code (e.g. fr, es): "), (answer) => {
13
+ rl.close();
14
+ const code = answer.trim();
15
+
16
+ if (!code) {
17
+ console.error(chalk.red("Language code is required."));
18
+ process.exit(1);
19
+ }
20
+
21
+ onDone(code);
22
+ });
23
+ }
24
+
25
+ function run(code) {
26
+ const appRoot = process.cwd();
27
+ const messagesDir = path.join(appRoot, "messages");
28
+ const preferredBasePath = path.join(messagesDir, "en.json");
29
+ const routingPath = path.join(appRoot, "src", "lib", "i18n", "routing.ts");
30
+
31
+ const resolveBaseMessagesPath = () => {
32
+ if (fs.existsSync(preferredBasePath)) return preferredBasePath;
33
+ if (!fs.existsSync(messagesDir)) return null;
34
+
35
+ const entries = fs.readdirSync(messagesDir);
36
+ const jsonFiles = entries
37
+ .filter((name) => name.toLowerCase().endsWith(".json"))
38
+ .sort((a, b) => a.localeCompare(b));
39
+
40
+ if (jsonFiles.length === 0) return null;
41
+ return path.join(messagesDir, jsonFiles[0]);
42
+ };
43
+
44
+ const baseMessagesPath = resolveBaseMessagesPath();
45
+
46
+ if (!baseMessagesPath) {
47
+ console.error(
48
+ chalk.red(
49
+ "Could not find any `messages/*.json` to use as a base. Make sure you run this in a CRDN Next.js app with a /messages folder containing at least one JSON file."
50
+ )
51
+ );
52
+ process.exit(1);
53
+ }
54
+
55
+ const targetPath = path.join(messagesDir, `${code}.json`);
56
+
57
+ if (fs.existsSync(targetPath)) {
58
+ console.error(
59
+ chalk.red(`Language file \`${code}.json\` already exists in /messages.`)
60
+ );
61
+ process.exit(1);
62
+ }
63
+
64
+ const baseMessages = fs.readJsonSync(baseMessagesPath);
65
+
66
+ const cloneWithEmptyValues = (node) => {
67
+ if (node === null || node === undefined) return node;
68
+
69
+ if (typeof node === "string") {
70
+ return "";
71
+ }
72
+
73
+ if (Array.isArray(node)) {
74
+ return node.map(cloneWithEmptyValues);
75
+ }
76
+
77
+ if (typeof node === "object") {
78
+ const result = {};
79
+ for (const key of Object.keys(node)) {
80
+ result[key] = cloneWithEmptyValues(node[key]);
81
+ }
82
+ return result;
83
+ }
84
+
85
+ return node;
86
+ };
87
+
88
+ const newMessages = cloneWithEmptyValues(baseMessages);
89
+
90
+ fs.ensureDirSync(messagesDir);
91
+ fs.writeJsonSync(targetPath, newMessages, { spaces: 2 });
92
+
93
+ console.log(
94
+ chalk.green(`Created messages file: messages/${code}.json (values empty).`)
95
+ );
96
+
97
+ // Try to register the new locale in src/lib/i18n/routing.ts
98
+ if (!fs.existsSync(routingPath)) {
99
+ console.warn(
100
+ chalk.yellow(
101
+ "Could not find `src/lib/i18n/routing.ts` to register the new locale. Please add it manually."
102
+ )
103
+ );
104
+ return;
105
+ }
106
+
107
+ let routingSource = fs.readFileSync(routingPath, "utf8");
108
+
109
+ const localesMatch = routingSource.match(/locales\s*:\s*\[([^\]]*)\]/);
110
+
111
+ if (!localesMatch) {
112
+ console.warn(
113
+ chalk.yellow(
114
+ "Could not find `locales: [...]` in `src/lib/i18n/routing.ts`. Please add the new locale manually."
115
+ )
116
+ );
117
+ return;
118
+ }
119
+
120
+ const inner = localesMatch[1];
121
+
122
+ const existingLocales = inner
123
+ .split(",")
124
+ .map((part) => part.trim().replace(/^"(.+)"$/, "$1").replace(/^'(.+)'$/, "$1"))
125
+ .filter(Boolean);
126
+
127
+ if (existingLocales.includes(code)) {
128
+ console.log(
129
+ chalk.yellow(
130
+ `Locale \`${code}\` is already present in src/lib/i18n/routing.ts.`
131
+ )
132
+ );
133
+ return;
134
+ }
135
+
136
+ const newInner = `${inner.trim()}${
137
+ inner.trim().length ? ", " : ""
138
+ }"${code}"`;
139
+
140
+ routingSource = routingSource.replace(
141
+ /locales\s*:\s*\[([^\]]*)\]/,
142
+ `locales: [${newInner}]`
143
+ );
144
+
145
+ fs.writeFileSync(routingPath, routingSource, "utf8");
146
+
147
+ console.log(
148
+ chalk.green(
149
+ `Registered locale \`${code}\` in src/lib/i18n/routing.ts.`
150
+ )
151
+ );
152
+ }
153
+
154
+ module.exports = function addLang(code) {
155
+ if (code) {
156
+ run(code);
157
+ return;
158
+ }
159
+
160
+ askForCode(run);
161
+ };
162
+
@@ -0,0 +1,132 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+ const readline = require("readline");
5
+
6
+ function ask(question) {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+
12
+ return new Promise((resolve) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolve(answer);
16
+ });
17
+ });
18
+ }
19
+
20
+ async function askForCode() {
21
+ const answer = await ask(chalk.cyan("Enter language code (e.g. fr, es): "));
22
+ return answer.trim();
23
+ }
24
+
25
+ async function confirmDelete(code) {
26
+ const answer = await ask(
27
+ chalk.yellow(
28
+ `Are you sure you want to delete language "${code}"? This will remove messages/${code}.json and update routing.ts. (y/N): `
29
+ )
30
+ );
31
+ const normalized = answer.trim().toLowerCase();
32
+ return normalized === "y" || normalized === "yes";
33
+ }
34
+
35
+ function removeLocaleFromRouting(routingPath, code) {
36
+ if (!fs.existsSync(routingPath)) {
37
+ console.warn(
38
+ chalk.yellow(
39
+ "Could not find `src/lib/i18n/routing.ts` to unregister the locale. Skipping."
40
+ )
41
+ );
42
+ return { updated: false, removed: false };
43
+ }
44
+
45
+ let routingSource = fs.readFileSync(routingPath, "utf8");
46
+
47
+ const localesMatch = routingSource.match(/locales\s*:\s*\[([^\]]*)\]/);
48
+ if (!localesMatch) {
49
+ console.warn(
50
+ chalk.yellow(
51
+ "Could not find `locales: [...]` in `src/lib/i18n/routing.ts`. Skipping."
52
+ )
53
+ );
54
+ return { updated: false, removed: false };
55
+ }
56
+
57
+ const inner = localesMatch[1];
58
+ const existingLocales = inner
59
+ .split(",")
60
+ .map((part) =>
61
+ part.trim().replace(/^"(.+)"$/, "$1").replace(/^'(.+)'$/, "$1")
62
+ )
63
+ .filter(Boolean);
64
+
65
+ if (!existingLocales.includes(code)) {
66
+ return { updated: false, removed: false };
67
+ }
68
+
69
+ const nextLocales = existingLocales.filter((l) => l !== code);
70
+ const nextInner = nextLocales.map((l) => `"${l}"`).join(", ");
71
+
72
+ routingSource = routingSource.replace(
73
+ /locales\s*:\s*\[([^\]]*)\]/,
74
+ `locales: [${nextInner}]`
75
+ );
76
+
77
+ fs.writeFileSync(routingPath, routingSource, "utf8");
78
+ return { updated: true, removed: true };
79
+ }
80
+
81
+ function deleteMessagesFile(messagesDir, code) {
82
+ const targetPath = path.join(messagesDir, `${code}.json`);
83
+
84
+ if (!fs.existsSync(targetPath)) {
85
+ return { deleted: false };
86
+ }
87
+
88
+ fs.removeSync(targetPath);
89
+ return { deleted: true };
90
+ }
91
+
92
+ async function run(code) {
93
+ const appRoot = process.cwd();
94
+ const messagesDir = path.join(appRoot, "messages");
95
+ const routingPath = path.join(appRoot, "src", "lib", "i18n", "routing.ts");
96
+
97
+ const confirmed = await confirmDelete(code);
98
+ if (!confirmed) {
99
+ console.log(chalk.yellow("Cancelled."));
100
+ return;
101
+ }
102
+
103
+ const { deleted } = deleteMessagesFile(messagesDir, code);
104
+ if (deleted) {
105
+ console.log(chalk.green(`Deleted messages/${code}.json`));
106
+ } else {
107
+ console.log(chalk.yellow(`messages/${code}.json not found (skipped).`));
108
+ }
109
+
110
+ const routingResult = removeLocaleFromRouting(routingPath, code);
111
+ if (routingResult.removed) {
112
+ console.log(
113
+ chalk.green(`Removed locale "${code}" from src/lib/i18n/routing.ts`)
114
+ );
115
+ } else {
116
+ console.log(
117
+ chalk.yellow(`Locale "${code}" not found in routing.ts (skipped).`)
118
+ );
119
+ }
120
+ }
121
+
122
+ module.exports = async function deleteLang(code) {
123
+ const resolvedCode = code ? String(code).trim() : await askForCode();
124
+
125
+ if (!resolvedCode) {
126
+ console.error(chalk.red("Language code is required."));
127
+ process.exit(1);
128
+ }
129
+
130
+ await run(resolvedCode);
131
+ };
132
+
package/lib/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ const { Command } = require("commander");
2
+
3
+ const addFeature = require("../commands/addFeature");
4
+ const addLang = require("../commands/addLang");
5
+ const deleteLang = require("../commands/deleteLang");
6
+
7
+ function createProgram() {
8
+ const program = new Command();
9
+
10
+ program
11
+ .name("crdn")
12
+ .description("CRDN CLI tools")
13
+ .version(require("../package.json").version);
14
+
15
+ program
16
+ .command("add-feature")
17
+ .description("Create a new feature from template")
18
+ .argument("[name]")
19
+ .action(addFeature);
20
+
21
+ program
22
+ .command("add-lang")
23
+ .description("Add a new language messages file")
24
+ .argument("[code]")
25
+ .action(addLang);
26
+
27
+ program
28
+ .command("delete-lang")
29
+ .description("Delete a language messages file and unregister locale")
30
+ .argument("[code]")
31
+ .action(deleteLang);
32
+
33
+ return program;
34
+ }
35
+
36
+ module.exports = { createProgram };
37
+
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "crdncli",
3
+ "version": "1.0.3",
4
+ "description": "",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "crdn": "bin/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "prepack": "node -c ./bin/index.js && node -c ./lib/cli.js && node -c ./commands/addFeature.js && node -c ./commands/addLang.js && node -c ./commands/deleteLang.js"
12
+ },
13
+ "keywords": [],
14
+ "author": "Creiden",
15
+ "homepage": "https://creiden.com/",
16
+ "license": "ISC",
17
+ "type": "commonjs",
18
+ "files": [
19
+ "bin",
20
+ "lib",
21
+ "commands",
22
+ "templates",
23
+ "package.json",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "chalk": "^4.1.2",
32
+ "commander": "^14.0.3",
33
+ "fs-extra": "^11.2.0"
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ import { get, post, put, del } from "@/lib/api/client";
2
+
3
+ const BASE_PATH = "/__FEATURE_RAW__";
4
+
5
+ // Basic CRUD helpers for the __FEATURE_RAW__ endpoint.
6
+ // Each function exposes a generic so callers can override the response type.
7
+
8
+ export async function list__FEATURE_PASCAL__s<T = unknown>(): Promise<T[]> {
9
+ return get<T[]>(BASE_PATH);
10
+ }
11
+
12
+ export async function get__FEATURE_PASCAL__<T = unknown>(
13
+ id: string
14
+ ): Promise<T> {
15
+ return get<T>(`${BASE_PATH}/${id}`);
16
+ }
17
+
18
+ export async function create__FEATURE_PASCAL__<TBody = unknown, TRes = unknown>(
19
+ payload: TBody
20
+ ): Promise<TRes> {
21
+ return post<TRes, TBody>(BASE_PATH, payload);
22
+ }
23
+
24
+ export async function update__FEATURE_PASCAL__<
25
+ TBody = unknown,
26
+ TRes = unknown
27
+ >(id: string, payload: TBody): Promise<TRes> {
28
+ return put<TRes, TBody>(`${BASE_PATH}/${id}`, payload);
29
+ }
30
+
31
+ export async function delete__FEATURE_PASCAL__<TRes = void>(
32
+ id: string
33
+ ): Promise<TRes> {
34
+ return del<TRes>(`${BASE_PATH}/${id}`);
35
+ }
36
+
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import React from "react";
3
+ type Props = {};
4
+
5
+ export function __FEATURE_PASCAL__Feature({}: Props) {
6
+ return <div>__FEATURE_PASCAL__Feature</div>;
7
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { featureKeys } from "../keys";
5
+ import {
6
+ list__FEATURE_PASCAL__s,
7
+ create__FEATURE_PASCAL__,
8
+ update__FEATURE_PASCAL__,
9
+ delete__FEATURE_PASCAL__,
10
+ } from "../api/featureApi";
11
+ import type {
12
+ __FEATURE_PASCAL__CreatePayload,
13
+ __FEATURE_PASCAL__UpdatePayload,
14
+ } from "../types/feature.types";
15
+
16
+ export function use__FEATURE_PASCAL__List() {
17
+ return useQuery({
18
+ queryKey: featureKeys.lists(),
19
+ queryFn: () => list__FEATURE_PASCAL__s(),
20
+ });
21
+ }
22
+
23
+ export function useCreate__FEATURE_PASCAL__() {
24
+ const queryClient = useQueryClient();
25
+
26
+ return useMutation({
27
+ mutationFn: (payload: __FEATURE_PASCAL__CreatePayload) =>
28
+ create__FEATURE_PASCAL__(payload),
29
+ onSuccess: () => {
30
+ queryClient.invalidateQueries({ queryKey: featureKeys.lists() });
31
+ },
32
+ });
33
+ }
34
+
35
+ export function useUpdate__FEATURE_PASCAL__() {
36
+ const queryClient = useQueryClient();
37
+
38
+ return useMutation({
39
+ mutationFn: (params: {
40
+ id: string;
41
+ payload: __FEATURE_PASCAL__UpdatePayload;
42
+ }) => update__FEATURE_PASCAL__(params.id, params.payload),
43
+ onSuccess: () => {
44
+ queryClient.invalidateQueries({ queryKey: featureKeys.lists() });
45
+ },
46
+ });
47
+ }
48
+
49
+ export function useDelete__FEATURE_PASCAL__() {
50
+ const queryClient = useQueryClient();
51
+
52
+ return useMutation({
53
+ mutationFn: (id: string) => delete__FEATURE_PASCAL__(id),
54
+ onSuccess: () => {
55
+ queryClient.invalidateQueries({ queryKey: featureKeys.lists() });
56
+ },
57
+ });
58
+ }
59
+
@@ -0,0 +1,10 @@
1
+ export { __FEATURE_PASCAL__Feature } from "./components/feature";
2
+ export {
3
+ use__FEATURE_PASCAL__List,
4
+ useCreate__FEATURE_PASCAL__,
5
+ useUpdate__FEATURE_PASCAL__,
6
+ useDelete__FEATURE_PASCAL__,
7
+ } from "./hooks/useFeature";
8
+ export { featureKeys } from "./keys";
9
+ export * as featureApi from "./api/featureApi";
10
+ export * from "./types/feature.types";
@@ -0,0 +1,4 @@
1
+ // Query keys for the __FEATURE_RAW__ feature
2
+ import { createQueryKeys } from "@/lib/react-query/queryKeys";
3
+
4
+ export const featureKeys = createQueryKeys("__FEATURE_RAW__");
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+
3
+ describe("__FEATURE_RAW__ feature (barrel exports)", () => {
4
+ it("exports public API", async () => {
5
+ const feature = await import("@/features/__FEATURE_CAMEL__");
6
+
7
+ expect(feature).toHaveProperty("__FEATURE_PASCAL__Feature");
8
+ expect(typeof feature.__FEATURE_PASCAL__Feature).toBe("function");
9
+
10
+ expect(feature).toHaveProperty("featureKeys");
11
+ expect(feature.featureKeys).toBeTruthy();
12
+ });
13
+ });
14
+
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import * as featureApi from "@/features/__FEATURE_CAMEL__/api/featureApi";
3
+
4
+ describe("__FEATURE_CAMEL__Api (smoke)", () => {
5
+ it("exports expected functions", () => {
6
+ expect(typeof featureApi.list__FEATURE_PASCAL__s).toBe("function");
7
+ expect(typeof featureApi.get__FEATURE_PASCAL__).toBe("function");
8
+ expect(typeof featureApi.create__FEATURE_PASCAL__).toBe("function");
9
+ expect(typeof featureApi.update__FEATURE_PASCAL__).toBe("function");
10
+ expect(typeof featureApi.delete__FEATURE_PASCAL__).toBe("function");
11
+ });
12
+ });
13
+
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { featureKeys } from "@/features/__FEATURE_CAMEL__/keys";
3
+
4
+ describe("__FEATURE_RAW__ keys", () => {
5
+ it("builds stable query keys", () => {
6
+ expect(featureKeys.root).toEqual(["__FEATURE_RAW__"]);
7
+ expect(featureKeys.all()).toEqual(["__FEATURE_RAW__"]);
8
+ expect(featureKeys.lists()).toEqual(["__FEATURE_RAW__", "list"]);
9
+ expect(featureKeys.detail("1")).toEqual([
10
+ "__FEATURE_RAW__",
11
+ "detail",
12
+ "1",
13
+ ]);
14
+ });
15
+ });
16
+
@@ -0,0 +1,9 @@
1
+ export interface __FEATURE_PASCAL__Item {
2
+ }
3
+
4
+ export interface __FEATURE_PASCAL__CreatePayload {
5
+ }
6
+
7
+ export interface __FEATURE_PASCAL__UpdatePayload {
8
+ }
9
+