plgg-bundle 0.0.1

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 (235) hide show
  1. package/README.md +68 -0
  2. package/bin/appAliasHook.mjs +89 -0
  3. package/bin/hook.mjs +94 -0
  4. package/bin/plgg-bundle.mjs +16 -0
  5. package/dist/Dev/fixtures/devPoc.spec.d.ts +2 -0
  6. package/dist/Dev/fixtures/devPoc.spec.d.ts.map +1 -0
  7. package/dist/Dev/fixtures/devPoc.spec.js +145 -0
  8. package/dist/Dev/fixtures/devPoc.spec.js.map +1 -0
  9. package/dist/Dev/model/Fetch.d.ts +20 -0
  10. package/dist/Dev/model/Fetch.d.ts.map +1 -0
  11. package/dist/Dev/model/Fetch.js +8 -0
  12. package/dist/Dev/model/Fetch.js.map +1 -0
  13. package/dist/Dev/model/ModuleGraph.d.ts +27 -0
  14. package/dist/Dev/model/ModuleGraph.d.ts.map +1 -0
  15. package/dist/Dev/model/ModuleGraph.js +2 -0
  16. package/dist/Dev/model/ModuleGraph.js.map +1 -0
  17. package/dist/Dev/model/Protocol.d.ts +22 -0
  18. package/dist/Dev/model/Protocol.d.ts.map +1 -0
  19. package/dist/Dev/model/Protocol.js +31 -0
  20. package/dist/Dev/model/Protocol.js.map +1 -0
  21. package/dist/Dev/node/devServer.d.ts +21 -0
  22. package/dist/Dev/node/devServer.d.ts.map +1 -0
  23. package/dist/Dev/node/devServer.js +298 -0
  24. package/dist/Dev/node/devServer.js.map +1 -0
  25. package/dist/Dev/node/httpAdapter.d.ts +19 -0
  26. package/dist/Dev/node/httpAdapter.d.ts.map +1 -0
  27. package/dist/Dev/node/httpAdapter.js +76 -0
  28. package/dist/Dev/node/httpAdapter.js.map +1 -0
  29. package/dist/Dev/node/scanGraph.d.ts +21 -0
  30. package/dist/Dev/node/scanGraph.d.ts.map +1 -0
  31. package/dist/Dev/node/scanGraph.js +145 -0
  32. package/dist/Dev/node/scanGraph.js.map +1 -0
  33. package/dist/Dev/node/watch.d.ts +10 -0
  34. package/dist/Dev/node/watch.d.ts.map +1 -0
  35. package/dist/Dev/node/watch.js +44 -0
  36. package/dist/Dev/node/watch.js.map +1 -0
  37. package/dist/Dev/usecase/allowedHost.d.ts +16 -0
  38. package/dist/Dev/usecase/allowedHost.d.ts.map +1 -0
  39. package/dist/Dev/usecase/allowedHost.js +33 -0
  40. package/dist/Dev/usecase/allowedHost.js.map +1 -0
  41. package/dist/Dev/usecase/allowedHost.spec.d.ts +2 -0
  42. package/dist/Dev/usecase/allowedHost.spec.d.ts.map +1 -0
  43. package/dist/Dev/usecase/allowedHost.spec.js +13 -0
  44. package/dist/Dev/usecase/allowedHost.spec.js.map +1 -0
  45. package/dist/Dev/usecase/buildGraph.d.ts +12 -0
  46. package/dist/Dev/usecase/buildGraph.d.ts.map +1 -0
  47. package/dist/Dev/usecase/buildGraph.js +24 -0
  48. package/dist/Dev/usecase/buildGraph.js.map +1 -0
  49. package/dist/Dev/usecase/buildGraph.spec.d.ts +2 -0
  50. package/dist/Dev/usecase/buildGraph.spec.d.ts.map +1 -0
  51. package/dist/Dev/usecase/buildGraph.spec.js +16 -0
  52. package/dist/Dev/usecase/buildGraph.spec.js.map +1 -0
  53. package/dist/Dev/usecase/decorateDevHtml.d.ts +12 -0
  54. package/dist/Dev/usecase/decorateDevHtml.d.ts.map +1 -0
  55. package/dist/Dev/usecase/decorateDevHtml.js +15 -0
  56. package/dist/Dev/usecase/decorateDevHtml.js.map +1 -0
  57. package/dist/Dev/usecase/decorateDevHtml.spec.d.ts +2 -0
  58. package/dist/Dev/usecase/decorateDevHtml.spec.d.ts.map +1 -0
  59. package/dist/Dev/usecase/decorateDevHtml.spec.js +19 -0
  60. package/dist/Dev/usecase/decorateDevHtml.spec.js.map +1 -0
  61. package/dist/Dev/usecase/invalidate.d.ts +17 -0
  62. package/dist/Dev/usecase/invalidate.d.ts.map +1 -0
  63. package/dist/Dev/usecase/invalidate.js +57 -0
  64. package/dist/Dev/usecase/invalidate.js.map +1 -0
  65. package/dist/Dev/usecase/invalidate.spec.d.ts +2 -0
  66. package/dist/Dev/usecase/invalidate.spec.d.ts.map +1 -0
  67. package/dist/Dev/usecase/invalidate.spec.js +64 -0
  68. package/dist/Dev/usecase/invalidate.spec.js.map +1 -0
  69. package/dist/Dev/usecase/parseImports.d.ts +24 -0
  70. package/dist/Dev/usecase/parseImports.d.ts.map +1 -0
  71. package/dist/Dev/usecase/parseImports.js +40 -0
  72. package/dist/Dev/usecase/parseImports.js.map +1 -0
  73. package/dist/Dev/usecase/parseImports.spec.d.ts +2 -0
  74. package/dist/Dev/usecase/parseImports.spec.d.ts.map +1 -0
  75. package/dist/Dev/usecase/parseImports.spec.js +24 -0
  76. package/dist/Dev/usecase/parseImports.spec.js.map +1 -0
  77. package/dist/Dev/usecase/reloadDecision.d.ts +20 -0
  78. package/dist/Dev/usecase/reloadDecision.d.ts.map +1 -0
  79. package/dist/Dev/usecase/reloadDecision.js +52 -0
  80. package/dist/Dev/usecase/reloadDecision.js.map +1 -0
  81. package/dist/Dev/usecase/reloadDecision.spec.d.ts +2 -0
  82. package/dist/Dev/usecase/reloadDecision.spec.d.ts.map +1 -0
  83. package/dist/Dev/usecase/reloadDecision.spec.js +17 -0
  84. package/dist/Dev/usecase/reloadDecision.spec.js.map +1 -0
  85. package/dist/domain/model/BundleConfig.d.ts +136 -0
  86. package/dist/domain/model/BundleConfig.d.ts.map +1 -0
  87. package/dist/domain/model/BundleConfig.js +18 -0
  88. package/dist/domain/model/BundleConfig.js.map +1 -0
  89. package/dist/domain/model/BundleConfig.spec.d.ts +2 -0
  90. package/dist/domain/model/BundleConfig.spec.d.ts.map +1 -0
  91. package/dist/domain/model/BundleConfig.spec.js +8 -0
  92. package/dist/domain/model/BundleConfig.spec.js.map +1 -0
  93. package/dist/domain/usecase/asBundleConfig.d.ts +17 -0
  94. package/dist/domain/usecase/asBundleConfig.d.ts.map +1 -0
  95. package/dist/domain/usecase/asBundleConfig.js +179 -0
  96. package/dist/domain/usecase/asBundleConfig.js.map +1 -0
  97. package/dist/domain/usecase/asBundleConfig.spec.d.ts +2 -0
  98. package/dist/domain/usecase/asBundleConfig.spec.d.ts.map +1 -0
  99. package/dist/domain/usecase/asBundleConfig.spec.js +119 -0
  100. package/dist/domain/usecase/asBundleConfig.spec.js.map +1 -0
  101. package/dist/domain/usecase/build.d.ts +14 -0
  102. package/dist/domain/usecase/build.d.ts.map +1 -0
  103. package/dist/domain/usecase/build.js +177 -0
  104. package/dist/domain/usecase/build.js.map +1 -0
  105. package/dist/domain/usecase/collectModules.d.ts +53 -0
  106. package/dist/domain/usecase/collectModules.d.ts.map +1 -0
  107. package/dist/domain/usecase/collectModules.js +118 -0
  108. package/dist/domain/usecase/collectModules.js.map +1 -0
  109. package/dist/domain/usecase/deriveExternal.d.ts +25 -0
  110. package/dist/domain/usecase/deriveExternal.d.ts.map +1 -0
  111. package/dist/domain/usecase/deriveExternal.js +79 -0
  112. package/dist/domain/usecase/deriveExternal.js.map +1 -0
  113. package/dist/domain/usecase/deriveExternal.spec.d.ts +2 -0
  114. package/dist/domain/usecase/deriveExternal.spec.d.ts.map +1 -0
  115. package/dist/domain/usecase/deriveExternal.spec.js +56 -0
  116. package/dist/domain/usecase/deriveExternal.spec.js.map +1 -0
  117. package/dist/domain/usecase/discoverWorkspace.d.ts +27 -0
  118. package/dist/domain/usecase/discoverWorkspace.d.ts.map +1 -0
  119. package/dist/domain/usecase/discoverWorkspace.js +116 -0
  120. package/dist/domain/usecase/discoverWorkspace.js.map +1 -0
  121. package/dist/domain/usecase/discoverWorkspace.spec.d.ts +2 -0
  122. package/dist/domain/usecase/discoverWorkspace.spec.d.ts.map +1 -0
  123. package/dist/domain/usecase/discoverWorkspace.spec.js +23 -0
  124. package/dist/domain/usecase/discoverWorkspace.spec.js.map +1 -0
  125. package/dist/domain/usecase/emitBundle.d.ts +43 -0
  126. package/dist/domain/usecase/emitBundle.d.ts.map +1 -0
  127. package/dist/domain/usecase/emitBundle.js +157 -0
  128. package/dist/domain/usecase/emitBundle.js.map +1 -0
  129. package/dist/domain/usecase/emitBundle.spec.d.ts +2 -0
  130. package/dist/domain/usecase/emitBundle.spec.d.ts.map +1 -0
  131. package/dist/domain/usecase/emitBundle.spec.js +80 -0
  132. package/dist/domain/usecase/emitBundle.spec.js.map +1 -0
  133. package/dist/domain/usecase/emitDts.d.ts +32 -0
  134. package/dist/domain/usecase/emitDts.d.ts.map +1 -0
  135. package/dist/domain/usecase/emitDts.js +99 -0
  136. package/dist/domain/usecase/emitDts.js.map +1 -0
  137. package/dist/domain/usecase/isExternal.d.ts +12 -0
  138. package/dist/domain/usecase/isExternal.d.ts.map +1 -0
  139. package/dist/domain/usecase/isExternal.js +16 -0
  140. package/dist/domain/usecase/isExternal.js.map +1 -0
  141. package/dist/domain/usecase/isExternal.spec.d.ts +2 -0
  142. package/dist/domain/usecase/isExternal.spec.d.ts.map +1 -0
  143. package/dist/domain/usecase/isExternal.spec.js +16 -0
  144. package/dist/domain/usecase/isExternal.spec.js.map +1 -0
  145. package/dist/domain/usecase/resolveSpecifier.d.ts +35 -0
  146. package/dist/domain/usecase/resolveSpecifier.d.ts.map +1 -0
  147. package/dist/domain/usecase/resolveSpecifier.js +83 -0
  148. package/dist/domain/usecase/resolveSpecifier.js.map +1 -0
  149. package/dist/domain/usecase/resolveSpecifier.spec.d.ts +2 -0
  150. package/dist/domain/usecase/resolveSpecifier.spec.d.ts.map +1 -0
  151. package/dist/domain/usecase/resolveSpecifier.spec.js +44 -0
  152. package/dist/domain/usecase/resolveSpecifier.spec.js.map +1 -0
  153. package/dist/domain/usecase/resolveWorkspaceSpecifier.d.ts +29 -0
  154. package/dist/domain/usecase/resolveWorkspaceSpecifier.d.ts.map +1 -0
  155. package/dist/domain/usecase/resolveWorkspaceSpecifier.js +89 -0
  156. package/dist/domain/usecase/resolveWorkspaceSpecifier.js.map +1 -0
  157. package/dist/domain/usecase/resolveWorkspaceSpecifier.spec.d.ts +2 -0
  158. package/dist/domain/usecase/resolveWorkspaceSpecifier.spec.d.ts.map +1 -0
  159. package/dist/domain/usecase/resolveWorkspaceSpecifier.spec.js +27 -0
  160. package/dist/domain/usecase/resolveWorkspaceSpecifier.spec.js.map +1 -0
  161. package/dist/domain/usecase/rewriteDtsAliases.d.ts +25 -0
  162. package/dist/domain/usecase/rewriteDtsAliases.d.ts.map +1 -0
  163. package/dist/domain/usecase/rewriteDtsAliases.js +71 -0
  164. package/dist/domain/usecase/rewriteDtsAliases.js.map +1 -0
  165. package/dist/domain/usecase/rewriteDtsAliases.spec.d.ts +2 -0
  166. package/dist/domain/usecase/rewriteDtsAliases.spec.d.ts.map +1 -0
  167. package/dist/domain/usecase/rewriteDtsAliases.spec.js +35 -0
  168. package/dist/domain/usecase/rewriteDtsAliases.spec.js.map +1 -0
  169. package/dist/entrypoints/cli.d.ts +2 -0
  170. package/dist/entrypoints/cli.d.ts.map +1 -0
  171. package/dist/entrypoints/cli.js +79 -0
  172. package/dist/entrypoints/cli.js.map +1 -0
  173. package/dist/index.d.ts +21 -0
  174. package/dist/index.d.ts.map +1 -0
  175. package/dist/index.js +28 -0
  176. package/dist/index.js.map +1 -0
  177. package/dist/vendors/runner.d.ts +20 -0
  178. package/dist/vendors/runner.d.ts.map +1 -0
  179. package/dist/vendors/runner.js +60 -0
  180. package/dist/vendors/runner.js.map +1 -0
  181. package/dist/vendors/runner.spec.d.ts +2 -0
  182. package/dist/vendors/runner.spec.d.ts.map +1 -0
  183. package/dist/vendors/runner.spec.js +20 -0
  184. package/dist/vendors/runner.spec.js.map +1 -0
  185. package/dist/vendors/transpiler.d.ts +19 -0
  186. package/dist/vendors/transpiler.d.ts.map +1 -0
  187. package/dist/vendors/transpiler.js +42 -0
  188. package/dist/vendors/transpiler.js.map +1 -0
  189. package/package.json +31 -0
  190. package/src/Dev/fixtures/devPoc.spec.ts +235 -0
  191. package/src/Dev/model/Fetch.ts +31 -0
  192. package/src/Dev/model/ModuleGraph.ts +30 -0
  193. package/src/Dev/model/Protocol.ts +35 -0
  194. package/src/Dev/node/devServer.ts +503 -0
  195. package/src/Dev/node/httpAdapter.ts +162 -0
  196. package/src/Dev/node/scanGraph.ts +203 -0
  197. package/src/Dev/node/watch.ts +54 -0
  198. package/src/Dev/usecase/allowedHost.spec.ts +50 -0
  199. package/src/Dev/usecase/allowedHost.ts +39 -0
  200. package/src/Dev/usecase/buildGraph.spec.ts +30 -0
  201. package/src/Dev/usecase/buildGraph.ts +31 -0
  202. package/src/Dev/usecase/decorateDevHtml.spec.ts +39 -0
  203. package/src/Dev/usecase/decorateDevHtml.ts +21 -0
  204. package/src/Dev/usecase/invalidate.spec.ts +88 -0
  205. package/src/Dev/usecase/invalidate.ts +67 -0
  206. package/src/Dev/usecase/parseImports.spec.ts +58 -0
  207. package/src/Dev/usecase/parseImports.ts +50 -0
  208. package/src/Dev/usecase/reloadDecision.spec.ts +50 -0
  209. package/src/Dev/usecase/reloadDecision.ts +65 -0
  210. package/src/domain/model/BundleConfig.spec.ts +35 -0
  211. package/src/domain/model/BundleConfig.ts +162 -0
  212. package/src/domain/usecase/asBundleConfig.spec.ts +230 -0
  213. package/src/domain/usecase/asBundleConfig.ts +257 -0
  214. package/src/domain/usecase/build.ts +259 -0
  215. package/src/domain/usecase/collectModules.ts +226 -0
  216. package/src/domain/usecase/deriveExternal.spec.ts +125 -0
  217. package/src/domain/usecase/deriveExternal.ts +108 -0
  218. package/src/domain/usecase/discoverWorkspace.spec.ts +63 -0
  219. package/src/domain/usecase/discoverWorkspace.ts +183 -0
  220. package/src/domain/usecase/emitBundle.spec.ts +162 -0
  221. package/src/domain/usecase/emitBundle.ts +215 -0
  222. package/src/domain/usecase/emitDts.ts +156 -0
  223. package/src/domain/usecase/isExternal.spec.ts +53 -0
  224. package/src/domain/usecase/isExternal.ts +20 -0
  225. package/src/domain/usecase/resolveSpecifier.spec.ts +112 -0
  226. package/src/domain/usecase/resolveSpecifier.ts +127 -0
  227. package/src/domain/usecase/resolveWorkspaceSpecifier.spec.ts +79 -0
  228. package/src/domain/usecase/resolveWorkspaceSpecifier.ts +120 -0
  229. package/src/domain/usecase/rewriteDtsAliases.spec.ts +123 -0
  230. package/src/domain/usecase/rewriteDtsAliases.ts +133 -0
  231. package/src/entrypoints/cli.ts +105 -0
  232. package/src/index.ts +28 -0
  233. package/src/vendors/runner.spec.ts +50 -0
  234. package/src/vendors/runner.ts +78 -0
  235. package/src/vendors/transpiler.ts +48 -0
