@final-commerce/common 1.1.4-beta.4

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.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gate-setup — postinstall scaffolder.
4
+ *
5
+ * Runs automatically when a consumer installs @final-commerce/common.
6
+ * Creates missing governance config files and wires Husky hooks.
7
+ * Non-destructive: skips any file that already exists.
8
+ *
9
+ * Repo type is detected from dependencies:
10
+ * @nestjs/core → backend-nestjs
11
+ * react → frontend-react
12
+ * otherwise → library
13
+ *
14
+ * Skips silently when:
15
+ * - CI=true (no scaffolding needed in pipelines)
16
+ * - No package.json found at project root
17
+ * - Running inside @final-commerce/common itself
18
+ */
19
+
20
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+
23
+ // ── guards ────────────────────────────────────────────────────────────────────
24
+
25
+ // INIT_CWD is set by npm to the directory where npm install was invoked.
26
+ // npm_config_local_prefix is a reliable fallback.
27
+ const root = process.env.INIT_CWD ?? process.env.npm_config_local_prefix;
28
+
29
+ if (!root) process.exit(0);
30
+ if (process.env.CI) process.exit(0);
31
+
32
+ const pkgPath = join(root, 'package.json');
33
+ if (!existsSync(pkgPath)) process.exit(0);
34
+
35
+ let pkg;
36
+ try {
37
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
38
+ } catch {
39
+ process.exit(0);
40
+ }
41
+
42
+ if (pkg.name === '@final-commerce/common') process.exit(0);
43
+
44
+ // ── detect repo type ──────────────────────────────────────────────────────────
45
+
46
+ const allDeps = {
47
+ ...pkg.dependencies,
48
+ ...pkg.devDependencies,
49
+ ...pkg.peerDependencies,
50
+ };
51
+
52
+ const type = allDeps['@nestjs/core'] ? 'backend-nestjs' : allDeps['react'] ? 'frontend-react' : 'library';
53
+
54
+ // ── helpers ───────────────────────────────────────────────────────────────────
55
+
56
+ const ok = (msg) => process.stdout.write(`\x1b[32m✔ gate-setup: ${msg}\x1b[0m\n`);
57
+ const skip = (msg) => process.stdout.write(`\x1b[90m– gate-setup: ${msg} (already exists)\x1b[0m\n`);
58
+ const warn = (msg) => process.stdout.write(`\x1b[33m⚠ gate-setup: ${msg}\x1b[0m\n`);
59
+ const info = (msg) => process.stdout.write(`\x1b[36mℹ gate-setup: ${msg}\x1b[0m\n`);
60
+
61
+ function writeIfMissing(filePath, content, label) {
62
+ if (existsSync(filePath)) {
63
+ skip(label);
64
+ return;
65
+ }
66
+ writeFileSync(filePath, content, 'utf8');
67
+ ok(`Created ${label}`);
68
+ }
69
+
70
+ // ── 1. eslint.config.mjs ──────────────────────────────────────────────────────
71
+
72
+ writeIfMissing(
73
+ join(root, 'eslint.config.mjs'),
74
+ `import config from '@final-commerce/common/eslint/${type}';\nexport default config;\n`,
75
+ 'eslint.config.mjs',
76
+ );
77
+
78
+ // ── 2. tsconfig.json ──────────────────────────────────────────────────────────
79
+
80
+ writeIfMissing(
81
+ join(root, 'tsconfig.json'),
82
+ JSON.stringify({ extends: `@final-commerce/common/typescript/${type}` }, null, 2) + '\n',
83
+ 'tsconfig.json',
84
+ );
85
+
86
+ // ── 3. commitlint.config.mjs ──────────────────────────────────────────────────
87
+
88
+ writeIfMissing(
89
+ join(root, 'commitlint.config.mjs'),
90
+ `import config from '@final-commerce/common/commitlint';\nexport default config;\n`,
91
+ 'commitlint.config.mjs',
92
+ );
93
+
94
+ // ── 4. .nvmrc ────────────────────────────────────────────────────────────────
95
+
96
+ writeIfMissing(join(root, '.nvmrc'), '22\n', '.nvmrc');
97
+
98
+ // ── 5. husky hooks ────────────────────────────────────────────────────────────
99
+
100
+ const huskyDir = join(root, '.husky');
101
+ if (!existsSync(huskyDir)) mkdirSync(huskyDir, { recursive: true });
102
+
103
+ function writeHook(name, content) {
104
+ const hookPath = join(huskyDir, name);
105
+ if (existsSync(hookPath)) {
106
+ skip(`.husky/${name}`);
107
+ return;
108
+ }
109
+ writeFileSync(hookPath, `#!/usr/bin/env sh\n\n${content}\n`, 'utf8');
110
+ chmodSync(hookPath, 0o755);
111
+ ok(`Created .husky/${name}`);
112
+ }
113
+
114
+ writeHook('pre-commit', 'npx gate-save');
115
+ writeHook('pre-push', 'npx gate-done');
116
+
117
+ const prepareCommitMsgHook = join(huskyDir, 'prepare-commit-msg');
118
+ if (existsSync(prepareCommitMsgHook)) {
119
+ skip('.husky/prepare-commit-msg');
120
+ } else {
121
+ writeFileSync(prepareCommitMsgHook, readFileSync(join(import.meta.dirname, 'prepare-commit-msg.sh'), 'utf8'), 'utf8');
122
+ chmodSync(prepareCommitMsgHook, 0o755);
123
+ ok('Created .husky/prepare-commit-msg');
124
+ }
125
+
126
+ writeHook('commit-msg', 'npx --no -- commitlint --edit "$1"');
127
+
128
+ // ── 6. package.json mutations ─────────────────────────────────────────────────
129
+
130
+ let dirty = false;
131
+
132
+ // Prettier via package.json key — no separate config file needed.
133
+ if (!pkg.prettier) {
134
+ pkg.prettier = '@final-commerce/common/prettier';
135
+ dirty = true;
136
+ ok('Added prettier key to package.json');
137
+ }
138
+
139
+ // prepare script.
140
+ pkg.scripts ??= {};
141
+ if (!pkg.scripts.prepare) {
142
+ pkg.scripts.prepare = 'husky';
143
+ dirty = true;
144
+ ok('Added prepare script to package.json');
145
+ } else if (!pkg.scripts.prepare.includes('husky')) {
146
+ warn(`prepare script ("${pkg.scripts.prepare}") doesn't include husky — add it manually`);
147
+ }
148
+
149
+ // lint-staged config.
150
+ if (!pkg['lint-staged']) {
151
+ const codeGlob = type === 'backend-nestjs' ? '*.{ts,js}' : '*.{ts,tsx}';
152
+ pkg['lint-staged'] = {
153
+ [codeGlob]: ['eslint --fix', 'prettier --write'],
154
+ '*.{json,md,yml,yaml}': ['prettier --write'],
155
+ };
156
+ dirty = true;
157
+ ok('Added lint-staged config to package.json');
158
+ }
159
+
160
+ // devDependencies — add husky, lint-staged, commitlint, and jira hook if missing.
161
+ pkg.devDependencies ??= {};
162
+ const devDepsToAdd = [
163
+ ['husky', '^9.0.0'],
164
+ ['lint-staged', '^15.0.0'],
165
+ ['@commitlint/cli', '^19.0.0'],
166
+ ['@commitlint/config-conventional', '^19.0.0'],
167
+ ];
168
+ for (const [dep, version] of devDepsToAdd) {
169
+ if (!pkg.devDependencies[dep] && !pkg.dependencies?.[dep]) {
170
+ pkg.devDependencies[dep] = version;
171
+ dirty = true;
172
+ ok(`Added ${dep} to devDependencies`);
173
+ }
174
+ }
175
+
176
+ if (dirty) {
177
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
178
+ info('package.json updated — run npm install to activate new devDependencies');
179
+ }
180
+
181
+ // ── 7. i18n config (frontend-react + react-i18next only) ─────────────────────
182
+
183
+ if (type === 'frontend-react' && allDeps['react-i18next']) {
184
+ // i18next-parser config — controls where the extractor looks for t() calls.
185
+ writeIfMissing(
186
+ join(root, 'i18next-parser.config.cjs'),
187
+ `/**
188
+ * i18next-parser config — used by gate-i18n-sync to extract t() keys.
189
+ * Output goes to node_modules/.cache/ — the canonical bundles in src/locales/
190
+ * are written by the sync script after a round-trip with the mt server.
191
+ */
192
+ module.exports = {
193
+ input: [
194
+ 'src/**/*.{ts,tsx}',
195
+ '!src/**/*.test.{ts,tsx}',
196
+ '!src/**/*.spec.{ts,tsx}',
197
+ '!src/test/**',
198
+ ],
199
+ output: 'node_modules/.cache/i18next-extract/$LOCALE.json',
200
+ locales: ['en'],
201
+ defaultNamespace: 'translation',
202
+ keySeparator: false,
203
+ namespaceSeparator: false,
204
+ keepRemoved: false,
205
+ createOldCatalogs: false,
206
+ sort: true,
207
+ verbose: false,
208
+ failOnWarnings: false,
209
+ failOnUpdate: false,
210
+ };\n`,
211
+ 'i18next-parser.config.cjs',
212
+ );
213
+
214
+ // fc-i18n config in package.json — gate-save reads this to activate the sync.
215
+ if (!pkg['fc-i18n']) {
216
+ const slug = pkg.name?.replace(/^@[^/]+\//, '') ?? pkg.name ?? 'my-project';
217
+ pkg['fc-i18n'] = { slug };
218
+ ok(
219
+ `Added fc-i18n config to package.json (slug: "${slug}") — set i18nIndexPath if you use a SUPPORTED_LOCALES marker`,
220
+ );
221
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
222
+ } else {
223
+ skip('fc-i18n config in package.json');
224
+ }
225
+ }
226
+
227
+ info(`Done (${type})`);
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gate-start — Developer session initialiser.
4
+ *
5
+ * 1. Verifies @final-commerce/common in node_modules matches latest registry release.
6
+ * 2. Prompts for (or accepts via arg/env) a Jira ticket ID.
7
+ *
8
+ * Branch checkout is handled by the tasks-manager workspace tooling.
9
+ *
10
+ * Usage:
11
+ * gate-start [TICKET-ID]
12
+ * JIRA_TICKET_ID=FI-1234 gate-start
13
+ */
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+ import { createInterface } from 'node:readline';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+
20
+ const PKG_NAME = '@final-commerce/common';
21
+ const TICKET_RE = /^[A-Z]+-\d+$/;
22
+
23
+ // ── helpers ──────────────────────────────────────────────────────────────────
24
+
25
+ function capture(cmd) {
26
+ return spawnSync(cmd, { shell: true, encoding: 'utf8' });
27
+ }
28
+
29
+ function fail(msg) {
30
+ process.stderr.write(`\x1b[31m✖ ${msg}\x1b[0m\n`);
31
+ process.exit(1);
32
+ }
33
+
34
+ function info(msg) {
35
+ process.stdout.write(`\x1b[36mℹ ${msg}\x1b[0m\n`);
36
+ }
37
+
38
+ function ok(msg) {
39
+ process.stdout.write(`\x1b[32m✔ ${msg}\x1b[0m\n`);
40
+ }
41
+
42
+ function ask(question) {
43
+ return new Promise((resolve) => {
44
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
45
+ rl.question(question, (answer) => {
46
+ rl.close();
47
+ resolve(answer.trim());
48
+ });
49
+ });
50
+ }
51
+
52
+ // ── 1. upstream version check ─────────────────────────────────────────────────
53
+
54
+ info(`Checking ${PKG_NAME} upstream version…`);
55
+
56
+ const localPkgPath = resolve(process.cwd(), 'node_modules', PKG_NAME, 'package.json');
57
+
58
+ if (!existsSync(localPkgPath)) {
59
+ fail(`${PKG_NAME} is not installed locally.\nRun: npm install ${PKG_NAME}`);
60
+ }
61
+
62
+ const localVersion = JSON.parse(readFileSync(localPkgPath, 'utf8')).version;
63
+
64
+ // Rely on .npmrc scope mapping — do not override with --registry flag,
65
+ // which would bypass the @final-commerce → npm.pkg.github.com mapping.
66
+ const regResult = capture(`npm view ${PKG_NAME} version 2>&1`);
67
+
68
+ if (regResult.status !== 0) {
69
+ fail(
70
+ `Could not fetch latest version of ${PKG_NAME} from registry.\n` +
71
+ `Error: ${regResult.stdout.trim()}\n` +
72
+ `Ensure .npmrc is present and NPM_REGISTRY_TOKEN is set.`,
73
+ );
74
+ }
75
+
76
+ const latestVersion = regResult.stdout.trim();
77
+
78
+ if (localVersion !== latestVersion) {
79
+ fail(
80
+ `${PKG_NAME} is out of date.\n` +
81
+ ` Installed : ${localVersion}\n` +
82
+ ` Registry : ${latestVersion}\n` +
83
+ `Run: npm install ${PKG_NAME}@latest`,
84
+ );
85
+ }
86
+
87
+ ok(`${PKG_NAME}@${localVersion} is current.`);
88
+
89
+ // ── 2. Jira ticket ID ─────────────────────────────────────────────────────────
90
+
91
+ let ticketId = (process.env.JIRA_TICKET_ID ?? process.argv[2] ?? '').toUpperCase();
92
+
93
+ if (!ticketId) {
94
+ ticketId = (await ask('\nJira ticket ID (e.g. FI-1234): ')).toUpperCase();
95
+ }
96
+
97
+ if (!TICKET_RE.test(ticketId)) {
98
+ fail(`"${ticketId}" is not a valid Jira ticket ID. Expected format: PROJECT-123`);
99
+ }
100
+
101
+ ok(`Ticket ${ticketId} confirmed. Ready to work.`);
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env sh
2
+
3
+ BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
4
+ if [ -z "$BRANCH" ]; then exit 0; fi
5
+
6
+ # Skip protected branches
7
+ if echo "$BRANCH" | grep -qE '^(main|master|develop|pre-prod|release-staging.*)$'; then exit 0; fi
8
+
9
+ TICKET=$(echo "$BRANCH" | grep -oE '[A-Z]+-[0-9]+' | head -1)
10
+ if [ -z "$TICKET" ]; then exit 0; fi
11
+
12
+ MSG_FILE="$1"
13
+ CURRENT=$(cat "$MSG_FILE")
14
+ if echo "$CURRENT" | grep -q "$TICKET"; then exit 0; fi
15
+
16
+ awk -v ticket="[$TICKET]" 'NR==1{print $0 " " ticket; next}{print}' "$MSG_FILE" > "$MSG_FILE.tmp" && mv "$MSG_FILE.tmp" "$MSG_FILE"
@@ -0,0 +1,14 @@
1
+ export default {
2
+ extends: ['@commitlint/config-conventional'],
3
+ rules: {
4
+ 'type-enum': [
5
+ 2,
6
+ 'always',
7
+ ['feat', 'fix', 'refactor', 'style', 'docs', 'test', 'chore', 'build', 'ci', 'perf', 'revert'],
8
+ ],
9
+ 'subject-empty': [2, 'never'],
10
+ 'type-empty': [2, 'never'],
11
+ 'subject-min-length': [2, 'always', 10],
12
+ 'header-max-length': [2, 'always', 120],
13
+ },
14
+ };