Skip to content

Commit

Permalink
fix(apps): transpile site apps on create (#25349)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored Oct 3, 2024
1 parent 869504a commit 8ef766a
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 49 deletions.
34 changes: 13 additions & 21 deletions posthog/api/plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import json
import os
import re
import subprocess
from typing import Any, Optional, cast, Literal
from typing import Any, Optional, cast

import requests
from dateutil.relativedelta import relativedelta
Expand Down Expand Up @@ -42,6 +40,7 @@
PluginSourceFile,
update_validated_data_from_url,
validate_plugin_job_payload,
transpile,
)
from posthog.models.utils import UUIDT, generate_random_token
from posthog.permissions import APIScopePermission
Expand Down Expand Up @@ -200,24 +199,6 @@ def _fix_formdata_config_json(request: request.Request, validated_data: dict):
validated_data["config"] = json.loads(request.POST["config"])


def transpile(input_string: str, type: Literal["site", "frontend"] = "site") -> Optional[str]:
from posthog.settings.base_variables import BASE_DIR

transpiler_path = os.path.join(BASE_DIR, "plugin-transpiler/dist/index.js")
if type not in ["site", "frontend"]:
raise Exception('Invalid type. Must be "site" or "frontend".')

process = subprocess.Popen(
["node", transpiler_path, "--type", type], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = process.communicate(input=input_string.encode())

if process.returncode != 0:
error = stderr.decode()
raise Exception(error)
return stdout.decode()


class PlainRenderer(renderers.BaseRenderer):
format = "txt"

Expand Down Expand Up @@ -311,6 +292,17 @@ def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Plugin:

plugin = Plugin.objects.install(**validated_data)

for source_file in PluginSourceFile.objects.filter(plugin=plugin):
if source_file.filename in ("site.tsx", "frontend.tsx"):
try:
source_file.transpiled = transpile(source_file.source, type=source_file.filename.split(".")[0])
source_file.status = PluginSourceFile.Status.TRANSPILED
source_file.save()
except Exception as e:
source_file.status = PluginSourceFile.Status.ERROR
source_file.error = str(e)
source_file.save()

return plugin

def update(self, plugin: Plugin, validated_data: dict, *args: Any, **kwargs: Any) -> Plugin: # type: ignore
Expand Down
62 changes: 54 additions & 8 deletions posthog/models/plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import datetime
import os
import subprocess
from dataclasses import dataclass
from enum import StrEnum
from typing import Any, Optional, cast
from typing import Any, Optional, cast, Literal
from uuid import UUID

from django.conf import settings
Expand Down Expand Up @@ -302,6 +303,24 @@ class PluginLogEntryType(StrEnum):
ERROR = "ERROR"


def transpile(input_string: str, type: Literal["site", "frontend"] = "site") -> Optional[str]:
from posthog.settings.base_variables import BASE_DIR

transpiler_path = os.path.join(BASE_DIR, "plugin-transpiler/dist/index.js")
if type not in ["site", "frontend"]:
raise Exception('Invalid type. Must be "site" or "frontend".')

process = subprocess.Popen(
["node", transpiler_path, "--type", type], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = process.communicate(input=input_string.encode())

if process.returncode != 0:
error = stderr.decode()
raise Exception(error)
return stdout.decode()


class PluginSourceFileManager(models.Manager):
def sync_from_plugin_archive(
self, plugin: Plugin, plugin_json_parsed: Optional[dict[str, Any]] = None
Expand All @@ -318,8 +337,10 @@ def sync_from_plugin_archive(
plugin_json, index_ts, frontend_tsx, site_ts = extract_plugin_code(plugin.archive, plugin_json_parsed)
except ValueError as e:
raise exceptions.ValidationError(f"{e} in plugin {plugin}")

# If frontend.tsx or index.ts are not present in the archive, make sure they aren't found in the DB either
filenames_to_delete = []

# Save plugin.json
plugin_json_instance, _ = PluginSourceFile.objects.update_or_create(
plugin=plugin,
Expand All @@ -331,36 +352,58 @@ def sync_from_plugin_archive(
"error": None,
},
)

# Save frontend.tsx
frontend_tsx_instance: Optional[PluginSourceFile] = None
if frontend_tsx is not None:
transpiled = None
status = None
error = None
try:
transpiled = transpile(frontend_tsx, type="site")
status = PluginSourceFile.Status.TRANSPILED
except Exception as e:
error = str(e)
status = PluginSourceFile.Status.ERROR
frontend_tsx_instance, _ = PluginSourceFile.objects.update_or_create(
plugin=plugin,
filename="frontend.tsx",
defaults={
"source": frontend_tsx,
"transpiled": None,
"status": None,
"error": None,
"transpiled": transpiled,
"status": status,
"error": error,
},
)
else:
filenames_to_delete.append("frontend.tsx")
# Save frontend.tsx

# Save site.ts
site_ts_instance: Optional[PluginSourceFile] = None
if site_ts is not None:
transpiled = None
status = None
error = None
try:
transpiled = transpile(site_ts, type="site")
status = PluginSourceFile.Status.TRANSPILED
except Exception as e:
error = str(e)
status = PluginSourceFile.Status.ERROR

site_ts_instance, _ = PluginSourceFile.objects.update_or_create(
plugin=plugin,
filename="site.ts",
defaults={
"source": site_ts,
"transpiled": None,
"status": None,
"error": None,
"transpiled": transpiled,
"status": status,
"error": error,
},
)
else:
filenames_to_delete.append("site.ts")

# Save index.ts
index_ts_instance: Optional[PluginSourceFile] = None
if index_ts is not None:
Expand All @@ -378,10 +421,13 @@ def sync_from_plugin_archive(
)
else:
filenames_to_delete.append("index.ts")

# Make sure files are gone
PluginSourceFile.objects.filter(plugin=plugin, filename__in=filenames_to_delete).delete()

# Trigger plugin server reload and code transpilation
plugin.save()

return (
plugin_json_instance,
index_ts_instance,
Expand Down
6 changes: 3 additions & 3 deletions posthog/test/__snapshots__/test_plugin.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
FROM "posthog_pluginsourcefile"
'''
# ---
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_Ts_works
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_ts_works
'''
SELECT "posthog_pluginsourcefile"."id",
"posthog_pluginsourcefile"."plugin_id",
Expand All @@ -227,7 +227,7 @@
UPDATE
'''
# ---
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_Ts_works.1
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_ts_works.1
'''
SELECT "posthog_pluginsourcefile"."id",
"posthog_pluginsourcefile"."plugin_id",
Expand All @@ -245,7 +245,7 @@
UPDATE
'''
# ---
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_Ts_works.2
# name: TestPluginSourceFile.test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_ts_works.2
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_pluginsourcefile"
Expand Down
38 changes: 21 additions & 17 deletions posthog/test/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,15 @@ def test_sync_from_plugin_archive_from_zip_with_explicit_index_js_works(self):
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON)
assert index_ts_file is not None
self.assertEqual(index_ts_file.source, HELLO_WORLD_PLUGIN_GITHUB_INDEX_JS)
self.assertIsNone(frontend_tsx_file)
self.assertIsNone(site_Ts_file)
self.assertIsNone(site_ts_file)

@snapshot_postgres_queries
def test_sync_from_plugin_archive_from_tgz_with_explicit_index_js_works(self):
Expand All @@ -201,30 +201,30 @@ def test_sync_from_plugin_archive_from_tgz_with_explicit_index_js_works(self):
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON)
assert index_ts_file is not None
self.assertEqual(index_ts_file.source, HELLO_WORLD_PLUGIN_NPM_INDEX_JS)
self.assertIsNone(frontend_tsx_file)
self.assertIsNone(site_Ts_file)
self.assertIsNone(site_ts_file)

# Second time - update
(
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON)
assert index_ts_file is not None
self.assertEqual(index_ts_file.source, HELLO_WORLD_PLUGIN_NPM_INDEX_JS)
self.assertIsNone(frontend_tsx_file)
self.assertIsNone(site_Ts_file)
self.assertIsNone(site_ts_file)

@snapshot_postgres_queries
def test_sync_from_plugin_archive_from_zip_with_index_ts_works(self):
Expand All @@ -239,15 +239,15 @@ def test_sync_from_plugin_archive_from_zip_with_index_ts_works(self):
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON_WITHOUT_MAIN)
assert index_ts_file is not None
self.assertEqual(index_ts_file.source, HELLO_WORLD_PLUGIN_GITHUB_INDEX_JS)
self.assertIsNone(frontend_tsx_file)
self.assertIsNone(site_Ts_file)
self.assertIsNone(site_ts_file)
self.assertFalse(self.team.inject_web_apps)

@snapshot_postgres_queries
Expand All @@ -263,19 +263,19 @@ def test_sync_from_plugin_archive_from_zip_without_index_ts_but_frontend_tsx_wor
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON_WITHOUT_MAIN)
self.assertIsNone(index_ts_file)
self.assertIsNone(site_Ts_file)
self.assertIsNone(site_ts_file)
assert frontend_tsx_file is not None
self.assertEqual(frontend_tsx_file.source, HELLO_WORLD_PLUGIN_FRONTEND_TSX)
self.assertFalse(self.team.inject_web_apps)

@snapshot_postgres_queries
def test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_Ts_works(self):
def test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_ts_works(self):
self.assertFalse(self.team.inject_web_apps)
test_plugin: Plugin = Plugin.objects.create(
organization=self.organization,
Expand All @@ -287,15 +287,17 @@ def test_sync_from_plugin_archive_from_zip_without_index_ts_but_site_Ts_works(se
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON_WITHOUT_MAIN)
self.assertIsNone(index_ts_file)
self.assertIsNone(frontend_tsx_file)
assert site_Ts_file is not None
self.assertEqual(site_Ts_file.source, HELLO_WORLD_PLUGIN_SITE_TS)
assert site_ts_file is not None
self.assertEqual(site_ts_file.source, HELLO_WORLD_PLUGIN_SITE_TS)
self.assertEqual(site_ts_file.status, PluginSourceFile.Status.TRANSPILED)
assert site_ts_file.transpiled is not None and len(site_ts_file.transpiled) > 0

@snapshot_postgres_queries
def test_sync_from_plugin_archive_from_zip_without_any_code_fails(self):
Expand Down Expand Up @@ -325,7 +327,7 @@ def test_sync_from_plugin_archive_twice_from_zip_with_index_ts_replaced_by_front
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
Expand All @@ -341,14 +343,16 @@ def test_sync_from_plugin_archive_twice_from_zip_with_index_ts_replaced_by_front
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2) # frontend.tsx replaced by index.ts
self.assertEqual(plugin_json_file.source, HELLO_WORLD_PLUGIN_PLUGIN_JSON_WITHOUT_MAIN)
self.assertIsNone(index_ts_file)
assert frontend_tsx_file is not None
self.assertEqual(frontend_tsx_file.source, HELLO_WORLD_PLUGIN_FRONTEND_TSX)
self.assertEqual(frontend_tsx_file.status, PluginSourceFile.Status.TRANSPILED)
assert frontend_tsx_file.transpiled is not None and len(frontend_tsx_file.transpiled) > 0

@snapshot_postgres_queries
def test_sync_from_plugin_archive_with_subdir_works(self):
Expand All @@ -363,7 +367,7 @@ def test_sync_from_plugin_archive_with_subdir_works(self):
plugin_json_file,
index_ts_file,
frontend_tsx_file,
site_Ts_file,
site_ts_file,
) = PluginSourceFile.objects.sync_from_plugin_archive(test_plugin)

self.assertEqual(PluginSourceFile.objects.count(), 2)
Expand Down

0 comments on commit 8ef766a

Please sign in to comment.