@dualkit/convert 0.0.0-stage → 0.2.0-beta.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.
@@ -0,0 +1,173 @@
1
+ /** One text replacement, as offsets into `SourceFile.text`. */
2
+ type Edit = {
3
+ start: number;
4
+ end: number;
5
+ text: string;
6
+ };
7
+ type FlagKind = 'no-logical-form' | 'computed-class' | 'inline-svg-icon' | 'mixed-text' | 'outside-component' | 'format' | 'unparsed' | 'unsupported' | 'info';
8
+ /** Something convert left for a person or agent to finish. */
9
+ type Flag = {
10
+ file: string;
11
+ line: number;
12
+ kind: FlagKind;
13
+ message: string;
14
+ suggestion: string;
15
+ };
16
+ /** What a transform pass returns for one file. */
17
+ type FileResult = {
18
+ edits: Edit[];
19
+ flags: Flag[];
20
+ };
21
+ /** `bilingual` keeps English and adds Arabic; `arabic` converts in place. */
22
+ type Mode = 'bilingual' | 'arabic';
23
+ /** Optional settings read from the app's dualkit config. */
24
+ type ConvertConfig = {
25
+ start?: string;
26
+ port?: number;
27
+ routes?: string[];
28
+ params?: Record<string, string>;
29
+ storageState?: string;
30
+ test?: string;
31
+ };
32
+ /** What convert detected about the app it will convert. `entry` is the root layout (Next) or the createRoot file (Vite). */
33
+ type Project = {
34
+ root: string;
35
+ framework: 'next-app' | 'vite-react';
36
+ appDir: string | null;
37
+ entry: string;
38
+ tailwind: string | null;
39
+ i18n: 'next-intl' | 'react-i18next' | null;
40
+ packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
41
+ };
42
+ /** Answers the person gave at a prompt. They are merged into dualkit.config.json under `convert` and committed. */
43
+ type ConfigAnswers = {
44
+ start?: string;
45
+ port?: number;
46
+ };
47
+ /** What phase 1 is asked to do. `root` is the app folder, inside a git repo. `configFile` is relative to root, dualkit.config.json by default. */
48
+ type ConvertOptions = {
49
+ root: string;
50
+ mode: Mode;
51
+ install: boolean;
52
+ baseline: boolean;
53
+ configAnswers?: ConfigAnswers;
54
+ configFile?: string;
55
+ };
56
+ /** What phase 2 is asked to do. `force` uses English for pending strings. `configFile` is as for ConvertOptions. */
57
+ type FinishOptions = {
58
+ root: string;
59
+ force?: boolean;
60
+ configAnswers?: ConfigAnswers;
61
+ configFile?: string;
62
+ };
63
+ /** Per-request browser settings. `cookies` are set before the page loads. */
64
+ type PageOptions = {
65
+ cookies?: Record<string, string>;
66
+ };
67
+ /** Takes a screenshot of url at a viewport width and returns the PNG. Injected by the CLI. */
68
+ type Shooter = (url: string, width: number, options?: PageOptions) => Promise<Buffer>;
69
+ /** What convert needs from outside: a logger, a command runner, the app's config, for the baseline a shooter, and an interrupt signal. */
70
+ type ConvertIo = {
71
+ log: (s: string) => void;
72
+ shoot?: Shooter;
73
+ run: (cmd: string, cwd: string) => Promise<{
74
+ code: number;
75
+ out: string;
76
+ }>;
77
+ config: ConvertConfig;
78
+ /** Fires when the person interrupts the run. Convert checks it after slow steps and rolls back. */
79
+ signal?: AbortSignal;
80
+ };
81
+ /** The passes that make edits, plus wiring. */
82
+ type EditKind = 'tailwind' | 'css' | 'markup' | 'strings' | 'formats' | 'wiring';
83
+ /** What a phase 1 run changed and left to finish. */
84
+ type ConvertSummary = {
85
+ branch: string;
86
+ filesChanged: number;
87
+ edits: number;
88
+ /** Applied edits per pass. Wiring counts its edits plus the files it wrote. */
89
+ byKind: Record<EditKind, number>;
90
+ /** Files with applied edits, at post-move paths, sorted by file. */
91
+ changed: {
92
+ file: string;
93
+ edits: number;
94
+ }[];
95
+ flags: Flag[];
96
+ strings: number;
97
+ skipped: {
98
+ file: string;
99
+ reason: string;
100
+ }[];
101
+ };
102
+ /** Saved to `.dualkit/convert.json`. `generated` lists the files convert wrote, which later runs leave alone. */
103
+ type ConvertState = {
104
+ mode: Mode;
105
+ base: string;
106
+ branch: string;
107
+ framework: Project['framework'];
108
+ phase: 'converted' | 'finished';
109
+ generated: string[];
110
+ };
111
+
112
+ /** Thrown when two edits touch the same span. */
113
+ declare class OverlapError extends Error {
114
+ readonly first: Edit;
115
+ readonly second: Edit;
116
+ constructor(first: Edit, second: Edit);
117
+ }
118
+ /**
119
+ * Applies non-overlapping edits in any order. Edits that start at the same offset apply in their given
120
+ * order, so an insert listed before a replacement at that offset lands before it. Throws on an edit
121
+ * outside the text.
122
+ */
123
+ declare function applyEdits(text: string, edits: Edit[]): string;
124
+
125
+ /** Thrown when the app is not one convert can handle. The message names what is supported. */
126
+ declare class UnsupportedError extends Error {
127
+ constructor(message: string);
128
+ }
129
+ /** Reads the app at root and says which framework, entry, styling and tooling it uses. Throws UnsupportedError for anything else. */
130
+ declare function detectProject(root: string): Project;
131
+
132
+ /** The branch a mode converts on. */
133
+ declare const branchFor: (mode: Mode) => string;
134
+ /** The flags a person or agent still has to act on. Info flags are kept in the summary but are never problems. */
135
+ declare const problems: (flags: readonly Flag[]) => Flag[];
136
+ /**
137
+ * Phase 1: converts the app at `options.root` on the branch `dualkit/<mode>` and commits it with the
138
+ * person's own git identity. Transforms run in memory first, so a refusal changes nothing. A re-run on
139
+ * a branch convert already converted runs again in place and commits only when something changed.
140
+ */
141
+ declare function convert(options: ConvertOptions, ioIn: ConvertIo): Promise<ConvertSummary>;
142
+
143
+ /** Thrown when convert will not start. The message is one line saying why. */
144
+ declare class RefusedError extends Error {
145
+ constructor(message: string);
146
+ }
147
+
148
+ /** Thrown when a run fails after the branch was created. The message names the step and where the repo now is. */
149
+ declare class ConvertFailedError extends Error {
150
+ readonly cause: unknown;
151
+ constructor(message: string, cause: unknown);
152
+ }
153
+
154
+ /** What phase 2 needs from outside: phase 1's io plus an accessibility audit and a lint count. */
155
+ type FinishIo = ConvertIo & {
156
+ audit: (url: string, width: number, options?: PageOptions) => Promise<{
157
+ score: number;
158
+ errors: number;
159
+ warnings: number;
160
+ }>;
161
+ lint: (root: string) => Promise<number>;
162
+ };
163
+ /**
164
+ * Phase 2: writes the messages from the ledger, serves the app, checks English against the baseline and
165
+ * audits the Arabic pages, writes dualkit-convert.md and commits it with the messages and the ledger.
166
+ * When a step fails after writing starts, finish puts its own files back and commits nothing.
167
+ */
168
+ declare function finish(options: FinishOptions, ioIn: FinishIo): Promise<{
169
+ reportPath: string;
170
+ passed: boolean;
171
+ }>;
172
+
173
+ export { type ConfigAnswers, type ConvertConfig, ConvertFailedError, type ConvertIo, type ConvertOptions, type ConvertState, type ConvertSummary, type Edit, type FileResult, type FinishIo, type FinishOptions, type Flag, type FlagKind, type Mode, OverlapError, type Project, RefusedError, type Shooter, UnsupportedError, applyEdits, branchFor, convert, detectProject, finish, problems };
@@ -0,0 +1,173 @@
1
+ /** One text replacement, as offsets into `SourceFile.text`. */
2
+ type Edit = {
3
+ start: number;
4
+ end: number;
5
+ text: string;
6
+ };
7
+ type FlagKind = 'no-logical-form' | 'computed-class' | 'inline-svg-icon' | 'mixed-text' | 'outside-component' | 'format' | 'unparsed' | 'unsupported' | 'info';
8
+ /** Something convert left for a person or agent to finish. */
9
+ type Flag = {
10
+ file: string;
11
+ line: number;
12
+ kind: FlagKind;
13
+ message: string;
14
+ suggestion: string;
15
+ };
16
+ /** What a transform pass returns for one file. */
17
+ type FileResult = {
18
+ edits: Edit[];
19
+ flags: Flag[];
20
+ };
21
+ /** `bilingual` keeps English and adds Arabic; `arabic` converts in place. */
22
+ type Mode = 'bilingual' | 'arabic';
23
+ /** Optional settings read from the app's dualkit config. */
24
+ type ConvertConfig = {
25
+ start?: string;
26
+ port?: number;
27
+ routes?: string[];
28
+ params?: Record<string, string>;
29
+ storageState?: string;
30
+ test?: string;
31
+ };
32
+ /** What convert detected about the app it will convert. `entry` is the root layout (Next) or the createRoot file (Vite). */
33
+ type Project = {
34
+ root: string;
35
+ framework: 'next-app' | 'vite-react';
36
+ appDir: string | null;
37
+ entry: string;
38
+ tailwind: string | null;
39
+ i18n: 'next-intl' | 'react-i18next' | null;
40
+ packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
41
+ };
42
+ /** Answers the person gave at a prompt. They are merged into dualkit.config.json under `convert` and committed. */
43
+ type ConfigAnswers = {
44
+ start?: string;
45
+ port?: number;
46
+ };
47
+ /** What phase 1 is asked to do. `root` is the app folder, inside a git repo. `configFile` is relative to root, dualkit.config.json by default. */
48
+ type ConvertOptions = {
49
+ root: string;
50
+ mode: Mode;
51
+ install: boolean;
52
+ baseline: boolean;
53
+ configAnswers?: ConfigAnswers;
54
+ configFile?: string;
55
+ };
56
+ /** What phase 2 is asked to do. `force` uses English for pending strings. `configFile` is as for ConvertOptions. */
57
+ type FinishOptions = {
58
+ root: string;
59
+ force?: boolean;
60
+ configAnswers?: ConfigAnswers;
61
+ configFile?: string;
62
+ };
63
+ /** Per-request browser settings. `cookies` are set before the page loads. */
64
+ type PageOptions = {
65
+ cookies?: Record<string, string>;
66
+ };
67
+ /** Takes a screenshot of url at a viewport width and returns the PNG. Injected by the CLI. */
68
+ type Shooter = (url: string, width: number, options?: PageOptions) => Promise<Buffer>;
69
+ /** What convert needs from outside: a logger, a command runner, the app's config, for the baseline a shooter, and an interrupt signal. */
70
+ type ConvertIo = {
71
+ log: (s: string) => void;
72
+ shoot?: Shooter;
73
+ run: (cmd: string, cwd: string) => Promise<{
74
+ code: number;
75
+ out: string;
76
+ }>;
77
+ config: ConvertConfig;
78
+ /** Fires when the person interrupts the run. Convert checks it after slow steps and rolls back. */
79
+ signal?: AbortSignal;
80
+ };
81
+ /** The passes that make edits, plus wiring. */
82
+ type EditKind = 'tailwind' | 'css' | 'markup' | 'strings' | 'formats' | 'wiring';
83
+ /** What a phase 1 run changed and left to finish. */
84
+ type ConvertSummary = {
85
+ branch: string;
86
+ filesChanged: number;
87
+ edits: number;
88
+ /** Applied edits per pass. Wiring counts its edits plus the files it wrote. */
89
+ byKind: Record<EditKind, number>;
90
+ /** Files with applied edits, at post-move paths, sorted by file. */
91
+ changed: {
92
+ file: string;
93
+ edits: number;
94
+ }[];
95
+ flags: Flag[];
96
+ strings: number;
97
+ skipped: {
98
+ file: string;
99
+ reason: string;
100
+ }[];
101
+ };
102
+ /** Saved to `.dualkit/convert.json`. `generated` lists the files convert wrote, which later runs leave alone. */
103
+ type ConvertState = {
104
+ mode: Mode;
105
+ base: string;
106
+ branch: string;
107
+ framework: Project['framework'];
108
+ phase: 'converted' | 'finished';
109
+ generated: string[];
110
+ };
111
+
112
+ /** Thrown when two edits touch the same span. */
113
+ declare class OverlapError extends Error {
114
+ readonly first: Edit;
115
+ readonly second: Edit;
116
+ constructor(first: Edit, second: Edit);
117
+ }
118
+ /**
119
+ * Applies non-overlapping edits in any order. Edits that start at the same offset apply in their given
120
+ * order, so an insert listed before a replacement at that offset lands before it. Throws on an edit
121
+ * outside the text.
122
+ */
123
+ declare function applyEdits(text: string, edits: Edit[]): string;
124
+
125
+ /** Thrown when the app is not one convert can handle. The message names what is supported. */
126
+ declare class UnsupportedError extends Error {
127
+ constructor(message: string);
128
+ }
129
+ /** Reads the app at root and says which framework, entry, styling and tooling it uses. Throws UnsupportedError for anything else. */
130
+ declare function detectProject(root: string): Project;
131
+
132
+ /** The branch a mode converts on. */
133
+ declare const branchFor: (mode: Mode) => string;
134
+ /** The flags a person or agent still has to act on. Info flags are kept in the summary but are never problems. */
135
+ declare const problems: (flags: readonly Flag[]) => Flag[];
136
+ /**
137
+ * Phase 1: converts the app at `options.root` on the branch `dualkit/<mode>` and commits it with the
138
+ * person's own git identity. Transforms run in memory first, so a refusal changes nothing. A re-run on
139
+ * a branch convert already converted runs again in place and commits only when something changed.
140
+ */
141
+ declare function convert(options: ConvertOptions, ioIn: ConvertIo): Promise<ConvertSummary>;
142
+
143
+ /** Thrown when convert will not start. The message is one line saying why. */
144
+ declare class RefusedError extends Error {
145
+ constructor(message: string);
146
+ }
147
+
148
+ /** Thrown when a run fails after the branch was created. The message names the step and where the repo now is. */
149
+ declare class ConvertFailedError extends Error {
150
+ readonly cause: unknown;
151
+ constructor(message: string, cause: unknown);
152
+ }
153
+
154
+ /** What phase 2 needs from outside: phase 1's io plus an accessibility audit and a lint count. */
155
+ type FinishIo = ConvertIo & {
156
+ audit: (url: string, width: number, options?: PageOptions) => Promise<{
157
+ score: number;
158
+ errors: number;
159
+ warnings: number;
160
+ }>;
161
+ lint: (root: string) => Promise<number>;
162
+ };
163
+ /**
164
+ * Phase 2: writes the messages from the ledger, serves the app, checks English against the baseline and
165
+ * audits the Arabic pages, writes dualkit-convert.md and commits it with the messages and the ledger.
166
+ * When a step fails after writing starts, finish puts its own files back and commits nothing.
167
+ */
168
+ declare function finish(options: FinishOptions, ioIn: FinishIo): Promise<{
169
+ reportPath: string;
170
+ passed: boolean;
171
+ }>;
172
+
173
+ export { type ConfigAnswers, type ConvertConfig, ConvertFailedError, type ConvertIo, type ConvertOptions, type ConvertState, type ConvertSummary, type Edit, type FileResult, type FinishIo, type FinishOptions, type Flag, type FlagKind, type Mode, OverlapError, type Project, RefusedError, type Shooter, UnsupportedError, applyEdits, branchFor, convert, detectProject, finish, problems };