@f5-sales-demo/pi-utils 19.51.2

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,213 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ getLocale,
4
+ getLocaleDisplayName,
5
+ initI18n,
6
+ LOCALE_DISPLAY_NAMES,
7
+ mapToSupportedLocale,
8
+ registerLocales,
9
+ setLocale,
10
+ t,
11
+ } from "./i18n";
12
+
13
+ const EN = {
14
+ greeting: "Hello",
15
+ "greeting.with.name": "Hello, {name}!",
16
+ "items.count.one": "{count} item",
17
+ "items.count.other": "{count} items",
18
+ "error.notFound": "File not found: {path}",
19
+ };
20
+
21
+ const JA = {
22
+ greeting: "こんにちは",
23
+ "greeting.with.name": "こんにちは、{name}さん!",
24
+ "items.count.one": "{count}個のアイテム",
25
+ "items.count.other": "{count}個のアイテム",
26
+ "error.notFound": "ファイルが見つかりません: {path}",
27
+ };
28
+
29
+ describe("i18n", () => {
30
+ beforeEach(() => {
31
+ registerLocales({ en: EN, ja: JA });
32
+ setLocale("en");
33
+ });
34
+
35
+ test("t() returns English string by key", () => {
36
+ expect(t("greeting")).toBe("Hello");
37
+ });
38
+
39
+ test("t() interpolates parameters", () => {
40
+ expect(t("greeting.with.name", { name: "World" })).toBe("Hello, World!");
41
+ });
42
+
43
+ test("t() falls back to key when key is missing", () => {
44
+ expect(t("nonexistent.key")).toBe("nonexistent.key");
45
+ });
46
+
47
+ test("t() handles plural: one", () => {
48
+ expect(t("items.count", { count: 1 })).toBe("1 item");
49
+ });
50
+
51
+ test("t() handles plural: other", () => {
52
+ expect(t("items.count", { count: 5 })).toBe("5 items");
53
+ });
54
+
55
+ test("setLocale() switches to Japanese", () => {
56
+ setLocale("ja");
57
+ expect(t("greeting")).toBe("こんにちは");
58
+ expect(t("greeting.with.name", { name: "太郎" })).toBe("こんにちは、太郎さん!");
59
+ });
60
+
61
+ test("getLocale() returns current locale", () => {
62
+ expect(getLocale()).toBe("en");
63
+ setLocale("ja");
64
+ expect(getLocale()).toBe("ja");
65
+ });
66
+
67
+ test("falls back to base locale (ja-jp -> ja)", () => {
68
+ setLocale("ja-jp");
69
+ expect(t("greeting")).toBe("こんにちは");
70
+ });
71
+
72
+ test("falls back to English for unknown locale", () => {
73
+ setLocale("xx");
74
+ expect(t("greeting")).toBe("Hello");
75
+ });
76
+
77
+ test("normalizes locale with underscores", () => {
78
+ setLocale("ja_JP");
79
+ expect(getLocale()).toBe("ja");
80
+ expect(t("greeting")).toBe("こんにちは");
81
+ });
82
+
83
+ test("handles locale with encoding suffix", () => {
84
+ setLocale("ja_JP.UTF-8");
85
+ expect(getLocale()).toBe("ja");
86
+ expect(t("greeting")).toBe("こんにちは");
87
+ });
88
+
89
+ test("interpolation leaves unmatched placeholders", () => {
90
+ expect(t("error.notFound", { path: "/foo" })).toBe("File not found: /foo");
91
+ expect(t("greeting.with.name", {})).toBe("Hello, {name}!");
92
+ });
93
+
94
+ test("initI18n() defaults to en when no env vars set", () => {
95
+ const oldLang = Bun.env.LANG;
96
+ const oldXcsh = Bun.env.XCSH_LOCALE;
97
+ delete Bun.env.LANG;
98
+ delete Bun.env.XCSH_LOCALE;
99
+ initI18n();
100
+ expect(getLocale()).toBe("en");
101
+ if (oldLang) Bun.env.LANG = oldLang;
102
+ if (oldXcsh) Bun.env.XCSH_LOCALE = oldXcsh;
103
+ });
104
+
105
+ test("initI18n() reads XCSH_LOCALE env var", () => {
106
+ Bun.env.XCSH_LOCALE = "ja";
107
+ initI18n();
108
+ expect(getLocale()).toBe("ja");
109
+ delete Bun.env.XCSH_LOCALE;
110
+ setLocale("en");
111
+ });
112
+
113
+ test("initI18n() prefers explicit locale argument", () => {
114
+ Bun.env.XCSH_LOCALE = "ja";
115
+ initI18n("en");
116
+ expect(getLocale()).toBe("en");
117
+ delete Bun.env.XCSH_LOCALE;
118
+ });
119
+ });
120
+
121
+ describe("LOCALE_DISPLAY_NAMES", () => {
122
+ test("has entries for all 13 supported locales", () => {
123
+ const expected = ["ar", "de", "en", "es", "fr", "hi", "it", "ja", "ko", "pt-br", "th", "zh-cn", "zh-tw"];
124
+ for (const code of expected) {
125
+ expect(LOCALE_DISPLAY_NAMES[code]).toBeDefined();
126
+ expect(typeof LOCALE_DISPLAY_NAMES[code]).toBe("string");
127
+ }
128
+ });
129
+
130
+ test("returns correct names for key locales", () => {
131
+ expect(LOCALE_DISPLAY_NAMES.ko).toBe("Korean");
132
+ expect(LOCALE_DISPLAY_NAMES.ja).toBe("Japanese");
133
+ expect(LOCALE_DISPLAY_NAMES.en).toBe("English");
134
+ expect(LOCALE_DISPLAY_NAMES["zh-cn"]).toBe("Simplified Chinese");
135
+ });
136
+ });
137
+
138
+ describe("getLocaleDisplayName", () => {
139
+ beforeEach(() => {
140
+ registerLocales({ en: EN, ja: JA, "zh-cn": {}, "zh-tw": {}, "pt-br": {}, fr: {}, de: {} });
141
+ setLocale("en");
142
+ });
143
+
144
+ test("returns display name for known locale", () => {
145
+ expect(getLocaleDisplayName("ko")).toBe("Korean");
146
+ expect(getLocaleDisplayName("ja")).toBe("Japanese");
147
+ expect(getLocaleDisplayName("fr")).toBe("French");
148
+ });
149
+
150
+ test("returns display name for regional variant", () => {
151
+ expect(getLocaleDisplayName("pt-br")).toBe("Brazilian Portuguese");
152
+ expect(getLocaleDisplayName("zh-cn")).toBe("Simplified Chinese");
153
+ });
154
+
155
+ test("returns undefined for unknown locale", () => {
156
+ expect(getLocaleDisplayName("xx")).toBeUndefined();
157
+ expect(getLocaleDisplayName("sv")).toBeUndefined();
158
+ });
159
+
160
+ test("returns 'English' for 'en'", () => {
161
+ expect(getLocaleDisplayName("en")).toBe("English");
162
+ });
163
+
164
+ test("normalizes case", () => {
165
+ expect(getLocaleDisplayName("KO")).toBe("Korean");
166
+ expect(getLocaleDisplayName("zh-CN")).toBe("Simplified Chinese");
167
+ });
168
+ });
169
+
170
+ describe("mapToSupportedLocale", () => {
171
+ beforeEach(() => {
172
+ registerLocales({ en: EN, ja: JA, "zh-cn": {}, "zh-tw": {}, "pt-br": {}, fr: {}, de: {} });
173
+ setLocale("en");
174
+ });
175
+
176
+ test("exact match", () => {
177
+ expect(mapToSupportedLocale("ja")).toBe("ja");
178
+ expect(mapToSupportedLocale("en")).toBe("en");
179
+ });
180
+
181
+ test("regional variant maps to base", () => {
182
+ expect(mapToSupportedLocale("en-US")).toBe("en");
183
+ expect(mapToSupportedLocale("ja-JP")).toBe("ja");
184
+ expect(mapToSupportedLocale("fr-FR")).toBe("fr");
185
+ expect(mapToSupportedLocale("de-DE")).toBe("de");
186
+ });
187
+
188
+ test("regional variant with exact match", () => {
189
+ expect(mapToSupportedLocale("pt-BR")).toBe("pt-br");
190
+ expect(mapToSupportedLocale("zh-CN")).toBe("zh-cn");
191
+ expect(mapToSupportedLocale("zh-TW")).toBe("zh-tw");
192
+ });
193
+
194
+ test("Apple zh-Hans maps to zh-cn", () => {
195
+ expect(mapToSupportedLocale("zh-Hans")).toBe("zh-cn");
196
+ expect(mapToSupportedLocale("zh-Hans-CN")).toBe("zh-cn");
197
+ });
198
+
199
+ test("Apple zh-Hant maps to zh-tw", () => {
200
+ expect(mapToSupportedLocale("zh-Hant")).toBe("zh-tw");
201
+ expect(mapToSupportedLocale("zh-Hant-TW")).toBe("zh-tw");
202
+ });
203
+
204
+ test("underscore format", () => {
205
+ expect(mapToSupportedLocale("pt_BR")).toBe("pt-br");
206
+ expect(mapToSupportedLocale("ja_JP.UTF-8")).toBe("ja");
207
+ });
208
+
209
+ test("unsupported locale returns undefined", () => {
210
+ expect(mapToSupportedLocale("xx")).toBeUndefined();
211
+ expect(mapToSupportedLocale("sv-SE")).toBeUndefined();
212
+ });
213
+ });
package/src/i18n.ts ADDED
@@ -0,0 +1,90 @@
1
+ import {
2
+ getLocaleDisplayName,
3
+ LOCALE_DISPLAY_NAMES,
4
+ mapToSupportedLocale,
5
+ normalizeLocale,
6
+ } from "@f5xc-salesdemos/i18n-core";
7
+ import { $pickenv } from "./env";
8
+
9
+ export { getLocaleDisplayName, LOCALE_DISPLAY_NAMES, mapToSupportedLocale };
10
+
11
+ type LocaleMap = Record<string, string>;
12
+ type LocaleBundle = Record<string, LocaleMap>;
13
+
14
+ let currentLocale = "en";
15
+ let bundles: LocaleBundle = {};
16
+ let active: LocaleMap = {};
17
+
18
+ /**
19
+ * Register all locale bundles. Call once at startup before any t() calls.
20
+ * Keys are locale codes ("en", "ja", "zh-cn"), values are flat key→string maps.
21
+ */
22
+ export function registerLocales(localeBundle: LocaleBundle): void {
23
+ bundles = localeBundle;
24
+ active = resolveBundle(currentLocale);
25
+ }
26
+
27
+ /**
28
+ * Initialize i18n from environment / config.
29
+ * Priority: explicit locale arg > XCSH_LOCALE env > PI_LOCALE env > LANG env > "en"
30
+ */
31
+ export function initI18n(locale?: string): void {
32
+ const resolved =
33
+ locale || $pickenv("XCSH_LOCALE", "PI_LOCALE") || parseSystemLocale($pickenv("LANG", "LC_MESSAGES")) || "en";
34
+ setLocale(resolved);
35
+ }
36
+
37
+ export function setLocale(locale: string): void {
38
+ currentLocale = mapToSupportedLocale(locale) ?? normalizeLocale(locale);
39
+ active = resolveBundle(currentLocale);
40
+ }
41
+
42
+ export function getLocale(): string {
43
+ return currentLocale;
44
+ }
45
+
46
+ /**
47
+ * Translate a key with optional parameter interpolation.
48
+ * Falls back to the English string, then to the key itself.
49
+ *
50
+ * Usage:
51
+ * t("cli.error.patternRequired")
52
+ * t("cli.error.fileNotFound", { path: "/foo" })
53
+ * t("items.count", { count: 5 }) // uses ".one" / ".other" suffixes
54
+ */
55
+ export function t(key: string, params?: Record<string, string | number>): string {
56
+ let template = active[key];
57
+
58
+ if (template === undefined && params && typeof params.count === "number") {
59
+ const pluralSuffix = params.count === 1 ? ".one" : ".other";
60
+ template = active[key + pluralSuffix];
61
+ if (template === undefined) {
62
+ template = bundles.en?.[key + pluralSuffix] ?? bundles.en?.[key];
63
+ }
64
+ }
65
+
66
+ if (template === undefined) {
67
+ template = bundles.en?.[key] ?? key;
68
+ }
69
+
70
+ if (!params) return template;
71
+
72
+ return template.replace(/\{(\w+)\}/g, (match, name: string) => {
73
+ const val = params[name];
74
+ return val !== undefined ? String(val) : match;
75
+ });
76
+ }
77
+
78
+ function parseSystemLocale(raw: string | undefined): string | undefined {
79
+ if (!raw) return undefined;
80
+ const normalized = normalizeLocale(raw);
81
+ if (normalized === "c" || normalized === "posix") return "en";
82
+ return normalized;
83
+ }
84
+
85
+ function resolveBundle(locale: string): LocaleMap {
86
+ if (bundles[locale]) return bundles[locale];
87
+ const base = locale.split("-")[0];
88
+ if (bundles[base]) return bundles[base];
89
+ return bundles.en ?? {};
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ export { abortableSleep, createAbortableStream, once, untilAborted } from "./abortable";
2
+ export * from "./async";
3
+ export * from "./color";
4
+ export * from "./dirs";
5
+ export * from "./env";
6
+ export * from "./format";
7
+ export * from "./frontmatter";
8
+ export * from "./fs-error";
9
+ export * from "./glob";
10
+ export * from "./hook-fetch";
11
+ export {
12
+ getLocale,
13
+ getLocaleDisplayName,
14
+ initI18n,
15
+ LOCALE_DISPLAY_NAMES,
16
+ mapToSupportedLocale,
17
+ registerLocales,
18
+ setLocale,
19
+ t,
20
+ } from "./i18n";
21
+ export * from "./json";
22
+ export * as logger from "./logger";
23
+ export * from "./mermaid-ascii";
24
+ export * from "./mermaid-color";
25
+ export * from "./mime";
26
+ export * from "./models-yml";
27
+ export * from "./peek-file";
28
+ export * as postmortem from "./postmortem";
29
+ export * as procmgr from "./procmgr";
30
+ export { setNativeKillTree } from "./procmgr";
31
+ export * as prompt from "./prompt";
32
+ export * as ptree from "./ptree";
33
+ export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
34
+ export * from "./snowflake";
35
+ export * from "./stream";
36
+ export * from "./tab-spacing";
37
+ export * from "./temp";
38
+ export * from "./type-guards";
39
+ export * from "./which";
40
+ export * from "./xcsh-context-paths";
41
+ export * from "./xcsh-context-resolver";
42
+ export * from "./xcsh-env-names";
43
+
44
+ function isPlainObject(val: object): val is Record<string, unknown> {
45
+ return Object.getPrototypeOf(val) === Object.prototype || Array.isArray(val);
46
+ }
47
+
48
+ export function structuredCloneJSON<T>(value: T): T {
49
+ // primitives|null|undefined, copy
50
+ if (!value || typeof value !== "object") {
51
+ return value;
52
+ }
53
+
54
+ // deep clone
55
+ if (isPlainObject(value)) {
56
+ try {
57
+ return structuredClone(value);
58
+ } catch {
59
+ // might still fail due to nested structures
60
+ }
61
+ }
62
+ return JSON.parse(JSON.stringify(value)) as T;
63
+ }
package/src/json.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Try to parse JSON, returning null on failure.
3
+ */
4
+ export function tryParseJson<T = unknown>(content: string): T | null {
5
+ try {
6
+ return JSON.parse(content) as T;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Centralized file logger for xcsh.
3
+ *
4
+ * Logs to ~/.xcsh/logs/ with size-based rotation, supporting concurrent xcsh instances.
5
+ * Each log entry includes process.pid for traceability.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import winston from "winston";
9
+ import DailyRotateFile from "winston-daily-rotate-file";
10
+ import { getLogsDir } from "./dirs";
11
+
12
+ /** Ensure logs directory exists */
13
+ function ensureLogsDir(): string {
14
+ const logsDir = getLogsDir();
15
+ if (!fs.existsSync(logsDir)) {
16
+ fs.mkdirSync(logsDir, { recursive: true });
17
+ }
18
+ return logsDir;
19
+ }
20
+
21
+ /** Custom format that includes pid and flattens metadata */
22
+ const logFormat = winston.format.combine(
23
+ winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
24
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
25
+ const entry: Record<string, unknown> = {
26
+ timestamp,
27
+ level,
28
+ pid: process.pid,
29
+ message,
30
+ };
31
+ // Flatten metadata into entry
32
+ for (const [key, value] of Object.entries(meta)) {
33
+ if (key !== "level" && key !== "timestamp" && key !== "message") {
34
+ entry[key] = value;
35
+ }
36
+ }
37
+ return JSON.stringify(entry);
38
+ }),
39
+ );
40
+
41
+ /** Size-based rotating file transport */
42
+ const fileTransport = new DailyRotateFile({
43
+ dirname: ensureLogsDir(),
44
+ filename: "xcsh.%DATE%.log",
45
+ datePattern: "YYYY-MM-DD",
46
+ maxSize: "10m",
47
+ maxFiles: 5,
48
+ zippedArchive: true,
49
+ });
50
+
51
+ /** The winston logger instance */
52
+ const winstonLogger = winston.createLogger({
53
+ level: "debug",
54
+ format: logFormat,
55
+ transports: [fileTransport],
56
+ // Don't exit on error - logging failures shouldn't crash the app
57
+ exitOnError: false,
58
+ });
59
+
60
+ /**
61
+ * Log an error message.
62
+ * @param message - The message to log.
63
+ * @param context - The context to log.
64
+ */
65
+ export function error(message: string, context?: Record<string, unknown>): void {
66
+ try {
67
+ winstonLogger.error(message, context);
68
+ } catch {
69
+ // Silently ignore logging failures
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Log a warning message.
75
+ * @param message - The message to log.
76
+ * @param context - The context to log.
77
+ */
78
+ export function warn(message: string, context?: Record<string, unknown>): void {
79
+ try {
80
+ winstonLogger.warn(message, context);
81
+ } catch {
82
+ // Silently ignore logging failures
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Log a debug message.
88
+ * @param message - The message to log.
89
+ * @param context - The context to log.
90
+ */
91
+ export function debug(message: string, context?: Record<string, unknown>): void {
92
+ try {
93
+ winstonLogger.debug(message, context);
94
+ } catch {
95
+ // Silently ignore logging failures
96
+ }
97
+ }
98
+
99
+ const LOGGED_TIMING_THRESHOLD_MS = 5;
100
+
101
+ /** Sequential wall-clock markers (next marker closes the previous segment). */
102
+ let gTimings: [op: string, ts: number][] = [];
103
+
104
+ /** Await-accurate durations (safe for parallel work; sums can overlap). */
105
+ let gAsyncSpans: [op: string, durationMs: number][] = [];
106
+
107
+ /** Whether to record timings. */
108
+ let gRecordTimings = false;
109
+
110
+ /**
111
+ * Print collected timings to stderr.
112
+ * Wall segments are gaps between consecutive {@link time} markers only; they are wrong when
113
+ * concurrent code also calls {@link time} (e.g. parallel capability loads). Use {@link timeAsync}
114
+ * for those awaits instead.
115
+ */
116
+ export function printTimings(): void {
117
+ if (!gRecordTimings || gTimings.length === 0) {
118
+ console.error("\n--- Startup Timings ---\n(no markers)\n");
119
+ return;
120
+ }
121
+
122
+ const endTs = performance.now();
123
+ gTimings.push(["(end)", endTs]);
124
+
125
+ console.error("\n--- Startup timings (wall segments between time() markers) ---");
126
+ const firstTs = gTimings[0][1];
127
+ for (let i = 0; i < gTimings.length - 1; i++) {
128
+ const [op, ts] = gTimings[i];
129
+ const [, nextTs] = gTimings[i + 1];
130
+ const dur = nextTs - ts;
131
+ if (dur > LOGGED_TIMING_THRESHOLD_MS) {
132
+ console.error(` ${op}: ${dur}ms`);
133
+ }
134
+ }
135
+ console.error(` span (first marker → end): ${endTs - firstTs}ms`);
136
+
137
+ if (gAsyncSpans.length > 0) {
138
+ console.error("\n--- Async (await-accurate; parallel spans may overlap) ---");
139
+ for (const [op, dur] of gAsyncSpans) {
140
+ if (dur > LOGGED_TIMING_THRESHOLD_MS) {
141
+ console.error(` ${op}: ${dur}ms`);
142
+ }
143
+ }
144
+ }
145
+
146
+ console.error("------------------------\n");
147
+
148
+ gTimings.pop();
149
+ }
150
+
151
+ /**
152
+ * Begin recording startup timings. Seeds the timeline so the first segment is meaningful.
153
+ */
154
+ export function startTiming(): void {
155
+ gTimings = [["(startup)", performance.now()]];
156
+ gAsyncSpans = [];
157
+ gRecordTimings = true;
158
+ }
159
+
160
+ /**
161
+ * End timing window and clear buffers.
162
+ */
163
+ export function endTiming(): void {
164
+ gTimings = [];
165
+ gAsyncSpans = [];
166
+ gRecordTimings = false;
167
+ }
168
+
169
+ function recordAsyncSpan(op: string, start: number): void {
170
+ const dur = performance.now() - start;
171
+ if (dur > LOGGED_TIMING_THRESHOLD_MS) {
172
+ gAsyncSpans.push([op, dur]);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Wall-clock segment boundary: duration for this label runs until the next {@link time} call.
178
+ * Do not use across `await` when other tasks may call {@link time}; use {@link timeAsync} for the awaited work.
179
+ */
180
+ export function time(op: string): void;
181
+ export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
182
+ export function time<T, A extends unknown[]>(op: string, fn?: (...args: A) => T, ...args: A): T | undefined {
183
+ if (fn === undefined) {
184
+ if (gRecordTimings) {
185
+ gTimings.push([op, performance.now()]);
186
+ }
187
+ return undefined as T;
188
+ } else if (gRecordTimings) {
189
+ const start = performance.now();
190
+ try {
191
+ const result = fn(...args);
192
+ if (result instanceof Promise) {
193
+ return result.finally(recordAsyncSpan.bind(null, op, start)) as T;
194
+ }
195
+ recordAsyncSpan(op, start);
196
+ return result;
197
+ } catch (error) {
198
+ recordAsyncSpan(op, start);
199
+ throw error;
200
+ }
201
+ } else {
202
+ return fn(...args);
203
+ }
204
+ }
@@ -0,0 +1,31 @@
1
+ import { type AsciiRenderOptions, renderMermaidASCII } from "beautiful-mermaid";
2
+
3
+ export type { AsciiRenderOptions as MermaidAsciiRenderOptions };
4
+
5
+ export function renderMermaidAscii(source: string, options?: AsciiRenderOptions): string {
6
+ return renderMermaidASCII(source, options);
7
+ }
8
+
9
+ export function renderMermaidAsciiSafe(source: string, options?: AsciiRenderOptions): string | null {
10
+ try {
11
+ return renderMermaidASCII(source, options);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Extract mermaid code blocks from markdown text.
19
+ */
20
+ export function extractMermaidBlocks(markdown: string): { source: string; hash: bigint | number }[] {
21
+ const blocks: { source: string; hash: bigint | number }[] = [];
22
+ const regex = /```mermaid\s*\n([\s\S]*?)```/g;
23
+
24
+ for (let match = regex.exec(markdown); match !== null; match = regex.exec(markdown)) {
25
+ const source = match[1].trim();
26
+ const hash = Bun.hash(source);
27
+ blocks.push({ source, hash });
28
+ }
29
+
30
+ return blocks;
31
+ }