oc 0.50.54 → 0.50.56

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.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +5 -5
  2. package/.turbo/turbo-test-silent.log +46 -43
  3. package/.turbo/turbo-test.log +6 -1614
  4. package/CHANGELOG.md +14 -0
  5. package/README.md +180 -0
  6. package/dist/cli/commands.d.ts +8 -0
  7. package/dist/cli/commands.js +8 -0
  8. package/dist/cli/facade/dev.d.ts +2 -2
  9. package/dist/cli/facade/registry-migrate-metadata.d.ts +20 -0
  10. package/dist/cli/facade/registry-migrate-metadata.js +105 -0
  11. package/dist/cli/index.js +2 -0
  12. package/dist/components/oc-client/_package/package.json +4 -4
  13. package/dist/components/oc-client/_package/server.js +1 -1
  14. package/dist/components/oc-client/package.json +1 -1
  15. package/dist/registry/domain/components-cache/index.d.ts +3 -1
  16. package/dist/registry/domain/components-cache/index.js +28 -5
  17. package/dist/registry/domain/components-details.d.ts +3 -1
  18. package/dist/registry/domain/components-details.js +26 -5
  19. package/dist/registry/domain/metadata-adapter-options.d.ts +6 -0
  20. package/dist/registry/domain/metadata-adapter-options.js +12 -0
  21. package/dist/registry/domain/metadata-index.d.ts +15 -0
  22. package/dist/registry/domain/metadata-index.js +132 -0
  23. package/dist/registry/domain/metadata-migration.d.ts +38 -0
  24. package/dist/registry/domain/metadata-migration.js +128 -0
  25. package/dist/registry/domain/repository.d.ts +1 -0
  26. package/dist/registry/domain/repository.js +106 -7
  27. package/dist/registry/domain/validators/registry-configuration.js +21 -0
  28. package/dist/registry/index.d.ts +1 -1
  29. package/dist/registry/index.js +6 -2
  30. package/dist/registry/views/partials/components-list.js +1 -1
  31. package/dist/registry/views/static/index.d.ts +1 -1
  32. package/dist/registry/views/static/index.js +134 -11
  33. package/dist/registry/views/static/style.d.ts +1 -1
  34. package/dist/registry/views/static/style.js +53 -0
  35. package/dist/resources/index.d.ts +3 -0
  36. package/dist/resources/index.js +3 -0
  37. package/dist/types.d.ts +41 -1
  38. package/package.json +3 -2
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createMetadataIndex = exports.getComponentRow = exports.getComponentsDetailsFromRows = exports.getComponentsListFromRows = void 0;
7
+ const semver_1 = __importDefault(require("semver"));
8
+ // Derive `lastEdit` from the data itself (the most recent publish) rather than
9
+ // the wall-clock time the snapshot happened to be built. This keeps the value
10
+ // semantically meaningful (it only advances when something is actually
11
+ // published) so the poll's `data.lastEdit > cached.lastEdit` guard works and
12
+ // the exported legacy `components.json` reflects the real last edit.
13
+ const getLastEditFromRows = (rows) => rows.reduce((max, row) => (row.publishDate > max ? row.publishDate : max), 0);
14
+ const getComponentsListFromRows = (rows) => {
15
+ const components = {};
16
+ for (const row of rows) {
17
+ const versions = components[row.name] || [];
18
+ versions.push(row.version);
19
+ components[row.name] = versions;
20
+ }
21
+ for (const versions of Object.values(components)) {
22
+ versions.sort(semver_1.default.compare);
23
+ }
24
+ return {
25
+ lastEdit: getLastEditFromRows(rows),
26
+ components
27
+ };
28
+ };
29
+ exports.getComponentsListFromRows = getComponentsListFromRows;
30
+ const getComponentsDetailsFromRows = (rows) => {
31
+ const components = {};
32
+ for (const row of rows) {
33
+ const componentDetails = components[row.name] || {};
34
+ componentDetails[row.version] = { publishDate: row.publishDate };
35
+ if (row.templateSize !== undefined) {
36
+ componentDetails[row.version].templateSize = row.templateSize;
37
+ }
38
+ components[row.name] = componentDetails;
39
+ }
40
+ return {
41
+ lastEdit: getLastEditFromRows(rows),
42
+ components
43
+ };
44
+ };
45
+ exports.getComponentsDetailsFromRows = getComponentsDetailsFromRows;
46
+ const getComponentRow = (name, version, component) => ({
47
+ name,
48
+ version,
49
+ publishDate: component.oc.date || 0,
50
+ templateSize: component.oc.files.template.size
51
+ });
52
+ exports.getComponentRow = getComponentRow;
53
+ const FORCED_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
54
+ const buildSnapshot = (rows) => ({
55
+ componentsList: (0, exports.getComponentsListFromRows)(rows),
56
+ componentsDetails: (0, exports.getComponentsDetailsFromRows)(rows)
57
+ });
58
+ const createMetadataIndex = (metadataStore) => {
59
+ let snapshot;
60
+ let lastChangeToken;
61
+ let lastForcedRefreshAt = 0;
62
+ let localAddGeneration = 0;
63
+ const refresh = async () => {
64
+ const refreshGeneration = localAddGeneration;
65
+ const changeToken = metadataStore.getChangeToken
66
+ ? await metadataStore.getChangeToken()
67
+ : undefined;
68
+ const now = Date.now();
69
+ if (snapshot &&
70
+ changeToken !== undefined &&
71
+ changeToken === lastChangeToken &&
72
+ now - lastForcedRefreshAt < FORCED_REFRESH_INTERVAL_MS) {
73
+ return snapshot;
74
+ }
75
+ const rows = await metadataStore.getAllComponents();
76
+ if (snapshot && refreshGeneration !== localAddGeneration) {
77
+ return snapshot;
78
+ }
79
+ snapshot = buildSnapshot(rows);
80
+ lastChangeToken = changeToken;
81
+ lastForcedRefreshAt = now;
82
+ return snapshot;
83
+ };
84
+ const add = (row) => {
85
+ // No snapshot yet: build one from the single row.
86
+ if (!snapshot) {
87
+ snapshot = buildSnapshot([row]);
88
+ localAddGeneration += 1;
89
+ return snapshot;
90
+ }
91
+ const { name, version } = row;
92
+ const prevList = snapshot.componentsList.components;
93
+ const prevDetails = snapshot.componentsDetails.components;
94
+ // Already present: keep the snapshot unchanged (mirrors the unique-constraint
95
+ // guarantee, so a duplicate publish doesn't disturb the cache).
96
+ if (prevDetails[name]?.[version]) {
97
+ return snapshot;
98
+ }
99
+ // Only the published component is rebuilt; every other component entry is
100
+ // shared by reference, so the cost is O(versions-of-this-component) rather
101
+ // than O(registry). New container objects are produced (rather than mutating
102
+ // in place) so in-flight readers keep a consistent view of the old snapshot.
103
+ const versions = [...(prevList[name] ?? []), version].sort(semver_1.default.compare);
104
+ const detail = {
105
+ ...(prevDetails[name] ?? {}),
106
+ [version]: { publishDate: row.publishDate }
107
+ };
108
+ if (row.templateSize !== undefined) {
109
+ detail[version].templateSize = row.templateSize;
110
+ }
111
+ const lastEdit = Math.max(snapshot.componentsList.lastEdit, row.publishDate);
112
+ snapshot = {
113
+ componentsList: {
114
+ lastEdit,
115
+ components: { ...prevList, [name]: versions }
116
+ },
117
+ componentsDetails: {
118
+ lastEdit,
119
+ components: { ...prevDetails, [name]: detail }
120
+ }
121
+ };
122
+ localAddGeneration += 1;
123
+ return snapshot;
124
+ };
125
+ return {
126
+ get: () => snapshot,
127
+ add,
128
+ refresh,
129
+ getOrRefresh: () => (snapshot ? Promise.resolve(snapshot) : refresh())
130
+ };
131
+ };
132
+ exports.createMetadataIndex = createMetadataIndex;
@@ -0,0 +1,38 @@
1
+ import type { StorageAdapter } from 'oc-storage-adapters-utils';
2
+ import type { ComponentRow, ComponentsDetails, MetadataStore } from '../../types';
3
+ export interface MetadataMigrationResult {
4
+ scanned: number;
5
+ inserted: number;
6
+ skipped: number;
7
+ }
8
+ export interface MetadataExportResult {
9
+ exported: number;
10
+ files: string[];
11
+ }
12
+ export declare const getComponentRowsFromComponentsDetails: (componentsDetails: ComponentsDetails) => ComponentRow[];
13
+ export declare const backfillMetadataRows: (metadataStore: MetadataStore, rows: ComponentRow[]) => Promise<MetadataMigrationResult>;
14
+ export declare const backfillMetadataFromComponentsDetails: (metadataStore: MetadataStore, componentsDetails: ComponentsDetails) => Promise<MetadataMigrationResult>;
15
+ export declare const getComponentRowsFromStorageDirectories: (options: {
16
+ cdn: StorageAdapter;
17
+ componentsDir: string;
18
+ }) => Promise<ComponentRow[]>;
19
+ export declare const backfillMetadataFromStorageDirectories: (options: {
20
+ metadataStore: MetadataStore;
21
+ cdn: StorageAdapter;
22
+ componentsDir: string;
23
+ }) => Promise<MetadataMigrationResult>;
24
+ export declare const reconcileMetadataFromStorage: (options: {
25
+ metadataStore: MetadataStore;
26
+ cdn: StorageAdapter;
27
+ componentsDir: string;
28
+ }) => Promise<MetadataMigrationResult>;
29
+ export declare const exportLegacyMetadataFiles: (options: {
30
+ metadataStore: MetadataStore;
31
+ cdn: StorageAdapter;
32
+ componentsDir: string;
33
+ }) => Promise<MetadataExportResult>;
34
+ export declare const backfillMetadataFromStorageDetails: (options: {
35
+ metadataStore: MetadataStore;
36
+ cdn: StorageAdapter;
37
+ componentsDir: string;
38
+ }) => Promise<MetadataMigrationResult>;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.backfillMetadataFromStorageDetails = exports.exportLegacyMetadataFiles = exports.reconcileMetadataFromStorage = exports.backfillMetadataFromStorageDirectories = exports.getComponentRowsFromStorageDirectories = exports.backfillMetadataFromComponentsDetails = exports.backfillMetadataRows = exports.getComponentRowsFromComponentsDetails = void 0;
7
+ const oc_metadata_adapters_utils_1 = require("oc-metadata-adapters-utils");
8
+ const pLimit_1 = __importDefault(require("../../utils/pLimit"));
9
+ const metadata_index_1 = require("./metadata-index");
10
+ const isNotFoundError = (err, code) => err === code || err?.code === code;
11
+ const getTemplateSize = (component) => component.oc.files.template.size;
12
+ const getComponentRowsFromComponentsDetails = (componentsDetails) => {
13
+ const rows = [];
14
+ for (const [name, versions] of Object.entries(componentsDetails.components || {})) {
15
+ for (const [version, details] of Object.entries(versions)) {
16
+ const row = {
17
+ name,
18
+ version,
19
+ publishDate: details.publishDate
20
+ };
21
+ if (details.templateSize !== undefined) {
22
+ row.templateSize = details.templateSize;
23
+ }
24
+ rows.push(row);
25
+ }
26
+ }
27
+ return rows;
28
+ };
29
+ exports.getComponentRowsFromComponentsDetails = getComponentRowsFromComponentsDetails;
30
+ const backfillMetadataRows = async (metadataStore, rows) => {
31
+ const result = {
32
+ scanned: rows.length,
33
+ inserted: 0,
34
+ skipped: 0
35
+ };
36
+ const limit = (0, pLimit_1.default)(10);
37
+ await Promise.all(rows.map((row) => limit(async () => {
38
+ try {
39
+ await metadataStore.addVersion(row);
40
+ result.inserted += 1;
41
+ }
42
+ catch (err) {
43
+ if (err?.code === oc_metadata_adapters_utils_1.VERSION_ALREADY_EXISTS ||
44
+ err?.code === oc_metadata_adapters_utils_1.VERSION_PUBLISH_IN_PROGRESS) {
45
+ result.skipped += 1;
46
+ }
47
+ else {
48
+ throw err;
49
+ }
50
+ }
51
+ })));
52
+ return result;
53
+ };
54
+ exports.backfillMetadataRows = backfillMetadataRows;
55
+ const backfillMetadataFromComponentsDetails = async (metadataStore, componentsDetails) => (0, exports.backfillMetadataRows)(metadataStore, (0, exports.getComponentRowsFromComponentsDetails)(componentsDetails));
56
+ exports.backfillMetadataFromComponentsDetails = backfillMetadataFromComponentsDetails;
57
+ const getComponentRowsFromStorageDirectories = async (options) => {
58
+ let components;
59
+ try {
60
+ components = await options.cdn.listSubDirectories(options.componentsDir);
61
+ }
62
+ catch (err) {
63
+ if (isNotFoundError(err, 'dir_not_found')) {
64
+ return [];
65
+ }
66
+ throw err;
67
+ }
68
+ const maxConcurrentRequests = options.cdn.maxConcurrentRequests || 10;
69
+ const componentLimit = (0, pLimit_1.default)(maxConcurrentRequests);
70
+ const packageLimit = (0, pLimit_1.default)(maxConcurrentRequests);
71
+ const rowsByComponent = await Promise.all(components.map((name) => componentLimit(async () => {
72
+ const versions = await options.cdn.listSubDirectories(`${options.componentsDir}/${name}`);
73
+ const rows = await Promise.all(versions.map((version) => packageLimit(async () => {
74
+ try {
75
+ const component = await options.cdn.getJson(`${options.componentsDir}/${name}/${version}/package.json`, true);
76
+ const row = {
77
+ name,
78
+ version,
79
+ publishDate: component.oc.date || 0
80
+ };
81
+ const templateSize = getTemplateSize(component);
82
+ if (templateSize !== undefined) {
83
+ row.templateSize = templateSize;
84
+ }
85
+ return row;
86
+ }
87
+ catch (err) {
88
+ if (isNotFoundError(err, 'file_not_found')) {
89
+ return null;
90
+ }
91
+ throw err;
92
+ }
93
+ })));
94
+ return rows.filter((row) => row !== null);
95
+ })));
96
+ return rowsByComponent.flat();
97
+ };
98
+ exports.getComponentRowsFromStorageDirectories = getComponentRowsFromStorageDirectories;
99
+ const backfillMetadataFromStorageDirectories = async (options) => (0, exports.backfillMetadataRows)(options.metadataStore, await (0, exports.getComponentRowsFromStorageDirectories)(options));
100
+ exports.backfillMetadataFromStorageDirectories = backfillMetadataFromStorageDirectories;
101
+ exports.reconcileMetadataFromStorage = exports.backfillMetadataFromStorageDirectories;
102
+ const exportLegacyMetadataFiles = async (options) => {
103
+ const rows = await options.metadataStore.getAllComponents();
104
+ const componentsListPath = `${options.componentsDir}/components.json`;
105
+ const componentsDetailsPath = `${options.componentsDir}/components-details.json`;
106
+ await Promise.all([
107
+ options.cdn.putFileContent(JSON.stringify((0, metadata_index_1.getComponentsListFromRows)(rows)), componentsListPath, true),
108
+ options.cdn.putFileContent(JSON.stringify((0, metadata_index_1.getComponentsDetailsFromRows)(rows)), componentsDetailsPath, true)
109
+ ]);
110
+ return {
111
+ exported: rows.length,
112
+ files: [componentsListPath, componentsDetailsPath]
113
+ };
114
+ };
115
+ exports.exportLegacyMetadataFiles = exportLegacyMetadataFiles;
116
+ const backfillMetadataFromStorageDetails = async (options) => {
117
+ try {
118
+ const componentsDetails = await options.cdn.getJson(`${options.componentsDir}/components-details.json`, true);
119
+ return (0, exports.backfillMetadataFromComponentsDetails)(options.metadataStore, componentsDetails);
120
+ }
121
+ catch (err) {
122
+ if (isNotFoundError(err, 'file_not_found')) {
123
+ return (0, exports.backfillMetadataFromStorageDirectories)(options);
124
+ }
125
+ throw err;
126
+ }
127
+ };
128
+ exports.backfillMetadataFromStorageDetails = backfillMetadataFromStorageDetails;
@@ -28,5 +28,6 @@ export default function repository(conf: Config): {
28
28
  dryRun?: boolean;
29
29
  user?: string;
30
30
  }): Promise<void>;
