@akanjs/devkit 3.1.0-rc.4 → 3.1.0-rc.5

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 (81) hide show
  1. package/abstractDoc.ts +1 -1
  2. package/agentsIndex.test.ts +4 -1
  3. package/akanConfig/akanConfig.ts +2 -2
  4. package/akanContext.ts +4 -4
  5. package/akanMcpContract.ts +1 -1
  6. package/applicationBuildRunner.test.ts +24 -1
  7. package/applicationBuildRunner.ts +11 -1
  8. package/biome.base.json +0 -10
  9. package/cloud/configLock.ts +2 -1
  10. package/cloud/globalConfig.ts +3 -2
  11. package/codeAgent/agent/CodeAgentProxy.ts +2 -2
  12. package/codeAgent/agent/akanCodeModel.ts +1 -1
  13. package/codeAgent/agent/akanContextFiles.test.ts +3 -1
  14. package/codeAgent/tools/WebToolPack.ts +4 -4
  15. package/codeAgent/tools/akanCodePlugins.test.ts +20 -16
  16. package/commandDecorators/command.ts +5 -4
  17. package/createTunnel.test.ts +14 -0
  18. package/createTunnel.ts +3 -1
  19. package/dependencyScanner.ts +1 -1
  20. package/devkitUtils.test.ts +1 -1
  21. package/executors.test.ts +62 -3
  22. package/executors.ts +26 -9
  23. package/frontendBuild/autoImportSync.test.ts +3 -1
  24. package/frontendBuild/clientEntryDiscovery.ts +1 -1
  25. package/frontendBuild/csrArtifactBuilder.test.ts +30 -0
  26. package/frontendBuild/csrArtifactBuilder.ts +5 -3
  27. package/frontendBuild/csrDevPatcher.test.ts +4 -1
  28. package/frontendBuild/cssBlockParser.test.ts +123 -0
  29. package/frontendBuild/cssBlockParser.ts +132 -0
  30. package/frontendBuild/cssCompiler.test.ts +53 -0
  31. package/frontendBuild/cssCompiler.ts +8 -4
  32. package/frontendBuild/fontOptimizer.ts +2 -1
  33. package/frontendBuild/frontendBuild.test.ts +14 -0
  34. package/frontendBuild/hmrWatcher.ts +1 -1
  35. package/frontendBuild/pagesBundleBuilder.test.ts +98 -0
  36. package/frontendBuild/pagesBundleBuilder.ts +64 -9
  37. package/frontendBuild/routeClientBuilder.ts +1 -1
  38. package/frontendBuild/sourceMtimeIndex.test.ts +1 -1
  39. package/frontendBuild/themeValidator.test.ts +83 -2
  40. package/frontendBuild/themeValidator.ts +37 -15
  41. package/getRelatedCnsts.ts +2 -1
  42. package/integration/devStabilityHarness.ts +5 -3
  43. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +1 -0
  44. package/lint/__fixtures__/no-deep-internal-import/good.tsx +1 -0
  45. package/lint/__fixtures__/no-import-client-functions/bad.tsx +1 -0
  46. package/lint/__fixtures__/no-import-client-functions/good.tsx +1 -0
  47. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +1 -0
  48. package/lint/__fixtures__/no-import-client-in-server/good.tsx +1 -0
  49. package/lint/__fixtures__/no-import-external-library/bad.tsx +1 -0
  50. package/lint/__fixtures__/no-import-external-library/good.tsx +1 -0
  51. package/lint/__fixtures__/no-import-external-library-in-test/bad.tsx +1 -0
  52. package/lint/__fixtures__/no-import-external-library-in-test/good.tsx +1 -0
  53. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +1 -0
  54. package/lint/__fixtures__/no-import-server-in-client/good.tsx +1 -0
  55. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -1
  56. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +2 -2
  57. package/mobile/mobileTarget.ts +1 -2
  58. package/mobile/nativeConfig.test.ts +3 -0
  59. package/mobile/nativeConfig.ts +3 -2
  60. package/mobile/nativeWebDir.ts +3 -2
  61. package/mobile/updateUploader.test.ts +1 -0
  62. package/mobile/updateUploader.ts +3 -3
  63. package/package.json +2 -2
  64. package/platformTest/LinuxDockerTarget.ts +2 -1
  65. package/platformTest/PlatformTestTarget.ts +7 -5
  66. package/platformTest/WindowsSshTarget.ts +2 -1
  67. package/qualityScanner.ts +1 -1
  68. package/recipeScanner.test.ts +9 -3
  69. package/scanInfo.ts +1 -1
  70. package/subspaceClone.ts +5 -4
  71. package/testHelpers.ts +2 -0
  72. package/transforms/barrelImportsPlugin.ts +1 -1
  73. package/transforms/browserDirnamePlugin.test.ts +88 -0
  74. package/transforms/browserDirnamePlugin.ts +25 -0
  75. package/transforms/rscUseClientTransform.ts +2 -2
  76. package/transforms/transforms.test.ts +18 -2
  77. package/transforms/tsconfigPackageResolver.test.ts +9 -0
  78. package/types.ts +1 -0
  79. package/workflow/artifacts.ts +1 -1
  80. package/workflow/rolloutGate.test.ts +5 -1
  81. package/workflow/source.ts +2 -2
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { realpathSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { tempDirs, writeText } from "../testHelpers";
5
+ import { createBrowserDirnamePlugin } from "./browserDirnamePlugin";
6
+
7
+ const makeTempRoot = tempDirs("akan-devkit-browser-dirname-");
8
+
9
+ const build = async (entry: string) => {
10
+ const result = await Bun.build({
11
+ entrypoints: [entry],
12
+ target: "browser",
13
+ minify: true,
14
+ plugins: [createBrowserDirnamePlugin()],
15
+ });
16
+ expect(result.success).toBe(true);
17
+ return (await Promise.all(result.outputs.map((output) => output.text()))).join("\n");
18
+ };
19
+
20
+ describe("createBrowserDirnamePlugin", () => {
21
+ test("gives a vendor module its path from node_modules on, never the absolute one", async () => {
22
+ const root = await makeTempRoot();
23
+ await writeText(path.join(root, "node_modules/cjsdep/package.json"), '{ "name": "cjsdep", "main": "index.js" }\n');
24
+ await writeText(
25
+ path.join(root, "node_modules/cjsdep/index.js"),
26
+ "module.exports = { dir: __dirname, file: __filename };\n",
27
+ );
28
+ await writeText(
29
+ path.join(root, "node_modules/esmdep/package.json"),
30
+ '{ "name": "esmdep", "type": "module", "main": "index.js" }\n',
31
+ );
32
+ await writeText(
33
+ path.join(root, "node_modules/esmdep/index.js"),
34
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: the fixture module's own source holds a template literal
35
+ 'export const dir = typeof __dirname !== "undefined" ? `${__dirname}/` : "browser";\n',
36
+ );
37
+ await writeText(path.join(root, "node_modules/esmdep/legacy.cjs"), "module.exports = __filename;\n");
38
+ const entry = path.join(root, "entry.ts");
39
+ await writeText(
40
+ entry,
41
+ 'import cjs from "cjsdep";\nimport { dir } from "esmdep";\nimport legacy from "esmdep/legacy.cjs";\nconsole.info(cjs, dir, legacy);\n',
42
+ );
43
+
44
+ const output = await build(entry);
45
+
46
+ expect(output).not.toContain(root);
47
+ expect(output).not.toContain(realpathSync(root));
48
+ expect(output).toContain('"/node_modules/cjsdep"');
49
+ expect(output).toContain('"/node_modules/cjsdep/index.js"');
50
+ expect(output).toContain('"/node_modules/esmdep/"');
51
+ expect(output).toContain('"/node_modules/esmdep/legacy.cjs"');
52
+ });
53
+
54
+ test("leaves a module's own __dirname binding alone", async () => {
55
+ const root = await makeTempRoot();
56
+ await writeText(path.join(root, "node_modules/owndep/package.json"), '{ "name": "owndep", "main": "index.js" }\n');
57
+ await writeText(
58
+ path.join(root, "node_modules/owndep/index.js"),
59
+ 'const __dirname = "own-binding";\nmodule.exports = __dirname;\n',
60
+ );
61
+ const entry = path.join(root, "entry.ts");
62
+ await writeText(entry, 'import own from "owndep";\nconsole.info(own);\n');
63
+
64
+ const output = await build(entry);
65
+
66
+ expect(output).toContain('"own-binding"');
67
+ expect(output).not.toContain("/node_modules/owndep");
68
+ });
69
+
70
+ //? `define` would leak here: Bun applies it to the server modules a macro runs, which then read `/`.
71
+ test("keeps the real __dirname in the modules a macro runs", async () => {
72
+ const root = await makeTempRoot();
73
+ await writeText(path.join(root, "node_modules/fsdep/package.json"), '{ "name": "fsdep", "main": "index.js" }\n');
74
+ await writeText(path.join(root, "node_modules/fsdep/marker.txt"), "beside-me\n");
75
+ await writeText(
76
+ path.join(root, "node_modules/fsdep/index.js"),
77
+ 'const fs = require("node:fs");\nmodule.exports = () => fs.readFileSync(__dirname + "/marker.txt", "utf8").trim();\n',
78
+ );
79
+ await writeText(
80
+ path.join(root, "macro.ts"),
81
+ 'import readMarker from "fsdep";\nexport const marker = () => readMarker();\n',
82
+ );
83
+ const entry = path.join(root, "entry.ts");
84
+ await writeText(entry, 'import { marker } from "./macro" with { type: "macro" };\nconsole.info(marker());\n');
85
+
86
+ expect(await build(entry)).toContain('"beside-me"');
87
+ });
88
+ });
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import type { BunPlugin } from "bun";
3
+
4
+ const vendorScript = /[\\/]node_modules[\\/].+\.[cm]?js$/;
5
+
6
+ //* Bun's browser build declares `__dirname` / `__filename` in each module naming them as the absolute path it read the
7
+ //* module from, so a CSR bundle carried the checkout location and its hash moved with it. A vendor module gets its path
8
+ //* from `node_modules` on instead. Not `define`: Bun applies that to the modules a macro imports and runs, where a
9
+ //* server module reading files beside itself would read `/`, while a plugin never sees the macro graph.
10
+ export const createBrowserDirnamePlugin = (): BunPlugin => ({
11
+ name: "akan-browser-dirname",
12
+ setup(build) {
13
+ build.onLoad({ filter: vendorScript }, async (args) => {
14
+ const source = await Bun.file(args.path).text();
15
+ if (!source.includes("__dirname") && !source.includes("__filename")) return undefined;
16
+ const posixPath = args.path.replaceAll("\\", "/");
17
+ const filename = posixPath.slice(posixPath.indexOf("/node_modules/"));
18
+ const define = { __dirname: JSON.stringify(path.posix.dirname(filename)), __filename: JSON.stringify(filename) };
19
+ return {
20
+ loader: "js",
21
+ contents: new Bun.Transpiler({ loader: "js", target: "browser", define }).transformSync(source),
22
+ };
23
+ });
24
+ },
25
+ });
@@ -2,9 +2,9 @@ import path from "node:path";
2
2
  import ts from "typescript";
3
3
  import { loaderFor } from "./moduleSyntax";
4
4
 
5
- const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
5
+ const USE_CLIENT_RE = /^\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
6
6
  const IMPLICIT_ROOT_LAYOUT_RE =
7
- /[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\].*__root_layout)\.(tsx|ts|jsx|js)$/;
7
+ /[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\][^/\\]*__root_layout)\.(tsx|ts|jsx|js)$/;
8
8
  const STAR_REEXPORT_HINT_RE = /\bexport\s*\*\s*from\b/;
