Skip to content

Commit 85ce4ff

Browse files
authored
fix(deploy): ensure import map or config file is included in the manifest (#326)
This commit adds a check before uploading assets if an import map specified in `--import-map` option or a config file (e.g. `deno.json`) is included in the manifest. This will help users understand why import maps don't get applied during deployment. ### case 1: import map not included If the specified import map is not included in the manifest due to `--include` and/or `--exclude` settings, this is a config conflict and most likely a wrong setup. So in this case, deployctl will error out with a specific error message: ![import_map.json not included in the manifest](https://github.com/user-attachments/assets/bde459b0-6bea-4788-8a89-f015861d5075) ### case 2: deno.json not included In this case, we can't necessarily say that this is a wrong setup, because deployctl usually infers the config file location and the config file may not have `imports` property. So instead of immediately erroring out, it shows a warning message that tells users that any import map settings in the config file won't be used: ![deno.json not included in the manifest](https://github.com/user-attachments/assets/96b01be7-5452-4292-badd-62609777c799) Closes #324
1 parent 7bded90 commit 85ce4ff

File tree

14 files changed

+429
-27
lines changed

14 files changed

+429
-27
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ jobs:
1818
os: [macOS-latest, windows-latest, ubuntu-latest]
1919

2020
steps:
21+
# Some test cases are sensitive to line endings. Disable autocrlf on
22+
# Windows to ensure consistent behavior.
23+
- name: Disable autocrlf
24+
if: runner.os == 'Windows'
25+
run: git config --global core.autocrlf false
26+
2127
- name: Setup repo
2228
uses: actions/checkout@v3
2329

action/deps.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3490,7 +3490,15 @@ function include(path, include, exclude) {
34903490
}
34913491
return true;
34923492
}
3493-
async function walk(cwd, dir, files, options) {
3493+
async function walk(cwd, dir, options) {
3494+
const hashPathMap = new Map();
3495+
const manifestEntries = await walkInner(cwd, dir, hashPathMap, options);
3496+
return {
3497+
manifestEntries,
3498+
hashPathMap
3499+
};
3500+
}
3501+
async function walkInner(cwd, dir, hashPathMap, options) {
34943502
const entries = {};
34953503
for await (const file of Deno.readDir(dir)){
34963504
const path = join2(dir, file.name);
@@ -3507,12 +3515,12 @@ async function walk(cwd, dir, files, options) {
35073515
gitSha1,
35083516
size: data.byteLength
35093517
};
3510-
files.set(gitSha1, path);
3518+
hashPathMap.set(gitSha1, path);
35113519
} else if (file.isDirectory) {
35123520
if (relative === "/.git") continue;
35133521
entry = {
35143522
kind: "directory",
3515-
entries: await walk(cwd, path, files, options)
3523+
entries: await walkInner(cwd, path, hashPathMap, options)
35163524
};
35173525
} else if (file.isSymlink) {
35183526
const target = await Deno.readLink(path);

action/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,14 @@ async function main() {
8080
if (!includes.some((i) => i.includes("node_modules"))) {
8181
excludes.push("**/node_modules");
8282
}
83-
const assets = new Map();
84-
const entries = await walk(cwd, cwd, assets, {
85-
include: includes.map(convertPatternToRegExp),
86-
exclude: excludes.map(convertPatternToRegExp),
87-
});
83+
const { manifestEntries: entries, hashPathMap: assets } = await walk(
84+
cwd,
85+
cwd,
86+
{
87+
include: includes.map(convertPatternToRegExp),
88+
exclude: excludes.map(convertPatternToRegExp),
89+
},
90+
);
8891
core.debug(`Discovered ${assets.size} assets`);
8992

9093
const api = new API(`GitHubOIDC ${token}`, ORIGIN, {

src/subcommands/deploy.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ import { error } from "../error.ts";
99
import { API, APIError, endpoint } from "../utils/api.ts";
1010
import type { ManifestEntry } from "../utils/api_types.ts";
1111
import { parseEntrypoint } from "../utils/entrypoint.ts";
12-
import { convertPatternToRegExp, walk } from "../utils/walk.ts";
12+
import {
13+
containsEntryInManifest,
14+
convertPatternToRegExp,
15+
walk,
16+
} from "../utils/manifest.ts";
1317
import TokenProvisioner from "../utils/access_token.ts";
1418
import type { Args as RawArgs } from "../args.ts";
1519
import organization from "../utils/organization.ts";
20+
import { relative } from "@std/path/relative";
21+
import { yellow } from "@std/fmt/colors";
1622

1723
const help = `deployctl deploy
1824
Deploy a script with static files to Deno Deploy.
@@ -274,14 +280,46 @@ async function deploy(opts: DeployOpts): Promise<void> {
274280
if (opts.static) {
275281
wait("").start().info(`Uploading all files from the current dir (${cwd})`);
276282
const assetSpinner = wait("Finding static assets...").start();
277-
const assets = new Map<string, string>();
278283
const include = opts.include.map(convertPatternToRegExp);
279284
const exclude = opts.exclude.map(convertPatternToRegExp);
280-
const entries = await walk(cwd, cwd, assets, { include, exclude });
285+
const { manifestEntries: entries, hashPathMap: assets } = await walk(
286+
cwd,
287+
cwd,
288+
{ include, exclude },
289+
);
281290
assetSpinner.succeed(
282291
`Found ${assets.size} asset${assets.size === 1 ? "" : "s"}.`,
283292
);
284293

294+
// If the import map is specified but not in the manifest, error out.
295+
if (
296+
opts.importMapUrl !== null &&
297+
!containsEntryInManifest(
298+
entries,
299+
relative(cwd, fromFileUrl(opts.importMapUrl)),
300+
)
301+
) {
302+
error(
303+
`Import map ${opts.importMapUrl} not found in the assets to be uploaded. Please check --include and --exclude options to make sure the import map is included.`,
304+
);
305+
}
306+
307+
// If the config file is present but not in the manifest, show a warning
308+
// that any import map settings in the config file will not be used.
309+
if (
310+
opts.importMapUrl === null && opts.config !== null &&
311+
!containsEntryInManifest(
312+
entries,
313+
relative(cwd, opts.config),
314+
)
315+
) {
316+
wait("").start().warn(
317+
yellow(
318+
`Config file ${opts.config} not found in the assets to be uploaded; any import map settings in the config file will not be applied during deployment. If this is not your intention, please check --include and --exclude options to make sure the config file is included.`,
319+
),
320+
);
321+
}
322+
285323
uploadSpinner = wait("Determining assets to upload...").start();
286324
const neededHashes = await api.projectNegotiateAssets(project.id, {
287325
entries,
Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,25 @@ function include(
3838
export async function walk(
3939
cwd: string,
4040
dir: string,
41-
files: Map<string, string>,
41+
options: { include: RegExp[]; exclude: RegExp[] },
42+
): Promise<
43+
{
44+
manifestEntries: Record<string, ManifestEntry>;
45+
hashPathMap: Map<string, string>;
46+
}
47+
> {
48+
const hashPathMap = new Map<string, string>();
49+
const manifestEntries = await walkInner(cwd, dir, hashPathMap, options);
50+
return {
51+
manifestEntries,
52+
hashPathMap,
53+
};
54+
}
55+
56+
async function walkInner(
57+
cwd: string,
58+
dir: string,
59+
hashPathMap: Map<string, string>,
4260
options: { include: RegExp[]; exclude: RegExp[] },
4361
): Promise<Record<string, ManifestEntry>> {
4462
const entries: Record<string, ManifestEntry> = {};
@@ -65,12 +83,12 @@ export async function walk(
6583
gitSha1,
6684
size: data.byteLength,
6785
};
68-
files.set(gitSha1, path);
86+
hashPathMap.set(gitSha1, path);
6987
} else if (file.isDirectory) {
7088
if (relative === "/.git") continue;
7189
entry = {
7290
kind: "directory",
73-
entries: await walk(cwd, path, files, options),
91+
entries: await walkInner(cwd, path, hashPathMap, options),
7492
};
7593
} else if (file.isSymlink) {
7694
const target = await Deno.readLink(path);
@@ -98,3 +116,42 @@ export function convertPatternToRegExp(pattern: string): RegExp {
98116
? new RegExp(globToRegExp(normalize(pattern)).toString().slice(1, -2))
99117
: new RegExp(`^${normalize(pattern)}`);
100118
}
119+
120+
/**
121+
* Determines if the manifest contains the entry at the given relative path.
122+
*
123+
* @param manifestEntries manifest entries to search
124+
* @param entryRelativePathToLookup a relative path to look up in the manifest
125+
* @returns `true` if the manifest contains the entry at the given relative path
126+
*/
127+
export function containsEntryInManifest(
128+
manifestEntries: Record<string, ManifestEntry>,
129+
entryRelativePathToLookup: string,
130+
): boolean {
131+
for (const [entryName, entry] of Object.entries(manifestEntries)) {
132+
switch (entry.kind) {
133+
case "file":
134+
case "symlink": {
135+
if (entryName === entryRelativePathToLookup) {
136+
return true;
137+
}
138+
break;
139+
}
140+
case "directory": {
141+
if (!entryRelativePathToLookup.startsWith(entryName)) {
142+
break;
143+
}
144+
145+
const relativePath = entryRelativePathToLookup.slice(
146+
entryName.length + 1,
147+
);
148+
return containsEntryInManifest(entry.entries, relativePath);
149+
}
150+
default: {
151+
const _: never = entry;
152+
}
153+
}
154+
}
155+
156+
return false;
157+
}

0 commit comments

Comments
 (0)