31
+ close(): Promise<void>;
31
32
  };
32
33
  export type Repository = ReturnType<typeof repository>;
@@ -41,11 +41,16 @@ const node_path_1 = __importDefault(require("node:path"));
41
41
  const dotenv_1 = __importDefault(require("dotenv"));
42
42
  const fs_extra_1 = __importDefault(require("fs-extra"));
43
43
  const oc_get_unix_utc_timestamp_1 = __importDefault(require("oc-get-unix-utc-timestamp"));
44
+ const oc_metadata_adapters_utils_1 = require("oc-metadata-adapters-utils");
44
45
  const resources_1 = __importDefault(require("../../resources"));
45
46
  const settings_1 = __importDefault(require("../../resources/settings"));
46
47
  const error_to_string_1 = __importDefault(require("../../utils/error-to-string"));
47
48
  const components_cache_1 = __importDefault(require("./components-cache"));
48
49
  const components_details_1 = __importDefault(require("./components-details"));
50
+ const events_handler_1 = __importDefault(require("./events-handler"));
51
+ const metadata_adapter_options_1 = __importDefault(require("./metadata-adapter-options"));
52
+ const metadata_index_1 = require("./metadata-index");
53
+ const metadata_migration_1 = require("./metadata-migration");
49
54
  const register_templates_1 = __importDefault(require("./register-templates"));