9
9
 
10
10
  export interface UseClientTransformArgs {
@@ -4,7 +4,12 @@ import { ApplicationBuildReporter } from "../applicationBuildReporter";
4
4
  import { tempDirs, writeText as write } from "../testHelpers";
5
5
  import { BarrelAnalyzer, type BarrelExportTarget } from "./barrelAnalyzer";
6
6
  import { rewriteBarrelImports } from "./barrelImportsPlugin";
7
- import { scanUseClientExports, toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
7
+ import {
8
+ hasUseClientDirective,
9
+ scanUseClientExports,
10
+ toClientReferencePath,
11
+ transformUseClient,
12
+ } from "./rscUseClientTransform";
8
13
  import { createUseClientBundlePlugin } from "./useClientBundlePlugin";
9
14
 
10
15
  const makeTempRoot = tempDirs("akan-devkit-transform-");
@@ -27,6 +32,12 @@ describe("transformUseClient", () => {
27
32
  ).toBeNull();
28
33
  });
29
34
 
35
+ test("reads the directive through the leading comments only, in linear time", () => {
36
+ expect(hasUseClientDirective('/* a */\n// b /* c\n/** d **/\n"use client";')).toBe(true);
37
+ expect(hasUseClientDirective('/* a */ x /* b */ "use client";')).toBe(false);
38
+ expect(hasUseClientDirective(`${"/* a */\n".repeat(40)}export const value = 1;`)).toBe(false);
39
+ });
40
+
30
41
  test("a client module with no exports stays server code and is not reported as a client module", () => {
31
42
  const reported: string[] = [];
32
43
  const onClientModule = (file: string) => reported.push(file);
@@ -93,7 +104,12 @@ describe('export * in a "use client" module', () => {
93
104
  "export const text = \"export * from './a string'\";",
94
105
  "",
95
106
  ].join("\n");
96
- expect(scanUseClientExports(shapes, file).sort()).toEqual(["LeaveInfo", "Voc", "ns", "text"]);
107
+ expect(scanUseClientExports(shapes, file).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))).toEqual([
108
+ "LeaveInfo",
109
+ "Voc",
110
+ "ns",
111
+ "text",
112
+ ]);
97
113
  expect(scanUseClientExports('export * from "./a";\nexport const X = 1;\n', file)).toEqual(["X"]);
98
114
  const stub = transformUseClient(shapes, { path: file, workspaceRoot: "/repo" });
99
115
  expect(stub).toContain("export const LeaveInfo = registerClientReference");
@@ -167,6 +167,15 @@ describe("createTsconfigPackageResolver — node_modules fallback", () => {
167
167
  expect((await resolve("akanjs/signal"))?.entryFile).toBe(entryFile);
168
168
  });
169
169
 
170
+ test("substitutes every * of a wildcard target, as Node does", async () => {
171
+ const root = await makeTempRoot();
172
+ await writePackageJson(root, "akanjs", { exports: { "./*": "./*/*.js" } });
173
+ const entryFile = await write(root, "node_modules/akanjs/signal/signal.js");
174
+ const resolve = await resolverFor(root, {});
175
+
176
+ expect((await resolve("akanjs/signal"))?.entryFile).toBe(entryFile);
177
+ });
178
+
170
179
  test("resolves a scoped package subpath", async () => {
171
180
  // Vendor-neutral on purpose: `packageExports.test.ts` reads every `"@akanjs/devkit/<x>"` literal here as an import.
172
181
  const root = await makeTempRoot();
package/types.ts CHANGED
@@ -44,4 +44,5 @@ export interface BaseDevEnv {
44
44
  env: "testing" | "debug" | "develop" | "main" | "local";
45
45
  portOffset: number;
46
46
  appName?: string | undefined;
47
+ defaulted?: ("AKAN_PUBLIC_REPO_NAME" | "AKAN_PUBLIC_SERVE_DOMAIN")[];
47
48
  }
@@ -205,7 +205,7 @@ const syncStateSlug = (target: string) =>
205
205
  target
206
206
  .trim()
207
207
  .replace(/[^a-zA-Z0-9]+/g, "-")
208
- .replace(/^-+|-+$/g, "")
208
+ .replace(/^-|-$/g, "")
209
209
  .toLowerCase();
210
210
 
211
211
  export const workflowSyncStatePath = (target: string) => `${workflowSyncDir}/${syncStateSlug(target) || "target"}.json`;
@@ -56,7 +56,11 @@ describe("tooling rollout gate", () => {
56
56
  }),
57
57
  );
58
58
 
59
- expect(violations.map((violation) => `${violation.packageName}:${violation.status}`).sort()).toEqual([
59
+ expect(
60
+ violations
61
+ .map((violation) => `${violation.packageName}:${violation.status}`)
62
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
63
+ ).toEqual([
60
64
  "@ttsc/graph:reference-only",
61
65
  "ast-grep:experiment-only",
62
66
  "recast:experiment-only",
@@ -32,7 +32,7 @@ export const sourceFile = (sys: Sys, path: string, action: PrimitiveChangedFile[
32
32
  export const moduleComponentName = (moduleName: string) =>
33
33
  moduleName
34
34
  .replace(/[-_]+/g, " ")
35
- .replace(/(?:^|\s+)([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase())
35
+ .replace(/(?<=^|\s)[a-zA-Z0-9]/g, (char) => char.toUpperCase())
36
36
  .replace(/\s+/g, "");
37
37
 
38
38
  export const moduleSourcePaths = (moduleName: string) => {
@@ -768,7 +768,7 @@ export const insertTemplateField = ({
768
768
 
769
769
  export const ensureEnumImport = (content: string) => {
770
770
  if (content.includes("enumOf")) return content;
771
- const baseImport = /import \{ ([^}]+) \} from "akanjs\/base";/.exec(content);
771
+ const baseImport = /import \{ ([^{}]+) \} from "akanjs\/base";/.exec(content);
772
772
  if (baseImport) {
773
773
  const names = baseImport[1]?.split(",").map((name) => name.trim()) ?? [];
774
774
  return content.replace(baseImport[0], `import { ${[...names, "enumOf"].sort().join(", ")} } from "akanjs/base";`);