webstore-lint 1.0.34 → 1.0.35

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.
package/README.md CHANGED
@@ -74,6 +74,14 @@ Run it on a copy of the built extension containing regular files to check that
74
74
  code. A linked `manifest.json` makes the package unreadable. The Action fails on
75
75
  that case at every threshold; other skipped links trigger `fail-on: warn`.
76
76
 
77
+ The tool also warns when `content_scripts` names an unsupported JavaScript or
78
+ stylesheet file type. Chrome skips the entire entry, including its other files.
79
+ Use built `.js` or `.mjs` scripts and `.css` stylesheets, then reload the unpacked
80
+ extension to verify it runs. `.user.js` is supported, and Chrome accepts `.scss`
81
+ for compatibility without compiling Sass. This loading warning has no policy
82
+ codename and triggers the Action's `fail-on: warn` threshold. See
83
+ [Chromium's file-type validation](https://chromium.googlesource.com/chromium/src/+/main/extensions/common/utils/content_script_utils.cc).
84
+
77
85
  ## In GitHub Actions
78
86
 
79
87
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstore-lint",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Check an unpacked Chrome extension against the Chrome Web Store program policies before you submit it. Every finding cites Google's own notification ID and verbatim policy text. Covers the 1 August 2026 policy updates.",
5
5
  "keywords": [
6
6
  "chrome-extension",
@@ -38,7 +38,7 @@
38
38
  "node": ">=18"
39
39
  },
40
40
  "scripts": {
41
- "test": "node test/lint.test.mjs && node test/action.test.mjs && node --test test/permissions-output.test.mjs test/cli-modes.test.mjs test/privacy-timeout.test.mjs test/symlinks.test.mjs"
41
+ "test": "node test/lint.test.mjs && node test/action.test.mjs && node --test test/permissions-output.test.mjs test/cli-modes.test.mjs test/privacy-timeout.test.mjs test/symlinks.test.mjs test/content-script-types.test.mjs"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
package/src/rules.mjs CHANGED
@@ -128,6 +128,30 @@ function bareOf(files) {
128
128
  // reads text files, so an icon that is present would be invisible to the check
129
129
  // and would report as missing on every package in the world.
130
130
  const norm = (p) => String(p).split(/[\\/]/).join("/").replace(/^\.\//, "").replace(/[?#].*$/, "");
131
+
132
+ // Chromium validates content-script types from the filename, before reading
133
+ // the resource. A bad type causes the whole content_scripts entry to be skipped.
134
+ // SCSS is explicitly allowed for compatibility; this does not compile Sass.
135
+ // Verified against extensions/common/utils/content_script_utils.cc and
136
+ // net/base/mime_util.cc in Chromium, plus browser probes on 10 October 2026.
137
+ const supportedContentFile = (path, type) =>
138
+ (type === "js" ? /\.(?:js|mjs)$/i : /\.(?:css|scss)$/i).test(path);
139
+
140
+ function invalidContentFiles(manifest) {
141
+ const out = [];
142
+ const scripts = Array.isArray(manifest?.content_scripts) ? manifest.content_scripts : [];
143
+ for (const [index, script] of scripts.entries()) {
144
+ for (const type of ["js", "css"]) {
145
+ for (const path of Array.isArray(script?.[type]) ? script[type] : []) {
146
+ if (typeof path === "string" && !supportedContentFile(path, type)) {
147
+ out.push({ path, key: `content_scripts[${index}].${type}` });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ return out;
153
+ }
154
+
131
155
  function declaredFiles(manifest) {
132
156
  const m = manifest || {};
133
157
  const out = [];
@@ -136,8 +160,10 @@ function declaredFiles(manifest) {
136
160
  add(m.background?.page, "background.page");
137
161
  for (const s of m.background?.scripts || []) add(s, "background.scripts");
138
162
  for (const cs of m.content_scripts || []) {
139
- for (const j of cs.js || []) add(j, "content_scripts[].js");
140
- for (const c of cs.css || []) add(c, "content_scripts[].css");
163
+ // Unsupported types get their own loading warning. They may exist even
164
+ // when the scanner does not read that suffix, so do not call them missing.
165
+ for (const j of cs.js || []) if (supportedContentFile(j, "js")) add(j, "content_scripts[].js");
166
+ for (const c of cs.css || []) if (supportedContentFile(c, "css")) add(c, "content_scripts[].css");
141
167
  }
142
168
  add(m.action?.default_popup, "action.default_popup");
143
169
  add(m.browser_action?.default_popup, "browser_action.default_popup");
@@ -167,6 +193,23 @@ function missingDeclaredOf(manifest, files, skipped = []) {
167
193
 
168
194
  export const RULES = [
169
195
 
196
+ {
197
+ id: "content-script-file-types",
198
+ run({ manifest }) {
199
+ const invalid = invalidContentFiles(manifest);
200
+ if (!invalid.length) return [];
201
+ return [finding({
202
+ severity: "warn",
203
+ title: `${invalid.length} content-script resource(s) have an unsupported file type`,
204
+ detail: "Chrome skips an entire content_scripts entry when any of its js or css filenames has an unsupported file type. "
205
+ + "Point js at built .js or .mjs files, and css at .css files, then reload the unpacked extension to verify it runs. "
206
+ + "The .user.js suffix is supported. Chrome also accepts .scss for compatibility, but does not compile Sass. "
207
+ + "This is a loading check, not a Chrome Web Store policy violation or a reason to delete permissions.",
208
+ evidence: invalid.map(d => ({ file: "manifest.json", line: 1, text: `${d.key}: ${d.path}` })),
209
+ })];
210
+ },
211
+ },
212
+
170
213
  {
171
214
  id: "symbolic-links",
172
215
  run({ skipped = [] }) {
@@ -1227,8 +1270,11 @@ export const RULES = [
1227
1270
  // names dist/background.js and we were linting a folder of seven JSON
1228
1271
  // files - "never used" there is a claim about nothing at all.
1229
1272
  const gone = missingDeclaredOf(manifest, files, skipped);
1230
- if (unread.length || minified.length || bare.length || gone.length) {
1231
- const why = gone.length
1273
+ const invalidTypes = invalidContentFiles(manifest);
1274
+ if (unread.length || minified.length || bare.length || gone.length || invalidTypes.length) {
1275
+ const why = invalidTypes.length
1276
+ ? `a content_scripts entry has an unsupported file type (${invalidTypes[0].path}); fix that loading error before deciding whether these permissions are unused`
1277
+ : gone.length
1232
1278
  ? `manifest.json names ${gone.length} file(s) that are not in this directory (${gone[0].key}: ${gone[0].path}), so the code that uses these permissions is not here to be read`
1233
1279
  : unread.length
1234
1280
  ? `${unread.length} code file(s) in this package were not read (${unread[0].path}: ${unread[0].why}), so this scan did not see all of your code`
package/src/scan.mjs CHANGED
@@ -100,7 +100,7 @@ export function buildHint(root, exists = existsSync) {
100
100
  const MAX_BYTES = 2 * 1024 * 1024;
101
101
  const TEXT_EXT = new Set([
102
102
  ".js", ".mjs", ".cjs", ".ts", ".jsx", ".tsx", ".json", ".html", ".htm",
103
- ".css", ".txt", ".md", ".webmanifest",
103
+ ".css", ".scss", ".txt", ".md", ".webmanifest",
104
104
  ]);
105
105
 
106
106
  // A link may name a file or a whole directory. Its descendants are unknown,