50
55
  const storage_adapter_1 = __importDefault(require("./storage-adapter"));
51
56
  const validator = __importStar(require("./validators"));
@@ -58,10 +63,56 @@ function repository(conf) {
58
63
  const repositorySource = conf.local
59
64
  ? 'local repository'
60
65
  : cdn.adapterType + ' cdn';
61
- const componentsCache = (0, components_cache_1.default)(conf, cdn);
62
- const componentsDetails = (0, components_details_1.default)(conf, cdn);
66
+ const metadataStore = !conf.local && conf.metadata
67
+ ? conf.metadata.adapter((0, metadata_adapter_options_1.default)(conf))
68
+ : undefined;
69
+ const metadataIndex = metadataStore
70
+ ? (0, metadata_index_1.createMetadataIndex)(metadataStore)
71
+ : undefined;
72
+ const componentsCache = (0, components_cache_1.default)(conf, cdn, metadataIndex);
73
+ const componentsDetails = (0, components_details_1.default)(conf, cdn, metadataIndex);
74
+ let exportLegacyFilesLoop;
75
+ let closed = false;
63
76
  const getFilePath = (component, version, filePath) => `${options.componentsDir}/${component}/${version}/${filePath}`;
77
+ const exportLegacyFiles = () => {
78
+ if (!metadataStore || !conf.metadata?.exportLegacyFiles) {
79
+ return;
80
+ }
81
+ return (0, metadata_migration_1.exportLegacyMetadataFiles)({
82
+ metadataStore,
83
+ cdn,
84
+ componentsDir: options.componentsDir
85
+ }).catch((err) => events_handler_1.default.fire('error', {
86
+ code: 'metadata_legacy_files_export',
87
+ message: err?.message || String(err)
88
+ }));
89
+ };
90
+ // Run the DB→components.json export on a non-overlapping background timer
91
+ // instead of on the publish path, so a publish stays an O(1) append rather
92
+ // than triggering a full-registry scan + blob rewrite. The timer only runs
93
+ // when an interval is explicitly configured.
94
+ const scheduleLegacyFilesExport = () => {
95
+ const intervalSeconds = conf.metadata?.exportLegacyFilesInterval;
96
+ if (closed ||
97
+ !metadataStore ||
98
+ !conf.metadata?.exportLegacyFiles ||
99
+ !intervalSeconds) {
100
+ return;
101
+ }
102
+ exportLegacyFilesLoop = setTimeout(async () => {
103
+ await exportLegacyFiles();
104
+ if (!closed) {
105
+ scheduleLegacyFilesExport();
106
+ }
107
+ }, intervalSeconds * 1000);
108
+ };
64
109
  const { templatesHash, templatesInfo } = (0, register_templates_1.default)(conf.templates, conf.local);
110
+ const throwVersionAlreadyFound = (componentName, componentVersion) => {
111
+ throw {
112
+ code: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE,
113
+ msg: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND(componentName, componentVersion, repositorySource)
114
+ };
115
+ };
65
116
  const local = {
66
117
  components: undefined,
67
118
  getCompiledView(componentName) {
@@ -222,8 +273,21 @@ function repository(conf) {
222
273
  // when in local this won't get called
223
274
  return;
224
275
  }
276
+ if (metadataStore) {
277
+ await metadataStore.initialise();
278
+ if (conf.metadata?.reconcileFromStorage) {
279
+ await (0, metadata_migration_1.reconcileMetadataFromStorage)({
280
+ metadataStore,
281
+ cdn,
282
+ componentsDir: options.componentsDir
283
+ });
284
+ }
285
+ }
225
286
  const componentsList = await componentsCache.load();
226
- return componentsDetails.refresh(componentsList);
287
+ const details = await componentsDetails.refresh(componentsList);
288
+ void exportLegacyFiles();
289
+ scheduleLegacyFilesExport();
290
+ return details;
227
291
  },
228
292
  async publishComponent({ componentName, componentVersion, pkgDetails, user, dryRun = false }) {
229
293
  if (conf.local) {
@@ -257,21 +321,56 @@ function repository(conf) {
257
321
  }
258
322
  const componentVersions = await repository.getComponentVersions(componentName);
259
323
  if (!versionHandler.validateNewVersion(componentVersion, componentVersions)) {
260
- throw {
261
- code: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE,
262
- msg: resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND(componentName, componentVersion, repositorySource)
263
- };
324
+ throwVersionAlreadyFound(componentName, componentVersion);
264
325
  }
265
326
  pkgDetails.packageJson.oc.date = (0, oc_get_unix_utc_timestamp_1.default)();
266
327
  pkgDetails.packageJson.oc.publisher = user;
267
328
  if (dryRun)
268
329
  return;
269
330
  await fs_extra_1.default.writeJson(node_path_1.default.join(pkgDetails.outputFolder, 'package.json'), pkgDetails.packageJson);
331
+ if (metadataStore) {
332
+ const componentRow = (0, metadata_index_1.getComponentRow)(componentName, componentVersion, pkgDetails.packageJson);
333
+ const { token } = await metadataStore
334
+ .reserveVersion(componentRow)
335
+ .catch((err) => {
336
+ if (err?.code === oc_metadata_adapters_utils_1.VERSION_ALREADY_EXISTS ||
337
+ err?.code === oc_metadata_adapters_utils_1.VERSION_PUBLISH_IN_PROGRESS ||
338
+ err?.code ===
339
+ resources_1.default.errors.registry.COMPONENT_VERSION_ALREADY_FOUND_CODE) {
340
+ throwVersionAlreadyFound(componentName, componentVersion);
341
+ }
342
+ throw err;
343
+ });
344
+ try {
345
+ await cdn.putDir(pkgDetails.outputFolder, `${options.componentsDir}/${componentName}/${componentVersion}`);
346
+ await metadataStore.commitVersion(componentName, componentVersion, token);
347
+ metadataIndex.add(componentRow);
348
+ }
349
+ catch (err) {
350
+ await metadataStore
351
+ .abortVersion(componentName, componentVersion, token)
352
+ .catch(() => undefined);
353
+ throw err;
354
+ }
355
+ return;
356
+ }
270
357
  await cdn.putDir(pkgDetails.outputFolder, `${options.componentsDir}/${componentName}/${componentVersion}`);
271
358
  void componentsCache
272
359
  .refresh()
273
360
  .then((componentsList) => componentsDetails.refresh(componentsList))
274
361
  .catch(() => undefined);
362
+ },
363
+ async close() {
364
+ closed = true;
365
+ componentsCache.close?.();
366
+ componentsDetails.close?.();
367
+ if (exportLegacyFilesLoop) {
368
+ clearTimeout(exportLegacyFilesLoop);
369
+ exportLegacyFilesLoop = undefined;
370
+ }
371
+ if (metadataStore?.close) {
372
+ await metadataStore.close();
373
+ }
275
374
  }
276
375
  };
277
376
  return repository;
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.default = registryConfiguration;
40
40
  const resources_1 = __importDefault(require("../../../resources"));
41
41
  const auth = __importStar(require("../authentication"));
42
+ const metadata_adapter_options_1 = __importDefault(require("../metadata-adapter-options"));
42
43
  function registryConfiguration(conf) {
43
44
  const returnError = (message) => {
44
45
  return {
@@ -111,6 +112,26 @@ function registryConfiguration(conf) {
111
112
  }
112
113
  }
113
114
  }
115
+ if (!conf.local && conf.metadata) {
116
+ if (!conf.metadata.adapter) {
117
+ return returnError(resources_1.default.errors.registry.CONFIGURATION_METADATA_NOT_VALID('metadata'));
118
+ }
119
+ if (typeof conf.metadata.exportLegacyFilesInterval !== 'undefined') {
120
+ if (!Number.isFinite(conf.metadata.exportLegacyFilesInterval) ||
121
+ conf.metadata.exportLegacyFilesInterval <= 0) {
122
+ return returnError(resources_1.default.errors.registry
123
+ .CONFIGURATION_METADATA_EXPORT_INTERVAL_NOT_VALID);
124
+ }
125
+ if (conf.metadata.exportLegacyFiles !== true) {
126
+ return returnError(resources_1.default.errors.registry
127
+ .CONFIGURATION_METADATA_EXPORT_INTERVAL_WITHOUT_EXPORT_NOT_VALID);
128
+ }
129
+ }
130
+ const metadataStore = conf.metadata.adapter((0, metadata_adapter_options_1.default)(conf));
131
+ if (!metadataStore.isValid()) {
132
+ return returnError(resources_1.default.errors.registry.CONFIGURATION_METADATA_NOT_VALID(metadataStore.adapterType));
133
+ }
134
+ }
114
135
  if (conf.customHeadersToSkipOnWeakVersion) {
115
136
  if (!Array.isArray(conf.customHeadersToSkipOnWeakVersion)) {
116
137
  return returnError(resources_1.default.errors.registry
@@ -4,7 +4,7 @@ import type { Plugin } from '../types';
4
4
  import { RegistryOptions } from './domain/options-sanitiser';
5
5
  export { RegistryOptions };
6
6
  export default function registry<T = any>(inputOptions: RegistryOptions<T>): {
7
- close: (callback: (err?: Error | undefined | string) => void) => void | http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
7
+ close: (callback: (err?: Error | undefined | string) => void) => void;
8
8
  on: <T_1 extends keyof {
9
9
  error: {
10
10
  code: string;
@@ -59,10 +59,14 @@ function registry(inputOptions) {
59
59
  let server;
60
60
  const repository = (0, repository_1.default)(options);
61
61
  const close = (callback) => {
62
+ const closeMetadataStore = () => Promise.resolve(repository.close?.()).catch(() => undefined);
62
63
  if (server?.listening) {
63
- return server.close(callback);
64
+ server.close((err) => {
65
+ void closeMetadataStore().finally(() => callback(err));
66
+ });
67
+ return;
64
68
  }
65
- return callback('not opened');
69
+ void closeMetadataStore().finally(() => callback('not opened'));
66
70
  };
67
71
  const register = (plugin, callback) => {
68
72
  plugins.push(Object.assign(plugin, { callback }));
@@ -17,6 +17,6 @@ const ComponentsList = (props) => {
17
17
  ? `${Math.round(component.oc.files.template.size / 1024)} kb`
18
18
  : '? Kb' }))] }) })] }) }));
