mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
Stage 3a of #780. - src/led-strip-gate.ts is the only initial-graph foothold (ТЗ §13.1): which markers a space shows as a strip (active and bound), the half-length anchor that replaces their icon position, and one page-wide load of the lazy led-strip-runtime chunk (fingerprint handshake, a hashed-URL retry on the next explicit entry, never in a loop). - The card: a represented marker takes no auto slot, draws no icon, casts no round pool and is placed at the anchor; two call sites render the stripe layer and the linear field from the chunk. The space model carries the stored strips untouched; scaling, validation and geometry are in the chunk. - src/led-strip-runtime.ts: the stripe is two strokes of one derived path (outline #383838 t, core t/2, round joins and caps, t = 0.08/0.12 D), white off, white core + field on with Glow, source-colour core without Glow, grey dashed unavailable without a field. The hit path takes the card's own device handlers (one action path) with radius max(22 px, t/2) and the nearest stripe as owner. The linear field is the exact distance field with the shared falloff: opaque grey bands of a luminance mask per piece, pieces joined by lighten (the maximum), each piece clipped to what its own emitters see, so a hidden part never lights through another part's visibility; buried emitters emit nothing; a failed clip is dark. A bounded per-space cache (50) counts geometry rebuilds. - The initial View graph had 501 B of headroom. The furniture library copy (furn.*, 104 keys × 4 languages, editor-only) moves into a new lazy `tools` namespace (#627 mechanism) that the editor runtime awaits; the initial View graph is 298 686 B gzip with the LED gate in it (−1 879 B vs the base). Tests: test/led-strip-runtime.test.mjs (6: representation, gate anchor = geometry anchor, falloff, states and radius, per-piece clipping and buried strips, bounded cache), bundle and i18n fixtures for the LED chunk and the fourth namespace. Issue: #780 User-Visible: no
111 lines
6.7 KiB
JavaScript
111 lines
6.7 KiB
JavaScript
// #627 AC3: словари пространств `settings`, `support`, `topology` — английский
|
||
// статически (синхронный слой отката), ru/de/fr — отдельные ленивые чанки, по
|
||
// одному на пару «пространство × язык», у каждого загрузчик с двумя попытками.
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { existsSync, readFileSync } from 'node:fs';
|
||
|
||
import { NAMESPACE_LOCALE_CHUNKS } from '../scripts/bundle-manifest.mjs';
|
||
|
||
const NAMESPACES = ['settings', 'support', 'topology', 'tools'];
|
||
const LAZY_LANGUAGES = ['ru', 'de', 'fr'];
|
||
const source = (path) => readFileSync(new URL(`../src/i18n/${path}`, import.meta.url), 'utf8');
|
||
const json = (path) => JSON.parse(source(path));
|
||
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
||
|
||
test('#627 AC3 ни один ru/de/fr словарь пространства не импортируется статически', () => {
|
||
for (const namespace of NAMESPACES) {
|
||
const text = source(`${namespace}.ts`);
|
||
const staticImports = [...text.matchAll(/^import\s[^;]*?from\s+['"]([^'"]+)['"]/gm)]
|
||
.map((match) => match[1]);
|
||
assert.ok(staticImports.includes(`./${namespace}/en.json`),
|
||
`${namespace}: английский слой отката обязан быть статическим`);
|
||
for (const language of LAZY_LANGUAGES) {
|
||
const lazy = new RegExp(`^\\./${namespace}/(?:${language}\\.json|${namespace}-${language})$`);
|
||
assert.deepEqual(staticImports.filter((path) => lazy.test(path)), [],
|
||
`${namespace}/${language}: статический импорт вернул бы словарь в граф редактора ×4`);
|
||
}
|
||
// Ни одного JSON, кроме английского, в модуле пространства вообще.
|
||
assert.deepEqual(staticImports.filter((path) => path.endsWith('.json') && path !== `./${namespace}/en.json`), []);
|
||
}
|
||
});
|
||
|
||
test('#627 AC3 у каждого из девяти чанков — загрузчик с двумя попытками и свой retry-токен', () => {
|
||
assert.equal(NAMESPACE_LOCALE_CHUNKS.length, NAMESPACES.length * LAZY_LANGUAGES.length);
|
||
for (const namespace of NAMESPACES) {
|
||
const text = source(`${namespace}.ts`);
|
||
for (const language of LAZY_LANGUAGES) {
|
||
const entry = NAMESPACE_LOCALE_CHUNKS.find((candidate) => candidate.namespace === namespace
|
||
&& candidate.language === language);
|
||
assert.ok(entry, `${namespace}-${language}: запись манифеста`);
|
||
assert.equal(entry.module, `/src/i18n/${namespace}/${namespace}-${language}.ts`);
|
||
assert.match(text, new RegExp(
|
||
`${language}: \\(attempt\\) => \\(attempt === 0 \\? import\\('${escape(`./${namespace}/${namespace}-${language}`)}'\\)\\n`
|
||
+ `\\s+: import\\(/\\* @vite-ignore \\*/ retryUrl\\('${entry.token}'\\)\\)\\)`,
|
||
), `${namespace}-${language}: первая попытка — ребро rollup, вторая — точный hashed-адрес`);
|
||
assert.equal(text.split(entry.token).length - 1, 1, `${entry.token}: ровно одно вхождение`);
|
||
// Чанк несёт свой язык и отпечаток сборки.
|
||
const loader = new URL(`../src/i18n/${namespace}/${namespace}-${language}.ts`, import.meta.url);
|
||
assert.ok(existsSync(loader), `${namespace}-${language}.ts существует`);
|
||
const loaderText = readFileSync(loader, 'utf8');
|
||
assert.match(loaderText, new RegExp(`^import dictionary from '\\./${language}\\.json' with \\{ type: 'json' \\};`, 'm'));
|
||
assert.match(loaderText, /export const fingerprint = '__HOUSEPLAN_SOURCE_FINGERPRINT__';/);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('#627 рантайм пространства: en готов сразу, ru/de/fr — после ensure и ровно своим словарём', async () => {
|
||
const modules = {
|
||
settings: await import('../test-build/i18n/settings.js'),
|
||
support: await import('../test-build/i18n/support.js'),
|
||
topology: await import('../test-build/i18n/topology.js'),
|
||
tools: await import('../test-build/i18n/tools.js'),
|
||
};
|
||
const runtimes = {
|
||
settings: modules.settings.SETTINGS_LANGUAGE_RUNTIME,
|
||
support: modules.support.SUPPORT_LANGUAGE_RUNTIME,
|
||
topology: modules.topology.TOPOLOGY_LANGUAGE_RUNTIME,
|
||
tools: modules.tools.TOOLS_LANGUAGE_RUNTIME,
|
||
};
|
||
const translate = {
|
||
settings: modules.settings.settingsT,
|
||
support: modules.support.supportT,
|
||
topology: modules.topology.topologyT,
|
||
tools: modules.tools.toolsT,
|
||
};
|
||
for (const namespace of NAMESPACES) {
|
||
const runtime = runtimes[namespace];
|
||
const english = json(`${namespace}/en.json`);
|
||
assert.equal(runtime.state('en'), 'ready', `${namespace}: английский статический`);
|
||
for (const language of LAZY_LANGUAGES) {
|
||
const expected = json(`${namespace}/${language}.json`);
|
||
const key = Object.keys(expected).find((candidate) => expected[candidate] !== english[candidate]
|
||
&& !/\{/.test(expected[candidate]));
|
||
assert.ok(key, `${namespace}/${language}: есть переведённый ключ без плейсхолдеров`);
|
||
assert.equal(runtime.state(language), 'pending', `${namespace}/${language}: не загружен до ensure`);
|
||
assert.equal(translate[namespace](language, key), english[key],
|
||
`${namespace}/${language}: до загрузки — синхронный английский откат, не сырой ключ`);
|
||
await runtime.ensure(language);
|
||
assert.equal(runtime.state(language), 'ready');
|
||
assert.deepEqual(runtime.dictionary(language), expected,
|
||
`${namespace}/${language}: загружен словарь своего языка`);
|
||
assert.equal(translate[namespace](language, key), expected[key]);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('#627 рантайм пространства — тот же проверенный класс и тот же слушатель отказа', async () => {
|
||
const { LanguageRuntime } = await import('../test-build/i18n/language-runtime.js');
|
||
const namespaceSource = source('namespace-language.ts');
|
||
assert.match(namespaceSource,
|
||
/\], BUILD_FINGERPRINT, console\.warn, notifyLanguageLoadFailures\);/,
|
||
'отказ словаря пространства идёт тем же тостом, что и основного каталога');
|
||
for (const [path, name] of [
|
||
['settings.js', 'SETTINGS_LANGUAGE_RUNTIME'], ['support.js', 'SUPPORT_LANGUAGE_RUNTIME'],
|
||
['topology.js', 'TOPOLOGY_LANGUAGE_RUNTIME'],
|
||
]) {
|
||
const module = await import(`../test-build/i18n/${path}`);
|
||
assert.ok(module[name] instanceof LanguageRuntime, `${name}: LanguageRuntime, а не двойник`);
|
||
}
|
||
});
|