Page MenuHomeSealhub

No OneTemporary

diff --git a/README.md b/README.md
index e24c8b6..78e1a88 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,69 @@
In order to add Font Squirrel fonts with get-fonts command you need to create fonts.json file with the following structure:
```
{
"fontSquirrel": {
"source-sans-pro": [],
"fira-sans": ["hair", "medium"]
}
}
```
where you put an empty brackets for a regular style font or a list of desired font styles.
+
+## Register External Controllers and Styles
+
+When creating a module that is a dependency for a Sealgen app, you can automatically register your module's controllers and styles with the parent project using these commands:
+
+### register-external-controllers
+
+Registers a module's stimulus controllers directory with the parent project's `sealgen.controllerDirs` configuration.
+
+```bash
+npx sealgen register-external-controllers <module-name> <subdirectory>
+```
+
+Example:
+```bash
+npx sealgen register-external-controllers my-module src/stimulus-controllers
+```
+
+This will add `node_modules/my-module/src/stimulus-controllers` to the parent project's `package.json` sealgen configuration.
+
+### register-external-styles
+
+Registers a module's styles directory with the parent project's `sealgen.styleDirs` configuration.
+
+```bash
+npx sealgen register-external-styles <module-name> <subdirectory>
+```
+
+Example:
+```bash
+npx sealgen register-external-styles my-module src/styles
+```
+
+This will add `node_modules/my-module/src/styles` to the parent project's `package.json` sealgen configuration.
+
+### Usage in Module Development
+
+You can add these commands to your module's `package.json` postinstall script to automatically register your controllers and styles when the module is installed:
+
+```json
+{
+ "name": "my-module",
+ "scripts": {
+ "postinstall": "npx sealgen register-external-controllers my-module src/stimulus-controllers && npx sealgen register-external-styles my-module src/styles"
+ }
+}
+```
+
+### How It Works
+
+1. The command traverses up the directory tree from the current working directory
+2. It looks for a `package.json` file that contains a `sealgen` configuration
+3. It adds the specified path to the appropriate array (`controllerDirs` or `styleDirs`)
+4. If the `sealgen` configuration doesn't exist, it creates it
+5. If the path is already present, it doesn't add a duplicate
+
+This ensures that when your module is installed as a dependency, the parent project will automatically know where to find your controllers and styles.
diff --git a/src/cli.ts b/src/cli.ts
index 2edf245..4a2e6b4 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,47 +1,53 @@
#!/usr/bin/env node
import yargs from "yargs/yargs";
import { addCollection } from "./add-collection.js";
import { addCRUD } from "./add-crud.js";
import { addJDDComponent } from "./add-jdd-component.js";
import { addRoute } from "./add-route.js";
import { addVerboseCRUD } from "./add-verbose-crud.js";
import { buildProject } from "./build.js";
import { generateCollections } from "./generate-collections.js";
import { generateComponents } from "./generate-components.js";
import { generateRoutes } from "./generate-routes.js";
import { getFonts } from "./get-fonts.js";
import { makeEnv } from "./make-env.js";
import { crudFieldSnippets } from "./crud-field-snippets.js";
+import {
+ registerExternalControllers,
+ registerExternalStyles,
+} from "./register-external.js";
const actions: Record<
string,
(args: Record<string, string | boolean>) => Promise<void> | undefined
> = {
"add-collection": addCollection,
"add-route": addRoute,
"add-verbose-crud": addVerboseCRUD,
"add-crud": addCRUD,
"generate-collections": generateCollections,
"generate-components": generateComponents,
"generate-routes": generateRoutes,
build: buildProject,
default: async function () {
console.log("Usage: `npx sealgen <action>`");
console.log(
`Available actions: ${Object.keys(actions)
.filter((e) => e != "default")
.join(", ")}`
);
},
"make-env": makeEnv,
"add-component": addJDDComponent,
"get-fonts": getFonts,
"crud-field-snippets": crudFieldSnippets,
+ "register-external-controllers": registerExternalControllers,
+ "register-external-styles": registerExternalStyles,
};
void (async function () {
const action = process.argv.at(2);
const fn = actions[action || "default"] || actions.default;
const args = yargs(process.argv).argv as Record<string, string | boolean>;
await fn(args);
})();
diff --git a/src/register-external.test.ts b/src/register-external.test.ts
new file mode 100644
index 0000000..cc62b5e
--- /dev/null
+++ b/src/register-external.test.ts
@@ -0,0 +1,469 @@
+import assert from "assert";
+import { promises as fs } from "fs";
+import path from "path";
+import { RealAppTest } from "./test_utils/test-on-real-app.js";
+
+// Helper function to ensure sealgen configuration exists
+async function ensureSealgenConfig(test_app: RealAppTest): Promise<void> {
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ let packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ let packageJson = JSON.parse(packageJsonContent);
+
+ if (!packageJson.sealgen) {
+ packageJson.sealgen = {};
+ }
+
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
+}
+
+// Helper function to create mock external module
+async function createMockExternalModule(
+ test_app: RealAppTest,
+ moduleName: string,
+ subdirectory: string,
+ files: Array<{ name: string; content: string }>
+): Promise<void> {
+ const externalModulePath = path.join(test_app.app_path, "node_modules", moduleName);
+ const targetPath = path.join(externalModulePath, subdirectory);
+
+ await fs.mkdir(targetPath, { recursive: true });
+
+ for (const file of files) {
+ await fs.writeFile(path.join(targetPath, file.name), file.content);
+ // If the file is a .ts controller, also create a .js file for build import
+ if (file.name.endsWith(".ts") && file.name.includes("stimulus")) {
+ const jsName = file.name.replace(/\.ts$/, ".js");
+ const exportName = file.name.replace(/\.stimulus\.ts$/, "");
+ await fs.writeFile(
+ path.join(targetPath, jsName),
+ `export default { connect() { console.log('Dummy JS for ${exportName}'); } }`
+ );
+ }
+ }
+}
+
+describe("register-external CLI commands", () => {
+ describe("package.json configuration", () => {
+ it("registers external controllers and verifies package.json is updated", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ const moduleName = "test-controller-module";
+ const subdirectory = "controllers";
+
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${moduleName} ${subdirectory}`
+ );
+
+ // Verify package.json was updated correctly
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent);
+
+ assert(packageJson.sealgen, "sealgen configuration should exist");
+ assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist");
+ assert(
+ Array.isArray(packageJson.sealgen.controllerDirs),
+ "controllerDirs should be an array"
+ );
+
+ const expectedPath = `node_modules/${moduleName}/${subdirectory}`;
+ assert(
+ packageJson.sealgen.controllerDirs.includes(expectedPath),
+ `controllerDirs should contain ${expectedPath}`
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("registers external styles and verifies package.json is updated", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ const moduleName = "test-style-module";
+ const subdirectory = "styles";
+
+ await test_app.runSealgenCommand(
+ `register-external-styles ${moduleName} ${subdirectory}`
+ );
+
+ // Verify package.json was updated correctly
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent);
+
+ assert(packageJson.sealgen, "sealgen configuration should exist");
+ assert(packageJson.sealgen.styleDirs, "styleDirs should exist");
+ assert(Array.isArray(packageJson.sealgen.styleDirs), "styleDirs should be an array");
+
+ const expectedPath = `node_modules/${moduleName}/${subdirectory}`;
+ assert(
+ packageJson.sealgen.styleDirs.includes(expectedPath),
+ `styleDirs should contain ${expectedPath}`
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("registers both controllers and styles in the same project", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ const controllerModule = "test-controller-module";
+ const controllerSubdir = "controllers";
+ const styleModule = "test-style-module";
+ const styleSubdir = "styles";
+
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${controllerModule} ${controllerSubdir}`
+ );
+ await test_app.runSealgenCommand(
+ `register-external-styles ${styleModule} ${styleSubdir}`
+ );
+
+ // Verify package.json was updated correctly for both
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent);
+
+ assert(packageJson.sealgen, "sealgen configuration should exist");
+ assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist");
+ assert(packageJson.sealgen.styleDirs, "styleDirs should exist");
+
+ const expectedControllerPath = `node_modules/${controllerModule}/${controllerSubdir}`;
+ const expectedStylePath = `node_modules/${styleModule}/${styleSubdir}`;
+
+ assert(
+ packageJson.sealgen.controllerDirs.includes(expectedControllerPath),
+ `controllerDirs should contain ${expectedControllerPath}`
+ );
+ assert(
+ packageJson.sealgen.styleDirs.includes(expectedStylePath),
+ `styleDirs should contain ${expectedStylePath}`
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("handles duplicate registrations gracefully", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ const moduleName = "test-duplicate-module";
+ const subdirectory = "controllers";
+
+ // Register the same module twice
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${moduleName} ${subdirectory}`
+ );
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${moduleName} ${subdirectory}`
+ );
+
+ // Verify package.json was updated correctly (should not have duplicates)
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent);
+
+ assert(packageJson.sealgen, "sealgen configuration should exist");
+ assert(packageJson.sealgen.controllerDirs, "controllerDirs should exist");
+
+ const expectedPath = `node_modules/${moduleName}/${subdirectory}`;
+ const controllerDirs = packageJson.sealgen.controllerDirs as string[];
+
+ // Should only appear once
+ const occurrences = controllerDirs.filter((dir) => dir === expectedPath).length;
+ assert(
+ occurrences === 1,
+ `controllerDirs should contain ${expectedPath} exactly once, found ${occurrences} times`
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("preserves existing sealgen configuration when adding new entries", async function () {
+ const test_app = await RealAppTest.init();
+
+ // First, manually add some existing configuration to package.json
+ const packageJsonPath = path.join(test_app.app_path, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent);
+
+ // Add existing sealgen configuration
+ packageJson.sealgen = {
+ controllerDirs: ["node_modules/existing-controller/controllers"],
+ styleDirs: ["node_modules/existing-style/styles"],
+ };
+
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
+
+ // Now register new external modules
+ const newControllerModule = "new-controller-module";
+ const newStyleModule = "new-style-module";
+
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${newControllerModule} controllers`
+ );
+ await test_app.runSealgenCommand(`register-external-styles ${newStyleModule} styles`);
+
+ // Verify both old and new configurations are preserved
+ const updatedPackageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const updatedPackageJson = JSON.parse(updatedPackageJsonContent);
+
+ assert(updatedPackageJson.sealgen, "sealgen configuration should exist");
+ assert(updatedPackageJson.sealgen.controllerDirs, "controllerDirs should exist");
+ assert(updatedPackageJson.sealgen.styleDirs, "styleDirs should exist");
+
+ // Check that existing entries are preserved
+ assert(
+ updatedPackageJson.sealgen.controllerDirs.includes(
+ "node_modules/existing-controller/controllers"
+ ),
+ "Existing controller directory should be preserved"
+ );
+ assert(
+ updatedPackageJson.sealgen.styleDirs.includes("node_modules/existing-style/styles"),
+ "Existing style directory should be preserved"
+ );
+
+ // Check that new entries are added
+ assert(
+ updatedPackageJson.sealgen.controllerDirs.includes(
+ `node_modules/${newControllerModule}/controllers`
+ ),
+ "New controller directory should be added"
+ );
+ assert(
+ updatedPackageJson.sealgen.styleDirs.includes(
+ `node_modules/${newStyleModule}/styles`
+ ),
+ "New style directory should be added"
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+ });
+
+ describe("build integration", () => {
+ it("registers external controllers and verifies they are included in build", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ // Create a mock external module with controllers
+ const externalModuleName = "test-external-controllers";
+ await createMockExternalModule(test_app, externalModuleName, "controllers", [
+ {
+ name: "test-external.stimulus.ts",
+ content: `
+import { Controller } from "stimulus";
+
+export default class TestExternalController extends Controller {
+ static targets = ["output"];
+
+ connect() {
+ console.log("External controller connected!");
+ }
+}
+`,
+ },
+ ]);
+
+ // Register the external controllers
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${externalModuleName} controllers`
+ );
+
+ // Run the build process
+ await test_app.runSealgenCommand("build");
+
+ // Verify the controllers are included in the build
+ const controllersFilePath = path.join(test_app.app_path, "src/front/controllers.ts");
+ const controllersContent = await fs.readFile(controllersFilePath, "utf-8");
+
+ // Check that the external controller is imported
+ assert(
+ controllersContent.includes(
+ `import { default as TestExternal } from "./../../node_modules/${externalModuleName}/controllers/test-external.stimulus.js"`
+ ),
+ "External controller should be imported in generated controllers file"
+ );
+
+ // Check that the controller is registered
+ assert(
+ controllersContent.includes(`application.register("test-external", TestExternal);`),
+ "External controller should be registered in generated controllers file"
+ );
+
+ // Verify the build output exists
+ const bundlePath = path.join(test_app.app_path, "public/dist/bundle.js");
+ assert(
+ await fs
+ .stat(bundlePath)
+ .then(() => true)
+ .catch(() => false),
+ "Bundle should be generated"
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("registers external styles and verifies they are included in build", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ // Create a mock external module with styles
+ const externalModuleName = "test-external-styles";
+ await createMockExternalModule(test_app, externalModuleName, "styles", [
+ {
+ name: "external.css",
+ content: `
+.external-style {
+ color: red;
+ background: blue;
+}
+`,
+ },
+ ]);
+
+ // Register the external styles
+ await test_app.runSealgenCommand(
+ `register-external-styles ${externalModuleName} styles`
+ );
+
+ // Run the build process
+ await test_app.runSealgenCommand("build");
+
+ // Verify the styles are included in the build
+ const styleEntrypointPath = path.join(
+ test_app.app_path,
+ "src/style-entrypoints/default.entrypoint.css"
+ );
+ const styleEntrypointContent = await fs.readFile(styleEntrypointPath, "utf-8");
+
+ // Check that the external CSS is imported
+ assert(
+ styleEntrypointContent.includes(
+ `@import "../../node_modules/${externalModuleName}/styles/external.css"`
+ ),
+ "External CSS should be imported in generated style entrypoint"
+ );
+
+ // Verify the CSS build output exists
+ const cssOutputPath = path.join(
+ test_app.app_path,
+ "public/dist/default.entrypoint.css"
+ );
+ assert(
+ await fs
+ .stat(cssOutputPath)
+ .then(() => true)
+ .catch(() => false),
+ "CSS should be generated"
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+
+ it("registers both external controllers and styles and verifies complete build integration", async function () {
+ const test_app = await RealAppTest.init();
+ await ensureSealgenConfig(test_app);
+
+ // Create mock external modules
+ const controllerModuleName = "test-external-controllers";
+ const styleModuleName = "test-external-styles";
+
+ // Create controller module
+ await createMockExternalModule(test_app, controllerModuleName, "controllers", [
+ {
+ name: "combined-test.stimulus.ts",
+ content: `
+import { Controller } from "stimulus";
+
+export default class CombinedTestController extends Controller {
+ static targets = ["output"];
+
+ connect() {
+ console.log("Combined test controller connected!");
+ }
+}
+`,
+ },
+ ]);
+
+ // Create style module
+ await createMockExternalModule(test_app, styleModuleName, "styles", [
+ {
+ name: "combined-external.css",
+ content: `
+.combined-external-style {
+ color: green;
+ background: yellow;
+}
+`,
+ },
+ ]);
+
+ // Register both external modules
+ await test_app.runSealgenCommand(
+ `register-external-controllers ${controllerModuleName} controllers`
+ );
+ await test_app.runSealgenCommand(`register-external-styles ${styleModuleName} styles`);
+
+ // Run the build process
+ await test_app.runSealgenCommand("build");
+
+ // Verify controllers are included
+ const controllersFilePath = path.join(test_app.app_path, "src/front/controllers.ts");
+ const controllersContent = await fs.readFile(controllersFilePath, "utf-8");
+
+ assert(
+ controllersContent.includes(
+ `import { default as CombinedTest } from "./../../node_modules/${controllerModuleName}/controllers/combined-test.stimulus.js"`
+ ),
+ "Combined test controller should be imported"
+ );
+ assert(
+ controllersContent.includes(`application.register("combined-test", CombinedTest);`),
+ "Combined test controller should be registered"
+ );
+
+ // Verify styles are included
+ const styleEntrypointPath = path.join(
+ test_app.app_path,
+ "src/style-entrypoints/default.entrypoint.css"
+ );
+ const styleEntrypointContent = await fs.readFile(styleEntrypointPath, "utf-8");
+
+ assert(
+ styleEntrypointContent.includes(
+ `@import "../../node_modules/${styleModuleName}/styles/combined-external.css"`
+ ),
+ "Combined external CSS should be imported"
+ );
+
+ // Verify build outputs exist
+ const bundlePath = path.join(test_app.app_path, "public/dist/bundle.js");
+ const cssOutputPath = path.join(
+ test_app.app_path,
+ "public/dist/default.entrypoint.css"
+ );
+
+ assert(
+ await fs
+ .stat(bundlePath)
+ .then(() => true)
+ .catch(() => false),
+ "Bundle should be generated"
+ );
+ assert(
+ await fs
+ .stat(cssOutputPath)
+ .then(() => true)
+ .catch(() => false),
+ "CSS should be generated"
+ );
+
+ await test_app.close();
+ }).timeout(100 * 1000);
+ });
+});
diff --git a/src/register-external.ts b/src/register-external.ts
new file mode 100644
index 0000000..d792c38
--- /dev/null
+++ b/src/register-external.ts
@@ -0,0 +1,148 @@
+import { promises as fs, existsSync, readFileSync } from "fs";
+import { resolve, dirname } from "path";
+
+import { formatWithPrettier } from "./utils/prettier.js";
+
+interface PackageJson {
+ sealgen?: {
+ controllerDirs?: string[];
+ styleDirs?: string[];
+ [key: string]: unknown;
+ };
+ [key: string]: unknown;
+}
+
+function findParentProjectWithSealgen(): string {
+ let currentDir = process.cwd();
+
+ while (currentDir !== dirname(currentDir)) {
+ const packageJsonPath = resolve(currentDir, "package.json");
+
+ if (existsSync(packageJsonPath)) {
+ try {
+ const packageJsonContent = readFileSync(
+ packageJsonPath,
+ "utf-8"
+ );
+ const packageJson = JSON.parse(
+ packageJsonContent
+ ) as PackageJson;
+
+ // Check if this package.json has a sealgen configuration
+ if (packageJson.sealgen) {
+ return currentDir;
+ }
+ } catch (error) {
+ // If we can't read or parse the package.json, continue to parent
+ console.warn(
+ `Warning: Could not read package.json at ${packageJsonPath}:`,
+ error
+ );
+ }
+ }
+
+ currentDir = dirname(currentDir);
+ }
+
+ throw new Error(
+ "Could not find a parent project with sealgen configuration"
+ );
+}
+
+async function updatePackageJson(
+ projectDir: string,
+ moduleName: string,
+ subdirectory: string,
+ type: "controllerDirs" | "styleDirs"
+): Promise<void> {
+ const packageJsonPath = resolve(projectDir, "package.json");
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
+ const packageJson = JSON.parse(packageJsonContent) as PackageJson;
+
+ // Initialize sealgen configuration if it doesn't exist
+ if (!packageJson.sealgen) {
+ packageJson.sealgen = {};
+ }
+
+ // Initialize the specific array if it doesn't exist
+ if (!packageJson.sealgen[type]) {
+ packageJson.sealgen[type] = [];
+ }
+
+ const pathToAdd = `node_modules/${moduleName}/${subdirectory}`;
+ const existingPaths = packageJson.sealgen[type] as string[];
+
+ // Check if the path is already present
+ if (existingPaths.includes(pathToAdd)) {
+ console.log(`${type} already contains: ${pathToAdd}`);
+ return;
+ }
+
+ // Add the new path
+ existingPaths.push(pathToAdd);
+
+ // Write back the updated package.json with proper formatting
+ const updatedContent = await formatWithPrettier(
+ JSON.stringify(packageJson, null, 2),
+ "json"
+ );
+
+ await fs.writeFile(packageJsonPath, updatedContent);
+ console.log(`Added ${pathToAdd} to ${type} in ${packageJsonPath}`);
+}
+
+export async function registerExternalControllers(
+ args: Record<string, string | boolean>
+): Promise<void> {
+ // Extract positional arguments from process.argv since yargs doesn't handle them for these commands
+ const moduleName = process.argv.at(3);
+ const subdirectory = process.argv.at(4);
+
+ if (!moduleName || !subdirectory) {
+ console.error(
+ "Usage: npx sealgen register-external-controllers <module-name> <subdirectory>"
+ );
+ process.exit(1);
+ }
+
+ try {
+ const projectDir = findParentProjectWithSealgen();
+ await updatePackageJson(
+ projectDir,
+ moduleName,
+ subdirectory,
+ "controllerDirs"
+ );
+ } catch (error) {
+ console.error("Error:", error instanceof Error ? error.message : error);
+ process.exit(1);
+ }
+}
+
+export async function registerExternalStyles(
+ args: Record<string, string | boolean>
+): Promise<void> {
+ // Extract positional arguments from process.argv since yargs doesn't handle them for these commands
+ const moduleName = process.argv.at(3);
+ const subdirectory = process.argv.at(4);
+
+ if (!moduleName || !subdirectory) {
+ console.error(
+ "Usage: npx sealgen register-external-styles <module-name> <subdirectory>"
+ );
+ process.exit(1);
+ }
+
+ try {
+ const projectDir = findParentProjectWithSealgen();
+ await updatePackageJson(
+ projectDir,
+ moduleName,
+ subdirectory,
+ "styleDirs"
+ );
+ } catch (error) {
+ console.error("Error:", error instanceof Error ? error.message : error);
+ process.exit(1);
+ }
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 05:17 (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034051
Default Alt Text
(24 KB)

Event Timeline