1 |
|
2 | import { resolve } from "url"
|
3 | import { fileRead, fileWrite } from "@dmail/helper"
|
4 | import { createOperation } from "@dmail/cancellation"
|
5 | import { resolvePath, hrefToPathname, hrefToScheme } from "@jsenv/module-resolution"
|
6 | import {
|
7 | pathnameToOperatingSystemPath,
|
8 | operatingSystemPathToPathname,
|
9 | pathnameToRelativePathname,
|
10 | isWindowsPath,
|
11 | } from "@jsenv/operating-system-path"
|
12 | import { generateImportMapForNodeModules } from "@jsenv/node-module-import-map"
|
13 | import { transformSource, jsenvTransform, findAsyncPluginNameInBabelPluginMap } from "@jsenv/core"
|
14 | import { generateSpecifierMapOption } from "./specifier-map/specifier-map.js"
|
15 | import { generateSpecifierDynamicMapOption } from "./specifier-dynamic-map/specifier-dynamic-map.js"
|
16 | import { generateBabelPluginMapOption } from "./babel-plugin-map/babel-plugin-map.js"
|
17 | import { fetchUsingHttp } from "./helpers/fetchUsingHttp.js"
|
18 | import { writeSourceMappingURL, parseSourceMappingURL } from "./helpers/source-mapping-url.js"
|
19 | import { readProjectImportMap } from "./read-project-import-map.js"
|
20 | import { mergeTwoImportMap } from "./mergeTwoImportMap.js"
|
21 | import { jsenvBundlingProjectPath } from "./jsenv-bundling-project.js"
|
22 |
|
23 | const { minify: minifyCode } = import.meta.require("terser")
|
24 |
|
25 | export const createJsenvRollupPlugin = async ({
|
26 | cancellationToken,
|
27 | projectPathname,
|
28 | importDefaultExtension,
|
29 | importMapRelativePath,
|
30 | importMapForBundle = {},
|
31 | specifierMap,
|
32 | specifierDynamicMap,
|
33 | origin = "http://example.com",
|
34 | babelPluginMap,
|
35 | convertMap,
|
36 | featureNameArray,
|
37 | minify,
|
38 | format,
|
39 | detectAndTransformIfNeededAsyncInsertedByRollup = format === "global",
|
40 | dir,
|
41 | entryPointMap,
|
42 | logger,
|
43 | }) => {
|
44 | const bundlingImportMap = await generateImportMapForNodeModules({
|
45 | projectPath: jsenvBundlingProjectPath,
|
46 | rootProjectPath: pathnameToOperatingSystemPath(projectPathname),
|
47 | })
|
48 | const projectImportMap = await readProjectImportMap({ projectPathname, importMapRelativePath })
|
49 | const importMap = [importMapForBundle, bundlingImportMap, projectImportMap].reduce(
|
50 | (previous, current) => mergeTwoImportMap(previous, current),
|
51 | {},
|
52 | )
|
53 |
|
54 | specifierMap = {
|
55 | ...specifierMap,
|
56 | ...generateSpecifierMapOption(),
|
57 | ["/.jsenv/importMap.json"]: `file://${projectPathname}${importMapRelativePath}`,
|
58 | }
|
59 | const chunkId = `${Object.keys(entryPointMap)[0]}.js`
|
60 | specifierDynamicMap = {
|
61 | ...specifierDynamicMap,
|
62 | ...generateSpecifierDynamicMapOption(),
|
63 | ["/BUNDLE_CONSTANTS.js"]: () => `export const chunkId = ${JSON.stringify(chunkId)}`,
|
64 | }
|
65 | babelPluginMap = generateBabelPluginMapOption({
|
66 | projectPathname,
|
67 | format,
|
68 | babelPluginMap,
|
69 | featureNameArray,
|
70 | })
|
71 |
|
72 |
|
73 | const idSkipTransformArray = []
|
74 | const idLoadMap = {}
|
75 |
|
76 | const getRollupGenerateOptions = async () => {
|
77 | return {}
|
78 | }
|
79 |
|
80 | const jsenvRollupPlugin = {
|
81 | name: "jsenv",
|
82 |
|
83 | resolveId: (specifier, importer) => {
|
84 | if (specifier in specifierDynamicMap) {
|
85 | const specifierDynamicMapping = specifierDynamicMap[specifier]
|
86 | if (typeof specifierDynamicMapping !== "function") {
|
87 | throw new Error(
|
88 | `specifier inside specifierDynamicMap must be functions, found ${specifierDynamicMapping} for ${specifier}`,
|
89 | )
|
90 | }
|
91 |
|
92 | const osPath = pathnameToOperatingSystemPath(`${projectPathname}${specifier}`)
|
93 | idLoadMap[osPath] = specifierDynamicMapping
|
94 | return osPath
|
95 | }
|
96 |
|
97 | if (specifier in specifierMap) {
|
98 | const specifierMapping = specifierMap[specifier]
|
99 | if (typeof specifierMapping !== "string") {
|
100 | throw new Error(
|
101 | `specifier inside specifierMap must be strings, found ${specifierMapping} for ${specifier}`,
|
102 | )
|
103 | }
|
104 | specifier = specifierMapping
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | if (specifier.startsWith("file:///")) {
|
113 | specifier = specifier.slice("file://".length)
|
114 | return pathnameToOperatingSystemPath(specifier)
|
115 | }
|
116 | }
|
117 |
|
118 | if (!importer) {
|
119 | if (specifier[0] === "/") specifier = specifier.slice(1)
|
120 | return pathnameToOperatingSystemPath(`${projectPathname}/${specifier}`)
|
121 | }
|
122 |
|
123 | let importerHref
|
124 | if (isWindowsPath(importer)) {
|
125 | importer = operatingSystemPathToPathname(importer)
|
126 | }
|
127 |
|
128 |
|
129 |
|
130 | if (importer.startsWith(`${projectPathname}/`)) {
|
131 | importerHref = `${origin}${pathnameToRelativePathname(importer, projectPathname)}`
|
132 | } else if (hrefToScheme(importer)) {
|
133 | if (importer.startsWith(`file://${projectPathname}/`)) {
|
134 | importerHref = `${origin}${pathnameToRelativePathname(
|
135 | hrefToPathname(importer),
|
136 | projectPathname,
|
137 | )}`
|
138 | } else {
|
139 |
|
140 |
|
141 | importerHref = importer
|
142 | }
|
143 | } else {
|
144 | throw createImporterOutsideProjectError({ importer, projectPathname })
|
145 | }
|
146 |
|
147 | const id = resolvePath({
|
148 | specifier,
|
149 | importer: importerHref,
|
150 | importMap,
|
151 | defaultExtension: importDefaultExtension,
|
152 | })
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | const resolvedIdIsInsideProject = id.startsWith(`${origin}/`)
|
158 | if (resolvedIdIsInsideProject) {
|
159 | const idPathname = hrefToPathname(id)
|
160 | return pathnameToOperatingSystemPath(`${projectPathname}${idPathname}`)
|
161 | }
|
162 |
|
163 | return id
|
164 | },
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | load: async (id) => {
|
172 | if (id in idLoadMap) {
|
173 | const returnValue = await idLoadMap[id]()
|
174 | if (typeof returnValue === "string") return returnValue
|
175 | if (returnValue.skipTransform) {
|
176 | idSkipTransformArray.push(id)
|
177 | }
|
178 | return returnValue.code
|
179 | }
|
180 |
|
181 | const hasSheme = isWindowsPath(id) ? false : Boolean(hrefToScheme(id))
|
182 | const href = hasSheme ? id : `file://${operatingSystemPathToPathname(id)}`
|
183 | let source = await fetchHref(href)
|
184 |
|
185 | if (id.endsWith(".json")) {
|
186 | source = `export default ${source}`
|
187 | }
|
188 |
|
189 | const sourcemapParsingResult = parseSourceMappingURL(source)
|
190 |
|
191 | if (!sourcemapParsingResult) return { code: source }
|
192 |
|
193 | if (sourcemapParsingResult.sourcemapString)
|
194 | return { code: source, map: JSON.parse(sourcemapParsingResult.sourcemapString) }
|
195 |
|
196 | const resolvedSourceMappingURL = resolve(href, sourcemapParsingResult.sourcemapURL)
|
197 | const sourcemapString = await fetchHref(resolvedSourceMappingURL)
|
198 | return { code: source, map: JSON.parse(sourcemapString) }
|
199 | },
|
200 |
|
201 |
|
202 |
|
203 | transform: async (source, id) => {
|
204 | if (idSkipTransformArray.includes(id)) {
|
205 | return null
|
206 | }
|
207 |
|
208 | let sourceHref
|
209 | if (isWindowsPath(id)) {
|
210 | sourceHref = `file://${operatingSystemPathToPathname(id)}`
|
211 | } else if (hrefToScheme(id)) {
|
212 | sourceHref = id
|
213 | } else {
|
214 | sourceHref = `file://${operatingSystemPathToPathname(id)}`
|
215 | }
|
216 |
|
217 | const { code, map } = await transformSource({
|
218 | projectPathname,
|
219 | source,
|
220 | sourceHref,
|
221 | babelPluginMap,
|
222 | convertMap,
|
223 |
|
224 | transformModuleIntoSystemFormat: false,
|
225 | })
|
226 | return { code, map }
|
227 | },
|
228 |
|
229 | renderChunk: (source) => {
|
230 | if (!minify) return null
|
231 |
|
232 |
|
233 | const minifyOptions = format === "global" ? { toplevel: false } : { toplevel: true }
|
234 | const result = minifyCode(source, {
|
235 | sourceMap: true,
|
236 | ...minifyOptions,
|
237 | })
|
238 | if (result.error) {
|
239 | throw result.error
|
240 | } else {
|
241 | return result
|
242 | }
|
243 | },
|
244 |
|
245 | writeBundle: async (bundle) => {
|
246 | if (detectAndTransformIfNeededAsyncInsertedByRollup) {
|
247 | await transformAsyncInsertedByRollup({ dir, babelPluginMap, bundle })
|
248 | }
|
249 |
|
250 | Object.keys(bundle).forEach((bundleFilename) => {
|
251 | logger.log(`-> ${dir}/${bundleFilename}`)
|
252 | })
|
253 | },
|
254 | }
|
255 |
|
256 | const fetchHref = async (href) => {
|
257 |
|
258 |
|
259 |
|
260 | if (href.startsWith("http://")) {
|
261 | const response = await fetchUsingHttp(href, { cancellationToken })
|
262 | ensureResponseSuccess(response)
|
263 | return response.body
|
264 | }
|
265 |
|
266 | if (href.startsWith("https://")) {
|
267 | const response = await fetchUsingHttp(href, { cancellationToken })
|
268 | ensureResponseSuccess(response)
|
269 | return response.body
|
270 | }
|
271 |
|
272 | if (href.startsWith("file:///")) {
|
273 | const code = await createOperation({
|
274 | cancellationToken,
|
275 | start: () => fileRead(pathnameToOperatingSystemPath(hrefToPathname(href))),
|
276 | })
|
277 | return code
|
278 | }
|
279 |
|
280 | return ""
|
281 | }
|
282 |
|
283 | return {
|
284 | jsenvRollupPlugin,
|
285 | relativePathAbstractArray: Object.keys(specifierDynamicMap),
|
286 | getRollupGenerateOptions,
|
287 | }
|
288 | }
|
289 |
|
290 | const ensureResponseSuccess = ({ url, status }) => {
|
291 | if (status < 200 || status > 299) {
|
292 | throw new Error(`unexpected response status for ${url}, got ${status}`)
|
293 | }
|
294 | }
|
295 |
|
296 | const createImporterOutsideProjectError = ({ importer, projectPathname }) =>
|
297 | new Error(`importer must be inside project
|
298 | importer: ${importer}
|
299 | project: ${pathnameToOperatingSystemPath(projectPathname)}`)
|
300 |
|
301 | const transformAsyncInsertedByRollup = async ({ dir, babelPluginMap, bundle }) => {
|
302 | const asyncPluginName = findAsyncPluginNameInBabelPluginMap(babelPluginMap)
|
303 |
|
304 | if (!asyncPluginName) return
|
305 |
|
306 |
|
307 |
|
308 |
|
309 | await Promise.all(
|
310 | Object.keys(bundle).map(async (bundleFilename) => {
|
311 | const bundleInfo = bundle[bundleFilename]
|
312 |
|
313 | const { code, map } = await jsenvTransform({
|
314 | inputCode: bundleInfo.code,
|
315 | inputMap: bundleInfo.map,
|
316 | inputPath: bundleFilename,
|
317 | babelPluginMap: { [asyncPluginName]: babelPluginMap[asyncPluginName] },
|
318 | transformModuleIntoSystemFormat: false,
|
319 | })
|
320 |
|
321 | await Promise.all([
|
322 | fileWrite(
|
323 | `${dir}/${bundleFilename}`,
|
324 | writeSourceMappingURL(code, `./${bundleFilename}.map`),
|
325 | ),
|
326 | fileWrite(`${dir}/${bundleFilename}.map`, JSON.stringify(map)),
|
327 | ])
|
328 | }),
|
329 | )
|
330 | }
|