vite-plugin-keywords 1.0.0 → 1.0.2

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
@@ -14,9 +14,9 @@
14
14
  > [!NOTE]
15
15
  > A Rollup version of this plugin, `rollup-plugin-keywords`, is also available. The primary difference is that the Vite plugin utilizes the `hotUpdate` hook to incrementally collect keywords and update modules and types during development. While this documentation is written primarily for the Vite plugin, the setup is almost identical—just add `rollup-plugin-keywords` to your Rollup configuration.
16
16
 
17
- A Vite plugin that provides a way to use minifiable `Symbols` (keywords) in place of string literals and object keys, offering a potential strategy for aggressive minification.
17
+ A Vite plugin that provides a way to use minifiable `Symbols` (keywords) in place of string literals and object keys, offering a potential strategy for aggressive minification/obfuscation.
18
18
 
19
- This approach introduces a trade-off between a small reduction in bundle size and an increase in code complexity. It is best suited for ~~applications where every byte counts~~ minification nerds.
19
+ This approach introduces a trade-off between a small reduction in bundle size and an increase in code complexity. It is best suited for ~~applications where every byte counts~~ minification/obfuscation nerds.
20
20
 
21
21
  ## Rationale
22
22
 
@@ -130,55 +130,55 @@ pnpm add -D vite-plugin-keywords
130
130
 
131
131
  ## Setup
132
132
 
133
- 1. Add the plugin to your `vite.config.ts`.
134
-
135
- ```ts
136
- // vite.config.ts
137
- import { defineConfig } from 'vite';
138
- import keywords from 'vite-plugin-keywords';
139
-
140
- export default defineConfig({
141
- plugins: [keywords()],
142
- });
143
- ```
144
-
145
- 2. Include the generated types file in your `tsconfig.json` or `src/env.d.ts`.
146
-
147
- ```jsonc
148
- // tsconfig.json
149
- {
150
- // ...
151
- "include": [
152
- "src",
153
- ".keywords/types.d.ts", // Add this line
154
- ],
155
- }
156
- ```
157
-
158
- ```ts
159
- // src/env.d.ts
160
- /// <reference path="../.keywords/types.d.ts" />
161
- ```
162
-
163
- 3. Exclude the generated types file from your version control system (e.g., Git).
164
-
165
- ```gitignore
166
- # .gitignore
167
- .keywords/
168
- ```
169
-
170
- 4. Ensure that your type-checking script in `package.json` is updated to run the plugin first:
171
-
172
- ```jsonc
173
- // package.json
174
- {
175
- "scripts": {
176
- "typecheck": "keywords && tsc --noEmit",
177
- },
178
- }
179
- ```
180
-
181
- 5. The `.keywords/types.d.ts` type file is created automatically on `vite dev/build`, or manually via the `keywords` script.
133
+ 1. Add the plugin to your `vite.config.ts`.
134
+
135
+ ```ts
136
+ // vite.config.ts
137
+ import { defineConfig } from 'vite';
138
+ import keywords from 'vite-plugin-keywords';
139
+
140
+ export default defineConfig({
141
+ plugins: [keywords()],
142
+ });
143
+ ```
144
+
145
+ 2. Include the generated types file in your `tsconfig.json` or `src/env.d.ts`.
146
+
147
+ ```jsonc
148
+ // tsconfig.json
149
+ {
150
+ // ...
151
+ "include": [
152
+ "src",
153
+ ".keywords/types.d.ts", // Add this line
154
+ ],
155
+ }
156
+ ```
157
+
158
+ ```ts
159
+ // src/env.d.ts
160
+ /// <reference path="../.keywords/types.d.ts" />
161
+ ```
162
+
163
+ 3. Exclude the generated types file from your version control system (e.g., Git).
164
+
165
+ ```gitignore
166
+ # .gitignore
167
+ .keywords/
168
+ ```
169
+
170
+ 4. Ensure that your type-checking script in `package.json` is updated to run the plugin first:
171
+
172
+ ```jsonc
173
+ // package.json
174
+ {
175
+ "scripts": {
176
+ "typecheck": "keywords && tsc --noEmit",
177
+ },
178
+ }
179
+ ```
180
+
181
+ 5. The `.keywords/types.d.ts` type file is created automatically on `vite dev/build`, or manually via the `keywords` script.
182
182
 
183
183
  ## Options
184
184
 
