Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
785 changes: 405 additions & 380 deletions package-lock.json

Large diffs are not rendered by default.

32 changes: 17 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,13 @@
"version": "npm-run-all --node-run build depcruise:graph:doc scm:stage"
},
"dependencies": {
"acorn": "8.16.0",
"acorn": "8.17.0",
"acorn-jsx": "5.3.2",
"acorn-jsx-walk": "2.0.0",
"acorn-loose": "8.5.2",
"acorn-walk": "8.3.5",
"commander": "15.0.0",
"enhanced-resolve": "5.22.1",
"enhanced-resolve": "5.24.0",
"ignore": "7.0.5",
"interpret": "3.1.1",
"is-installed-globally": "1.0.0",
Expand All @@ -226,7 +226,7 @@
"prompts": "2.4.2",
"rechoir": "0.8.0",
"safe-regex": "2.1.1",
"semver": "7.8.1",
"semver": "7.8.4",
"tsconfig-paths-webpack-plugin": "4.2.0",
"watskeburt": "5.0.3"
},
Expand All @@ -236,17 +236,18 @@
"@babel/preset-typescript": "7.29.7",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "^9.39.4",
"@swc/core": "1.15.40",
"@types/node": "25.9.1",
"@swc/core": "1.15.41",
"@types/node": "25.9.3",
"@astrojs/compiler": "2.13.0",
"@types/prompts": "2.4.9",
"@typescript-eslint/eslint-plugin": "8.60.0",
"@typescript-eslint/parser": "8.60.0",
"@vue/compiler-sfc": "3.5.35",
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@vue/compiler-sfc": "3.5.38",
"ajv": "8.20.0",
"c8": "11.0.0",
"clean-pkg-json": "1.4.2",
"clean-pkg-json": "2.0.0",
"coffeescript": "2.7.0",
"esbuild": "0.28.0",
"esbuild": "0.28.1",
"eslint": "^9.39.4",
"eslint-config-moving-meadow": "5.0.0",
"eslint-config-prettier": "10.1.8",
Expand All @@ -255,17 +256,17 @@
"eslint-plugin-import": "2.32.0",
"eslint-plugin-mocha": "^10.5.0",
"eslint-plugin-n": "^17.24.0",
"eslint-plugin-security": "4.0.0",
"eslint-plugin-security": "4.0.1",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "17.6.0",
"husky": "9.1.7",
"lint-staged": "17.0.5",
"lint-staged": "17.0.7",
"mocha": "11.7.6",
"normalize-newline": "5.0.0",
"npm-run-all2": "9.0.1",
"prettier": "3.8.3",
"npm-run-all2": "9.0.2",
"prettier": "3.8.4",
"proxyquire": "2.1.3",
"svelte": "5.55.10",
"svelte": "5.56.3",
"svgo": "4.0.1",
"symlink-dir": "^7.1.0",
"typescript": "^6.0.3",
Expand Down Expand Up @@ -364,6 +365,7 @@
"node": "^22||^24||>=26"
},
"supportedTranspilers": {
"@astrojs/compiler": ">=2.0.0 <3.0.0",
"babel": ">=7.0.0 <8.0.0",
"coffee-script": ">=1.0.0 <2.0.0",
"coffeescript": ">=1.0.0 <3.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/extract/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const TS_COMPATIBLE_EXTENSIONS = new Set([
".mjs",
".cjs",
".vue",
".astro",
]);

/**
Expand Down
115 changes: 115 additions & 0 deletions src/extract/transpile/astro-preprocess.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const FRONTMATTER_PATTERN = /^---\s*\r?\n([\s\S]*?)\r?\n---/;
const SCRIPT_TAG_PATTERN = /<script\b[^>]*>([\s\S]*?)<\/script>/gm;
const STATIC_IMPORT_PATTERN = /^\s*(import[^"']+["'][^"']+["'][;]?)\s*$/gm;

Check warning on line 3 in src/extract/transpile/astro-preprocess.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this character class by the character itself.

See more on https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&issues=AZ7uKb-shRsR-v_mSe_T&open=AZ7uKb-shRsR-v_mSe_T&pullRequest=1009

/**
* Ensures that an import statement ends with a semicolon.
* @param {string} pImportStatement
* @returns {string}
*/
function ensureImportTerminator(pImportStatement) {
const lTrimmed = pImportStatement.trim();

return lTrimmed.endsWith(";") ? lTrimmed : `${lTrimmed};`;
}

