|
1 | 1 | import * as fs from "fs"; |
2 | 2 | import * as path from "path"; |
| 3 | +import { exec } from "child_process"; |
3 | 4 | import { schema } from "yaml-cfn"; |
4 | 5 | import yaml from "js-yaml"; |
5 | 6 |
|
@@ -32,11 +33,20 @@ interface IEntryForResult { |
32 | 33 | samConfigs: SamConfig[]; |
33 | 34 | } |
34 | 35 |
|
| 36 | +interface ILayerConfig { |
| 37 | + templateName: string; |
| 38 | + resourceKey: string; |
| 39 | + buildRoot: string; |
| 40 | + contentDir: string; |
| 41 | + buildMethod?: string; |
| 42 | +} |
| 43 | + |
35 | 44 | class AwsSamPlugin { |
36 | 45 | private static defaultTemplates = ["template.yaml", "template.yml"]; |
37 | 46 | private launchConfig: any; |
38 | 47 | private options: AwsSamPluginOptions; |
39 | 48 | private samConfigs: SamConfig[]; |
| 49 | + private layersConfigs: ILayerConfig[] = []; |
40 | 50 |
|
41 | 51 | constructor(options?: Partial<AwsSamPluginOptions>) { |
42 | 52 | this.options = { |
@@ -252,6 +262,42 @@ class AwsSamPlugin { |
252 | 262 | templateName: projectTemplateName, |
253 | 263 | }); |
254 | 264 | } |
| 265 | + |
| 266 | + if (resource.Type === "AWS::Serverless::LayerVersion") { |
| 267 | + const properties = resource.Properties; |
| 268 | + if (!properties || typeof properties !== "object") { |
| 269 | + throw new Error(`${resourceKey} is missing Properties`); |
| 270 | + } |
| 271 | + |
| 272 | + // Check we have a CodeUri |
| 273 | + const contentUri = properties.ContentUri ?? defaultCodeUri; |
| 274 | + if (!contentUri) { |
| 275 | + throw new Error(`${resourceKey} is missing a CodeUri`); |
| 276 | + } |
| 277 | + |
| 278 | + const basePathPrefix = projectPath === "" ? "." : `./${projectPath}`; |
| 279 | + const contentDir = `${basePathPrefix}/${contentUri}`; |
| 280 | + |
| 281 | + const buildMethod = resource.Metadata?.BuildMethod; |
| 282 | + if (buildMethod === "makefile") { |
| 283 | + if ( |
| 284 | + !this.layersConfigs.find( |
| 285 | + (e) => |
| 286 | + e.templateName === projectTemplateName && e.resourceKey === resourceKey && e.buildRoot === buildRoot |
| 287 | + ) |
| 288 | + ) { |
| 289 | + this.layersConfigs.push({ |
| 290 | + templateName: projectTemplateName, |
| 291 | + resourceKey, |
| 292 | + buildRoot, |
| 293 | + contentDir, |
| 294 | + buildMethod, |
| 295 | + }); |
| 296 | + } |
| 297 | + } else { |
| 298 | + throw new Error(`Unsupported layer BuildMethod '${buildMethod}'`); |
| 299 | + } |
| 300 | + } |
255 | 301 | } |
256 | 302 |
|
257 | 303 | return { entryPoints, launchConfigs, samConfigs }; |
@@ -317,53 +363,102 @@ class AwsSamPlugin { |
317 | 363 | } |
318 | 364 |
|
319 | 365 | public apply(compiler: any) { |
320 | | - compiler.hooks.afterEmit.tap("SamPlugin", (_compilation: any) => { |
321 | | - if (this.samConfigs && this.launchConfig) { |
322 | | - for (const samConfig of this.samConfigs) { |
323 | | - fs.writeFileSync( |
324 | | - `${samConfig.buildRoot}/template.yaml`, |
325 | | - yaml.dump(samConfig.samConfig, { indent: 2, quotingType: '"', schema }) |
326 | | - ); |
| 366 | + compiler.hooks.afterEmit?.tapPromise( |
| 367 | + "SamPlugin", |
| 368 | + async (_compilation: any /* webpack.Compilation */): Promise<void> => { |
| 369 | + if (!(this.samConfigs && this.launchConfig)) { |
| 370 | + throw new Error("It looks like AwsSamPlugin.entry() was not called"); |
327 | 371 | } |
328 | | - if (this.options.vscodeDebug !== false) { |
329 | | - if (!fs.existsSync(".vscode")) { |
330 | | - fs.mkdirSync(".vscode"); |
| 372 | + |
| 373 | + for (const layerConfig of this.layersConfigs) { |
| 374 | + const { templateName, resourceKey, buildRoot, contentDir, buildMethod } = layerConfig; |
| 375 | + if (buildMethod === "makefile") { |
| 376 | + console.log("Start building layer %s#%s ... ", templateName, resourceKey); |
| 377 | + const artifactsDir = `${buildRoot}/${resourceKey}`; |
| 378 | + try { |
| 379 | + fs.mkdirSync(buildRoot); |
| 380 | + } catch (err) { |
| 381 | + if (!(err?.code === "EEXIST")) throw err; |
| 382 | + } |
| 383 | + try { |
| 384 | + fs.mkdirSync(artifactsDir); |
| 385 | + } catch (err) { |
| 386 | + if (!(err?.code === "EEXIST")) throw err; |
| 387 | + } |
| 388 | + const cmdLine = [ |
| 389 | + // |
| 390 | + `make`, |
| 391 | + `-C "${contentDir}"`, |
| 392 | + `ARTIFACTS_DIR="${path.resolve(artifactsDir)}"`, |
| 393 | + `build-${resourceKey}`, |
| 394 | + ].join(" "); |
| 395 | + // console.info("MAKE %s cmdLine: %s", resourceKey, cmdLine); |
| 396 | + try { |
| 397 | + await new Promise((res, rej) => { |
| 398 | + exec(cmdLine, (e) => (e?.code ? rej(e) : res(e))); |
| 399 | + }); |
| 400 | + } catch (err) { |
| 401 | + if (err.cmd) { |
| 402 | + console.error(err.stdout); |
| 403 | + console.error(err.stderr); |
| 404 | + } |
| 405 | + throw err; |
| 406 | + } |
| 407 | + } else { |
| 408 | + throw new Error(`Unsupported layer BuildMethod '${buildMethod}'`); |
331 | 409 | } |
| 410 | + } |
| 411 | + } |
| 412 | + ); |
| 413 | + compiler.hooks.afterEmit.tap("SamPlugin", (_compilation: any) => { |
| 414 | + if (!(this.samConfigs && this.launchConfig)) { |
| 415 | + throw new Error("It looks like AwsSamPlugin.entry() was not called"); |
| 416 | + } |
| 417 | + const yamlUnique = this.samConfigs.reduce((a, e) => { |
| 418 | + const { buildRoot, samConfig } = e; |
| 419 | + a[buildRoot] = samConfig; |
| 420 | + return a; |
| 421 | + }, {} as Record<string, any>); |
| 422 | + for (const buildRoot in yamlUnique) { |
| 423 | + const samConfig = yamlUnique[buildRoot]; |
| 424 | + fs.writeFileSync(`${buildRoot}/template.yaml`, yaml.dump(samConfig, { indent: 2, quotingType: '"', schema })); |
| 425 | + } |
332 | 426 |
|
333 | | - const launchPath = ".vscode/launch.json"; |
| 427 | + if (this.options.vscodeDebug !== false) { |
| 428 | + if (!fs.existsSync(".vscode")) { |
| 429 | + fs.mkdirSync(".vscode"); |
| 430 | + } |
| 431 | + const launchPath = ".vscode/launch.json"; |
334 | 432 |
|
335 | | - const launchContent = JSON.stringify(this.launchConfig, null, 2) |
336 | | - .replace(/^(.*"configurations": \[\s*)$/m, "$1\n // BEGIN AwsSamPlugin") |
337 | | - .replace(/(\n \s*\][\r\n]+\})$/m, "\n // END AwsSamPlugin$1"); |
338 | | - const regexBlock = /\s+\/\/ BEGIN AwsSamPlugin(\r|\n|.)+\/\/ END AwsSamPlugin/m; |
| 433 | + const launchContent = JSON.stringify(this.launchConfig, null, 2) |
| 434 | + .replace(/^(.*"configurations": \[\s*)$/m, "$1\n // BEGIN AwsSamPlugin") |
| 435 | + .replace(/(\n \s*\][\r\n]+\})$/m, "\n // END AwsSamPlugin$1"); |
| 436 | + const regexBlock = /\s+\/\/ BEGIN AwsSamPlugin(\r|\n|.)+\/\/ END AwsSamPlugin/m; |
339 | 437 |
|
340 | | - // get new "configurations" content |
341 | | - const matches = launchContent.match(regexBlock); |
342 | | - if (!matches) { |
343 | | - throw new Error(launchPath + " new content does not match"); |
344 | | - } |
345 | | - const launchConfigurations = matches[0]; |
346 | | - |
347 | | - if (fs.existsSync(launchPath)) { |
348 | | - const launchContentOld = fs.readFileSync(launchPath).toString("utf8"); |
349 | | - if (launchContentOld.match(regexBlock)) { |
350 | | - // partial rewrite contents |
351 | | - const newContent = launchContentOld.replace(regexBlock, () => launchConfigurations); |
352 | | - fs.writeFileSync(launchPath, newContent); |
353 | | - } else { |
354 | | - // add configurations |
355 | | - const newContent = launchContentOld.replace( |
356 | | - /(\n \]\n\})$/m, |
357 | | - (p0, p1) => `,${launchConfigurations}${p1}` |
358 | | - ); |
359 | | - fs.writeFileSync(launchPath, newContent); |
360 | | - } |
| 438 | + // get new "configurations" content |
| 439 | + const matches = launchContent.match(regexBlock); |
| 440 | + if (!matches) { |
| 441 | + throw new Error(launchPath + " new content does not match"); |
| 442 | + } |
| 443 | + const launchConfigurations = matches[0]; |
| 444 | + |
| 445 | + if (fs.existsSync(launchPath)) { |
| 446 | + const launchContentOld = fs.readFileSync(launchPath).toString("utf8"); |
| 447 | + if (launchContentOld.match(regexBlock)) { |
| 448 | + // partial rewrite contents |
| 449 | + const newContent = launchContentOld.replace(regexBlock, () => launchConfigurations); |
| 450 | + fs.writeFileSync(launchPath, newContent); |
361 | 451 | } else { |
362 | | - fs.writeFileSync(launchPath, launchContent); |
| 452 | + // add configurations |
| 453 | + const newContent = launchContentOld.replace( |
| 454 | + /(\n \]\n\})$/m, |
| 455 | + (p0, p1) => `,${launchConfigurations}${p1}` |
| 456 | + ); |
| 457 | + fs.writeFileSync(launchPath, newContent); |
363 | 458 | } |
| 459 | + } else { |
| 460 | + fs.writeFileSync(launchPath, launchContent); |
364 | 461 | } |
365 | | - } else { |
366 | | - throw new Error("It looks like AwsSamPlugin.entry() was not called"); |
367 | 462 | } |
368 | 463 | }); |
369 | 464 | } |
|
0 commit comments