@@ -0,0 +1,226 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { relative } from "node:path";
3
+ import { type External } from "plgg-bundle/domain/model/BundleConfig";
4
+ import { resolveSpecifier } from "plgg-bundle/domain/usecase/resolveSpecifier";
5
+ import { isExternal } from "plgg-bundle/domain/usecase/isExternal";
6
+ import { transpileToCjs } from "plgg-bundle/vendors/transpiler";
7
+
8
+ /**
9
+ * One bundled module: its stable id (path relative to
10
+ * the package root, the same string the runtime keys
11
+ * on), its transpiled-and-rewritten CJS body, and the
12
+ * external specifiers it still requires at runtime.
13
+ */
14
+ export type Module = Readonly<{
15
+ id: string;
16
+ code: string;
17
+ externals: ReadonlyArray<string>;
18
+ }>;
19
+
20
+ /**
21
+ * The module graph reachable from an entry — the set of
22
+ * reachable modules plus the entry's own id. Dependency
23
+ * order is not required: the registry runtime links
24
+ * lazily by id.
25
+ */
26
+ export type Graph = Readonly<{
27
+ entryId: string;
28
+ modules: ReadonlyArray<Module>;
29
+ }>;
30
+
31
+ /**
32
+ * Walk the import graph from `entryFile`, transpiling
33
+ * each `.ts` to CJS, resolving every `require(...)`
34
+ * specifier to either an intra-bundle module id (to
35
+ * inline) or an external (to keep as a runtime
36
+ * require), and rewriting the inlined `require("spec")`
37
+ * calls to `require("<id>")`. Pure in-house graph logic;
38
+ * only the per-file TS→JS step is vendored. Throws on an
39
+ * unresolvable import or a read failure.
40
+ */
41
+ export const collectModules = (args: {
42
+ entryFile: string;
43
+ root: string;
44
+ aliasPrefix: string;
45
+ aliasSrcRoot: string;
46
+ external: External;
47
+ /**
48
+ * App-mode override: resolve a specifier to a source
49
+ * file (inlining workspace siblings). When absent, the
50
+ * library default resolves only relative imports and
51
+ * the package's own self-alias.
52
+ */
53
+ resolve?: Resolve;
54
+ }): Graph => {
55
+ const ctx: Ctx = {
56
+ root: args.root,
57
+ external: args.external,
58
+ resolve:
59
+ args.resolve ??
60
+ ((specifier, fromFile) =>
61
+ resolveSpecifier({
62
+ specifier,
63
+ fromFile,
64
+ aliasPrefix: args.aliasPrefix,
65
+ aliasSrcRoot: args.aliasSrcRoot,
66
+ })),
67
+ };
68
+ const acc = new Map<string, Module>();
69
+ walk(ctx, args.entryFile, acc);
70
+ return {
71
+ entryId: idOf(args.root, args.entryFile),
72
+ modules: [...acc.values()],
73
+ };
74
+ };
75
+
76
+ /**
77
+ * Resolve an import specifier (from `fromFile`) to an
78
+ * absolute source file, or `undefined` if it is not a
79
+ * bundled module (external / unresolvable).
80
+ */
81
+ export type Resolve = (
82
+ specifier: string,
83
+ fromFile: string,
84
+ ) => string | undefined;
85
+
86
+ /**
87
+ * The threaded walk context: the package root (for ids),
88
+ * the external predicate, and the resolver.
89
+ */
90
+ type Ctx = Readonly<{
91
+ root: string;
92
+ external: External;
93
+ resolve: Resolve;
94
+ }>;
95
+
96
+ /**
97
+ * Depth-first accumulation into a Map keyed by id, so a
98
+ * diamond import is transpiled once. Mutates `acc`.
99
+ */
100
+ const walk = (
101
+ ctx: Ctx,
102
+ file: string,
103
+ acc: Map<string, Module>,
104
+ ): void => {
105
+ const id = idOf(ctx.root, file);
106
+ if (acc.has(id)) {
107
+ return;
108
+ }
109
+ const cjs = transpileToCjs(
110
+ file,
111
+ readSource(file),
112
+ );
113
+ const deps = linkModule(
114
+ ctx,
115
+ file,
116
+ id,
117
+ cjs,
118
+ acc,
119
+ );
120
+ for (const depFile of deps) {
121
+ walk(ctx, depFile, acc);
122
+ }
123
+ };
124
+
125
+ /**
126
+ * Resolve and rewrite every `require(...)` in a
127
+ * transpiled module, register it in `acc`, and return
128
+ * the resolved file paths of its intra-bundle deps.
129
+ * Throws on an unresolvable (non-external) specifier.
130
+ */
131
+ const linkModule = (
132
+ ctx: Ctx,
133
+ file: string,
134
+ id: string,
135
+ cjs: string,
136
+ acc: Map<string, Module>,
137
+ ): ReadonlyArray<string> => {
138
+ const specifiers = requireSpecifiers(cjs);
139
+ const externals: string[] = [];
140
+ const deps: string[] = [];
141
+ let rewritten = cjs;
142
+ for (const spec of specifiers) {
143
+ if (isExternal(ctx.external, spec)) {
144
+ externals.push(spec);
145
+ continue;
146
+ }
147
+ const resolved = ctx.resolve(spec, file);
148
+ if (resolved === undefined) {
149
+ throw new Error(
150
+ `ResolveError: cannot resolve "${spec}" from ${id}`,
151
+ );
152
+ }
153
+ deps.push(resolved);
154
+ rewritten = replaceRequire(
155
+ rewritten,
156
+ spec,
157
+ idOf(ctx.root, resolved),
158
+ );
159
+ }
160
+ acc.set(id, {
161
+ id,
162
+ code: rewritten,
163
+ externals,
164
+ });
165
+ return deps;
166
+ };
167
+
168
+ /**
169
+ * Read a source file, re-throwing with context.
170
+ */
171
+ const readSource = (file: string): string => {
172
+ try {
173
+ return readFileSync(file, "utf8");
174
+ } catch (cause) {
175
+ throw new Error(
176
+ `IoError: cannot read ${file}: ${
177
+ cause instanceof Error
178
+ ? cause.message
179
+ : String(cause)
180
+ }`,
181
+ );
182
+ }
183
+ };
184
+
185
+ /**
186
+ * A module's stable id: its path relative to the
187
+ * package root, POSIX-normalized. The runtime keys on
188
+ * exactly this string.
189
+ */
190
+ const idOf = (
191
+ root: string,
192
+ file: string,
193
+ ): string =>
194
+ relative(root, file).split("\\").join("/");
195
+
196
+ /**
197
+ * Extract the literal string specifiers of all
198
+ * top-level `require("...")` / `require('...')` calls
199
+ * a transpiled CJS module makes.
200
+ */
201
+ const requireSpecifiers = (
202
+ cjs: string,
203
+ ): ReadonlyArray<string> =>
204
+ [
205
+ ...cjs.matchAll(
206
+ /require\(\s*["']([^"']+)["']\s*\)/g,
207
+ ),
208
+ ].flatMap((m) =>
209
+ m[1] === undefined ? [] : [m[1]],
210
+ );
211
+
212
+ /**
213
+ * Rewrite every `require("spec")` occurrence to
214
+ * `require("id")`. Operates on the exact specifier
215
+ * string so distinct specifiers do not interfere.
216
+ */
217
+ const replaceRequire = (
218
+ cjs: string,
219
+ spec: string,
220
+ id: string,
221
+ ): string =>
222
+ cjs
223
+ .split(`require("${spec}")`)
224
+ .join(`require("${id}")`)
225
+ .split(`require('${spec}')`)
226
+ .join(`require("${id}")`);
@@ -0,0 +1,125 @@
1
+ import {
2
+ test,
3
+ check,
4
+ all,
5
+ toBe,
6
+ } from "plgg-test";
7
+ import { join } from "node:path";
8
+ import {
9
+ mkdtempSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { deriveExternal } from "plgg-bundle/domain/usecase/deriveExternal";
14
+ import { isExternal } from "plgg-bundle/domain/usecase/isExternal";
15
+
16
+ /** True when `deriveExternal(root)` throws an Error. */
17
+ const rejects = (root: string): boolean => {
18
+ try {
19
+ deriveExternal(root);
20
+ return false;
21
+ } catch (e) {
22
+ return e instanceof Error;
23
+ }
24
+ };
25
+
26
+ /** A temp dir carrying a specific package.json body. */
27
+ const withManifest = (body: string): string => {
28
+ const dir = mkdtempSync(
29
+ join(tmpdir(), "plgg-derive-"),
30
+ );
31
+ writeFileSync(
32
+ join(dir, "package.json"),
33
+ body,
34
+ );
35
+ return dir;
36
+ };
37
+
38
+ // plgg-bundle's own root: its package.json declares
39
+ // `typescript` as the sole dependency.
40
+ const root = join(
41
+ import.meta.dirname,
42
+ "..",
43
+ "..",
44
+ "..",
45
+ );
46
+ const external = deriveExternal(root);
47
+
48
+ test("deriveExternal externalizes declared deps and node builtins", () =>
49
+ all([
50
+ check(
51
+ isExternal(external, "typescript"),
52
+ toBe(true),
53
+ ),
54
+ check(
55
+ isExternal(external, "node:fs"),
56
+ toBe(true),
57
+ ),
58
+ check(
59
+ isExternal(external, "node:child_process"),
60
+ toBe(true),
61
+ ),
62
+ ]));
63
+
64
+ test("deriveExternal externalizes a PUBLIC SUBPATH of a declared dep", () =>
65
+ // `typescript/lib/...` is a subpath of the declared
66
+ // `typescript` dep → external (resolved at runtime via
67
+ // the dep's own exports), mirroring `plgg-server/ssg`.
68
+ check(
69
+ isExternal(external, "typescript/lib/typescript"),
70
+ toBe(true),
71
+ ));
72
+
73
+ test("deriveExternal does NOT externalize undeclared / own-source specifiers", () =>
74
+ all([
75
+ // plgg is not a dependency of plgg-bundle → not
76
+ // external (an import of it would fail loudly).
77
+ check(
78
+ isExternal(external, "plgg"),
79
+ toBe(false),
80
+ ),
81
+ check(
82
+ isExternal(external, "./resolveSpecifier"),
83
+ toBe(false),
84
+ ),
85
+ check(
86
+ isExternal(
87
+ external,
88
+ "plgg-bundle/domain/model/BundleConfig",
89
+ ),
90
+ toBe(false),
91
+ ),
92
+ ]));
93
+
94
+ test("deriveExternal throws when package.json is missing", () =>
95
+ check(
96
+ rejects(
97
+ join(tmpdir(), "plgg-derive-missing-xyz"),
98
+ ),
99
+ toBe(true),
100
+ ));
101
+
102
+ test("deriveExternal throws on an unparseable package.json", () =>
103
+ check(
104
+ rejects(withManifest("{ not valid json")),
105
+ toBe(true),
106
+ ));
107
+
108
+ test("deriveExternal treats a non-object manifest as zero declared deps", () => {
109
+ const ext = deriveExternal(
110
+ withManifest(`"just a string"`),
111
+ );
112
+ return all([
113
+ // node:* is still external by prefix …
114
+ check(
115
+ isExternal(ext, "node:fs"),
116
+ toBe(true),
117
+ ),
118
+ // … but nothing is declared, so any package specifier
119
+ // is undeclared (not external).
120
+ check(
121
+ isExternal(ext, "whatever"),
122
+ toBe(false),
123
+ ),
124
+ ]);
125
+ });
@@ -0,0 +1,108 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { type External } from "plgg-bundle/domain/model/BundleConfig";
4
+
5
+ /**
6
+ * Derive a package's externals from its `package.json`
7
+ * (the declared dependency graph) plus Node builtins —
8
+ * the ruling-B rule, so externals cannot drift from what
9
+ * the manifest says the package depends on. Anything a
10
+ * module imports that is neither the package's own
11
+ * source (self-alias / relative) nor a declared
12
+ * dependency nor `node:*` is an *undeclared* dependency
13
+ * and the graph walk fails loudly rather than silently
14
+ * inlining or externalizing it.
15
+ *
16
+ * Declared = keys of `dependencies` + `peerDependencies`
17
+ * + `optionalDependencies`. A specifier is external when
18
+ * it is a declared dep exactly (`plgg-view`) OR a PUBLIC
19
+ * SUBPATH export of one (`plgg-view/style`,
20
+ * `plgg-server/ssg`) — the consumer resolves the subpath
21
+ * at runtime via the dep's own `exports` map, so a library
22
+ * bundle must never inline it. `node:*` is matched by
23
+ * prefix. An undeclared specifier (neither a dep nor a
24
+ * dep subpath nor `node:*`) still fails the graph walk
25
+ * loudly.
26
+ */
27
+ export const deriveExternal = (
28
+ root: string,
29
+ ): External => {
30
+ const declared = declaredDeps(root);
31
+ return (specifier: string): boolean =>
32
+ declared.has(specifier) ||
33
+ [...declared].some((dep: string): boolean =>
34
+ specifier.startsWith(`${dep}/`),
35
+ ) ||
36
+ specifier.startsWith("node:");
37
+ };
38
+
39
+ /**
40
+ * The set of declared dependency names from a package's
41
+ * `package.json`. Throws (re-thrown with context) if the
42
+ * manifest is missing or unparseable.
43
+ */
44
+ const declaredDeps = (
45
+ root: string,
46
+ ): ReadonlySet<string> => {
47
+ const path = join(root, "package.json");
48
+ const parsed = parse(path, read(path));
49
+ return new Set([
50
+ ...keysOf(parsed, "dependencies"),
51
+ ...keysOf(parsed, "peerDependencies"),
52
+ ...keysOf(parsed, "optionalDependencies"),
53
+ ]);
54
+ };
55
+
56
+ const read = (path: string): string => {
57
+ try {
58
+ return readFileSync(path, "utf8");
59
+ } catch (cause) {
60
+ throw new Error(
61
+ `IoError: cannot read ${path}: ${messageOf(
62
+ cause,
63
+ )}`,
64
+ );
65
+ }
66
+ };
67
+
68
+ const parse = (
69
+ path: string,
70
+ text: string,
71
+ ): unknown => {
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch (cause) {
75
+ throw new Error(
76
+ `ConfigError: ${path} is not valid JSON: ${messageOf(
77
+ cause,
78
+ )}`,
79
+ );
80
+ }
81
+ };
82
+
83
+ /**
84
+ * The keys of one dependency map on a parsed
85
+ * package.json, narrowing at the `unknown` boundary.
86
+ */
87
+ const keysOf = (
88
+ pkg: unknown,
89
+ field: string,
90
+ ): ReadonlyArray<string> => {
91
+ if (!isRecord(pkg)) {
92
+ return [];
93
+ }
94
+ const map = pkg[field];
95
+ return isRecord(map) ? Object.keys(map) : [];
96
+ };
97
+
98
+ /**
99
+ * Whether a value is a non-null object (records the
100
+ * narrowing for property reads).
101
+ */
102
+ const isRecord = (
103
+ v: unknown,
104
+ ): v is Record<string, unknown> =>
105
+ typeof v === "object" && v !== null;
106
+
107
+ const messageOf = (e: unknown): string =>
108
+ e instanceof Error ? e.message : String(e);
@@ -0,0 +1,63 @@
1
+ import {
2
+ test,
3
+ check,
4
+ all,
5
+ toBe,
6
+ } from "plgg-test";
7
+ import { join } from "node:path";
8
+ import { discoverWorkspace } from "plgg-bundle/domain/usecase/discoverWorkspace";
9
+
10
+ // Run against the real monorepo: this package's root is
11
+ // `packages/plgg-bundle`, so discovery scans `packages/*`.
12
+ const bundleRoot = join(
13
+ import.meta.dirname,
14
+ "..",
15
+ "..",
16
+ "..",
17
+ );
18
+
19
+ const packages = discoverWorkspace(bundleRoot);
20
+ const byName = (name: string) =>
21
+ packages.find((p) => p.name === name);
22
+
23
+ test("discoverWorkspace finds sibling packages", () =>
24
+ all([
25
+ check(
26
+ byName("plgg") !== undefined,
27
+ toBe(true),
28
+ ),
29
+ check(
30
+ byName("plgg-view") !== undefined,
31
+ toBe(true),
32
+ ),
33
+ ]));
34
+
35
+ test("discoverWorkspace reads the conditions-only '.' export (plgg)", () =>
36
+ check(
37
+ byName("plgg")?.exports.get("."),
38
+ toBe("./dist/index.es.js"),
39
+ ));
40
+
41
+ test("discoverWorkspace reads a subpath export incl. the styleEntry rename", () =>
42
+ all([
43
+ check(
44
+ byName("plgg-view")?.exports.get(
45
+ "./client",
46
+ ),
47
+ toBe("./dist/client.es.js"),
48
+ ),
49
+ check(
50
+ byName("plgg-view")?.exports.get("./style"),
51
+ toBe("./dist/styleEntry.es.js"),
52
+ ),
53
+ ]));
54
+
55
+ test("discoverWorkspace sorts longest-name first (so plgg-view beats plgg)", () => {
56
+ const iView = packages.findIndex(
57
+ (p) => p.name === "plgg-view",
58
+ );
59
+ const iCore = packages.findIndex(
60
+ (p) => p.name === "plgg",
61
+ );
62
+ return check(iView < iCore, toBe(true));
63
+ });
@@ -0,0 +1,183 @@
1
+ import {
2
+ readdirSync,
3
+ readFileSync,
4
+ existsSync,
5
+ statSync,
6
+ } from "node:fs";
7
+ import { join, dirname } from "node:path";
8
+
9
+ /**
10
+ * A workspace sibling the app bundler can INLINE from
11
+ * source: its package `name`, its `dir`, and the map from
12
+ * each public export subpath (`"."`, `"./client"`, …) to
13
+ * the `import.default` dist path declared in its
14
+ * `package.json` `exports`. The app resolver reverses
15
+ * that dist path to the entry's source file (so the
16
+ * `./style` → `dist/styleEntry.es.js` → `src/styleEntry`
17
+ * rename is honoured), and falls back to a self-alias
18
+ * `name/<path>` → `src/<path>` for non-export internal
19
+ * imports.
20
+ */
21
+ export type WorkspacePackage = Readonly<{
22
+ name: string;
23
+ dir: string;
24
+ /** export subpath (`"."` | `"./x"`) → dist default. */
25
+ exports: ReadonlyMap<string, string>;
26
+ }>;
27
+
28
+ /**
29
+ * Discover every sibling package under the directory that
30
+ * holds `packageRoot` (the monorepo `packages/` dir) —
31
+ * each entry that has a `package.json` with a `name`.
32
+ * Sorted longest-name-first so a later prefix match picks
33
+ * `plgg-view` over `plgg`. Throws on a read/parse failure.
34
+ */
35
+ export const discoverWorkspace = (
36
+ packageRoot: string,
37
+ ): ReadonlyArray<WorkspacePackage> => {
38
+ const siblingsDir = dirname(packageRoot);
39
+ return readdirSync(siblingsDir)
40
+ .map((name) => join(siblingsDir, name))
41
+ .filter(isPackageDir)
42
+ .map(readPackage)
43
+ .flatMap((p) => (p === undefined ? [] : [p]))
44
+ .sort(
45
+ (a, b) => b.name.length - a.name.length,
46
+ );
47
+ };
48
+
49
+ /**
50
+ * Whether a path is a directory holding a
51
+ * `package.json`.
52
+ */
53
+ const isPackageDir = (dir: string): boolean =>
54
+ statSync(dir).isDirectory() &&
55
+ existsSync(join(dir, "package.json"));
56
+
57
+ /**
58
+ * Read one package's manifest into a
59
+ * {@link WorkspacePackage}, or `undefined` when it
60
+ * declares no `name`. Throws on unreadable/invalid JSON.
61
+ */
62
+ const readPackage = (
63
+ dir: string,
64
+ ): WorkspacePackage | undefined => {
65
+ const pkg = parseJson(
66
+ join(dir, "package.json"),
67
+ );
68
+ const name = pkg["name"];
69
+ return typeof name === "string"
70
+ ? { name, dir, exports: exportMap(pkg) }
71
+ : undefined;
72
+ };
73
+
74
+ /**
75
+ * Parse a `package.json`, re-throwing with context.
76
+ */
77
+ const parseJson = (
78
+ path: string,
79
+ ): Record<string, unknown> => {
80
+ try {
81
+ const parsed: unknown = JSON.parse(
82
+ readFileSync(path, "utf8"),
83
+ );
84
+ return isRecord(parsed) ? parsed : {};
85
+ } catch (cause) {
86
+ throw new Error(
87
+ `WorkspaceError: cannot read ${path}: ${
88
+ cause instanceof Error
89
+ ? cause.message
90
+ : String(cause)
91
+ }`,
92
+ );
93
+ }
94
+ };
95
+
96
+ /**
97
+ * Build the subpath → dist-default map from an `exports`
98
+ * field, tolerating both shapes used in the repo:
99
+ * - a subpath map (`{ ".": …, "./client": … }`), and
100
+ * - a bare conditions object (`{ import: …, require: … }`)
101
+ * which IS the `"."` export.
102
+ */
103
+ const exportMap = (
104
+ pkg: Record<string, unknown>,
105
+ ): ReadonlyMap<string, string> => {
106
+ const exports = pkg["exports"];
107
+ const out = new Map<string, string>();
108
+ if (!isRecord(exports)) {
109
+ return out;
110
+ }
111
+ if (isSubpathMap(exports)) {
112
+ for (const [key, val] of Object.entries(
113
+ exports,
114
+ )) {
115
+ const def = conditionDefault(val);
116
+ if (def !== undefined) {
117
+ out.set(key, def);
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+ const def = conditionDefault(exports);
123
+ if (def !== undefined) {
124
+ out.set(".", def);
125
+ }
126
+ return out;
127
+ };
128
+
129
+ /**
130
+ * Whether an `exports` object is a subpath map (its keys
131
+ * are `.`/`./…` paths) rather than a bare conditions
132
+ * object (keys like `import`/`require`).
133
+ */
134
+ const isSubpathMap = (
135
+ exports: Record<string, unknown>,
136
+ ): boolean =>
137
+ Object.keys(exports).some((k) =>
138
+ k.startsWith("."),
139
+ );
140
+
141
+ /**
142
+ * Extract the resolved default file from a conditions
143
+ * object: the `import` (or `require`) branch's `default`,
144
+ * or a directly-stringed condition. Returns `undefined`
145
+ * when none is present.
146
+ */
147
+ const conditionDefault = (
148
+ conditions: unknown,
149
+ ): string | undefined => {
150
+ if (typeof conditions === "string") {
151
+ return conditions;
152
+ }
153
+ if (!isRecord(conditions)) {
154
+ return undefined;
155
+ }
156
+ return (
157
+ branchDefault(conditions["import"]) ??
158
+ branchDefault(conditions["require"]) ??
159
+ branchDefault(conditions["default"])
160
+ );
161
+ };
162
+
163
+ /**
164
+ * The `default` of one condition branch, where the branch
165
+ * is either a string or a `{ types, default }` object.
166
+ */
167
+ const branchDefault = (
168
+ branch: unknown,
169
+ ): string | undefined =>
170
+ typeof branch === "string"
171
+ ? branch
172
+ : isRecord(branch) &&
173
+ typeof branch["default"] === "string"
174
+ ? branch["default"]
175
+ : undefined;
176
+
177
+ /**
178
+ * Whether a value is a non-null object.
179
+ */
180
+ const isRecord = (
181
+ v: unknown,
182
+ ): v is Record<string, unknown> =>
183
+ typeof v === "object" && v !== null;