/**
* Injects import statements into the frontmatter of an Astro file.
*
* @param {string} pSource
* @param {string[]} pImports
* @returns {string}
*/
function injectImportsIntoFrontmatter(pSource, pImports) {
if (pImports.length === 0) {
return pSource;
}

const lFrontmatterMatch = pSource.match(FRONTMATTER_PATTERN);

Check warning on line 28 in src/extract/transpile/astro-preprocess.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "RegExp.exec()" method instead.

See more on https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&issues=AZ7uKb-shRsR-v_mSe_U&open=AZ7uKb-shRsR-v_mSe_U&pullRequest=1009
const lEol = pSource.includes("\r\n") ? "\r\n" : "\n";
const lInsertion = pImports.join(lEol);

if (!lFrontmatterMatch) {
const lFrontmatter = `---${lEol}${lInsertion}${lEol}---${lEol}`;

return `${lFrontmatter}${pSource}`;
}

const lFullMatch = lFrontmatterMatch[0];
const lBody = lFrontmatterMatch[1] ?? "";
const lNewBody =
lBody.length > 0 ? `${lInsertion}${lEol}${lBody}` : lInsertion;
const lNewFrontmatter = `---${lEol}${lNewBody}${lEol}---`;

return pSource.replace(lFullMatch, lNewFrontmatter);
}

/**
* We cannot analyze static imports inside `<script>` blocks in Astro files without special handling.
* This is because `convertToTSX()` transforms script tag like below.
*
* ```tsx
* // from this Astro file:
* <script>
* import foo from "./foo.js";
* </script>
*
* // to this TSX:
* <script>
* {() => {
* import foo from "./foo.js";
* }}
* </script>
* ```
*
* To work around this, we hoist static imports from <script> blocks into the frontmatter.
*
* This differs from how Astro / Vite actually handles modules during the build process or on the runtime, but it's sufficient for tracking import dependencies at the file level.
*
* @param {string} pSource
* @returns {string}
*/
function hoistScriptImportsIntoFrontmatter(pSource) {
/**
* @type {string[]}
*/
const lImportsToBeHoisted = [];

const lSourceWithoutScriptImports = pSource.replace(
SCRIPT_TAG_PATTERN,
(pFullMatch, pScriptContent) => {
let lShouldHoistThisScript = false;

const lScriptAfterHoisting = pScriptContent.replace(
STATIC_IMPORT_PATTERN,
(_pMatch, pImport) => {
if (pImport) {
lImportsToBeHoisted.push(ensureImportTerminator(pImport));
lShouldHoistThisScript = true;
}
return "";
},
);

return lShouldHoistThisScript
? pFullMatch.replace(pScriptContent, lScriptAfterHoisting)
: pFullMatch;
},
);

return injectImportsIntoFrontmatter(
lSourceWithoutScriptImports,
lImportsToBeHoisted,
);
}

/**
* Preprocesses Astro source code by hoisting static imports from <script> blocks
* into the frontmatter section.
*
* @param {string} pSource - The Astro source code to preprocess
* @returns {string} - The preprocessed source code with hoisted imports
*/
export function preProcess(pSource) {
return hoistScriptImportsIntoFrontmatter(pSource);
}
67 changes: 67 additions & 0 deletions src/extract/transpile/astro-wrap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { preProcess } from "./astro-preprocess.mjs";
import tryImport from "#utl/try-import.mjs";
import meta from "#meta.cjs";

/**
* @type {import("@astrojs/compiler/sync") | false}
*/
const astro = await tryImport(
"@astrojs/compiler/sync",
meta.supportedTranspilers["@astrojs/compiler"],
);

/**
* @type {Record<string, string | Record<string,string>> | false}
*/
const astroPackageInfo = await tryImport(
"@astrojs/compiler/package.json",
meta.supportedTranspilers["@astrojs/compiler"],
);

