Skip to content

Commit 8f17be6

Browse files
authored
Rewrite dist => lib in serverless.ymls (#387)
skuba's default configuration compiles to `lib`, though it's common to see `dist` and `lib` out in the wild. `skuba configure` already tries to rewire Dockerfiles to point to this output directory, and now we do the same for Serverless files. This implements globbing in our configure modules so we can match on multiple `serverless*.yml`s in the one repo.
1 parent 4ebe2ca commit 8f17be6

File tree

11 files changed

+350
-25
lines changed

11 files changed

+350
-25
lines changed

.changeset/tall-crews-visit.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'skuba': patch
3+
---
4+
5+
**configure:** Rewrite `dist => lib` in `serverless.yml`s

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@types/lodash.mergewith": "4.6.6",
4646
"@types/module-alias": "2.0.0",
4747
"@types/npm-which": "3.0.0",
48+
"@types/picomatch": "2.2.1",
4849
"@types/supertest": "2.0.10",
4950
"express": "4.17.1",
5051
"koa": "2.13.1",
@@ -67,6 +68,7 @@
6768
"eslint": "^7.18.0",
6869
"eslint-config-skuba": "1.0.10",
6970
"execa": "^5.0.0",
71+
"fdir": "^5.0.0",
7072
"fs-extra": "^9.1.0",
7173
"get-port": "^5.1.1",
7274
"ignore": "^5.1.8",
@@ -79,6 +81,7 @@
7981
"normalize-package-data": "^3.0.0",
8082
"npm-run-path": "^4.0.1",
8183
"npm-which": "^3.0.1",
84+
"picomatch": "^2.2.2",
8285
"prettier": "2.2.1",
8386
"read-pkg-up": "^7.0.1",
8487
"runtypes": "^5.0.1",

src/cli/configure/analysis/project.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as dir from '../../../utils/dir';
12
import { getSkubaVersion } from '../../../utils/version';
23
import { defaultOpts } from '../testing/module';
34

@@ -10,6 +11,8 @@ describe('diffFiles', () => {
1011
.spyOn(project, 'createDestinationFileReader')
1112
.mockReturnValue(() => Promise.resolve(undefined));
1213

14+
jest.spyOn(dir, 'crawlDirectory').mockResolvedValue([]);
15+
1316
const [outputFiles, version] = await Promise.all([
1417
diffFiles(defaultOpts),
1518
getSkubaVersion(),

src/cli/configure/analysis/project.ts

+39-17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'path';
22

33
import fs from 'fs-extra';
44

5+
import { buildPatternToFilepathMap, crawlDirectory } from '../../../utils/dir';
56
import { isErrorWithCode } from '../../../utils/error';
67
import { loadModules } from '../modules';
78
import { FileDiff, Files, Module, Options } from '../types';
@@ -25,30 +26,51 @@ export const createDestinationFileReader = (root: string) => async (
2526
const loadModuleFiles = async (modules: Module[], destinationRoot: string) => {
2627
const readDestinationFile = createDestinationFileReader(destinationRoot);
2728

28-
const allFilenames = modules.flatMap((module) => Object.keys(module));
29+
const allFilepaths = await crawlDirectory(destinationRoot);
2930

30-
const uniqueFilenames = [...new Set(allFilenames)];
31+
const patterns = [...new Set(modules.flatMap((m) => Object.keys(m)))];
32+
33+
const patternToFilepaths = buildPatternToFilepathMap(patterns, allFilepaths);
34+
35+
const matchedFilepaths = [
36+
...new Set(Object.values(patternToFilepaths).flat()),
37+
];
3138

3239
const fileEntries = await Promise.all(
33-
uniqueFilenames.map(
34-
async (filename) =>
35-
[filename, await readDestinationFile(filename)] as const,
40+
matchedFilepaths.map(
41+
async (filepath) =>
42+
[filepath, await readDestinationFile(filepath)] as const,
3643
),
3744
);
3845

39-
return Object.fromEntries(fileEntries);
46+
return {
47+
inputFiles: Object.fromEntries(fileEntries),
48+
patternToFilepaths,
49+
};
4050
};
4151

42-
const processTextFiles = (modules: Module[], inputFiles: Readonly<Files>) => {
52+
const processTextFiles = (
53+
modules: Module[],
54+
inputFiles: Readonly<Files>,
55+
patternToFilepaths: Record<string, string[]>,
56+
) => {
4357
const outputFiles = { ...inputFiles };
4458

4559
const textProcessorEntries = modules.flatMap((module) =>
46-
Object.entries(module),
60+
Object.entries(module).flatMap(([pattern, processText]) => {
61+
// Include the raw pattern along with any matched filepaths.
62+
// Some modules create a new file at the specified pattern.
63+
const filepaths = [pattern, ...(patternToFilepaths[pattern] ?? [])];
64+
65+
return [...new Set(filepaths)].map(
66+
(filepath) => [filepath, processText] as const,
67+
);
68+
}),
4769
);
4870

49-
for (const [filename, processText] of textProcessorEntries) {
50-
outputFiles[filename] = processText(
51-
outputFiles[filename],
71+
for (const [filepath, processText] of textProcessorEntries) {
72+
outputFiles[filepath] = processText(
73+
outputFiles[filepath],
5274
outputFiles,
5375
inputFiles,
5476
);
@@ -60,18 +82,18 @@ const processTextFiles = (modules: Module[], inputFiles: Readonly<Files>) => {
6082
export const diffFiles = async (opts: Options): Promise<FileDiff> => {
6183
const modules = await loadModules(opts);
6284

63-
const inputFiles = Object.freeze(
85+
const { inputFiles, patternToFilepaths } = Object.freeze(
6486
await loadModuleFiles(modules, opts.destinationRoot),
6587
);
6688

67-
const outputFiles = processTextFiles(modules, inputFiles);
89+
const outputFiles = processTextFiles(modules, inputFiles, patternToFilepaths);
6890

6991
const diffEntries = Object.entries(outputFiles)
70-
.filter(([filename, data]) => inputFiles[filename] !== data)
71-
.map(([filename, data]) => {
72-
const operation = determineOperation(inputFiles[filename], data);
92+
.filter(([filepath, data]) => inputFiles[filepath] !== data)
93+
.map(([filepath, data]) => {
94+
const operation = determineOperation(inputFiles[filepath], data);
7395

74-
return [filename, { data, operation }] as const;
96+
return [filepath, { data, operation }] as const;
7597
});
7698

7799
return Object.fromEntries(diffEntries);

src/cli/configure/modules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { nodemonModule } from './nodemon';
77
import { packageModule } from './package';
88
import { prettierModule } from './prettier';
99
import { renovateModule } from './renovate';
10+
import { serverlessModule } from './serverless';
1011
import { skubaDiveModule } from './skubaDive';
1112
import { tsconfigModule } from './tsconfig';
1213
import { tslintModule } from './tslint';
@@ -21,6 +22,7 @@ export const loadModules = (opts: Options): Promise<Module[]> =>
2122
packageModule,
2223
prettierModule,
2324
renovateModule,
25+
serverlessModule,
2426
skubaDiveModule,
2527
tsconfigModule,
2628
tslintModule,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { defaultOpts, executeModule } from '../testing/module';
2+
3+
import { serverlessModule } from './serverless';
4+
5+
describe('serverlessModule', () => {
6+
it('does not touch empty input', async () => {
7+
const inputFiles = {};
8+
9+
const outputFiles = await executeModule(
10+
serverlessModule,
11+
inputFiles,
12+
defaultOpts,
13+
);
14+
15+
expect(outputFiles).toEqual({});
16+
});
17+
18+
it('rewires a variety of patterns', async () => {
19+
const INPUT_SERVERLESS_YAML = `
20+
service: unrelated-dist-reference
21+
22+
package:
23+
include:
24+
- dist/**
25+
26+
package:
27+
include:
28+
- /dist/**
29+
30+
package:
31+
include:
32+
- ./dist/**
33+
34+
functions:
35+
WorkerA:
36+
handler: dist/app.aHandler
37+
WorkerB:
38+
handler: /dist/app.bHandler
39+
WorkerC:
40+
handler: ./dist/app.cHandler
41+
`;
42+
43+
const EXPECTED_SERVERLESS_YAML = `
44+
service: unrelated-dist-reference
45+
46+
package:
47+
include:
48+
- lib/**
49+
50+
package:
51+
include:
52+
- lib/**
53+
54+
package:
55+
include:
56+
- lib/**
57+
58+
functions:
59+
WorkerA:
60+
handler: lib/app.aHandler
61+
WorkerB:
62+
handler: lib/app.bHandler
63+
WorkerC:
64+
handler: lib/app.cHandler
65+
`;
66+
67+
const inputFiles = {
68+
'serverless.yml': INPUT_SERVERLESS_YAML,
69+
};
70+
71+
const outputFiles = await executeModule(
72+
serverlessModule,
73+
inputFiles,
74+
defaultOpts,
75+
);
76+
77+
expect(outputFiles).toEqual({
78+
'serverless.yml': EXPECTED_SERVERLESS_YAML,
79+
});
80+
});
81+
82+
it('rewires a variety of Serverless files', async () => {
83+
const INPUT_SERVERLESS_YAML = `
84+
package:
85+
include:
86+
- dist/**
87+
88+
functions:
89+
Worker:
90+
handler: dist/app.handler
91+
`;
92+
93+
const EXPECTED_SERVERLESS_YAML = `
94+
package:
95+
include:
96+
- lib/**
97+
98+
functions:
99+
Worker:
100+
handler: lib/app.handler
101+
`;
102+
103+
const inputFiles = {
104+
'serverless.yml': INPUT_SERVERLESS_YAML,
105+
'serverless.service.yml': INPUT_SERVERLESS_YAML,
106+
'service/serverless.yml': INPUT_SERVERLESS_YAML,
107+
'unrelated.txt': 'dist',
108+
};
109+
110+
const outputFiles = await executeModule(
111+
serverlessModule,
112+
inputFiles,
113+
defaultOpts,
114+
);
115+
116+
expect(outputFiles).toEqual({
117+
'serverless.yml': EXPECTED_SERVERLESS_YAML,
118+
'serverless.service.yml': EXPECTED_SERVERLESS_YAML,
119+
'service/serverless.yml': EXPECTED_SERVERLESS_YAML,
120+
'unrelated.txt': 'dist',
121+
});
122+
});
123+
});
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Module, Options } from '../types';
2+
3+
export const serverlessModule = ({}: Options): Module => ({
4+
'**/serverless*.yml': (inputFile, _files, _initialFiles) => {
5+
if (!inputFile) {
6+
// Only configure files that exist.
7+
return;
8+
}
9+
10+
return (
11+
inputFile
12+
// Rewire packaging patterns.
13+
.replace(/- (\.?\/)?dist\//g, '- lib/')
14+
// Rewire handler paths.
15+
.replace(/handler: (\.?\/)?dist\//g, 'handler: lib/')
16+
);
17+
},
18+
});

src/cli/configure/testing/module.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import picomatch from 'picomatch';
2+
13
import { Files, Module, Options } from '../types';
24

35
export function assertDefined<T>(value?: T): asserts value is T {
@@ -27,12 +29,22 @@ export const executeModule = async (
2729

2830
const outputFiles = { ...inputFiles };
2931

30-
for (const [filename, processText] of Object.entries(mod)) {
31-
outputFiles[filename] = processText(
32-
outputFiles[filename],
33-
outputFiles,
34-
inputFiles,
35-
);
32+
const allFilepaths = Object.keys(outputFiles);
33+
34+
for (const [pattern, processText] of Object.entries(mod)) {
35+
const isMatch = picomatch(pattern);
36+
37+
// Include the raw pattern along with any matched filepaths.
38+
// Some modules create a new file at the specified pattern.
39+
const filepaths = [pattern, ...allFilepaths.filter((p) => isMatch(p))];
40+
41+
for (const filepath of [...new Set(filepaths)]) {
42+
outputFiles[filepath] = processText(
43+
outputFiles[filepath],
44+
outputFiles,
45+
inputFiles,
46+
);
47+
}
3648
}
3749

3850
return outputFiles;

0 commit comments

Comments
 (0)