@akanjs/devkit 3.0.0-beta.26 → 3.0.0-beta.28

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 (49) hide show
  1. package/akanApp/akanApp.host.ts +22 -2
  2. package/akanConfig/akanBin.test.ts +62 -0
  3. package/akanConfig/akanBin.ts +78 -0
  4. package/akanConfig/akanConfig.test.ts +212 -2
  5. package/akanConfig/akanConfig.ts +93 -26
  6. package/akanConfig/index.ts +1 -0
  7. package/akanConfig/types.ts +7 -1
  8. package/applicationBuildRunner.test.ts +36 -2
  9. package/applicationBuildRunner.ts +21 -6
  10. package/biome.base.json +3 -1
  11. package/csrE2e/csrE2eHarness.fixture.ts +55 -5
  12. package/csrE2e/csrPageStack.e2e.test.ts +65 -0
  13. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  14. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  15. package/executors.test.ts +43 -0
  16. package/executors.ts +17 -2
  17. package/frontendBuild/csrDevArtifactWriter.test.ts +8 -0
  18. package/frontendBuild/csrDevArtifactWriter.ts +4 -0
  19. package/frontendBuild/csrDevBundler.ts +1 -1
  20. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  21. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  22. package/frontendBuild/csrDevModuleCompiler.test.ts +210 -6
  23. package/frontendBuild/csrDevModuleCompiler.ts +133 -19
  24. package/frontendBuild/csrDevPaths.ts +1 -0
  25. package/frontendBuild/csrDevResolver.ts +89 -9
  26. package/frontendBuild/ssrBaseArtifactBuilder.ts +7 -2
  27. package/frontendBuild/ssrDevBundler.ts +1 -1
  28. package/integration/devStability.integration.test.ts +35 -0
  29. package/mobile/desktopBin.test.ts +177 -0
  30. package/mobile/desktopBin.ts +143 -0
  31. package/mobile/desktopServerStage.test.ts +129 -0
  32. package/mobile/desktopServerStage.ts +120 -0
  33. package/mobile/index.ts +3 -0
  34. package/mobile/mobileTarget.test.ts +50 -1
  35. package/mobile/mobileTarget.ts +20 -4
  36. package/mobile/nativeApp.test.ts +37 -1
  37. package/mobile/nativeApp.ts +176 -27
  38. package/mobile/nativeAppLine.test.ts +69 -0
  39. package/mobile/nativeAppLine.ts +71 -0
  40. package/mobile/nativeConfig.test.ts +215 -4
  41. package/mobile/nativeConfig.ts +117 -8
  42. package/mobile/nativePluginFolders.test.ts +81 -0
  43. package/mobile/nativePluginFolders.ts +60 -0
  44. package/package.json +2 -2
  45. package/transforms/serverEnvPlugin.test.ts +100 -0
  46. package/transforms/serverEnvPlugin.ts +41 -0
  47. package/types.ts +1 -0
  48. package/workspaceLayout.test.ts +5 -0
  49. package/workspaceLayout.ts +2 -0
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import { tempDirs, writeText as write } from "../testHelpers";
4
+ import { createServerEnvPlugin } from "./serverEnvPlugin";
5
+
6
+ const makeTempRoot = tempDirs("akan-devkit-server-env-");
7
+ const environments = ["local", "testing", "debug", "develop", "main", "qa"];
8
+
9
+ const writeApp = async (root: string) => {
10
+ const envDir = path.join(root, "apps/demo/env");
11
+ await write(
12
+ path.join(envDir, "env.server.ts"),
13
+ [
14
+ ...environments.map((name) => `import { env as ${name} } from "./env.server.${name}";`),
15
+ `const envConfigs = { ${environments.join(", ")} };`,
16
+ 'const currentEnv = process.env.AKAN_PUBLIC_ENV ?? "local";',
17
+ "export const env = envConfigs[currentEnv as keyof typeof envConfigs];",
18
+ "",
19
+ ].join("\n"),
20
+ );
21
+ await write(path.join(envDir, "env.server.type.ts"), 'export const libEnv = { shared: "shared-value" };\n');
22
+ for (const name of environments)
23
+ await write(
24
+ path.join(envDir, `env.server.${name}.ts`),
25
+ [
26
+ 'import { libEnv } from "./env.server.type";',
27
+ `export const env = { ...libEnv, secret: "${name}-secret" };`,
28
+ `export default "${name}-default";`,
29
+ "",
30
+ ].join("\n"),
31
+ );
32
+ const entry = path.join(root, "apps/demo/server.ts");
33
+ await write(entry, 'import { env } from "./env/env.server";\nconsole.info(JSON.stringify({ ...env }));\n');
34
+ return { envDir, entry };
35
+ };
36
+
37
+ const build = async (root: string, environment: string) => {
38
+ const { envDir, entry } = await writeApp(root);
39
+ const result = await Bun.build({
40
+ entrypoints: [entry],
41
+ outdir: path.join(root, "out"),
42
+ target: "bun",
43
+ plugins: [createServerEnvPlugin({ envDir, environment, environments })],
44
+ });
45
+ expect(result.success).toBe(true);
46
+ const output = result.outputs.find((artifact) => artifact.kind === "entry-point");
47
+ if (!output) throw new Error("no entry output");
48
+ return { file: output.path, text: await output.text() };
49
+ };
50
+
51
+ const run = (file: string, environment: string) =>
52
+ Bun.spawnSync([process.execPath, file], { env: { ...process.env, AKAN_PUBLIC_ENV: environment } });
53
+
54
+ describe("createServerEnvPlugin", () => {
55
+ test("bundles only the environment the image runs", async () => {
56
+ const { text } = await build(await makeTempRoot(), "main");
57
+ expect(text).toContain("main-secret");
58
+ expect(text).toContain("shared-value");
59
+ for (const name of environments.filter((name) => name !== "main")) {
60
+ expect(text).not.toContain(`${name}-secret`);
61
+ expect(text).not.toContain(`${name}-default`);
62
+ }
63
+ });
64
+
65
+ test("the built environment boots with its own values", async () => {
66
+ const { file } = await build(await makeTempRoot(), "main");
67
+ const proc = run(file, "main");
68
+ expect(proc.exitCode).toBe(0);
69
+ expect(JSON.parse(proc.stdout.toString())).toEqual({ shared: "shared-value", secret: "main-secret" });
70
+ });
71
+
72
+ test("another environment refuses to boot and names the build to make", async () => {
73
+ const { file } = await build(await makeTempRoot(), "main");
74
+ const proc = run(file, "develop");
75
+ expect(proc.exitCode).not.toBe(0);
76
+ expect(proc.stderr.toString()).toContain(
77
+ "env/env.server.develop.ts is not in this build: `akan build` bundles the server env of AKAN_PUBLIC_ENV=main alone",
78
+ );
79
+ });
80
+
81
+ test("a custom branch is its own environment", async () => {
82
+ const { text } = await build(await makeTempRoot(), "qa");
83
+ expect(text).toContain("qa-secret");
84
+ expect(text).not.toContain("main-secret");
85
+ });
86
+
87
+ test("leaves env files outside the app's env directory alone", async () => {
88
+ const root = await makeTempRoot();
89
+ const other = path.join(root, "libs/shared/env/env.server.testing.ts");
90
+ await write(other, 'export const env = { secret: "lib-testing-secret" };\n');
91
+ const { envDir, entry } = await writeApp(root);
92
+ await write(entry, `import { env } from ${JSON.stringify(other)};\nconsole.info(env.secret);\n`);
93
+ const result = await Bun.build({
94
+ entrypoints: [entry],
95
+ target: "bun",
96
+ plugins: [createServerEnvPlugin({ envDir, environment: "main", environments })],
97
+ });
98
+ expect(await result.outputs[0]?.text()).toContain("lib-testing-secret");
99
+ });
100
+ });
@@ -0,0 +1,41 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { BunPlugin } from "bun";
4
+
5
+ export interface ServerEnvPluginOptions {
6
+ envDir: string;
7
+ /** The environment the image runs: its Dockerfile fixes `AKAN_PUBLIC_ENV` to it. */
8
+ environment: string;
9
+ environments: Iterable<string>;
10
+ }
11
+
12
+ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+
14
+ //* `env.server.ts` imports every environment's file, so an image would hand out the secrets of each environment it
15
+ //* never runs. Every file but the image's own is swapped for exports that refuse to be read.
16
+ export const createServerEnvPlugin = ({ envDir, environment, environments }: ServerEnvPluginOptions): BunPlugin => {
17
+ const others = [...new Set(environments)].filter((name) => name !== environment);
18
+ //? Bun hands onLoad the real path, which differs from the given one under a symlink (macOS's /var -> /private/var).
19
+ const dirs = new Set([path.resolve(envDir), existsSync(envDir) ? realpathSync.native(envDir) : envDir]);
20
+ const names = others.map(escapeRegExp).join("|");
21
+ const filter = new RegExp(`^(?:${[...dirs].map(escapeRegExp).join("|")})[\\\\/]env\\.server\\.(?:${names})\\.ts$`);
22
+ return {
23
+ name: "akan-server-env",
24
+ setup(build) {
25
+ if (!others.length) return;
26
+ build.onLoad({ filter }, async (args) => {
27
+ const name = path.basename(args.path, ".ts").slice("env.server.".length);
28
+ const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(args.path).text());
29
+ const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs. Build with AKAN_PUBLIC_ENV=${name} to run ${name}.`;
30
+ return {
31
+ loader: "js",
32
+ contents: [
33
+ `const refuse = () => { throw new Error(${JSON.stringify(message)}); };`,
34
+ "const unbuilt = new Proxy({}, { get: refuse, has: refuse, ownKeys: refuse, getOwnPropertyDescriptor: refuse });",
35
+ `export { ${exports.map((exported) => `unbuilt as ${JSON.stringify(exported)}`).join(", ")} };`,
36
+ ].join("\n"),
37
+ };
38
+ });
39
+ },
40
+ };
41
+ };
package/types.ts CHANGED
@@ -10,6 +10,7 @@ export interface PackageJson {
10
10
  peerDependencies?: Record<string, string>;
11
11
  peerDependenciesMeta?: Record<string, { optional?: boolean }>;
12
12
  optionalDependencies?: Record<string, string>;
13
+ trustedDependencies?: string[];
13
14
  overrides?: Record<string, string>;
14
15
  engines?: Record<string, string>;
15
16
  exports?: Record<string, string | Record<string, string>>;
@@ -21,6 +21,11 @@ describe("app root layout allowlist", () => {
21
21
  }
22
22
  });
23
23
 
24
+ test("admits the native plugins an app owns, and a lib too", () => {
25
+ expect(appRootAllowedDirs.has("native")).toBe(true);
26
+ expect(libRootAllowedDirs.has("native")).toBe(true);
27
+ });
28
+
24
29
  test("rejects an app root entry no facet owns", () => {
25
30
  expect(appRootAllowedFiles.has("helper.ts")).toBe(false);
26
31
  expect(appRootAllowedDirs.has("base")).toBe(false);
@@ -21,6 +21,7 @@ export const appRootAllowedDirs = new Set([
21
21
  "common",
22
22
  "env",
23
23
  "lib",
24
+ "native",
24
25
  "page",
25
26
  "plugin",
26
27
  "private",
@@ -52,6 +53,7 @@ export const libRootAllowedDirs = new Set([
52
53
  "common",
53
54
  "env",
54
55
  "lib",
56
+ "native",
55
57
  "page",
56
58
  "plugin",
57
59
  "private",