/**
*
* @param {import("./meta").ITranspilerWrapper} pTsxWrapper
*
* @returns {import("./meta").ITranspilerWrapper["transpile"]}
*/
function getTranspiler(pTsxWrapper) {
return (pSource, pFileName, pTranspilerOptions) => {
const lPreparedSource = preProcess(pSource);
const lTSXCode = astro.convertToTSX(lPreparedSource, {
filename: pFileName,
sourcemap: false,
includeStyles: false,
}).code;

let lJSCode = lTSXCode;
if (pTsxWrapper.isAvailable()) {
lJSCode = pTsxWrapper.transpile(lTSXCode, "dummy.tsx", {
...pTranspilerOptions,
tsConfig: {
...(pTranspilerOptions?.tsConfig || {}),

Check warning on line 41 in src/extract/transpile/astro-wrap.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The empty object is useless.

See more on https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&issues=AZ7uKb8whRsR-v_mSe_P&open=AZ7uKb8whRsR-v_mSe_P&pullRequest=1009
options: {
...(pTranspilerOptions?.tsConfig?.options || {}),

Check warning on line 43 in src/extract/transpile/astro-wrap.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The empty object is useless.

See more on https://sonarcloud.io/project/issues?id=sverweij_dependency-cruiser&issues=AZ7uKb8whRsR-v_mSe_Q&open=AZ7uKb8whRsR-v_mSe_Q&pullRequest=1009
// https://github.com/withastro/astro/blob/dacebaf8df6293558091a18123aed966e4e57415/packages/astro/tsconfigs/base.json#L28-L29
jsx: "preserve",
},
},
});
}
return lJSCode;
};
}

/**
*
* @param {import("./meta").ITranspilerWrapper} pTsxWrapper
*
* @returns {import("./meta").ITranspilerWrapper}
*/
export default function astroWrap(pTsxWrapper) {
return {
isAvailable: () => astro !== false,
version: () =>
`@astrojs/compiler@${astroPackageInfo?.version ?? "unknown"}`,
transpile: getTranspiler(pTsxWrapper),
};
}
3 changes: 3 additions & 0 deletions src/extract/transpile/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import coffeeWrap from "./coffeescript-wrap.mjs";
import vueWrap from "./vue-template-wrap.cjs";
import babelWrap from "./babel-wrap.mjs";
import svelteDingus from "./svelte-wrap.mjs";
import astroWrap from "./astro-wrap.mjs";

const typeScriptVanillaWrap = typeScriptWrap();
const typeScriptESMWrap = typeScriptWrap("esm");
const typeScriptTsxWrap = typeScriptWrap("tsx");
const coffeeVanillaWrap = coffeeWrap();
const litCoffeeWrap = coffeeWrap(true);
const svelteWrap = svelteDingus(typeScriptVanillaWrap);
const astroWrapInstance = astroWrap(typeScriptTsxWrap);