package/dist/cli.js CHANGED
@@ -1,8 +1,125 @@
1
+ // ../minifiable-keywords/dist/index.js
2
+ import { mkdir, readFile, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import { parse } from "@babel/parser";
5
+ import _traverse from "@babel/traverse";
6
+ import { globby } from "globby";
7
+ var VIRTUAL_MODULE_ID = "virtual:keywords";
8
+ var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
9
+ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
10
+ const prefix = usePrefix ? `[${pluginName}] ` : "";
11
+ const prefixed = (message) => `${prefix}${message}`;
12
+ return {
13
+ pluginName,
14
+ info: (message) => logger.info(prefixed(message)),
15
+ warn: (message) => logger.warn(prefixed(message)),
16
+ error: (message) => logger.error(prefixed(message))
17
+ };
18
+ };
19
+ var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
20
+ var extractKeywords = (code) => {
21
+ const keywords = /* @__PURE__ */ new Set();
22
+ let ast;
23
+ try {
24
+ ast = parse(code, {
25
+ sourceType: "module",
26
+ plugins: ["typescript", "jsx"],
27
+ errorRecovery: true
28
+ });
29
+ } catch (e) {
30
+ return keywords;
31
+ }
32
+ const keywordNamespaces = /* @__PURE__ */ new Set();
33
+ traverse(ast, {
34
+ enter(nodePath) {
35
+ const node = nodePath.node;
36
+ if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
37
+ for (const specifier of node.specifiers) {
38
+ if (specifier.type === "ImportNamespaceSpecifier") {
39
+ keywordNamespaces.add(specifier.local.name);
40
+ }
41
+ if (specifier.type === "ImportDefaultSpecifier") {
42
+ keywords.add("default");
43
+ }
44
+ if (specifier.type === "ImportSpecifier") {
45
+ if (specifier.imported.type === "Identifier") {
46
+ keywords.add(specifier.imported.name);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ });
53
+ if (keywordNamespaces.size === 0) {
54
+ return keywords;
55
+ }
56
+ traverse(ast, {
57
+ enter(nodePath) {
58
+ const node = nodePath.node;
59
+ if (node.type === "MemberExpression" && !node.computed && // Exclude computed properties like K['xyz']
60
+ node.object.type === "Identifier" && keywordNamespaces.has(node.object.name) && node.property.type === "Identifier") {
61
+ keywords.add(node.property.name);
62
+ }
63
+ if (node.type === "TSQualifiedName" && node.left.type === "Identifier" && keywordNamespaces.has(node.left.name) && node.right.type === "Identifier") {
64
+ keywords.add(node.right.name);
65
+ }
66
+ }
67
+ });
68
+ return keywords;
69
+ };
70
+ var keywordConstPrefix = "_";
71
+ var createExportDeclaration = (keywords) => {
72
+ const aliases = [...keywords].map(
73
+ (key) => ` ${keywordConstPrefix}${key} as ${key},`
74
+ );
75
+ return [`export {`, ...aliases, `};`];
76
+ };
77
+ var generateTypesFile = async (collectedKeywords, root, dirname = ".keywords", filename = "types.d.ts") => {
78
+ const keywordDeclarations = [...collectedKeywords].map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`).map((line) => ` ${line}`).join("\n");
79
+ const exportDeclaration = createExportDeclaration(collectedKeywords).map((line) => ` ${line}`).join("\n");
80
+ const content = `declare module '${VIRTUAL_MODULE_ID}' {
81
+ ${keywordDeclarations}
82
+ ${exportDeclaration}
83
+ }`;
84
+ const pluginRoot = path.join(root, dirname);
85
+ await mkdir(pluginRoot, { recursive: true });
86
+ await writeFile(path.join(pluginRoot, filename), `${content.trim()}
87
+ `);
88
+ };
89
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
90
+ const collectedKeywords = /* @__PURE__ */ new Set();
91
+ logger.info("Scanning project files for keywords...");
92
+ const files = await globby("**/*.{js,ts,jsx,tsx}", {
93
+ cwd: root,
94
+ absolute: true,
95
+ ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
96
+ gitignore: true
97
+ });
98
+ await Promise.all(
99
+ files.map(async (file) => {
100
+ const code = await readFile(file, "utf-8");
101
+ const keywords = extractKeywords(code);
102
+ for (const key of keywords) {
103
+ collectedKeywords.add(key);
104
+ }
105
+ })
106
+ );
107
+ logger.info(
108
+ `Scan complete. Found ${collectedKeywords.size} unique keywords.`
109
+ );
110
+ return collectedKeywords;
111
+ };
112
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
113
+ const collectedKeywords = await collectKeywordsFromFiles(
114
+ root,
115
+ logger,
116
+ ignoredDirs
117
+ );
118
+ await generateTypesFile(collectedKeywords, root);
119
+ return collectedKeywords;
120
+ };
121
+
1
122
  // src/cli.ts
2
- import {
3
- collectKeywordsAndGenerateTypes,
4
- createPrefixedLogger
5
- } from "minifiable-keywords";
6
123
  import { resolveConfig } from "vite";
7
124
 
8
125
  // src/shared.ts
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/shared.ts"],"sourcesContent":["import {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n} from 'minifiable-keywords';\nimport { resolveConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nconst main = async () => {\n const config = await resolveConfig({}, 'build');\n const logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n\n // const keywordsPlugin = config.plugins.find(\n // (plugin) => plugin.name === PLUGIN_NAME,\n // );\n // if (!keywordsPlugin) {\n // logger.error('Keywords plugin not found in Vite configuration.');\n // process.exit(1);\n // }\n\n await collectKeywordsAndGenerateTypes(config.root, logger, [\n config.build.outDir,\n config.cacheDir,\n ]);\n};\n\nawait main();\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;;;ACJvB,IAAM,cAAc;;;ADO3B,IAAM,OAAO,YAAY;AACvB,QAAM,SAAS,MAAM,cAAc,CAAC,GAAG,OAAO;AAC9C,QAAM,SAAS,qBAAqB,OAAO,QAAQ,WAAW;AAU9D,QAAM,gCAAgC,OAAO,MAAM,QAAQ;AAAA,IACzD,OAAO,MAAM;AAAA,IACb,OAAO;AAAA,EACT,CAAC;AACH;AAEA,MAAM,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../minifiable-keywords/src/index.ts","../src/cli.ts","../src/shared.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'types.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`)\n .map((line) => ` ${line}`)\n .join('\\n');\n const exportDeclaration = createExportDeclaration(collectedKeywords)\n .map((line) => ` ${line}`)\n .join('\\n');\n const content = `declare module '${VIRTUAL_MODULE_ID}' {\\n${keywordDeclarations}\\n${exportDeclaration}\\n}`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","import {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n} from 'minifiable-keywords';\nimport { resolveConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nconst main = async () => {\n const config = await resolveConfig({}, 'build');\n const logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n\n // const keywordsPlugin = config.plugins.find(\n // (plugin) => plugin.name === PLUGIN_NAME,\n // );\n // if (!keywordsPlugin) {\n // logger.error('Keywords plugin not found in Vite configuration.');\n // process.exit(1);\n // }\n\n await collectKeywordsAndGenerateTypes(config.root, logger, [\n config.build.outDir,\n config.cacheDir,\n ]);\n};\n\nawait main();\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -35,7 +35,140 @@ __export(index_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
37
  var import_node_path = __toESM(require("path"), 1);
38
- var import_minifiable_keywords = require("minifiable-keywords");
38
+
39
+ // ../minifiable-keywords/dist/index.js
40
+ var import_promises = require("fs/promises");
41
+ var import_path = __toESM(require("path"), 1);
42
+ var import_parser = require("@babel/parser");
43
+ var import_traverse = __toESM(require("@babel/traverse"), 1);
44
+ var import_globby = require("globby");
45
+ var VIRTUAL_MODULE_ID = "virtual:keywords";
46
+ var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
47
+ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
48
+ const prefix = usePrefix ? `[${pluginName}] ` : "";
49
+ const prefixed = (message) => `${prefix}${message}`;
50
+ return {
51
+ pluginName,
52
+ info: (message) => logger.info(prefixed(message)),
53
+ warn: (message) => logger.warn(prefixed(message)),
54
+ error: (message) => logger.error(prefixed(message))
55
+ };
56
+ };
57
+ var traverse = typeof import_traverse.default === "function" ? import_traverse.default : import_traverse.default.default;
58
+ var extractKeywords = (code) => {
59
+ const keywords = /* @__PURE__ */ new Set();
60
+ let ast;
61
+ try {
62
+ ast = (0, import_parser.parse)(code, {
63
+ sourceType: "module",
64
+ plugins: ["typescript", "jsx"],
65
+ errorRecovery: true
66
+ });
67
+ } catch (e) {
68
+ return keywords;
69
+ }
70
+ const keywordNamespaces = /* @__PURE__ */ new Set();
71
+ traverse(ast, {
72
+ enter(nodePath) {
73
+ const node = nodePath.node;
74
+ if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
75
+ for (const specifier of node.specifiers) {
76
+ if (specifier.type === "ImportNamespaceSpecifier") {
77
+ keywordNamespaces.add(specifier.local.name);
78
+ }
79
+ if (specifier.type === "ImportDefaultSpecifier") {
80
+ keywords.add("default");
81
+ }
82
+ if (specifier.type === "ImportSpecifier") {
83
+ if (specifier.imported.type === "Identifier") {
84
+ keywords.add(specifier.imported.name);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ });
91
+ if (keywordNamespaces.size === 0) {
92
+ return keywords;
93
+ }
94
+ traverse(ast, {
95
+ enter(nodePath) {
96
+ const node = nodePath.node;
97
+ if (node.type === "MemberExpression" && !node.computed && // Exclude computed properties like K['xyz']
98
+ node.object.type === "Identifier" && keywordNamespaces.has(node.object.name) && node.property.type === "Identifier") {
99
+ keywords.add(node.property.name);
100
+ }
101
+ if (node.type === "TSQualifiedName" && node.left.type === "Identifier" && keywordNamespaces.has(node.left.name) && node.right.type === "Identifier") {
102
+ keywords.add(node.right.name);
103
+ }
104
+ }
105
+ });
106
+ return keywords;
107
+ };
108
+ var keywordConstPrefix = "_";
109
+ var createExportDeclaration = (keywords) => {
110
+ const aliases = [...keywords].map(
111
+ (key) => ` ${keywordConstPrefix}${key} as ${key},`
112
+ );
113
+ return [`export {`, ...aliases, `};`];
114
+ };
115
+ var generateTypesFile = async (collectedKeywords, root, dirname = ".keywords", filename = "types.d.ts") => {
116
+ const keywordDeclarations = [...collectedKeywords].map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`).map((line) => ` ${line}`).join("\n");
117
+ const exportDeclaration = createExportDeclaration(collectedKeywords).map((line) => ` ${line}`).join("\n");
118
+ const content = `declare module '${VIRTUAL_MODULE_ID}' {
119
+ ${keywordDeclarations}
120
+ ${exportDeclaration}
121
+ }`;
122
+ const pluginRoot = import_path.default.join(root, dirname);
123
+ await (0, import_promises.mkdir)(pluginRoot, { recursive: true });
124
+ await (0, import_promises.writeFile)(import_path.default.join(pluginRoot, filename), `${content.trim()}
125
+ `);
126
+ };
127
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
128
+ const collectedKeywords = /* @__PURE__ */ new Set();
129
+ logger.info("Scanning project files for keywords...");
130
+ const files = await (0, import_globby.globby)("**/*.{js,ts,jsx,tsx}", {
131
+ cwd: root,
132
+ absolute: true,
133
+ ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
134
+ gitignore: true
135
+ });
136
+ await Promise.all(
137
+ files.map(async (file) => {
138
+ const code = await (0, import_promises.readFile)(file, "utf-8");
139
+ const keywords = extractKeywords(code);
140
+ for (const key of keywords) {
141
+ collectedKeywords.add(key);
142
+ }
143
+ })
144
+ );
145
+ logger.info(
146
+ `Scan complete. Found ${collectedKeywords.size} unique keywords.`
147
+ );
148
+ return collectedKeywords;
149
+ };
150
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
151
+ const collectedKeywords = await collectKeywordsFromFiles(
152
+ root,
153
+ logger,
154
+ ignoredDirs
155
+ );
156
+ await generateTypesFile(collectedKeywords, root);
157
+ return collectedKeywords;
158
+ };
159
+ var generateModuleCode = (collectedKeywords, isDev) => {
160
+ const symbolConstructorName = "__SYMBOL__";
161
+ const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;
162
+ const keywordDeclarations = [...collectedKeywords].map(
163
+ (key) => `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ""});`
164
+ ).join("\n");
165
+ const exportDeclaration = createExportDeclaration(collectedKeywords).join("\n");
166
+ return `${symbolDeclaration}
167
+ ${keywordDeclarations}
168
+ ${exportDeclaration}
169
+ `;
170
+ };
171
+ var splitQuery = (id) => id.split("?");
39
172
 
40
173
  // src/shared.ts
41
174
  var PLUGIN_NAME = "vite-plugin-keywords";
@@ -56,10 +189,10 @@ var keywordsPlugin = () => {
56
189
  name: PLUGIN_NAME,
57
190
  configResolved(resolvedConfig) {
58
191
  config = resolvedConfig;
59
- logger = (0, import_minifiable_keywords.createPrefixedLogger)(config.logger, PLUGIN_NAME);
192
+ logger = createPrefixedLogger(config.logger, PLUGIN_NAME);
60
193
  },
61
194
  async buildStart() {
62
- collectedKeywords = await (0, import_minifiable_keywords.collectKeywordsAndGenerateTypes)(
195
+ collectedKeywords = await collectKeywordsAndGenerateTypes(
63
196
  config.root,
64
197
  logger,
65
198
  [config.build.outDir, config.cacheDir]
@@ -69,16 +202,16 @@ var keywordsPlugin = () => {
69
202
  if (!importer) {
70
203
  return;
71
204
  }
72
- const [validSource] = (0, import_minifiable_keywords.splitQuery)(source);
73
- if (validSource === import_minifiable_keywords.VIRTUAL_MODULE_ID) {
74
- return import_minifiable_keywords.RESOLVED_VIRTUAL_MODULE_ID;
205
+ const [validSource] = splitQuery(source);
206
+ if (validSource === VIRTUAL_MODULE_ID) {
207
+ return RESOLVED_VIRTUAL_MODULE_ID;
75
208
  }
76
209
  },
77
210
  load(id) {
78
- const [validId] = (0, import_minifiable_keywords.splitQuery)(id);
79
- if (validId === import_minifiable_keywords.RESOLVED_VIRTUAL_MODULE_ID) {
211
+ const [validId] = splitQuery(id);
212
+ if (validId === RESOLVED_VIRTUAL_MODULE_ID) {
80
213
  const isDev = config.mode === "development";
81
- return (0, import_minifiable_keywords.generateModuleCode)(collectedKeywords, isDev);
214
+ return generateModuleCode(collectedKeywords, isDev);
82
215
  }
83
216
  },
84
217
  async hotUpdate({ type, file, read }) {
@@ -88,7 +221,7 @@ var keywordsPlugin = () => {
88
221
  return;
89
222
  }
90
223
  const code = await read();
91
- const keywordsInFile = (0, import_minifiable_keywords.extractKeywords)(code);
224
+ const keywordsInFile = extractKeywords(code);
92
225
  if (keywordsInFile.size === 0) return;
93
226
  const initialSize = collectedKeywords.size;
94
227
  for (const key of keywordsInFile) {
@@ -97,10 +230,10 @@ var keywordsPlugin = () => {
97
230
  const newKeywordsAdded = collectedKeywords.size > initialSize;
98
231
  if (newKeywordsAdded) {
99
232
  invalidateModule(
100
- import_minifiable_keywords.RESOLVED_VIRTUAL_MODULE_ID,
233
+ RESOLVED_VIRTUAL_MODULE_ID,
101
234
  this.environment.moduleGraph
102
235
  );
103
- await (0, import_minifiable_keywords.generateTypesFile)(collectedKeywords, config.root);
236
+ await generateTypesFile(collectedKeywords, config.root);
104
237
  }
105
238
  }
106
239
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAAiB;AACjB,iCAUO;;;ACXA,IAAM,cAAc;;;ADepB,IAAM,iBAAiB,MAAc;AAC1C,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,mBAAmB,CACvB,YACA,gBACS;AACT,UAAMA,UAAS,YAAY,cAAc,UAAU;AACnD,QAAIA,SAAQ;AACV,kBAAY,iBAAiBA,OAAM;AACnC,MAAAA,QAAO,mBAAmBA,QAAO,6BAA6B,KAAK,IAAI;AAAA,IACzE;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,eAAe,gBAAgB;AAC7B,eAAS;AACT,mBAAS,iDAAqB,OAAO,QAAQ,WAAW;AAAA,IAC1D;AAAA,IAEA,MAAM,aAAa;AACjB,0BAAoB,UAAM;AAAA,QACxB,OAAO;AAAA,QACP;AAAA,QACA,CAAC,OAAO,MAAM,QAAQ,OAAO,QAAQ;AAAA,MACvC;AAAA,IACF;AAAA,IAEA,UAAU,QAAQ,UAAU;AAC1B,UAAI,CAAC,UAAU;AACb;AAAA,MACF;AACA,YAAM,CAAC,WAAW,QAAI,uCAAW,MAAM;AACvC,UAAI,gBAAgB,8CAAmB;AACrC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,KAAK,IAAI;AACP,YAAM,CAAC,OAAO,QAAI,uCAAW,EAAE;AAC/B,UAAI,YAAY,uDAA4B;AAC1C,cAAM,QAAQ,OAAO,SAAS;AAC9B,mBAAO,+CAAmB,mBAAmB,KAAK;AAAA,MACpD;AAAA,IACF;AAAA,IAEA,MAAM,UAAU,EAAE,MAAM,MAAM,KAAK,GAAG;AACpC,UAAI,SAAS,SAAU;AAEvB,YAAM,UAAU,iBAAAC,QAAK,QAAQ,IAAI;AACjC,UACE,CAAC,CAAC,OAAO,OAAO,QAAQ,MAAM,EAAE,SAAS,OAAO,KAChD,KAAK,SAAS,IAAI,GAClB;AACA;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,KAAK;AACxB,YAAM,qBAAiB,4CAAgB,IAAI;AAC3C,UAAI,eAAe,SAAS,EAAG;AAE/B,YAAM,cAAc,kBAAkB;AACtC,iBAAW,OAAO,gBAAgB;AAChC,0BAAkB,IAAI,GAAG;AAAA,MAC3B;AAEA,YAAM,mBAAmB,kBAAkB,OAAO;AAClD,UAAI,kBAAkB;AACpB;AAAA,UACE;AAAA,UACA,KAAK,YAAY;AAAA,QACnB;AACA,kBAAM,8CAAkB,mBAAmB,OAAO,IAAI;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":["module","path"]}
1
+ {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'types.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`)\n .map((line) => ` ${line}`)\n .join('\\n');\n const exportDeclaration = createExportDeclaration(collectedKeywords)\n .map((line) => ` ${line}`)\n .join('\\n');\n const content = `declare module '${VIRTUAL_MODULE_ID}' {\\n${keywordDeclarations}\\n${exportDeclaration}\\n}`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["_traverse","path","module","path"]}
package/dist/index.js CHANGED
@@ -1,15 +1,139 @@
1
1
  // src/index.ts
2
+ import path2 from "path";
3
+
4
+ // ../minifiable-keywords/dist/index.js
5
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
6
  import path from "path";
3
- import {
4
- collectKeywordsAndGenerateTypes,
5
- createPrefixedLogger,
6
- extractKeywords,
7
- generateModuleCode,
8
- generateTypesFile,
9
- RESOLVED_VIRTUAL_MODULE_ID,
10
- splitQuery,
11
- VIRTUAL_MODULE_ID
12
- } from "minifiable-keywords";
7
+ import { parse } from "@babel/parser";
8
+ import _traverse from "@babel/traverse";
9
+ import { globby } from "globby";
10
+ var VIRTUAL_MODULE_ID = "virtual:keywords";
11
+ var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
12
+ var createPrefixedLogger = (logger, pluginName, usePrefix = true) => {
13
+ const prefix = usePrefix ? `[${pluginName}] ` : "";
14
+ const prefixed = (message) => `${prefix}${message}`;
15
+ return {
16
+ pluginName,
17
+ info: (message) => logger.info(prefixed(message)),
18
+ warn: (message) => logger.warn(prefixed(message)),
19
+ error: (message) => logger.error(prefixed(message))
20
+ };
21
+ };
22
+ var traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
23
+ var extractKeywords = (code) => {
24
+ const keywords = /* @__PURE__ */ new Set();
25
+ let ast;
26
+ try {
27
+ ast = parse(code, {
28
+ sourceType: "module",
29
+ plugins: ["typescript", "jsx"],
30
+ errorRecovery: true
31
+ });
32
+ } catch (e) {
33
+ return keywords;
34
+ }
35
+ const keywordNamespaces = /* @__PURE__ */ new Set();
36
+ traverse(ast, {
37
+ enter(nodePath) {
38
+ const node = nodePath.node;
39
+ if (node.type === "ImportDeclaration" && node.source.value === VIRTUAL_MODULE_ID) {
40
+ for (const specifier of node.specifiers) {
41
+ if (specifier.type === "ImportNamespaceSpecifier") {
42
+ keywordNamespaces.add(specifier.local.name);
43
+ }
44
+ if (specifier.type === "ImportDefaultSpecifier") {
45
+ keywords.add("default");
46
+ }
47
+ if (specifier.type === "ImportSpecifier") {
48
+ if (specifier.imported.type === "Identifier") {
49
+ keywords.add(specifier.imported.name);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ });
56
+ if (keywordNamespaces.size === 0) {
57
+ return keywords;
58
+ }
59
+ traverse(ast, {
60
+ enter(nodePath) {
61
+ const node = nodePath.node;
62
+ if (node.type === "MemberExpression" && !node.computed && // Exclude computed properties like K['xyz']
63
+ node.object.type === "Identifier" && keywordNamespaces.has(node.object.name) && node.property.type === "Identifier") {
64
+ keywords.add(node.property.name);
65
+ }
66
+ if (node.type === "TSQualifiedName" && node.left.type === "Identifier" && keywordNamespaces.has(node.left.name) && node.right.type === "Identifier") {
67
+ keywords.add(node.right.name);
68
+ }
69
+ }
70
+ });
71
+ return keywords;
72
+ };
73
+ var keywordConstPrefix = "_";
74
+ var createExportDeclaration = (keywords) => {
75
+ const aliases = [...keywords].map(
76
+ (key) => ` ${keywordConstPrefix}${key} as ${key},`
77
+ );
78
+ return [`export {`, ...aliases, `};`];
79
+ };
80
+ var generateTypesFile = async (collectedKeywords, root, dirname = ".keywords", filename = "types.d.ts") => {
81
+ const keywordDeclarations = [...collectedKeywords].map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`).map((line) => ` ${line}`).join("\n");
82
+ const exportDeclaration = createExportDeclaration(collectedKeywords).map((line) => ` ${line}`).join("\n");
83
+ const content = `declare module '${VIRTUAL_MODULE_ID}' {
84
+ ${keywordDeclarations}
85
+ ${exportDeclaration}
86
+ }`;
87
+ const pluginRoot = path.join(root, dirname);
88
+ await mkdir(pluginRoot, { recursive: true });
89
+ await writeFile(path.join(pluginRoot, filename), `${content.trim()}
90
+ `);
91
+ };
92
+ var collectKeywordsFromFiles = async (root, logger, ignoredDirs = []) => {
93
+ const collectedKeywords = /* @__PURE__ */ new Set();
94
+ logger.info("Scanning project files for keywords...");
95
+ const files = await globby("**/*.{js,ts,jsx,tsx}", {
96
+ cwd: root,
97
+ absolute: true,
98
+ ignore: ["**/node_modules/**", ...ignoredDirs.map((dir) => `${dir}/**`)],
99
+ gitignore: true
100
+ });
101
+ await Promise.all(
102
+ files.map(async (file) => {
103
+ const code = await readFile(file, "utf-8");
104
+ const keywords = extractKeywords(code);
105
+ for (const key of keywords) {
106
+ collectedKeywords.add(key);
107
+ }
108
+ })
109
+ );
110
+ logger.info(
111
+ `Scan complete. Found ${collectedKeywords.size} unique keywords.`
112
+ );
113
+ return collectedKeywords;
114
+ };
115
+ var collectKeywordsAndGenerateTypes = async (root, logger, ignoredDirs) => {
116
+ const collectedKeywords = await collectKeywordsFromFiles(
117
+ root,
118
+ logger,
119
+ ignoredDirs
120
+ );
121
+ await generateTypesFile(collectedKeywords, root);
122
+ return collectedKeywords;
123
+ };
124
+ var generateModuleCode = (collectedKeywords, isDev) => {
125
+ const symbolConstructorName = "__SYMBOL__";
126
+ const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;
127
+ const keywordDeclarations = [...collectedKeywords].map(
128
+ (key) => `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ""});`
129
+ ).join("\n");
130
+ const exportDeclaration = createExportDeclaration(collectedKeywords).join("\n");
131
+ return `${symbolDeclaration}
132
+ ${keywordDeclarations}
133
+ ${exportDeclaration}
134
+ `;
135
+ };
136
+ var splitQuery = (id) => id.split("?");
13
137
 
14
138
  // src/shared.ts
15
139
  var PLUGIN_NAME = "vite-plugin-keywords";
@@ -57,7 +181,7 @@ var keywordsPlugin = () => {
57
181
  },
58
182
  async hotUpdate({ type, file, read }) {
59
183
  if (type === "delete") return;
60
- const fileExt = path.extname(file);
184
+ const fileExt = path2.extname(file);
61
185
  if (![".js", ".ts", ".jsx", ".tsx"].includes(fileExt) || file.includes("/.")) {
62
186
  return;
63
187
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":";AAAA,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;ACXA,IAAM,cAAc;;;ADepB,IAAM,iBAAiB,MAAc;AAC1C,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,mBAAmB,CACvB,YACA,gBACS;AACT,UAAM,SAAS,YAAY,cAAc,UAAU;AACnD,QAAI,QAAQ;AACV,kBAAY,iBAAiB,MAAM;AACnC,aAAO,mBAAmB,OAAO,6BAA6B,KAAK,IAAI;AAAA,IACzE;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,eAAe,gBAAgB;AAC7B,eAAS;AACT,eAAS,qBAAqB,OAAO,QAAQ,WAAW;AAAA,IAC1D;AAAA,IAEA,MAAM,aAAa;AACjB,0BAAoB,MAAM;AAAA,QACxB,OAAO;AAAA,QACP;AAAA,QACA,CAAC,OAAO,MAAM,QAAQ,OAAO,QAAQ;AAAA,MACvC;AAAA,IACF;AAAA,IAEA,UAAU,QAAQ,UAAU;AAC1B,UAAI,CAAC,UAAU;AACb;AAAA,MACF;AACA,YAAM,CAAC,WAAW,IAAI,WAAW,MAAM;AACvC,UAAI,gBAAgB,mBAAmB;AACrC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,KAAK,IAAI;AACP,YAAM,CAAC,OAAO,IAAI,WAAW,EAAE;AAC/B,UAAI,YAAY,4BAA4B;AAC1C,cAAM,QAAQ,OAAO,SAAS;AAC9B,eAAO,mBAAmB,mBAAmB,KAAK;AAAA,MACpD;AAAA,IACF;AAAA,IAEA,MAAM,UAAU,EAAE,MAAM,MAAM,KAAK,GAAG;AACpC,UAAI,SAAS,SAAU;AAEvB,YAAM,UAAU,KAAK,QAAQ,IAAI;AACjC,UACE,CAAC,CAAC,OAAO,OAAO,QAAQ,MAAM,EAAE,SAAS,OAAO,KAChD,KAAK,SAAS,IAAI,GAClB;AACA;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,KAAK;AACxB,YAAM,iBAAiB,gBAAgB,IAAI;AAC3C,UAAI,eAAe,SAAS,EAAG;AAE/B,YAAM,cAAc,kBAAkB;AACtC,iBAAW,OAAO,gBAAgB;AAChC,0BAAkB,IAAI,GAAG;AAAA,MAC3B;AAEA,YAAM,mBAAmB,kBAAkB,OAAO;AAClD,UAAI,kBAAkB;AACpB;AAAA,UACE;AAAA,UACA,KAAK,YAAY;AAAA,QACnB;AACA,cAAM,kBAAkB,mBAAmB,OAAO,IAAI;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../../minifiable-keywords/src/index.ts","../src/shared.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n collectKeywordsAndGenerateTypes,\n createPrefixedLogger,\n extractKeywords,\n generateModuleCode,\n generateTypesFile,\n RESOLVED_VIRTUAL_MODULE_ID,\n splitQuery,\n VIRTUAL_MODULE_ID,\n type PrefixedLogger,\n} from 'minifiable-keywords';\nimport type { EnvironmentModuleGraph, Plugin, ResolvedConfig } from 'vite';\nimport { PLUGIN_NAME } from './shared';\n\nexport const keywordsPlugin = (): Plugin => {\n let collectedKeywords: Set<string>;\n let config: ResolvedConfig;\n let logger: PrefixedLogger;\n\n const invalidateModule = (\n absoluteId: string,\n moduleGraph: EnvironmentModuleGraph,\n ): void => {\n const module = moduleGraph.getModuleById(absoluteId);\n if (module) {\n moduleGraph.invalidateModule(module);\n module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now();\n }\n };\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(resolvedConfig) {\n config = resolvedConfig;\n logger = createPrefixedLogger(config.logger, PLUGIN_NAME);\n },\n\n async buildStart() {\n collectedKeywords = await collectKeywordsAndGenerateTypes(\n config.root,\n logger,\n [config.build.outDir, config.cacheDir],\n );\n },\n\n resolveId(source, importer) {\n if (!importer) {\n return;\n }\n const [validSource] = splitQuery(source);\n if (validSource === VIRTUAL_MODULE_ID) {\n return RESOLVED_VIRTUAL_MODULE_ID;\n }\n },\n\n load(id) {\n const [validId] = splitQuery(id);\n if (validId === RESOLVED_VIRTUAL_MODULE_ID) {\n const isDev = config.mode === 'development';\n return generateModuleCode(collectedKeywords, isDev);\n }\n },\n\n async hotUpdate({ type, file, read }) {\n if (type === 'delete') return;\n\n const fileExt = path.extname(file);\n if (\n !['.js', '.ts', '.jsx', '.tsx'].includes(fileExt) ||\n file.includes('/.')\n ) {\n return;\n }\n\n const code = await read();\n const keywordsInFile = extractKeywords(code);\n if (keywordsInFile.size === 0) return;\n\n const initialSize = collectedKeywords.size;\n for (const key of keywordsInFile) {\n collectedKeywords.add(key);\n }\n\n const newKeywordsAdded = collectedKeywords.size > initialSize;\n if (newKeywordsAdded) {\n invalidateModule(\n RESOLVED_VIRTUAL_MODULE_ID,\n this.environment.moduleGraph,\n );\n await generateTypesFile(collectedKeywords, config.root);\n }\n },\n };\n};\n\nexport default keywordsPlugin;\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parse } from '@babel/parser';\nimport _traverse, { type Node } from '@babel/traverse';\nimport { globby } from 'globby';\n\nexport const VIRTUAL_MODULE_ID = 'virtual:keywords';\nexport const RESOLVED_VIRTUAL_MODULE_ID = `\\0${VIRTUAL_MODULE_ID}`;\n\nexport interface Logger {\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport interface PrefixedLogger extends Logger {\n pluginName: string;\n}\n\nexport const createPrefixedLogger = (\n logger: Logger,\n pluginName: string,\n usePrefix: boolean = true,\n): PrefixedLogger => {\n const prefix = usePrefix ? `[${pluginName}] ` : '';\n const prefixed = (message: string) => `${prefix}${message}`;\n return {\n pluginName,\n info: (message: string) => logger.info(prefixed(message)),\n warn: (message: string) => logger.warn(prefixed(message)),\n error: (message: string) => logger.error(prefixed(message)),\n };\n};\n\n// ref: https://github.com/babel/babel/discussions/13093\nconst traverse =\n typeof _traverse === 'function'\n ? _traverse\n : ((_traverse as any).default as typeof _traverse);\n\nexport const extractKeywords = (code: string): Set<string> => {\n const keywords = new Set<string>();\n\n let ast: Node;\n try {\n ast = parse(code, {\n sourceType: 'module',\n plugins: ['typescript', 'jsx'],\n errorRecovery: true,\n });\n } catch (e) {\n return keywords;\n }\n\n const keywordNamespaces = new Set<string>();\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'ImportDeclaration' &&\n node.source.value === VIRTUAL_MODULE_ID\n ) {\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportNamespaceSpecifier') {\n keywordNamespaces.add(specifier.local.name);\n }\n\n if (specifier.type === 'ImportDefaultSpecifier') {\n keywords.add('default');\n }\n\n if (specifier.type === 'ImportSpecifier') {\n if (specifier.imported.type === 'Identifier') {\n keywords.add(specifier.imported.name);\n }\n }\n }\n }\n },\n });\n\n if (keywordNamespaces.size === 0) {\n return keywords;\n }\n\n traverse(ast, {\n enter(nodePath) {\n const node = nodePath.node;\n\n if (\n node.type === 'MemberExpression' &&\n !node.computed && // Exclude computed properties like K['xyz']\n node.object.type === 'Identifier' &&\n keywordNamespaces.has(node.object.name) &&\n node.property.type === 'Identifier'\n ) {\n keywords.add(node.property.name);\n }\n\n if (\n node.type === 'TSQualifiedName' &&\n node.left.type === 'Identifier' &&\n keywordNamespaces.has(node.left.name) &&\n node.right.type === 'Identifier'\n ) {\n keywords.add(node.right.name);\n }\n },\n });\n\n return keywords;\n};\n\nconst keywordConstPrefix = '_';\nconst createExportDeclaration = (keywords: Set<string>): string[] => {\n const aliases = [...keywords].map(\n (key) => ` ${keywordConstPrefix}${key} as ${key},`,\n );\n return [`export {`, ...aliases, `};`];\n};\n\nexport const generateTypesFile = async (\n collectedKeywords: Set<string>,\n root: string,\n dirname: string = '.keywords',\n filename: string = 'types.d.ts',\n): Promise<void> => {\n const keywordDeclarations = [...collectedKeywords]\n .map((key) => `const ${keywordConstPrefix}${key}: unique symbol;`)\n .map((line) => ` ${line}`)\n .join('\\n');\n const exportDeclaration = createExportDeclaration(collectedKeywords)\n .map((line) => ` ${line}`)\n .join('\\n');\n const content = `declare module '${VIRTUAL_MODULE_ID}' {\\n${keywordDeclarations}\\n${exportDeclaration}\\n}`;\n const pluginRoot = path.join(root, dirname);\n await mkdir(pluginRoot, { recursive: true });\n await writeFile(path.join(pluginRoot, filename), `${content.trim()}\\n`);\n};\n\nexport const collectKeywordsFromFiles = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs: string[] = [],\n): Promise<Set<string>> => {\n const collectedKeywords = new Set<string>();\n\n logger.info('Scanning project files for keywords...');\n\n const files = await globby('**/*.{js,ts,jsx,tsx}', {\n cwd: root,\n absolute: true,\n ignore: ['**/node_modules/**', ...ignoredDirs.map((dir) => `${dir}/**`)],\n gitignore: true,\n });\n\n await Promise.all(\n files.map(async (file) => {\n const code = await readFile(file, 'utf-8');\n const keywords = extractKeywords(code);\n for (const key of keywords) {\n collectedKeywords.add(key);\n }\n }),\n );\n\n logger.info(\n `Scan complete. Found ${collectedKeywords.size} unique keywords.`,\n );\n\n return collectedKeywords;\n};\n\nexport const collectKeywordsAndGenerateTypes = async (\n root: string,\n logger: PrefixedLogger,\n ignoredDirs?: string[],\n): Promise<Set<string>> => {\n const collectedKeywords = await collectKeywordsFromFiles(\n root,\n logger,\n ignoredDirs,\n );\n await generateTypesFile(collectedKeywords, root);\n return collectedKeywords;\n};\n\nexport const generateModuleCode = (\n collectedKeywords: Set<string>,\n isDev: boolean,\n): string => {\n const symbolConstructorName = '__SYMBOL__';\n const symbolDeclaration = `const ${symbolConstructorName} = Symbol;`;\n const keywordDeclarations = [...collectedKeywords]\n .map(\n (key) =>\n `const ${keywordConstPrefix}${key} = /* @__PURE__ */ ${symbolConstructorName}(${isDev ? `'${key}'` : ''});`,\n )\n .join('\\n');\n const exportDeclaration =\n createExportDeclaration(collectedKeywords).join('\\n');\n return `${symbolDeclaration}\\n${keywordDeclarations}\\n${exportDeclaration}\\n`;\n};\n\nexport const splitQuery = (id: string) => id.split('?');\n","export const PLUGIN_NAME = 'vite-plugin-keywords';\n"],"mappings":"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","names":["path","path"]}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "vite-plugin-keywords",
3
- "version": "1.0.0",
4
- "description": "A Vite plugin that provides minifiable Symbols (keywords) to use in place of string literals for aggressive minification.",
3
+ "version": "1.0.2",
4
+ "description": "A Vite plugin that provides minifiable Symbols (keywords) to use in place of string literals for aggressive minification/obfuscation.",
5
5
  "keywords": [
6
6
  "vite",
7
7
  "vite-plugin",
8
- "minification"
8
+ "minification",
9
+ "obfuscation"
9
10
  ],
10
11
  "repository": {
11
12
  "type": "git",
@@ -36,13 +37,16 @@
36
37
  "dist"
37
38
  ],
38
39
  "dependencies": {
39
- "minifiable-keywords": "1.0.0"
40
+ "@babel/parser": "^7.28.0",
41
+ "@babel/traverse": "^7.28.0",
42
+ "globby": "^16.0.0"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@types/node": "^24.1.0",
43
46
  "rimraf": "^6.0.1",
44
47
  "tsup": "^8.5.0",
45
- "vitest": "^3.2.4"
48
+ "vitest": "^4.0.0",
49
+ "minifiable-keywords": "1.0.2"
46
50
  },
47
51
  "peerDependencies": {
48
52
  "vite": "^6.0.0 || ^7.0.0"