19
19
  };
20
- return ((0, jsx_runtime_1.jsxs)("div", { id: "components-list", class: "box", children: [(0, jsx_runtime_1.jsxs)("form", { id: "filter-components", children: [(0, jsx_runtime_1.jsxs)("div", { class: "filters", children: [(0, jsx_runtime_1.jsx)("input", { id: "search-filter", type: "text", "aria-label": "Filter by component name", placeholder: "Filter by name\u2026 ( press / )" }), (0, jsx_runtime_1.jsx)("input", { id: "author-filter", type: "text", "aria-label": "Filter by component author", placeholder: "Filter by component author" })] }), (0, jsx_runtime_1.jsxs)("div", { class: "states", children: [(0, jsx_runtime_1.jsx)("span", { children: "Hide:" }), (0, jsx_runtime_1.jsx)(selected_checkbox_1.default, { name: "deprecated", stateCounts: props.stateCounts }), (0, jsx_runtime_1.jsx)(selected_checkbox_1.default, { name: "experimental", stateCounts: props.stateCounts })] })] }), (0, jsx_runtime_1.jsx)("div", { class: "list-meta", children: (0, jsx_runtime_1.jsxs)("span", { id: "results-count", children: [props.components.length, " components"] }) }), (0, jsx_runtime_1.jsxs)("div", { class: "row header componentRow table", children: [(0, jsx_runtime_1.jsx)("div", { class: "title", children: "Component" }), (0, jsx_runtime_1.jsx)("div", { class: "author", children: "Author" }), (0, jsx_runtime_1.jsx)("div", { children: "Latest version" }), isRemote && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { class: "date", children: "Updated" }), (0, jsx_runtime_1.jsx)("div", { class: "activity", children: "Activity" })] })), sizeAvailable && (0, jsx_runtime_1.jsx)("div", { children: "Size" })] }), props.components.map(componentRow), (0, jsx_runtime_1.jsx)("div", { id: "components-empty", class: "empty-state hide", children: "No components match your filters." })] }));
20
+ return ((0, jsx_runtime_1.jsxs)("div", { id: "components-list", class: "box", children: [(0, jsx_runtime_1.jsxs)("form", { id: "filter-components", children: [(0, jsx_runtime_1.jsxs)("div", { class: "filters", children: [(0, jsx_runtime_1.jsx)("input", { id: "search-filter", type: "text", "aria-label": "Filter by component name", placeholder: "Filter by name\u2026 ( press / )" }), (0, jsx_runtime_1.jsx)("input", { id: "author-filter", type: "text", "aria-label": "Filter by component author", placeholder: "Filter by component author" })] }), (0, jsx_runtime_1.jsxs)("div", { class: "states", children: [(0, jsx_runtime_1.jsx)("span", { children: "Hide:" }), (0, jsx_runtime_1.jsx)(selected_checkbox_1.default, { name: "deprecated", stateCounts: props.stateCounts }), (0, jsx_runtime_1.jsx)(selected_checkbox_1.default, { name: "experimental", stateCounts: props.stateCounts })] })] }), (0, jsx_runtime_1.jsx)("div", { class: "list-meta", children: (0, jsx_runtime_1.jsxs)("span", { id: "results-count", children: [props.components.length, " components"] }) }), (0, jsx_runtime_1.jsx)("nav", { id: "components-pagination-top", class: "pagination components-pagination pagination-top hide", "aria-label": "Components pagination" }), (0, jsx_runtime_1.jsxs)("div", { class: "row header componentRow table", children: [(0, jsx_runtime_1.jsx)("div", { class: "title", children: "Component" }), (0, jsx_runtime_1.jsx)("div", { class: "author", children: "Author" }), (0, jsx_runtime_1.jsx)("div", { children: "Latest version" }), isRemote && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { class: "date", children: "Updated" }), (0, jsx_runtime_1.jsx)("div", { class: "activity", children: "Activity" })] })), sizeAvailable && (0, jsx_runtime_1.jsx)("div", { children: "Size" })] }), props.components.map(componentRow), (0, jsx_runtime_1.jsx)("div", { id: "components-empty", class: "empty-state hide", children: "No components match your filters." }), (0, jsx_runtime_1.jsx)("nav", { id: "components-pagination-bottom", class: "pagination components-pagination hide", "aria-label": "Components pagination" })] }));
21
21
  };