export const EXTENSION2WRAPPER = new Map([
[".js", javaScriptWrap],
Expand All @@ -33,6 +35,7 @@ export const EXTENSION2WRAPPER = new Map([
[".coffee.md", litCoffeeWrap],
[".csx", coffeeVanillaWrap],
[".cjsx", coffeeVanillaWrap],
[".astro", astroWrapInstance],
]);

const BABEL_ABLE_EXTENSIONS = new Set([
Expand Down
9 changes: 9 additions & 0 deletions src/extract/transpile/meta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import svelteWrapFunction from "./svelte-wrap.mjs";
import typescriptWrapFunction from "./typescript-wrap.mjs";
import vueTemplateWrap from "./vue-template-wrap.cjs";
import astroWrapFunction from "./astro-wrap.mjs";
import meta from "#meta.cjs";

/**
Expand All @@ -24,6 +25,8 @@
const coffeeScriptWrap = coffeeScriptWrapFunction(false);
const svelteWrap = svelteWrapFunction(javascriptWrap);
const typescriptWrap = typescriptWrapFunction("esm");
const typescriptTsxWrap = typescriptWrapFunction("tsx");
const astroWrap = astroWrapFunction(typescriptTsxWrap);

function gotCoffee() {
return (
Expand All @@ -42,6 +45,10 @@
}

const TRANSPILER2AVAILABLE = {
"@astrojs/compiler": tryAvailable(

Check failure on line 48 in src/extract/transpile/meta.mjs

View workflow job for this annotation

GitHub Actions / check-linux (26.x, ubuntu-latest)

'tryAvailable' is not defined
"@astrojs/compiler",
meta.supportedTranspilers["@astrojs/compiler"],
),
babel: tryImportAvailable("@babel/core", meta.supportedTranspilers.babel),
javascript: true,
"coffee-script": gotCoffee(),
Expand Down Expand Up @@ -70,6 +77,7 @@
};

const TRANSPILER2WRAPPER = {
"@astrojs/compiler": astroWrap,
babel: babelWrap,
javascript: javascriptWrap,
"coffee-script": coffeeScriptWrap,
Expand Down Expand Up @@ -106,6 +114,7 @@
[".coffee.md", gotCoffee()],
[".csx", gotCoffee()],
[".cjsx", gotCoffee()],
[".astro", TRANSPILER2AVAILABLE["@astrojs/compiler"]],
]);

const EXTENSIONS_PER_PARSER = {
Expand Down
2 changes: 1 addition & 1 deletion src/extract/tsc/parse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const CACHE = new Map();
*/
export function getASTFromSource(pFileRecord, pTranspileOptions) {
let lSource = pFileRecord.source;
if (pFileRecord.extension === ".vue") {
if (pFileRecord.extension === ".vue" || pFileRecord.extension === ".astro") {
lSource = transpile(pFileRecord, pTranspileOptions);
}

Expand Down
1 change: 1 addition & 0 deletions src/meta.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions test/extract/transpile/__fixtures__/astro-with-imports.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* @jsxImportSource astro */
import { Button } from "./components/Button.astro";
import { formatDate } from "../utils/date.js";
const { title, date } = Astro.props;
const formattedDate = formatDate(date);
<Fragment>
<div>
<h1>{title}</h1>
<time>{formattedDate}</time>
<Button />
</div>

<script>
{() => {
import { logAnalytics } from "./analytics.js";
logAnalytics("page-view");
}}
</script>
</Fragment>;
export default function __AstroComponent_(_props) {}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiPHN0ZGluPiJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLS0tXG5pbXBvcnQgeyBCdXR0b24gfSBmcm9tIFwiLi9jb21wb25lbnRzL0J1dHRvbi5hc3Ryb1wiO1xuaW1wb3J0IHsgZm9ybWF0RGF0ZSB9IGZyb20gXCIuLi91dGlscy9kYXRlLmpzXCI7XG5cbmludGVyZmFjZSBQcm9wcyB7XG4gIHRpdGxlOiBzdHJpbmc7XG4gIGRhdGU6IERhdGU7XG59XG5cbmNvbnN0IHsgdGl0bGUsIGRhdGUgfSA9IEFzdHJvLnByb3BzO1xuY29uc3QgZm9ybWF0dGVkRGF0ZSA9IGZvcm1hdERhdGUoZGF0ZSk7XG4tLS1cblxuXHUwMDNjZGl2XHUwMDNlXG4gIFx1MDAzY2gxXHUwMDNle3RpdGxlfVx1MDAzYy9oMVx1MDAzZVxuICBcdTAwM2N0aW1lXHUwMDNle2Zvcm1hdHRlZERhdGV9XHUwMDNjL3RpbWVcdTAwM2VcbiAgXHUwMDNjQnV0dG9uIC9cdTAwM2Vcblx1MDAzYy9kaXZcdTAwM2VcblxuXHUwMDNjc2NyaXB0XHUwMDNlXG4gIGltcG9ydCB7IGxvZ0FuYWx5dGljcyB9IGZyb20gXCIuL2FuYWx5dGljcy5qc1wiO1xuXG4gIGxvZ0FuYWx5dGljcyhcInBhZ2Utdmlld1wiKTtcblx1MDAzYy9zY3JpcHRcdTAwM2VcbiJdLAogICJtYXBwaW5ncyI6ICI7O0FBQUEsQUFBRztBQUNILENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ25ELENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDOUM7QUFDQSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDakIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUNoQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2IsQ0FBQztBQUNEO0FBQ0EsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDcEMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDdkMsQUFBRyxBQVhIO0FBQUE7QUFhQSxDQUFDLEdBQUcsQ0FBQztBQUNMLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQztBQUM5QixDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsRUFBRTtBQUNaLEVBQUUsR0FBRyxDQUFDO0FBQ047QUFDQSxDQUFDLE1BQU0sQUFuQlA7QUFBQSxRQW1CUTtBQUNSLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2hEO0FBQ0EsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxBQXRCNUI7QUFBQTtBQXVCQSxTQUFTO0FBQ1QsQUF4QkE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTsiLAogICJuYW1lcyI6IFtdCn0=
Loading
Loading