@canmi/seam-react 0.2.14 → 0.4.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.
@@ -0,0 +1,139 @@
1
+ /* packages/client/react/scripts/skeleton/cache.mjs */
2
+
3
+ import { build } from "esbuild";
4
+ import { createHash } from "node:crypto";
5
+ import { readFileSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ /** Parse import statements to map local names to specifiers */
9
+ function parseComponentImports(source) {
10
+ const map = new Map();
11
+ const re = /import\s+(?:(\w+)\s*,?\s*)?(?:\{([^}]*)\}\s*)?from\s+['"]([^'"]+)['"]/g;
12
+ let m;
13
+ while ((m = re.exec(source)) !== null) {
14
+ const [, defaultName, namedPart, specifier] = m;
15
+ if (defaultName) map.set(defaultName, specifier);
16
+ if (namedPart) {
17
+ for (const part of namedPart.split(",")) {
18
+ const t = part.trim();
19
+ if (!t) continue;
20
+ const asMatch = t.match(/^(\w+)\s+as\s+(\w+)$/);
21
+ if (asMatch) {
22
+ map.set(asMatch[2], specifier);
23
+ map.set(asMatch[1], specifier);
24
+ } else {
25
+ map.set(t, specifier);
26
+ }
27
+ }
28
+ }
29
+ }
30
+ return map;
31
+ }
32
+
33
+ /** Bundle each component via esbuild (write: false) and SHA-256 hash the output */
34
+ async function computeComponentHashes(names, importMap, routesDir) {
35
+ const hashes = new Map();
36
+ const seen = new Set();
37
+ const tasks = [];
38
+ for (const name of names) {
39
+ const specifier = importMap.get(name);
40
+ if (!specifier || seen.has(specifier)) continue;
41
+ seen.add(specifier);
42
+ tasks.push(
43
+ build({
44
+ stdin: { contents: `import '${specifier}'`, resolveDir: routesDir, loader: "js" },
45
+ bundle: true,
46
+ write: false,
47
+ format: "esm",
48
+ platform: "node",
49
+ treeShaking: false,
50
+ external: ["react", "react-dom", "@canmi/seam-react", "@canmi/seam-i18n"],
51
+ logLevel: "silent",
52
+ })
53
+ .then((result) => {
54
+ const content = result.outputFiles[0]?.text || "";
55
+ const hash = createHash("sha256").update(content).digest("hex");
56
+ for (const [n, s] of importMap) {
57
+ if (s === specifier) hashes.set(n, hash);
58
+ }
59
+ })
60
+ .catch(() => {}),
61
+ );
62
+ }
63
+ await Promise.all(tasks);
64
+ return hashes;
65
+ }
66
+
67
+ /**
68
+ * Hash the build scripts themselves to invalidate cache when tooling changes.
69
+ * @param {string[]} scriptFiles - absolute paths of script files to hash
70
+ */
71
+ function computeScriptHash(scriptFiles) {
72
+ const h = createHash("sha256");
73
+ for (const f of scriptFiles) h.update(readFileSync(f, "utf-8"));
74
+ return h.digest("hex");
75
+ }
76
+
77
+ function pathToSlug(path) {
78
+ const t = path
79
+ .replace(/^\/|\/$/g, "")
80
+ .replace(/\//g, "-")
81
+ .replace(/:/g, "");
82
+ return t || "index";
83
+ }
84
+
85
+ function readCache(cacheDir, slug) {
86
+ try {
87
+ return JSON.parse(readFileSync(join(cacheDir, `${slug}.json`), "utf-8"));
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function writeCache(cacheDir, slug, key, data) {
94
+ writeFileSync(join(cacheDir, `${slug}.json`), JSON.stringify({ key, data }));
95
+ }
96
+
97
+ function computeCacheKey(componentHash, manifestContent, config, scriptHash, locale, messagesJson) {
98
+ const h = createHash("sha256");
99
+ h.update(componentHash);
100
+ h.update(manifestContent);
101
+ h.update(JSON.stringify(config));
102
+ h.update(scriptHash);
103
+ if (locale) h.update(locale);
104
+ if (messagesJson) h.update(messagesJson);
105
+ return h.digest("hex").slice(0, 16);
106
+ }
107
+
108
+ let _createI18n = null;
109
+ async function buildI18nValue(locale, messages, defaultLocale) {
110
+ if (!_createI18n) {
111
+ const mod = await import("@canmi/seam-i18n");
112
+ _createI18n = mod.createI18n;
113
+ }
114
+ const localeMessages = messages?.[locale] || {};
115
+ const fallback =
116
+ defaultLocale && locale !== defaultLocale ? messages?.[defaultLocale] || {} : undefined;
117
+ const instance = _createI18n(locale, localeMessages, fallback);
118
+ const usedKeys = new Set();
119
+ const origT = instance.t;
120
+ return {
121
+ locale: instance.locale,
122
+ t(key, params) {
123
+ usedKeys.add(key);
124
+ return origT(key, params);
125
+ },
126
+ _usedKeys: usedKeys,
127
+ };
128
+ }
129
+
130
+ export {
131
+ parseComponentImports,
132
+ computeComponentHashes,
133
+ computeScriptHash,
134
+ pathToSlug,
135
+ readCache,
136
+ writeCache,
137
+ computeCacheKey,
138
+ buildI18nValue,
139
+ };
@@ -0,0 +1,109 @@
1
+ /* packages/client/react/scripts/skeleton/layout.mjs */
2
+
3
+ import { createElement } from "react";
4
+ import { buildSentinelData } from "@canmi/seam-react";
5
+ import {
6
+ generateMockFromSchema,
7
+ flattenLoaderMock,
8
+ deepMerge,
9
+ collectHtmlPaths,
10
+ createAccessTracker,
11
+ checkFieldAccess,
12
+ } from "../mock-generator.mjs";
13
+ import { guardedRender } from "./render.mjs";
14
+ import { buildPageSchema } from "./schema.mjs";
15
+
16
+ function toLayoutId(path) {
17
+ return path === "/"
18
+ ? "_layout_root"
19
+ : `_layout_${path.replace(/^\/|\/$/g, "").replace(/\//g, "-")}`;
20
+ }
21
+
22
+ /** Extract layout components and metadata from route tree */
23
+ function extractLayouts(routes) {
24
+ const seen = new Map();
25
+ (function walk(defs, parentId) {
26
+ for (const def of defs) {
27
+ if (def.layout && def.children) {
28
+ const id = toLayoutId(def.path);
29
+ if (!seen.has(id)) {
30
+ seen.set(id, {
31
+ component: def.layout,
32
+ loaders: def.loaders || {},
33
+ mock: def.mock || null,
34
+ parentId: parentId || null,
35
+ });
36
+ }
37
+ walk(def.children, id);
38
+ }
39
+ }
40
+ })(routes, null);
41
+ return seen;
42
+ }
43
+
44
+ /**
45
+ * Resolve mock data for a layout: auto-generate from schema when loaders exist,
46
+ * then deep-merge any user-provided partial mock on top.
47
+ * Unlike resolveRouteMock, a layout with no loaders and no mock is valid (empty shell).
48
+ */
49
+ function resolveLayoutMock(entry, manifest) {
50
+ if (Object.keys(entry.loaders).length > 0) {
51
+ const schema = buildPageSchema(entry, manifest);
52
+ if (schema) {
53
+ const keyedMock = generateMockFromSchema(schema);
54
+ const autoMock = flattenLoaderMock(keyedMock);
55
+ return entry.mock ? deepMerge(autoMock, entry.mock) : autoMock;
56
+ }
57
+ }
58
+ return entry.mock || {};
59
+ }
60
+
61
+ /**
62
+ * Render layout with seam-outlet placeholder, optionally with sentinel data.
63
+ * @param {{ buildWarnings: string[], seenWarnings: Set<string> }} ctx - shared warning state
64
+ */
65
+ function renderLayout(LayoutComponent, id, entry, manifest, i18nValue, ctx) {
66
+ const mock = resolveLayoutMock(entry, manifest);
67
+ const schema =
68
+ Object.keys(entry.loaders || {}).length > 0 ? buildPageSchema(entry, manifest) : null;
69
+ const htmlPaths = schema ? collectHtmlPaths(schema) : new Set();
70
+ const data = Object.keys(mock).length > 0 ? buildSentinelData(mock, "", htmlPaths) : {};
71
+
72
+ // Wrap data with Proxy to detect schema/component field mismatches
73
+ const accessed = new Set();
74
+ const trackedData = Object.keys(data).length > 0 ? createAccessTracker(data, accessed) : data;
75
+
76
+ function LayoutWithOutlet() {
77
+ return createElement(LayoutComponent, null, createElement("seam-outlet", null));
78
+ }
79
+ const html = guardedRender(`layout:${id}`, LayoutWithOutlet, trackedData, i18nValue, ctx);
80
+
81
+ const fieldWarnings = checkFieldAccess(accessed, schema, `layout:${id}`);
82
+ for (const w of fieldWarnings) {
83
+ const msg = `[seam] warning: ${w}`;
84
+ if (!ctx.seenWarnings.has(msg)) {
85
+ ctx.seenWarnings.add(msg);
86
+ ctx.buildWarnings.push(msg);
87
+ }
88
+ }
89
+
90
+ return html;
91
+ }
92
+
93
+ /** Flatten routes, annotating each leaf with its parent layout id */
94
+ function flattenRoutes(routes, currentLayout) {
95
+ const leaves = [];
96
+ for (const route of routes) {
97
+ if (route.layout && route.children) {
98
+ leaves.push(...flattenRoutes(route.children, toLayoutId(route.path)));
99
+ } else if (route.children) {
100
+ leaves.push(...flattenRoutes(route.children, currentLayout));
101
+ } else {
102
+ if (currentLayout) route._layoutId = currentLayout;
103
+ leaves.push(route);
104
+ }
105
+ }
106
+ return leaves;
107
+ }
108
+
109
+ export { toLayoutId, extractLayouts, resolveLayoutMock, renderLayout, flattenRoutes };
@@ -0,0 +1,188 @@
1
+ /* packages/client/react/scripts/skeleton/process.mjs */
2
+
3
+ import { buildI18nValue, computeCacheKey, pathToSlug, readCache, writeCache } from "./cache.mjs";
4
+ import { renderLayout } from "./layout.mjs";
5
+ import { renderRoute } from "./schema.mjs";
6
+
7
+ async function processLayoutsWithCache(layoutMap, ctx) {
8
+ return Promise.all(
9
+ [...layoutMap.entries()].map(async ([id, entry]) => {
10
+ // i18n: render once per locale, return localeHtml map
11
+ if (ctx.i18n) {
12
+ const localeHtml = {};
13
+ let collectedKeys = null;
14
+ for (const locale of ctx.i18n.locales) {
15
+ const i18nValue = await buildI18nValue(locale, ctx.i18n.messages, ctx.i18n.default);
16
+ const messagesJson = JSON.stringify(ctx.i18n.messages?.[locale] || {});
17
+ const compHash = ctx.componentHashes.get(entry.component?.name);
18
+ if (compHash) {
19
+ const config = { id, loaders: entry.loaders, mock: entry.mock };
20
+ const key = computeCacheKey(
21
+ compHash,
22
+ ctx.manifestContent,
23
+ config,
24
+ ctx.scriptHash,
25
+ locale,
26
+ messagesJson,
27
+ );
28
+ const slug = `layout_${id}_${locale}`;
29
+ const cached = readCache(ctx.cacheDir, slug);
30
+ if (cached && cached.key === key) {
31
+ ctx.stats.hits++;
32
+ localeHtml[locale] = cached.data.html;
33
+ if (!collectedKeys) collectedKeys = cached.data.i18nKeys;
34
+ continue;
35
+ }
36
+ const html = renderLayout(
37
+ entry.component,
38
+ id,
39
+ entry,
40
+ ctx.manifest,
41
+ i18nValue,
42
+ ctx.warnCtx,
43
+ );
44
+ const i18nKeys = [...i18nValue._usedKeys].sort();
45
+ writeCache(ctx.cacheDir, slug, key, { html, i18nKeys });
46
+ ctx.stats.misses++;
47
+ localeHtml[locale] = html;
48
+ if (!collectedKeys) collectedKeys = i18nKeys;
49
+ } else {
50
+ ctx.stats.misses++;
51
+ const html = renderLayout(
52
+ entry.component,
53
+ id,
54
+ entry,
55
+ ctx.manifest,
56
+ i18nValue,
57
+ ctx.warnCtx,
58
+ );
59
+ const i18nKeys = [...i18nValue._usedKeys].sort();
60
+ localeHtml[locale] = html;
61
+ if (!collectedKeys) collectedKeys = i18nKeys;
62
+ }
63
+ }
64
+ return {
65
+ id,
66
+ localeHtml,
67
+ loaders: entry.loaders,
68
+ parent: entry.parentId,
69
+ i18nKeys: collectedKeys || [],
70
+ };
71
+ }
72
+
73
+ // No i18n: original behavior
74
+ const compHash = ctx.componentHashes.get(entry.component?.name);
75
+ if (compHash) {
76
+ const config = { id, loaders: entry.loaders, mock: entry.mock };
77
+ const key = computeCacheKey(compHash, ctx.manifestContent, config, ctx.scriptHash);
78
+ const slug = `layout_${id}`;
79
+ const cached = readCache(ctx.cacheDir, slug);
80
+ if (cached && cached.key === key) {
81
+ ctx.stats.hits++;
82
+ return cached.data;
83
+ }
84
+ const data = {
85
+ id,
86
+ html: renderLayout(entry.component, id, entry, ctx.manifest, undefined, ctx.warnCtx),
87
+ loaders: entry.loaders,
88
+ parent: entry.parentId,
89
+ };
90
+ writeCache(ctx.cacheDir, slug, key, data);
91
+ ctx.stats.misses++;
92
+ return data;
93
+ }
94
+ ctx.stats.misses++;
95
+ return {
96
+ id,
97
+ html: renderLayout(entry.component, id, entry, ctx.manifest, undefined, ctx.warnCtx),
98
+ loaders: entry.loaders,
99
+ parent: entry.parentId,
100
+ };
101
+ }),
102
+ );
103
+ }
104
+
105
+ async function processRoutesWithCache(flat, ctx) {
106
+ return Promise.all(
107
+ flat.map(async (r) => {
108
+ // i18n: render once per locale, return localeVariants map
109
+ if (ctx.i18n) {
110
+ const localeVariants = {};
111
+ let collectedKeys = null;
112
+ for (const locale of ctx.i18n.locales) {
113
+ const i18nValue = await buildI18nValue(locale, ctx.i18n.messages, ctx.i18n.default);
114
+ const messagesJson = JSON.stringify(ctx.i18n.messages?.[locale] || {});
115
+ const compHash = ctx.componentHashes.get(r.component?.name);
116
+ if (compHash) {
117
+ const config = { path: r.path, loaders: r.loaders, mock: r.mock, nullable: r.nullable };
118
+ const key = computeCacheKey(
119
+ compHash,
120
+ ctx.manifestContent,
121
+ config,
122
+ ctx.scriptHash,
123
+ locale,
124
+ messagesJson,
125
+ );
126
+ const slug = `route_${pathToSlug(r.path)}_${locale}`;
127
+ const cached = readCache(ctx.cacheDir, slug);
128
+ if (cached && cached.key === key) {
129
+ ctx.stats.hits++;
130
+ localeVariants[locale] = cached.data;
131
+ if (!collectedKeys) collectedKeys = cached.data.i18nKeys;
132
+ continue;
133
+ }
134
+ const data = renderRoute(r, ctx.manifest, i18nValue, ctx.warnCtx);
135
+ data.i18nKeys = [...i18nValue._usedKeys].sort();
136
+ writeCache(ctx.cacheDir, slug, key, data);
137
+ ctx.stats.misses++;
138
+ localeVariants[locale] = data;
139
+ if (!collectedKeys) collectedKeys = data.i18nKeys;
140
+ } else {
141
+ ctx.stats.misses++;
142
+ const data = renderRoute(r, ctx.manifest, i18nValue, ctx.warnCtx);
143
+ data.i18nKeys = [...i18nValue._usedKeys].sort();
144
+ localeVariants[locale] = data;
145
+ if (!collectedKeys) collectedKeys = data.i18nKeys;
146
+ }
147
+ }
148
+ // Combine per-locale data into the expected output format
149
+ const first = localeVariants[ctx.i18n.locales[0]];
150
+ return {
151
+ path: r.path,
152
+ loaders: first.loaders,
153
+ layout: first.layout,
154
+ mock: first.mock,
155
+ pageSchema: first.pageSchema,
156
+ i18nKeys: collectedKeys || [],
157
+ localeVariants: Object.fromEntries(
158
+ Object.entries(localeVariants).map(([loc, data]) => [
159
+ loc,
160
+ { axes: data.axes, variants: data.variants, mockHtml: data.mockHtml },
161
+ ]),
162
+ ),
163
+ };
164
+ }
165
+
166
+ // No i18n: original behavior
167
+ const compHash = ctx.componentHashes.get(r.component?.name);
168
+ if (compHash) {
169
+ const config = { path: r.path, loaders: r.loaders, mock: r.mock, nullable: r.nullable };
170
+ const key = computeCacheKey(compHash, ctx.manifestContent, config, ctx.scriptHash);
171
+ const slug = `route_${pathToSlug(r.path)}`;
172
+ const cached = readCache(ctx.cacheDir, slug);
173
+ if (cached && cached.key === key) {
174
+ ctx.stats.hits++;
175
+ return cached.data;
176
+ }
177
+ const data = renderRoute(r, ctx.manifest, undefined, ctx.warnCtx);
178
+ writeCache(ctx.cacheDir, slug, key, data);
179
+ ctx.stats.misses++;
180
+ return data;
181
+ }
182
+ ctx.stats.misses++;
183
+ return renderRoute(r, ctx.manifest, undefined, ctx.warnCtx);
184
+ }),
185
+ );
186
+ }
187
+
188
+ export { processLayoutsWithCache, processRoutesWithCache };
@@ -0,0 +1,160 @@
1
+ /* packages/client/react/scripts/skeleton/render.mjs */
2
+
3
+ import { createElement } from "react";
4
+ import { renderToString } from "react-dom/server";
5
+ import { SeamDataProvider } from "@canmi/seam-react";
6
+
7
+ let _I18nProvider = null;
8
+
9
+ export function setI18nProvider(provider) {
10
+ _I18nProvider = provider;
11
+ }
12
+
13
+ class SeamBuildError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "SeamBuildError";
17
+ }
18
+ }
19
+
20
+ // Matches React-injected resource hint <link> tags.
21
+ // Only rel values used by React's resource APIs are targeted (preload, dns-prefetch, preconnect,
22
+ // data-precedence); user-authored <link> tags (canonical, alternate, stylesheet) are unaffected.
23
+ const RESOURCE_HINT_RE =
24
+ /<link[^>]+rel\s*=\s*"(?:preload|dns-prefetch|preconnect)"[^>]*>|<link[^>]+data-precedence[^>]*>/gi;
25
+
26
+ function renderWithData(component, data, i18nValue) {
27
+ const inner = createElement(SeamDataProvider, { value: data }, createElement(component));
28
+ if (i18nValue && _I18nProvider) {
29
+ return renderToString(createElement(_I18nProvider, { value: i18nValue }, inner));
30
+ }
31
+ return renderToString(inner);
32
+ }
33
+
34
+ function installRenderTraps(violations, teardowns) {
35
+ function trapCall(obj, prop, label) {
36
+ const orig = obj[prop];
37
+ obj[prop] = function () {
38
+ violations.push({ severity: "error", reason: `${label} called during skeleton render` });
39
+ throw new SeamBuildError(`${label} is not allowed in skeleton components`);
40
+ };
41
+ teardowns.push(() => {
42
+ obj[prop] = orig;
43
+ });
44
+ }
45
+
46
+ trapCall(globalThis, "fetch", "fetch()");
47
+ trapCall(Math, "random", "Math.random()");
48
+ trapCall(Date, "now", "Date.now()");
49
+ if (globalThis.crypto?.randomUUID) {
50
+ trapCall(globalThis.crypto, "randomUUID", "crypto.randomUUID()");
51
+ }
52
+
53
+ // Timer APIs — these don't affect renderToString output, but pending handles
54
+ // prevent the build process from exiting (Node keeps the event loop alive).
55
+ trapCall(globalThis, "setTimeout", "setTimeout()");
56
+ trapCall(globalThis, "setInterval", "setInterval()");
57
+ if (globalThis.setImmediate) {
58
+ trapCall(globalThis, "setImmediate", "setImmediate()");
59
+ }
60
+ trapCall(globalThis, "queueMicrotask", "queueMicrotask()");
61
+
62
+ // Trap browser globals (only if not already defined — these are undefined in Node;
63
+ // typeof checks bypass getters, so `typeof window !== 'undefined'` remains safe)
64
+ for (const name of ["window", "document", "localStorage"]) {
65
+ if (!(name in globalThis)) {
66
+ Object.defineProperty(globalThis, name, {
67
+ get() {
68
+ violations.push({ severity: "error", reason: `${name} accessed during skeleton render` });
69
+ throw new SeamBuildError(`${name} is not available in skeleton components`);
70
+ },
71
+ configurable: true,
72
+ });
73
+ teardowns.push(() => {
74
+ delete globalThis[name];
75
+ });
76
+ }
77
+ }
78
+ }
79
+
80
+ function validateOutput(html, violations) {
81
+ if (html.includes("<!--$!-->")) {
82
+ violations.push({
83
+ severity: "error",
84
+ reason:
85
+ "Suspense abort detected \u2014 a component used an unresolved async resource\n" +
86
+ " (e.g. use(promise)) inside a <Suspense> boundary, producing an incomplete\n" +
87
+ " template with fallback content baked in.\n" +
88
+ " Fix: remove use() from skeleton components. Async data belongs in loaders.",
89
+ });
90
+ }
91
+
92
+ const hints = Array.from(html.matchAll(RESOURCE_HINT_RE));
93
+ if (hints.length > 0) {
94
+ violations.push({
95
+ severity: "warning",
96
+ reason:
97
+ `stripped ${hints.length} resource hint <link> tag(s) injected by React's preload()/preinit().\n` +
98
+ " These are not data-driven and would cause hydration mismatch.",
99
+ });
100
+ }
101
+ }
102
+
103
+ function stripResourceHints(html) {
104
+ return html.replace(RESOURCE_HINT_RE, "");
105
+ }
106
+
107
+ /**
108
+ * Render a component with full safety guards.
109
+ * @param {string} routePath - route or layout identifier for error messages
110
+ * @param {Function} component - React component to render
111
+ * @param {object} data - sentinel or tracked data
112
+ * @param {object|null} i18nValue - i18n context value
113
+ * @param {{ buildWarnings: string[], seenWarnings: Set<string> }} ctx - shared warning state
114
+ */
115
+ function guardedRender(routePath, component, data, i18nValue, ctx) {
116
+ const violations = [];
117
+ const teardowns = [];
118
+
119
+ installRenderTraps(violations, teardowns);
120
+
121
+ let html;
122
+ try {
123
+ html = renderWithData(component, data, i18nValue);
124
+ } catch (e) {
125
+ if (e instanceof SeamBuildError) {
126
+ throw new SeamBuildError(
127
+ `[seam] error: Skeleton rendering failed for route "${routePath}":\n` +
128
+ ` ${e.message}\n\n` +
129
+ " Move browser API calls into useEffect() or event handlers.",
130
+ );
131
+ }
132
+ throw e;
133
+ } finally {
134
+ for (const teardown of teardowns) teardown();
135
+ }
136
+
137
+ validateOutput(html, violations);
138
+
139
+ const fatal = violations.filter((v) => v.severity === "error");
140
+ if (fatal.length > 0) {
141
+ const msg = fatal.map((v) => `[seam] error: ${routePath}\n ${v.reason}`).join("\n\n");
142
+ throw new SeamBuildError(msg);
143
+ }
144
+
145
+ // After fatal check, only warnings remain — dedup per message
146
+ for (const v of violations) {
147
+ const msg = `[seam] warning: ${routePath}\n ${v.reason}`;
148
+ if (!ctx.seenWarnings.has(msg)) {
149
+ ctx.seenWarnings.add(msg);
150
+ ctx.buildWarnings.push(msg);
151
+ }
152
+ }
153
+ if (violations.length > 0) {
154
+ html = stripResourceHints(html);
155
+ }
156
+
157
+ return html;
158
+ }
159
+
160
+ export { SeamBuildError, guardedRender, stripResourceHints, RESOURCE_HINT_RE };