22
22
  exports.default = ComponentsList;
@@ -1,2 +1,2 @@
1
- declare const _default: "'use strict';\n\nvar oc = oc || {};\noc.cmd = oc.cmd || [];\n\noc.cmd.push(function() {\n var escapeRegExp = function(s) {\n return String(s).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n };\n\n var safeRegExp = function(pattern, flags) {\n try {\n return new RegExp(pattern, flags);\n } catch (e) {\n return new RegExp(escapeRegExp(pattern), flags);\n }\n };\n\n var componentsListChanged = function() {\n $('.componentRow').removeClass('hide');\n var s = $('#search-filter').val(),\n a = $('#author-filter').val(),\n r = safeRegExp(s, ''),\n ar = safeRegExp(a, 'i'),\n selectedCheckboxes = $('input[type=checkbox]:checked'),\n hiddenStates = [],\n hidden = 0,\n i;\n\n for (i = 0; i < selectedCheckboxes.length; i++) {\n hiddenStates.push($(selectedCheckboxes[i]).attr('name'));\n }\n\n for (i = 0; i < componentsList.length; i++) {\n var matches = !s || !!componentsList[i].name.match(r),\n matchesAuthor =\n !a ||\n (componentsList[i].author.name &&\n !!componentsList[i].author.name.match(ar)),\n selector = $('#component-' + componentsList[i].name),\n isHidden = false;\n\n for (var j = 0; j < hiddenStates.length; j++) {\n if (componentsList[i].state.toLowerCase() === hiddenStates[j]) {\n isHidden = true;\n }\n }\n\n var show = matches && matchesAuthor && !isHidden;\n selector[show ? 'removeClass' : 'addClass']('hide');\n if (!show) {\n hidden += 1;\n }\n }\n\n var totalShowing = componentsList.length - hidden,\n result = totalShowing + (totalShowing === 1 ? ' component' : ' components');\n\n if (s) {\n result += ' matching \"' + s + '\"';\n if (a) {\n result += ' and';\n }\n }\n if (a) {\n result += ' by author \"' + a + '\"';\n }\n\n $('#results-count').text(result);\n $('#components-empty')[totalShowing === 0 ? 'removeClass' : 'addClass']('hide');\n\n return false;\n };\n\n var historyData = [];\n var historyRenderedCount = 0;\n var historyBatchSize = 50; // Number of items to render per batch\n var isLoadingMore = false;\n\n var renderHistoryBatch = function() {\n if (historyRenderedCount >= historyData.length || isLoadingMore) {\n return;\n }\n\n isLoadingMore = true;\n var historyContent = $('#history-content');\n var batchEnd = Math.min(historyRenderedCount + historyBatchSize, historyData.length);\n var batchHtml = '';\n\n for (var i = historyRenderedCount; i < batchEnd; i++) {\n var item = historyData[i];\n var templateSizeText = item.templateSize ? \n ' [' + Math.round(item.templateSize / 1024) + ' kb]' : '';\n \n batchHtml += '<a href=\"' + item.name + '/' + item.version + '/~info\">' +\n '<div class=\"componentRow row table\">' +\n '<p class=\"release\">' +\n item.publishDate + ' - Published ' + item.name + '@' + item.version +\n templateSizeText +\n '</p>' +\n '</div>' +\n '</a>';\n }\n\n // Append new batch to existing content\n historyContent.append(batchHtml);\n historyRenderedCount = batchEnd;\n \n // Add loading indicator if there are more items\n if (historyRenderedCount < historyData.length) {\n historyContent.append('<div id=\"history-loading-more\" class=\"loading-more\">Loading more...</div>');\n }\n \n isLoadingMore = false;\n };\n\n var setupHistoryScrollListener = function() {\n var historyContainer = $('#components-history');\n var checkScroll = function() {\n if (historyRenderedCount >= historyData.length) {\n return;\n }\n\n var containerTop = historyContainer.offset().top;\n var containerHeight = historyContainer.outerHeight();\n var scrollTop = $(window).scrollTop();\n var windowHeight = $(window).height();\n \n // Check if user scrolled close to the bottom of the history container\n var distanceFromBottom = (containerTop + containerHeight) - (scrollTop + windowHeight);\n \n if (distanceFromBottom < 200) { // Load more when 200px from bottom\n $('#history-loading-more').remove();\n renderHistoryBatch();\n }\n };\n\n $(window).on('scroll.history', checkScroll);\n \n // Also check on resize\n $(window).on('resize.history', checkScroll);\n };\n\n var loadComponentsHistory = function() {\n var historyLoader = $('#history-loader');\n var historyContent = $('#history-content');\n var historyError = $('#history-error');\n\n // Show loader\n historyLoader.show();\n historyContent.hide();\n historyError.hide();\n\n // Reset state\n historyData = [];\n historyRenderedCount = 0;\n isLoadingMore = false;\n\n // Remove any existing scroll listeners\n $(window).off('scroll.history resize.history');\n\n // Fetch history data\n fetch('~registry/history')\n .then(function(response) {\n if (!response.ok) {\n throw new Error('Failed to fetch history');\n }\n return response.json();\n })\n .then(function(data) {\n historyData = data.componentsHistory || [];\n \n // Clear content and show container\n historyContent.empty();\n historyLoader.hide();\n historyContent.show();\n \n if (historyData.length === 0) {\n historyContent.html('<p class=\"empty-state\">No components history available.</p>');\n return;\n }\n\n // Render first batch\n renderHistoryBatch();\n \n // Setup scroll listener for infinite loading\n setupHistoryScrollListener();\n })\n .catch(function(error) {\n console.error('Error loading components history:', error);\n historyLoader.hide();\n historyError.show();\n });\n };\n\n var isHistoryLoaded = false;\n\n var initialiseTabs = function() {\n var selectItem = function(target) {\n var $target = $(target);\n $('.box').hide();\n $target.show();\n $('#menuList a').removeClass('selected');\n $('#menuList a[href=\"' + target + '\"]').addClass('selected');\n\n // Keep URL in sync so tab survives refresh and back/forward\n try {\n if (history && history.replaceState && location.hash !== target) {\n history.replaceState(null, '', target);\n }\n } catch (e) {}\n\n // Clean up scroll listeners when leaving history tab\n if (target !== '#components-history') {\n $(window).off('scroll.history resize.history');\n }\n \n // Load history data when history tab is selected for the first time\n if (target === '#components-history' && !isHistoryLoaded) {\n loadComponentsHistory();\n isHistoryLoaded = true;\n }\n // Re-enable scroll listeners when returning to history tab\n else if (target === '#components-history' && isHistoryLoaded && historyRenderedCount < historyData.length) {\n setupHistoryScrollListener();\n }\n };\n\n var hash = location.href.split('#')[1] || '';\n var isHashValid = hash && $('#' + hash);\n var target = isHashValid ? '#' + hash : $($('#menuList a')[0]).attr('href');\n selectItem(target);\n\n $('#menuList a').click(function() {\n selectItem($(this).attr('href'));\n });\n };\n\n var debounce = function(fn, wait) {\n var t;\n return function() {\n var ctx = this, args = arguments;\n clearTimeout(t);\n t = setTimeout(function(){ fn.apply(ctx, args); }, wait);\n };\n };\n\n $('#filter-components')\n .submit(componentsListChanged)\n .keyup(debounce(componentsListChanged, 80));\n $('#filter-components input[type=checkbox]').change(componentsListChanged);\n\n if (q) {\n $('.search').val(q);\n }\n\n // Keyboard shortcuts: \"/\" focuses search, \"Esc\" clears the focused filter\n $(document).on('keydown', function(e) {\n var tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';\n var typing = tag === 'input' || tag === 'textarea' || tag === 'select';\n\n if (e.key === '/' && !typing) {\n var $s = $('#search-filter');\n if ($s.length) {\n e.preventDefault();\n $s.focus();\n }\n } else if (e.key === 'Escape' && typing && $(e.target).closest('#filter-components').length) {\n $(e.target).val('');\n componentsListChanged();\n }\n });\n\n // Focus the search input only on a fresh load (no tab hash, no in-page nav)\n if (!location.hash) {\n var $searchInput = $('#search-filter');\n if ($searchInput.length) { $searchInput.focus(); }\n }\n\n componentsListChanged();\n initialiseTabs();\n});";
1
+ declare const _default: "'use strict';\n\nvar oc = oc || {};\noc.cmd = oc.cmd || [];\n\noc.cmd.push(function() {\n var escapeRegExp = function(s) {\n return String(s).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n };\n\n var safeRegExp = function(pattern, flags) {\n try {\n return new RegExp(pattern, flags);\n } catch (e) {\n return new RegExp(escapeRegExp(pattern), flags);\n }\n };\n\n // Client-side pagination for the components list. Enabled only once the\n // registry has more components than a single page so smaller registries keep\n // the simple single-page view.\n var COMPONENTS_PAGE_SIZE = 50;\n var paginationEnabled = componentsList.length > COMPONENTS_PAGE_SIZE;\n var currentPage = 1;\n\n var renderComponentsPagination = function(totalMatching, totalPages) {\n // Rendered into both the top and bottom containers so they stay in sync.\n var containers = $('.components-pagination');\n if (!paginationEnabled || totalMatching <= COMPONENTS_PAGE_SIZE) {\n containers.addClass('hide').empty();\n return;\n }\n\n // Build a compact list of page numbers: first, last, and a window around\n // the current page, with ellipses for the gaps.\n var pages = [];\n var windowSize = 1;\n var start = Math.max(2, currentPage - windowSize);\n var end = Math.min(totalPages - 1, currentPage + windowSize);\n pages.push(1);\n if (start > 2) {\n pages.push('...');\n }\n for (var p = start; p <= end; p++) {\n pages.push(p);\n }\n if (end < totalPages - 1) {\n pages.push('...');\n }\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n\n var html =\n '<button type=\"button\" class=\"pagination-btn pagination-prev\" data-page=\"' +\n (currentPage - 1) +\n '\"' +\n (currentPage <= 1 ? ' disabled' : '') +\n '>Prev</button>';\n for (var k = 0; k < pages.length; k++) {\n if (pages[k] === '...') {\n html += '<span class=\"pagination-ellipsis\">\u2026</span>';\n } else {\n html +=\n '<button type=\"button\" class=\"pagination-btn pagination-page' +\n (pages[k] === currentPage ? ' active' : '') +\n '\" data-page=\"' +\n pages[k] +\n '\">' +\n pages[k] +\n '</button>';\n }\n }\n html +=\n '<button type=\"button\" class=\"pagination-btn pagination-next\" data-page=\"' +\n (currentPage + 1) +\n '\"' +\n (currentPage >= totalPages ? ' disabled' : '') +\n '>Next</button>';\n\n containers.html(html).removeClass('hide');\n };\n\n var applyComponentsView = function() {\n var s = $('#search-filter').val(),\n a = $('#author-filter').val(),\n r = safeRegExp(s, ''),\n ar = safeRegExp(a, 'i'),\n selectedCheckboxes = $('input[type=checkbox]:checked'),\n hiddenStates = [],\n matchingNames = [],\n i;\n\n for (i = 0; i < selectedCheckboxes.length; i++) {\n hiddenStates.push($(selectedCheckboxes[i]).attr('name'));\n }\n\n // First pass: figure out which components match the current filters.\n for (i = 0; i < componentsList.length; i++) {\n var matches = !s || !!componentsList[i].name.match(r),\n matchesAuthor =\n !a ||\n (componentsList[i].author.name &&\n !!componentsList[i].author.name.match(ar)),\n isHidden = false;\n\n for (var j = 0; j < hiddenStates.length; j++) {\n if (componentsList[i].state.toLowerCase() === hiddenStates[j]) {\n isHidden = true;\n }\n }\n\n if (matches && matchesAuthor && !isHidden) {\n matchingNames.push(componentsList[i].name);\n }\n }\n\n var totalMatching = matchingNames.length;\n var totalPages = paginationEnabled\n ? Math.max(1, Math.ceil(totalMatching / COMPONENTS_PAGE_SIZE))\n : 1;\n if (currentPage > totalPages) {\n currentPage = totalPages;\n }\n if (currentPage < 1) {\n currentPage = 1;\n }\n\n var startIdx = paginationEnabled\n ? (currentPage - 1) * COMPONENTS_PAGE_SIZE\n : 0;\n var endIdx = paginationEnabled ? startIdx + COMPONENTS_PAGE_SIZE : totalMatching;\n var visibleNames = {};\n for (i = startIdx; i < endIdx && i < totalMatching; i++) {\n visibleNames[matchingNames[i]] = true;\n }\n\n // Second pass: only rows that match the filters AND fall on the current\n // page are shown.\n for (i = 0; i < componentsList.length; i++) {\n var name = componentsList[i].name,\n show = visibleNames[name] === true;\n $('#component-' + name)[show ? 'removeClass' : 'addClass']('hide');\n }\n\n var result =\n totalMatching + (totalMatching === 1 ? ' component' : ' components');\n\n if (s) {\n result += ' matching \"' + s + '\"';\n if (a) {\n result += ' and';\n }\n }\n if (a) {\n result += ' by author \"' + a + '\"';\n }\n if (paginationEnabled && totalMatching > COMPONENTS_PAGE_SIZE) {\n result +=\n ' (showing ' +\n (startIdx + 1) +\n '\u2013' +\n Math.min(endIdx, totalMatching) +\n ')';\n }\n\n $('#results-count').text(result);\n $('#components-empty')[totalMatching === 0 ? 'removeClass' : 'addClass'](\n 'hide'\n );\n\n renderComponentsPagination(totalMatching, totalPages);\n\n return false;\n };\n\n var componentsListChanged = function() {\n // Any filter change resets back to the first page.\n currentPage = 1;\n return applyComponentsView();\n };\n\n var historyData = [];\n var historyRenderedCount = 0;\n var historyBatchSize = 50; // Number of items to render per batch\n var isLoadingMore = false;\n\n var renderHistoryBatch = function() {\n if (historyRenderedCount >= historyData.length || isLoadingMore) {\n return;\n }\n\n isLoadingMore = true;\n var historyContent = $('#history-content');\n var batchEnd = Math.min(historyRenderedCount + historyBatchSize, historyData.length);\n var batchHtml = '';\n\n for (var i = historyRenderedCount; i < batchEnd; i++) {\n var item = historyData[i];\n var templateSizeText = item.templateSize ? \n ' [' + Math.round(item.templateSize / 1024) + ' kb]' : '';\n \n batchHtml += '<a href=\"' + item.name + '/' + item.version + '/~info\">' +\n '<div class=\"componentRow row table\">' +\n '<p class=\"release\">' +\n item.publishDate + ' - Published ' + item.name + '@' + item.version +\n templateSizeText +\n '</p>' +\n '</div>' +\n '</a>';\n }\n\n // Append new batch to existing content\n historyContent.append(batchHtml);\n historyRenderedCount = batchEnd;\n \n // Add loading indicator if there are more items\n if (historyRenderedCount < historyData.length) {\n historyContent.append('<div id=\"history-loading-more\" class=\"loading-more\">Loading more...</div>');\n }\n \n isLoadingMore = false;\n };\n\n var setupHistoryScrollListener = function() {\n var historyContainer = $('#components-history');\n var checkScroll = function() {\n if (historyRenderedCount >= historyData.length) {\n return;\n }\n\n var containerTop = historyContainer.offset().top;\n var containerHeight = historyContainer.outerHeight();\n var scrollTop = $(window).scrollTop();\n var windowHeight = $(window).height();\n \n // Check if user scrolled close to the bottom of the history container\n var distanceFromBottom = (containerTop + containerHeight) - (scrollTop + windowHeight);\n \n if (distanceFromBottom < 200) { // Load more when 200px from bottom\n $('#history-loading-more').remove();\n renderHistoryBatch();\n }\n };\n\n $(window).on('scroll.history', checkScroll);\n \n // Also check on resize\n $(window).on('resize.history', checkScroll);\n };\n\n var loadComponentsHistory = function() {\n var historyLoader = $('#history-loader');\n var historyContent = $('#history-content');\n var historyError = $('#history-error');\n\n // Show loader\n historyLoader.show();\n historyContent.hide();\n historyError.hide();\n\n // Reset state\n historyData = [];\n historyRenderedCount = 0;\n isLoadingMore = false;\n\n // Remove any existing scroll listeners\n $(window).off('scroll.history resize.history');\n\n // Fetch history data\n fetch('~registry/history')\n .then(function(response) {\n if (!response.ok) {\n throw new Error('Failed to fetch history');\n }\n return response.json();\n })\n .then(function(data) {\n historyData = data.componentsHistory || [];\n \n // Clear content and show container\n historyContent.empty();\n historyLoader.hide();\n historyContent.show();\n \n if (historyData.length === 0) {\n historyContent.html('<p class=\"empty-state\">No components history available.</p>');\n return;\n }\n\n // Render first batch\n renderHistoryBatch();\n \n // Setup scroll listener for infinite loading\n setupHistoryScrollListener();\n })\n .catch(function(error) {\n console.error('Error loading components history:', error);\n historyLoader.hide();\n historyError.show();\n });\n };\n\n var isHistoryLoaded = false;\n\n var initialiseTabs = function() {\n var selectItem = function(target) {\n var $target = $(target);\n $('.box').hide();\n $target.show();\n $('#menuList a').removeClass('selected');\n $('#menuList a[href=\"' + target + '\"]').addClass('selected');\n\n // Keep URL in sync so tab survives refresh and back/forward\n try {\n if (history && history.replaceState && location.hash !== target) {\n history.replaceState(null, '', target);\n }\n } catch (e) {}\n\n // Clean up scroll listeners when leaving history tab\n if (target !== '#components-history') {\n $(window).off('scroll.history resize.history');\n }\n \n // Load history data when history tab is selected for the first time\n if (target === '#components-history' && !isHistoryLoaded) {\n loadComponentsHistory();\n isHistoryLoaded = true;\n }\n // Re-enable scroll listeners when returning to history tab\n else if (target === '#components-history' && isHistoryLoaded && historyRenderedCount < historyData.length) {\n setupHistoryScrollListener();\n }\n };\n\n var hash = location.href.split('#')[1] || '';\n var isHashValid = hash && $('#' + hash);\n var target = isHashValid ? '#' + hash : $($('#menuList a')[0]).attr('href');\n selectItem(target);\n\n $('#menuList a').click(function() {\n selectItem($(this).attr('href'));\n });\n };\n\n var debounce = function(fn, wait) {\n var t;\n return function() {\n var ctx = this, args = arguments;\n clearTimeout(t);\n t = setTimeout(function(){ fn.apply(ctx, args); }, wait);\n };\n };\n\n $('#filter-components')\n .submit(componentsListChanged)\n .keyup(debounce(componentsListChanged, 80));\n $('#filter-components input[type=checkbox]').change(componentsListChanged);\n\n // Delegated from the stable list container so both the top and bottom\n // pagination bars work. No auto-scroll, so repeated clicks stay put.\n $('#components-list').on('click', '.pagination-btn', function() {\n var $btn = $(this);\n if ($btn.is('[disabled]')) {\n return;\n }\n var page = parseInt($btn.attr('data-page'), 10);\n if (isNaN(page)) {\n return;\n }\n currentPage = page;\n applyComponentsView();\n });\n\n if (q) {\n $('.search').val(q);\n }\n\n // Keyboard shortcuts: \"/\" focuses search, \"Esc\" clears the focused filter\n $(document).on('keydown', function(e) {\n var tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';\n var typing = tag === 'input' || tag === 'textarea' || tag === 'select';\n\n if (e.key === '/' && !typing) {\n var $s = $('#search-filter');\n if ($s.length) {\n e.preventDefault();\n $s.focus();\n }\n } else if (e.key === 'Escape' && typing && $(e.target).closest('#filter-components').length) {\n $(e.target).val('');\n componentsListChanged();\n }\n });\n\n // Focus the search input only on a fresh load (no tab hash, no in-page nav)\n if (!location.hash) {\n var $searchInput = $('#search-filter');\n if ($searchInput.length) { $searchInput.focus(); }\n }\n\n componentsListChanged();\n initialiseTabs();\n});";
2
2
  export default _default;