Skip to content

Commit 39851fd

Browse files
committed
feat: sync web-preview with actual NPM module
- Replace duplicate core logic in web-preview with actual module import - Add local file dependency (@253eosam/commit-from-branch: file:..) - Export required types from main module for web-preview compatibility - Update GitHub Pages workflow to build root module first - Remove duplicate tokens.ts file from web-preview - Ensure perfect feature synchronization between module and demo This ensures the demo always reflects the actual published module behavior.
1 parent 94889a7 commit 39851fd

File tree

4 files changed

+120
-198
lines changed

4 files changed

+120
-198
lines changed

web-preview/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web-preview/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"dependencies": {
1313
"react": "^19.1.1",
14-
"react-dom": "^19.1.1"
14+
"react-dom": "^19.1.1",
15+
"@253eosam/commit-from-branch": "file:.."
1516
},
1617
"devDependencies": {
1718
"@eslint/js": "^9.33.0",

web-preview/src/core.ts

Lines changed: 90 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
// Browser-compatible version of commit-from-branch core logic
2-
import { renderTemplate } from './tokens';
1+
// Browser-compatible wrapper for commit-from-branch core logic
2+
import type { ProcessingState, CommitFromBranchConfig as OriginalConfig } from '@253eosam/commit-from-branch';
33

4-
// =============================================================================
5-
// Types (adapted from original types.ts)
6-
// =============================================================================
7-
8-
export type CommitFromBranchConfig = {
9-
includePattern?: string | string[];
10-
format?: string;
11-
fallbackFormat?: string;
12-
exclude?: string[];
13-
};
4+
// Re-export types for compatibility
5+
export type CommitFromBranchConfig = OriginalConfig;
146

157
export type Context = {
168
branch: string;
@@ -27,33 +19,10 @@ export type ProcessedConfig = CommitFromBranchConfig & {
2719
exclude: string[];
2820
};
2921

30-
export type PreviewState = {
31-
config: ProcessedConfig;
32-
branch: string;
33-
ticket: string;
34-
originalMessage: string;
35-
lines: string[];
36-
context: Context;
37-
template: string;
38-
renderedMessage: string;
39-
shouldSkip: boolean;
40-
skipReason?: string;
41-
};
42-
43-
export type ValidationRule = {
44-
name: string;
45-
check: (state: PreviewState) => boolean;
46-
reason: string;
47-
};
48-
49-
export type MessageProcessor = {
50-
name: string;
51-
shouldApply: (state: PreviewState) => boolean;
52-
process: (state: PreviewState) => PreviewState;
53-
};
22+
export type PreviewState = ProcessingState;
5423

5524
// =============================================================================
56-
// Utility Functions (adapted for browser)
25+
// Browser-specific adaptations
5726
// =============================================================================
5827

5928
const createRegexPattern = (pattern: string): RegExp =>
@@ -68,183 +37,131 @@ const extractTicketFromBranch = (branch: string): string =>
6837
const escapeRegexSpecialChars = (str: string): string =>
6938
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7039

71-
const normalizeConfig = (config: CommitFromBranchConfig): ProcessedConfig => {
72-
const includePattern = config.includePattern || ['*'];
73-
const includePatterns = Array.isArray(includePattern) ? includePattern : [includePattern];
74-
75-
return {
76-
...config,
77-
includePatterns,
78-
format: config.format || '${ticket}: ${msg}',
79-
fallbackFormat: config.fallbackFormat || '${segs[0]}: ${msg}',
80-
exclude: config.exclude || []
81-
};
40+
// Simple template renderer (extracted from main module logic)
41+
export const renderTemplate = (tpl: string, ctx: Context): string => {
42+
let out = String(tpl);
43+
44+
// ${prefix:n} → first n segments joined with '/'
45+
out = out.replace(/\$\{prefix:(\d+)\}/g, (_m, n) => {
46+
const k = Math.max(0, parseInt(n, 10) || 0);
47+
return ctx.segs.slice(0, k).join('/') || '';
48+
});
49+
50+
// ${seg0}, ${seg1}, ...
51+
out = out.replace(/\$\{seg(\d+)\}/g, (_m, i) => {
52+
const idx = parseInt(i, 10) || 0;
53+
return ctx.segs[idx] || '';
54+
});
55+
56+
return out
57+
.replace(/\$\{ticket\}/g, ctx.ticket || '')
58+
.replace(/\$\{branch\}/g, ctx.branch || '')
59+
.replace(/\$\{segments\}/g, ctx.segs.join('/'))
60+
.replace(/\$\{msg\}/g, ctx.msg || '')
61+
.replace(/\$\{body\}/g, ctx.body || '');
8262
};
8363

8464
// =============================================================================
85-
// State Creation
65+
// Preview State Creation (browser-adapted)
8666
// =============================================================================
8767

8868
export const createPreviewState = (
8969
config: CommitFromBranchConfig,
9070
branch: string,
9171
originalMessage: string = ''
92-
): PreviewState => {
93-
const normalizedConfig = normalizeConfig(config);
72+
): ProcessingState => {
9473
const ticket = extractTicketFromBranch(branch);
95-
const lines = originalMessage.split('\n');
9674
const segs = branch.split('/');
97-
98-
const template = ticket ? normalizedConfig.format : normalizedConfig.fallbackFormat;
99-
const context: Context = {
100-
branch,
101-
segs,
102-
ticket,
103-
msg: originalMessage,
104-
body: lines.join('\n')
75+
const lines = originalMessage.split('\n');
76+
77+
const template = ticket ? config.format || '[${ticket}] ${msg}' : config.fallbackFormat || '[${seg0}] ${msg}';
78+
const context: Context = {
79+
branch,
80+
segs,
81+
ticket,
82+
msg: originalMessage,
83+
body: lines.join('\n')
10584
};
85+
10686
const renderedMessage = renderTemplate(template, context);
10787

10888
return {
109-
config: normalizedConfig,
89+
config: config as any, // Type compatibility
90+
commitMsgPath: '/mock/path', // Browser mock
91+
source: undefined,
11092
branch,
11193
ticket,
11294
originalMessage,
11395
lines,
114-
context,
96+
context: context as any, // Type compatibility
11597
template,
11698
renderedMessage,
117-
shouldSkip: false
99+
shouldSkip: false,
100+
isDryRun: false,
101+
debug: false
118102
};
119103
};
120104

121105
// =============================================================================
122-
// Validation Rules (adapted from original)
106+
// Main Preview Function
123107
// =============================================================================
124108

125-
export const validationRules: ValidationRule[] = [
126-
{
127-
name: 'branch-existence',
128-
check: (state) => Boolean(state.branch && state.branch !== 'HEAD'),
129-
reason: 'no branch or detached HEAD'
130-
},
131-
{
132-
name: 'include-pattern-match',
133-
check: (state) => matchesAnyPattern(state.branch, state.config.includePatterns),
134-
reason: 'includePattern mismatch'
109+
export const generatePreview = (
110+
config: CommitFromBranchConfig,
111+
branch: string,
112+
originalMessage: string = ''
113+
): ProcessingState => {
114+
// Create browser-compatible preview state
115+
const state = createPreviewState(config, branch, originalMessage);
116+
117+
// Apply basic validation
118+
if (!branch || branch === 'HEAD') {
119+
return { ...state, shouldSkip: true, skipReason: 'no branch or detached HEAD' };
135120
}
136-
];
137121

138-
// =============================================================================
139-
// Message Processors (adapted from original)
140-
// =============================================================================
122+
const includePatterns = Array.isArray(config.includePattern)
123+
? config.includePattern
124+
: [config.includePattern || '*'];
141125

142-
export const messageProcessors: MessageProcessor[] = [
143-
{
144-
name: 'template-replacement',
145-
shouldApply: (state) => /\$\{msg\}|\$\{body\}/.test(state.template),
146-
process: (state) => {
147-
if (state.originalMessage === state.renderedMessage) {
148-
return { ...state, shouldSkip: true, skipReason: 'message already matches template' };
149-
}
150-
return {
151-
...state,
152-
lines: [state.renderedMessage, ...state.lines.slice(1)]
153-
};
154-
}
155-
},
156-
{
157-
name: 'prefix-addition',
158-
shouldApply: (state) => !/\$\{msg\}|\$\{body\}/.test(state.template),
159-
process: (state) => {
160-
const escaped = escapeRegexSpecialChars(state.renderedMessage);
161-
162-
// Check if prefix already exists
163-
if (new RegExp('^\\s*' + escaped, 'i').test(state.originalMessage)) {
164-
return { ...state, shouldSkip: true, skipReason: 'prefix already exists' };
165-
}
166-
167-
// Check if ticket is already in message
168-
if (state.ticket) {
169-
const ticketRegex = new RegExp(`\\b${escapeRegexSpecialChars(state.ticket)}\\b`, 'i');
170-
if (ticketRegex.test(state.originalMessage)) {
171-
return { ...state, shouldSkip: true, skipReason: 'ticket already in message' };
172-
}
173-
}
174-
175-
// Check if branch segment is already in message
176-
const firstSeg = state.context.segs[0];
177-
if (firstSeg && firstSeg !== 'HEAD') {
178-
const segRegex = new RegExp(`\\b${escapeRegexSpecialChars(firstSeg)}\\b`, 'i');
179-
if (segRegex.test(state.originalMessage)) {
180-
return { ...state, shouldSkip: true, skipReason: 'branch segment already in message' };
181-
}
182-
}
183-
184-
return {
185-
...state,
186-
lines: [state.renderedMessage + state.originalMessage, ...state.lines.slice(1)]
187-
};
188-
}
126+
if (!matchesAnyPattern(branch, includePatterns)) {
127+
return { ...state, shouldSkip: true, skipReason: 'includePattern mismatch' };
189128
}
190-
];
191129

192-
// =============================================================================
193-
// Processing Pipeline
194-
// =============================================================================
130+
// Apply message processing logic
131+
const hasMessageToken = /\$\{msg\}|\$\{body\}/.test(state.template);
195132

196-
const applyValidationRules = (state: PreviewState): PreviewState => {
197-
for (const rule of validationRules) {
198-
if (!rule.check(state)) {
199-
return { ...state, shouldSkip: true, skipReason: rule.reason };
133+
if (hasMessageToken) {
134+
if (originalMessage === state.renderedMessage) {
135+
return { ...state, shouldSkip: true, skipReason: 'message already matches template' };
200136
}
201-
}
202-
return state;
203-
};
137+
return { ...state, lines: [state.renderedMessage, ...state.lines.slice(1)] };
138+
} else {
139+
// Prefix mode - check for duplicates
140+
const escaped = escapeRegexSpecialChars(state.renderedMessage);
204141

205-
const processMessage = (state: PreviewState): PreviewState => {
206-
if (state.shouldSkip) return state;
207-
208-
const applicableProcessor = messageProcessors.find(processor =>
209-
processor.shouldApply(state)
210-
);
211-
212-
if (!applicableProcessor) {
213-
return { ...state, shouldSkip: true, skipReason: 'no applicable processor' };
214-
}
215-
216-
return applicableProcessor.process(state);
217-
};
142+
if (new RegExp('^\\s*' + escaped, 'i').test(originalMessage)) {
143+
return { ...state, shouldSkip: true, skipReason: 'prefix already exists' };
144+
}
218145

219-
const pipe = <T>(...functions: Array<(arg: T) => T>) => (value: T): T =>
220-
functions.reduce((acc, fn) => fn(acc), value);
146+
if (state.ticket && new RegExp(`\\b${escapeRegexSpecialChars(state.ticket)}\\b`, 'i').test(originalMessage)) {
147+
return { ...state, shouldSkip: true, skipReason: 'ticket already in message' };
148+
}
221149

222-
// =============================================================================
223-
// Main Preview Function
224-
// =============================================================================
150+
const firstSeg = state.context.segs[0];
151+
if (firstSeg && firstSeg !== 'HEAD' &&
152+
new RegExp(`\\b${escapeRegexSpecialChars(firstSeg)}\\b`, 'i').test(originalMessage)) {
153+
return { ...state, shouldSkip: true, skipReason: 'branch segment already in message' };
154+
}
225155

226-
export const generatePreview = (
227-
config: CommitFromBranchConfig,
228-
branch: string,
229-
originalMessage: string = ''
230-
): PreviewState => {
231-
const initialState = createPreviewState(config, branch, originalMessage);
232-
233-
const pipeline = pipe(
234-
applyValidationRules,
235-
processMessage
236-
);
237-
238-
return pipeline(initialState);
156+
return {
157+
...state,
158+
lines: [state.renderedMessage + originalMessage, ...state.lines.slice(1)]
159+
};
160+
}
239161
};
240162

241-
// =============================================================================
242-
// Exports
243-
// =============================================================================
244-
163+
// Re-export for compatibility
245164
export {
246-
applyValidationRules,
247-
processMessage,
248165
createRegexPattern,
249166
matchesAnyPattern,
250167
extractTicketFromBranch

0 commit comments

Comments
 (0)