perf(cli): skip relock more accurate (#7993)
* perf(cli): skip relock more accurate Signed-off-by: pyranota <pyra@duck.com> * Update cli/src/utils/metadata.ts Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update cli/src/commands/flow/flow_metadata.ts Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * fix Signed-off-by: pyranota <pyra@duck.com> * use structuredClone for safety Signed-off-by: pyranota <pyra@duck.com> * chore: update ee-repo-ref to 592848d59ca2304926fb2bd85d000668a7f46a77 This commit updates the EE repository reference after PR #420 was merged in windmill-ee-private. Previous ee-repo-ref: 931813b75b8260faa13ddc07f36a11607b7e3bf6 New ee-repo-ref: 592848d59ca2304926fb2bd85d000668a7f46a77 Automated by sync-ee-ref workflow. * fix ci Signed-off-by: pyranota <pyra@duck.com> * add simple tests Signed-off-by: pyranota <pyra@duck.com> --------- Signed-off-by: pyranota <pyra@duck.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
931813b75b8260faa13ddc07f36a11607b7e3bf6
|
||||
592848d59ca2304926fb2bd85d000668a7f46a77
|
||||
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
inferSchema,
|
||||
getRawWorkspaceDependencies,
|
||||
normalizeLockPath,
|
||||
filterWorkspaceDependencies,
|
||||
filterWorkspaceDependenciesForScripts,
|
||||
InlineScriptInfo,
|
||||
} from "../../utils/metadata.ts";
|
||||
import {
|
||||
ScriptLanguage,
|
||||
@@ -118,8 +121,23 @@ export async function generateAppLocksInternal(
|
||||
const rawWorkspaceDependencies: Record<string, string> =
|
||||
await getRawWorkspaceDependencies();
|
||||
|
||||
let hashes = await generateAppHash(
|
||||
// Read the app file first to filter workspace dependencies
|
||||
const appFilePath = path.join(
|
||||
appFolder,
|
||||
rawApp ? "raw_app.yaml" : "app.yaml"
|
||||
);
|
||||
const appFile = (await yamlParseFile(appFilePath)) as AppFile;
|
||||
|
||||
// Filter workspace dependencies based on inline scripts' languages and annotations
|
||||
const appValue = rawApp ? (appFile as RawAppFile).runnables : (appFile as NormalAppFile).value;
|
||||
const filteredDeps = await filterWorkspaceDependenciesForApp(
|
||||
appValue,
|
||||
rawWorkspaceDependencies,
|
||||
appFolder
|
||||
);
|
||||
|
||||
let hashes = await generateAppHash(
|
||||
filteredDeps,
|
||||
appFolder,
|
||||
rawApp,
|
||||
opts.defaultTs
|
||||
@@ -141,7 +159,7 @@ export async function generateAppLocksInternal(
|
||||
return remote_path;
|
||||
}
|
||||
|
||||
if (Object.keys(rawWorkspaceDependencies).length > 0) {
|
||||
if (Object.keys(filteredDeps).length > 0) {
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Found workspace dependencies (${workspaceDependenciesLanguages
|
||||
@@ -151,13 +169,6 @@ export async function generateAppLocksInternal(
|
||||
);
|
||||
}
|
||||
|
||||
// Read the app file
|
||||
const appFilePath = path.join(
|
||||
appFolder,
|
||||
rawApp ? "raw_app.yaml" : "app.yaml"
|
||||
);
|
||||
const appFile = (await yamlParseFile(appFilePath)) as AppFile;
|
||||
|
||||
if (!justUpdateMetadataLock) {
|
||||
const changedScripts = [];
|
||||
// Find hashes that do not correspond to previous hashes
|
||||
@@ -195,7 +206,7 @@ export async function generateAppLocksInternal(
|
||||
runnables,
|
||||
remote_path,
|
||||
appFolder,
|
||||
rawWorkspaceDependencies,
|
||||
filteredDeps,
|
||||
opts.defaultTs
|
||||
);
|
||||
// Note: updateRawAppRunnables now writes each runnable to its own file
|
||||
@@ -211,7 +222,7 @@ export async function generateAppLocksInternal(
|
||||
normalAppFile.value,
|
||||
remote_path,
|
||||
appFolder,
|
||||
rawWorkspaceDependencies,
|
||||
filteredDeps,
|
||||
opts.defaultTs
|
||||
);
|
||||
|
||||
@@ -228,7 +239,7 @@ export async function generateAppLocksInternal(
|
||||
|
||||
// Regenerate hashes after updates
|
||||
hashes = await generateAppHash(
|
||||
rawWorkspaceDependencies,
|
||||
filteredDeps,
|
||||
appFolder,
|
||||
rawApp,
|
||||
opts.defaultTs
|
||||
@@ -252,6 +263,31 @@ type InlineScriptProcessor = (
|
||||
}
|
||||
) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Filters raw workspace dependencies for an app by traversing all inline scripts,
|
||||
* filtering deps for each based on language and annotations, then computing the union.
|
||||
*/
|
||||
export async function filterWorkspaceDependenciesForApp(
|
||||
appValue: any,
|
||||
rawWorkspaceDependencies: Record<string, string>,
|
||||
folder: string
|
||||
): Promise<Record<string, string>> {
|
||||
// Collect all inline scripts (use clone to avoid any mutations)
|
||||
const scripts: InlineScriptInfo[] = [];
|
||||
|
||||
await traverseAndProcessInlineScripts(structuredClone(appValue), async (inlineScript) => {
|
||||
if (inlineScript.content && inlineScript.language) {
|
||||
scripts.push({
|
||||
content: inlineScript.content,
|
||||
language: inlineScript.language as ScriptLanguage,
|
||||
});
|
||||
}
|
||||
return inlineScript;
|
||||
});
|
||||
|
||||
return await filterWorkspaceDependenciesForScripts(scripts, rawWorkspaceDependencies, folder, SEP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses an app structure (either app.value for normal apps or app.runnables for raw apps)
|
||||
* and processes all inline scripts found, returning the updated structure
|
||||
@@ -552,6 +588,11 @@ async function generateInlineScriptLock(
|
||||
scriptPath: string,
|
||||
rawWorkspaceDependencies: Record<string, string> | undefined
|
||||
): Promise<string> {
|
||||
// Filter workspace dependencies to only include those matching this script's language and annotations
|
||||
const filteredDeps = rawWorkspaceDependencies
|
||||
? filterWorkspaceDependencies(rawWorkspaceDependencies, content, language as ScriptLanguage)
|
||||
: undefined;
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
|
||||
const rawResponse = await fetch(
|
||||
@@ -572,9 +613,8 @@ async function generateInlineScriptLock(
|
||||
},
|
||||
],
|
||||
raw_workspace_dependencies:
|
||||
rawWorkspaceDependencies &&
|
||||
Object.keys(rawWorkspaceDependencies).length > 0
|
||||
? rawWorkspaceDependencies
|
||||
filteredDeps && Object.keys(filteredDeps).length > 0
|
||||
? filteredDeps
|
||||
: null,
|
||||
entrypoint: scriptPath,
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
LockfileGenerationError,
|
||||
getRawWorkspaceDependencies,
|
||||
normalizeLockPath,
|
||||
filterWorkspaceDependenciesForScripts,
|
||||
} from "../../utils/metadata.ts";
|
||||
import { ScriptLanguage } from "../../utils/script_common.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
|
||||
import { generateHash, getHeaders, writeIfChanged } from "../../utils/utils.ts";
|
||||
@@ -70,8 +72,16 @@ export async function generateFlowLockInternal(
|
||||
// Always get out-of-sync workspace dependencies
|
||||
const rawWorkspaceDependencies: Record<string, string> =
|
||||
await getRawWorkspaceDependencies();
|
||||
|
||||
const flowValue = (await yamlParseFile(
|
||||
folder! + SEP + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
|
||||
// Filter workspace dependencies based on inline scripts' languages and annotations
|
||||
const filteredDeps = await filterWorkspaceDependenciesForFlow(flowValue.value as FlowValue, rawWorkspaceDependencies, folder);
|
||||
|
||||
let hashes = await generateFlowHash(
|
||||
rawWorkspaceDependencies,
|
||||
filteredDeps,
|
||||
folder,
|
||||
opts.defaultTs
|
||||
);
|
||||
@@ -88,7 +98,7 @@ export async function generateFlowLockInternal(
|
||||
return remote_path;
|
||||
}
|
||||
|
||||
if (Object.keys(rawWorkspaceDependencies).length > 0) {
|
||||
if (Object.keys(filteredDeps).length > 0) {
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Found workspace dependencies (${workspaceDependenciesLanguages
|
||||
@@ -98,9 +108,6 @@ export async function generateFlowLockInternal(
|
||||
);
|
||||
}
|
||||
|
||||
const flowValue = (await yamlParseFile(
|
||||
folder! + SEP + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
|
||||
if (!justUpdateMetadataLock) {
|
||||
const changedScripts = [];
|
||||
@@ -131,7 +138,7 @@ export async function generateFlowLockInternal(
|
||||
workspace,
|
||||
flowValue.value,
|
||||
remote_path,
|
||||
rawWorkspaceDependencies
|
||||
filteredDeps
|
||||
);
|
||||
|
||||
const inlineScripts = extractInlineScriptsForFlows(
|
||||
@@ -152,7 +159,7 @@ export async function generateFlowLockInternal(
|
||||
}
|
||||
|
||||
hashes = await generateFlowHash(
|
||||
rawWorkspaceDependencies,
|
||||
filteredDeps,
|
||||
folder,
|
||||
opts.defaultTs
|
||||
);
|
||||
@@ -163,6 +170,25 @@ export async function generateFlowLockInternal(
|
||||
log.info(colors.green(`Flow ${remote_path} lockfiles updated`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters raw workspace dependencies for a flow by extracting all inline scripts,
|
||||
* filtering deps for each based on language and annotations, then computing the union.
|
||||
*/
|
||||
async function filterWorkspaceDependenciesForFlow(
|
||||
flowValue: FlowValue,
|
||||
rawWorkspaceDependencies: Record<string, string>,
|
||||
folder: string
|
||||
): Promise<Record<string, string>> {
|
||||
const inlineScripts = extractInlineScriptsForFlows(structuredClone(flowValue.modules), {}, SEP, undefined);
|
||||
|
||||
// Filter out lock files and map to common interface
|
||||
const scripts = inlineScripts
|
||||
.filter(s => !s.is_lock)
|
||||
.map(s => ({ content: s.content, language: s.language as ScriptLanguage }));
|
||||
|
||||
return await filterWorkspaceDependenciesForScripts(scripts, rawWorkspaceDependencies, folder, SEP);
|
||||
}
|
||||
|
||||
export async function updateFlow(
|
||||
workspace: Workspace,
|
||||
flow_value: FlowValue,
|
||||
@@ -191,10 +217,7 @@ export async function updateFlow(
|
||||
flow_value,
|
||||
path: remotePath,
|
||||
use_local_lockfiles: true,
|
||||
raw_workspace_dependencies:
|
||||
Object.keys(rawWorkspaceDependencies).length > 0
|
||||
? rawWorkspaceDependencies
|
||||
: null,
|
||||
raw_workspace_dependencies: rawWorkspaceDependencies,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2018,7 +2018,7 @@ export async function pull(
|
||||
log.info(`Updating lock metadata for raw app ${change}`);
|
||||
await generateAppLocksInternal(
|
||||
change,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
workspace,
|
||||
opts,
|
||||
|
||||
@@ -54,19 +54,90 @@ export async function getRawWorkspaceDependencies(): Promise<Record<string, stri
|
||||
} catch {
|
||||
// dependencies directory doesn't exist
|
||||
}
|
||||
return rawWorkspaceDeps;
|
||||
return rawWorkspaceDeps;
|
||||
}
|
||||
|
||||
export function workspaceDependenciesPathToLanguageAndFilename(path: string): { name: string | undefined, language: ScriptLanguage } | undefined {
|
||||
const relativePath = path.replace("dependencies/", "");
|
||||
for (const { filename, language } of workspaceDependenciesLanguages) {
|
||||
if (relativePath.endsWith(filename)) {
|
||||
return {
|
||||
name: relativePath === filename ? undefined : relativePath.replace("." + filename, ""),
|
||||
language
|
||||
};
|
||||
const relativePath = path.replace("dependencies/", "");
|
||||
for (const { filename, language } of workspaceDependenciesLanguages) {
|
||||
if (relativePath.endsWith(filename)) {
|
||||
return {
|
||||
name: relativePath === filename ? undefined : relativePath.replace("." + filename, ""),
|
||||
language
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters raw workspace dependencies to only include those that:
|
||||
* 1. Match the given script language
|
||||
* 2. Are referenced by the script's workspace dependency annotation, OR
|
||||
* are the default dependency (no name) when there's no annotation
|
||||
*/
|
||||
export function filterWorkspaceDependencies(
|
||||
rawWorkspaceDependencies: Record<string, string>,
|
||||
scriptContent: string,
|
||||
language: ScriptLanguage
|
||||
): Record<string, string> {
|
||||
const wda = extractWorkspaceDepsAnnotation(scriptContent, language);
|
||||
const filtered: Record<string, string> = {};
|
||||
|
||||
for (const [depPath, depContent] of Object.entries(rawWorkspaceDependencies)) {
|
||||
const depInfo = workspaceDependenciesPathToLanguageAndFilename(depPath);
|
||||
|
||||
if (depInfo && depInfo.language === language) {
|
||||
if ((wda && wda.external.includes(depInfo.name ?? "default")) || (wda == null && depInfo.name == undefined)) {
|
||||
filtered[depPath] = depContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export interface InlineScriptInfo {
|
||||
content: string;
|
||||
language: ScriptLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters workspace dependencies for multiple scripts, resolving !inline refs and computing union.
|
||||
* Common helper used by flows and apps.
|
||||
*/
|
||||
export async function filterWorkspaceDependenciesForScripts(
|
||||
scripts: InlineScriptInfo[],
|
||||
rawWorkspaceDependencies: Record<string, string>,
|
||||
folder: string,
|
||||
sep: string
|
||||
): Promise<Record<string, string>> {
|
||||
const filtered: Record<string, string> = {};
|
||||
|
||||
for (const script of scripts) {
|
||||
let content = script.content;
|
||||
|
||||
// Resolve !inline reference to actual content
|
||||
if (content.startsWith("!inline ")) {
|
||||
const filePath = folder + sep + content.replace("!inline ", "");
|
||||
try {
|
||||
content = await Deno.readTextFile(filePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const scriptFiltered = filterWorkspaceDependencies(
|
||||
rawWorkspaceDependencies,
|
||||
content,
|
||||
script.language
|
||||
);
|
||||
|
||||
for (const [depPath, depContent] of Object.entries(scriptFiltered)) {
|
||||
filtered[depPath] = depContent;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// on windows, when using powershell, blue is not readable
|
||||
@@ -95,14 +166,6 @@ export async function generateScriptMetadataInternal(
|
||||
|
||||
const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs);
|
||||
|
||||
// Filter workspace dependencies to only include those matching the script's language
|
||||
const filteredRawWorkspaceDependencies: Record<string, string> = {};
|
||||
for (const [depPath, depContent] of Object.entries(rawWorkspaceDependencies)) {
|
||||
const depInfo = workspaceDependenciesPathToLanguageAndFilename(depPath);
|
||||
if (depInfo && depInfo.language === language) {
|
||||
filteredRawWorkspaceDependencies[depPath] = depContent;
|
||||
}
|
||||
}
|
||||
|
||||
const metadataWithType = await parseMetadataFile(
|
||||
remotePath,
|
||||
@@ -112,7 +175,14 @@ export async function generateScriptMetadataInternal(
|
||||
// read script content
|
||||
const scriptContent = await Deno.readTextFile(scriptPath);
|
||||
const metadataContent = await Deno.readTextFile(metadataWithType.path);
|
||||
|
||||
|
||||
const filteredRawWorkspaceDependencies = filterWorkspaceDependencies(
|
||||
rawWorkspaceDependencies,
|
||||
scriptContent,
|
||||
language
|
||||
);
|
||||
|
||||
|
||||
// Note: rawWorkspaceDependencies are now passed in as parameter instead of being searched hierarchically
|
||||
let hash = await generateScriptHash(filteredRawWorkspaceDependencies, scriptContent, metadataContent);
|
||||
|
||||
@@ -671,12 +741,12 @@ export async function parseMetadataFile(
|
||||
scriptPath: string,
|
||||
generateMetadataIfMissing:
|
||||
| (GlobalOptions & {
|
||||
path: string;
|
||||
workspaceRemote: Workspace;
|
||||
schemaOnly?: boolean;
|
||||
rawWorkspaceDependencies: Record<string, string>;
|
||||
codebases: SyncCodebase[]
|
||||
})
|
||||
path: string;
|
||||
workspaceRemote: Workspace;
|
||||
schemaOnly?: boolean;
|
||||
rawWorkspaceDependencies: Record<string, string>;
|
||||
codebases: SyncCodebase[]
|
||||
})
|
||||
| undefined
|
||||
): Promise<{ isJson: boolean; payload: any; path: string }> {
|
||||
let metadataFilePath = scriptPath + ".script.json";
|
||||
|
||||
32
cli/test.nu
32
cli/test.nu
@@ -30,12 +30,12 @@ const languages = [
|
||||
];
|
||||
|
||||
def main [] {
|
||||
main test deno;
|
||||
main test languages deno;
|
||||
main build;
|
||||
main test node;
|
||||
main test languages node;
|
||||
}
|
||||
|
||||
def 'main test deno' [] {
|
||||
def 'main test languages deno' [] {
|
||||
main clean;
|
||||
print $"Testing Deno"
|
||||
$languages | each { |l|
|
||||
@@ -46,7 +46,7 @@ def 'main test deno' [] {
|
||||
print $"\nDeno has passed!\n"
|
||||
}
|
||||
|
||||
def 'main test node' [] {
|
||||
def 'main test languages node' [] {
|
||||
main clean;
|
||||
print $"Testing Node"
|
||||
$languages | each { |l|
|
||||
@@ -69,6 +69,30 @@ def 'main build' [] {
|
||||
cd ../local
|
||||
}
|
||||
|
||||
alias wm-cli = deno run -A --no-check ../cli/src/main.ts
|
||||
|
||||
|
||||
def 'main setup workspace_deps' [] {
|
||||
mkdir f/workspace_deps
|
||||
mkdir dependencies
|
||||
|
||||
wm-cli script bootstrap f/workspace_deps/test python3
|
||||
"# requirements: test" | save -f f/workspace_deps/test.py
|
||||
|
||||
wm-cli script bootstrap f/workspace_deps/demo python3
|
||||
"# requirements: demo" | save -f f/workspace_deps/demo.py
|
||||
|
||||
wm-cli script bootstrap f/workspace_deps/default python3
|
||||
"# requirements: default" | save -f f/workspace_deps/default.py
|
||||
|
||||
# wm-cli script bootstrap f/workspace_deps/default_and_demo python3
|
||||
# "# requirements: default" | save -f f/workspace_deps/default_and_demo.py
|
||||
|
||||
"tiny" | save -f dependencies/test.requirements.in
|
||||
"tiny" | save -f dependencies/demo.requirements.in
|
||||
"tiny" | save -f dependencies/requirements.in
|
||||
}
|
||||
|
||||
# def main [] {
|
||||
# main clean;
|
||||
# $languages | each { |l|
|
||||
|
||||
600
cli/test/workspace_deps_filter.test.ts
Normal file
600
cli/test/workspace_deps_filter.test.ts
Normal file
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* Workspace Dependencies Filtering Tests
|
||||
*
|
||||
* Tests that workspace dependencies are correctly filtered based on:
|
||||
* 1. Script language (only matching language deps)
|
||||
* 2. Annotations (explicit deps vs default)
|
||||
*
|
||||
* These tests use --dry-run mode to avoid needing workers to generate locks.
|
||||
* We pre-compute hashes to simulate an initial state, then verify that
|
||||
* changing specific deps only marks the expected scripts as stale.
|
||||
*/
|
||||
|
||||
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { addWorkspace } from "../workspace.ts";
|
||||
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
|
||||
import { stringify as stringifyYaml } from "jsr:@std/yaml";
|
||||
|
||||
// Import hash generation utilities from CLI
|
||||
import { generateHash } from "../src/utils/utils.ts";
|
||||
import { filterWorkspaceDependencies } from "../src/utils/metadata.ts";
|
||||
import { filterWorkspaceDependenciesForApp } from "../src/commands/app/app_metadata.ts";
|
||||
|
||||
/**
|
||||
* Generate a script hash the same way the CLI does
|
||||
*/
|
||||
async function generateScriptHash(
|
||||
filteredWorkspaceDependencies: Record<string, string>,
|
||||
scriptContent: string,
|
||||
metadataContent: string
|
||||
): Promise<string> {
|
||||
return await generateHash(
|
||||
JSON.stringify(filteredWorkspaceDependencies) + scriptContent + metadataContent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create wmill-lock.yaml content
|
||||
*/
|
||||
function createLockfile(locks: Record<string, string>): string {
|
||||
return stringifyYaml({ version: "v2", locks });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test 1: Scripts - changing default dep only marks scripts without annotation as stale
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "Workspace deps: Scripts - dry-run shows correct stale scripts when default dep changes",
|
||||
ignore: false,
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
fn: async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_deps_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []`);
|
||||
|
||||
// Setup dependencies folder (Bun/TypeScript)
|
||||
await ensureDir(`${tempDir}/dependencies`);
|
||||
const defaultDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
const explicitDep = `{"dependencies": {"axios": "1.6.0"}}`;
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultDep);
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, explicitDep);
|
||||
|
||||
// Setup script folder
|
||||
await ensureDir(`${tempDir}/f/test`);
|
||||
|
||||
// Script 1: No annotation - uses default dep
|
||||
const script1Content = `export async function main() {
|
||||
return "uses default deps";
|
||||
}
|
||||
`;
|
||||
const script1Metadata = `summary: "uses default"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/uses_default.ts`, script1Content);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/uses_default.script.yaml`, script1Metadata);
|
||||
|
||||
// Script 2: Uses explicit dep (TypeScript/Bun with annotation)
|
||||
const script2Content = `// package_json: explicit
|
||||
export async function main() {
|
||||
return "uses explicit deps";
|
||||
}
|
||||
`;
|
||||
const script2Metadata = `summary: "uses explicit"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/uses_explicit.ts`, script2Content);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/uses_explicit.script.yaml`, script2Metadata);
|
||||
|
||||
// Build raw workspace dependencies map (as the CLI would)
|
||||
const rawWorkspaceDeps: Record<string, string> = {
|
||||
"dependencies/package.json": defaultDep,
|
||||
"dependencies/explicit.package.json": explicitDep,
|
||||
};
|
||||
|
||||
// Filter deps for each script and compute hashes
|
||||
const script1FilteredDeps = filterWorkspaceDependencies(rawWorkspaceDeps, script1Content, "bun");
|
||||
const script2FilteredDeps = filterWorkspaceDependencies(rawWorkspaceDeps, script2Content, "bun");
|
||||
|
||||
const script1Hash = await generateScriptHash(script1FilteredDeps, script1Content, script1Metadata);
|
||||
const script2Hash = await generateScriptHash(script2FilteredDeps, script2Content, script2Metadata);
|
||||
|
||||
// Create initial wmill-lock.yaml with these hashes
|
||||
await Deno.writeTextFile(`${tempDir}/wmill-lock.yaml`, createLockfile({
|
||||
"f/test/uses_default": script1Hash,
|
||||
"f/test/uses_explicit": script2Hash,
|
||||
}));
|
||||
|
||||
// Verify initial state - both scripts should be up-to-date
|
||||
const initialResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/uses_default*,f/test/uses_explicit*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_test"
|
||||
);
|
||||
assertEquals(initialResult.code, 0, `Initial dry-run should succeed: ${initialResult.stderr}`);
|
||||
assertStringIncludes(initialResult.stdout, "No metadata to update",
|
||||
`Initial state should show no updates needed. Output: ${initialResult.stdout}`);
|
||||
|
||||
// Now change package.json (default dep)
|
||||
const newDefaultDep = `{"dependencies": {"lodash": "4.17.22"}}`;
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, newDefaultDep);
|
||||
|
||||
// Run dry-run again
|
||||
const afterDefaultChangeResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/uses_default*,f/test/uses_explicit*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_test"
|
||||
);
|
||||
assertEquals(afterDefaultChangeResult.code, 0, `Dry-run should succeed: ${afterDefaultChangeResult.stderr}`);
|
||||
|
||||
// uses_default should be stale (uses default dep which changed)
|
||||
assertStringIncludes(afterDefaultChangeResult.stdout, "uses_default",
|
||||
`uses_default should be marked stale after default dep change. Output: ${afterDefaultChangeResult.stdout}`);
|
||||
|
||||
// uses_explicit should NOT be stale (uses explicit dep, not default)
|
||||
assert(!afterDefaultChangeResult.stdout.includes("uses_explicit"),
|
||||
`uses_explicit should NOT be marked stale after default dep change. Output: ${afterDefaultChangeResult.stdout}`);
|
||||
|
||||
// Reset and test the reverse: change explicit dep
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultDep); // restore original
|
||||
const newExplicitDep = `{"dependencies": {"axios": "1.6.1"}}`;
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, newExplicitDep);
|
||||
|
||||
// Run dry-run again
|
||||
const afterExplicitChangeResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/uses_default*,f/test/uses_explicit*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_test"
|
||||
);
|
||||
assertEquals(afterExplicitChangeResult.code, 0, `Dry-run should succeed: ${afterExplicitChangeResult.stderr}`);
|
||||
|
||||
// uses_explicit should be stale (uses explicit dep which changed)
|
||||
assertStringIncludes(afterExplicitChangeResult.stdout, "uses_explicit",
|
||||
`uses_explicit should be marked stale after explicit dep change. Output: ${afterExplicitChangeResult.stdout}`);
|
||||
|
||||
// uses_default should NOT be stale (uses default dep, not explicit)
|
||||
assert(!afterExplicitChangeResult.stdout.includes("uses_default"),
|
||||
`uses_default should NOT be marked stale after explicit dep change. Output: ${afterExplicitChangeResult.stdout}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 2: Flows - filterWorkspaceDependenciesForScripts correctly filters by annotation
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "Workspace deps: Flows - filterWorkspaceDependenciesForScripts correctly filters inline scripts",
|
||||
ignore: false,
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
fn: async () => {
|
||||
// This test verifies the filtering logic used by flows without needing workers
|
||||
// We test filterWorkspaceDependenciesForScripts directly since flow generate-locks
|
||||
// doesn't have a --dry-run option
|
||||
|
||||
const defaultDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
const explicitDep = `{"dependencies": {"axios": "1.6.0"}}`;
|
||||
|
||||
const rawWorkspaceDeps: Record<string, string> = {
|
||||
"dependencies/package.json": defaultDep,
|
||||
"dependencies/explicit.package.json": explicitDep,
|
||||
};
|
||||
|
||||
// Script using default dep (no annotation)
|
||||
const defaultScriptContent = `export async function main() {
|
||||
return "uses default deps";
|
||||
}
|
||||
`;
|
||||
|
||||
// Script using explicit dep (with annotation)
|
||||
const explicitScriptContent = `// package_json: explicit
|
||||
export async function main() {
|
||||
return "uses explicit deps";
|
||||
}
|
||||
`;
|
||||
|
||||
// Filter for default script - should only include default dep
|
||||
const defaultFiltered = filterWorkspaceDependencies(rawWorkspaceDeps, defaultScriptContent, "bun");
|
||||
assertEquals(Object.keys(defaultFiltered).length, 1,
|
||||
`Default script should have 1 filtered dep, got: ${JSON.stringify(defaultFiltered)}`);
|
||||
assert("dependencies/package.json" in defaultFiltered,
|
||||
`Default script should have package.json`);
|
||||
assert(!("dependencies/explicit.package.json" in defaultFiltered),
|
||||
`Default script should NOT have explicit.package.json`);
|
||||
|
||||
// Filter for explicit script - should only include explicit dep
|
||||
const explicitFiltered = filterWorkspaceDependencies(rawWorkspaceDeps, explicitScriptContent, "bun");
|
||||
assertEquals(Object.keys(explicitFiltered).length, 1,
|
||||
`Explicit script should have 1 filtered dep, got: ${JSON.stringify(explicitFiltered)}`);
|
||||
assert("dependencies/explicit.package.json" in explicitFiltered,
|
||||
`Explicit script should have explicit.package.json`);
|
||||
assert(!("dependencies/package.json" in explicitFiltered),
|
||||
`Explicit script should NOT have package.json`);
|
||||
|
||||
// Verify hashes change correctly when deps change
|
||||
const defaultHash1 = await generateScriptHash(defaultFiltered, defaultScriptContent, "metadata");
|
||||
const explicitHash1 = await generateScriptHash(explicitFiltered, explicitScriptContent, "metadata");
|
||||
|
||||
// Change default dep
|
||||
const newDefaultDep = `{"dependencies": {"lodash": "4.17.22"}}`;
|
||||
const newRawWorkspaceDeps1: Record<string, string> = {
|
||||
"dependencies/package.json": newDefaultDep,
|
||||
"dependencies/explicit.package.json": explicitDep,
|
||||
};
|
||||
|
||||
const defaultFiltered2 = filterWorkspaceDependencies(newRawWorkspaceDeps1, defaultScriptContent, "bun");
|
||||
const explicitFiltered2 = filterWorkspaceDependencies(newRawWorkspaceDeps1, explicitScriptContent, "bun");
|
||||
|
||||
const defaultHash2 = await generateScriptHash(defaultFiltered2, defaultScriptContent, "metadata");
|
||||
const explicitHash2 = await generateScriptHash(explicitFiltered2, explicitScriptContent, "metadata");
|
||||
|
||||
// Default script hash should change (its dep changed)
|
||||
assert(defaultHash1 !== defaultHash2,
|
||||
`Default script hash should change when default dep changes`);
|
||||
|
||||
// Explicit script hash should NOT change (its dep didn't change)
|
||||
assertEquals(explicitHash1, explicitHash2,
|
||||
`Explicit script hash should NOT change when default dep changes`);
|
||||
|
||||
// Now change explicit dep
|
||||
const newExplicitDep = `{"dependencies": {"axios": "1.6.1"}}`;
|
||||
const newRawWorkspaceDeps2: Record<string, string> = {
|
||||
"dependencies/package.json": defaultDep, // back to original
|
||||
"dependencies/explicit.package.json": newExplicitDep,
|
||||
};
|
||||
|
||||
const defaultFiltered3 = filterWorkspaceDependencies(newRawWorkspaceDeps2, defaultScriptContent, "bun");
|
||||
const explicitFiltered3 = filterWorkspaceDependencies(newRawWorkspaceDeps2, explicitScriptContent, "bun");
|
||||
|
||||
const defaultHash3 = await generateScriptHash(defaultFiltered3, defaultScriptContent, "metadata");
|
||||
const explicitHash3 = await generateScriptHash(explicitFiltered3, explicitScriptContent, "metadata");
|
||||
|
||||
// Default script hash should be back to original (dep is back to original)
|
||||
assertEquals(defaultHash1, defaultHash3,
|
||||
`Default script hash should be same as original when dep reverts`);
|
||||
|
||||
// Explicit script hash should change (its dep changed)
|
||||
assert(explicitHash1 !== explicitHash3,
|
||||
`Explicit script hash should change when explicit dep changes`);
|
||||
},
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 3: Cross-language isolation - Python dep change doesn't affect Bun script
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "Workspace deps: Cross-language - Python dep change doesn't affect Bun script",
|
||||
ignore: false,
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
fn: async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_deps_cross_lang_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []`);
|
||||
|
||||
// Setup dependencies folder with deps for multiple languages
|
||||
await ensureDir(`${tempDir}/dependencies`);
|
||||
const pythonDep = "requests==2.31.0";
|
||||
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep);
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, bunDep);
|
||||
|
||||
// Setup script folder
|
||||
await ensureDir(`${tempDir}/f/test`);
|
||||
|
||||
// Python script
|
||||
const pythonContent = `def main():
|
||||
return "python script"
|
||||
`;
|
||||
const pythonMetadata = `summary: "python script"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/python_script.py`, pythonContent);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/python_script.script.yaml`, pythonMetadata);
|
||||
|
||||
// Bun script
|
||||
const bunContent = `export async function main() {
|
||||
return "bun script";
|
||||
}
|
||||
`;
|
||||
const bunMetadata = `summary: "bun script"
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
lock: ""
|
||||
`;
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/bun_script.ts`, bunContent);
|
||||
await Deno.writeTextFile(`${tempDir}/f/test/bun_script.script.yaml`, bunMetadata);
|
||||
|
||||
// Build raw workspace dependencies map
|
||||
const rawWorkspaceDeps: Record<string, string> = {
|
||||
"dependencies/requirements.in": pythonDep,
|
||||
"dependencies/package.json": bunDep,
|
||||
};
|
||||
|
||||
// Filter deps for each script - this is where language filtering happens
|
||||
const pythonFilteredDeps = filterWorkspaceDependencies(rawWorkspaceDeps, pythonContent, "python3");
|
||||
const bunFilteredDeps = filterWorkspaceDependencies(rawWorkspaceDeps, bunContent, "bun");
|
||||
|
||||
// Python script should only get requirements.in
|
||||
assertEquals(Object.keys(pythonFilteredDeps).length, 1,
|
||||
`Python script should only have 1 filtered dep, got: ${JSON.stringify(pythonFilteredDeps)}`);
|
||||
assert("dependencies/requirements.in" in pythonFilteredDeps,
|
||||
`Python script should have requirements.in in filtered deps`);
|
||||
|
||||
// Bun script should only get package.json
|
||||
assertEquals(Object.keys(bunFilteredDeps).length, 1,
|
||||
`Bun script should only have 1 filtered dep, got: ${JSON.stringify(bunFilteredDeps)}`);
|
||||
assert("dependencies/package.json" in bunFilteredDeps,
|
||||
`Bun script should have package.json in filtered deps`);
|
||||
|
||||
// Compute initial hashes
|
||||
const pythonHash = await generateScriptHash(pythonFilteredDeps, pythonContent, pythonMetadata);
|
||||
const bunHash = await generateScriptHash(bunFilteredDeps, bunContent, bunMetadata);
|
||||
|
||||
// Create initial wmill-lock.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill-lock.yaml`, createLockfile({
|
||||
"f/test/python_script": pythonHash,
|
||||
"f/test/bun_script": bunHash,
|
||||
}));
|
||||
|
||||
// Verify initial state - both scripts should be up-to-date
|
||||
const initialResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/python_script*,f/test/bun_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_cross_lang_test"
|
||||
);
|
||||
assertEquals(initialResult.code, 0, `Initial dry-run should succeed: ${initialResult.stderr}`);
|
||||
assertStringIncludes(initialResult.stdout, "No metadata to update",
|
||||
`Initial state should show no updates needed. Output: ${initialResult.stdout}`);
|
||||
|
||||
// Change Python dep (requirements.in)
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, "requests==2.32.0");
|
||||
|
||||
// Run dry-run
|
||||
const afterPythonChangeResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/python_script*,f/test/bun_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_cross_lang_test"
|
||||
);
|
||||
assertEquals(afterPythonChangeResult.code, 0, `Dry-run should succeed: ${afterPythonChangeResult.stderr}`);
|
||||
|
||||
// python_script should be stale
|
||||
assertStringIncludes(afterPythonChangeResult.stdout, "python_script",
|
||||
`python_script should be marked stale after Python dep change. Output: ${afterPythonChangeResult.stdout}`);
|
||||
|
||||
// bun_script should NOT be stale (different language)
|
||||
assert(!afterPythonChangeResult.stdout.includes("bun_script"),
|
||||
`bun_script should NOT be marked stale after Python dep change. Output: ${afterPythonChangeResult.stdout}`);
|
||||
|
||||
// Reset and test the reverse
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep);
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, `{"dependencies": {"lodash": "4.17.22"}}`);
|
||||
|
||||
const afterBunChangeResult = await backend.runCLICommand(
|
||||
["script", "generate-metadata", "-i", "f/test/python_script*,f/test/bun_script*", "--yes", "--dry-run"],
|
||||
tempDir,
|
||||
"workspace_deps_cross_lang_test"
|
||||
);
|
||||
assertEquals(afterBunChangeResult.code, 0, `Dry-run should succeed: ${afterBunChangeResult.stderr}`);
|
||||
|
||||
// bun_script should be stale
|
||||
assertStringIncludes(afterBunChangeResult.stdout, "bun_script",
|
||||
`bun_script should be marked stale after Bun dep change. Output: ${afterBunChangeResult.stdout}`);
|
||||
|
||||
// python_script should NOT be stale (different language)
|
||||
assert(!afterBunChangeResult.stdout.includes("python_script"),
|
||||
`python_script should NOT be marked stale after Bun dep change. Output: ${afterBunChangeResult.stdout}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Test 4: Apps - Create app via API and test filterWorkspaceDependenciesForApp
|
||||
// =============================================================================
|
||||
|
||||
Deno.test({
|
||||
name: "Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API",
|
||||
ignore: false,
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
fn: async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
// Set up workspace
|
||||
const testWorkspace = {
|
||||
remote: backend.baseUrl,
|
||||
workspaceId: backend.workspace,
|
||||
name: "workspace_deps_app_test",
|
||||
token: backend.token
|
||||
};
|
||||
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
|
||||
|
||||
// Create wmill.yaml
|
||||
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
|
||||
includes:
|
||||
- "**"
|
||||
excludes: []`);
|
||||
|
||||
// Create app with multiple inline scripts via backend API
|
||||
const appPath = "f/test/multi_script_app";
|
||||
const appValue = {
|
||||
type: "app",
|
||||
grid: [
|
||||
{
|
||||
id: "default_bun",
|
||||
data: {
|
||||
type: "buttoncomponent",
|
||||
componentInput: {
|
||||
type: "runnable",
|
||||
runnable: {
|
||||
type: "runnableByName",
|
||||
inlineScript: {
|
||||
content: `export async function main() { return "uses default bun deps"; }`,
|
||||
language: "bun",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "explicit_bun",
|
||||
data: {
|
||||
type: "buttoncomponent",
|
||||
componentInput: {
|
||||
type: "runnable",
|
||||
runnable: {
|
||||
type: "runnableByName",
|
||||
inlineScript: {
|
||||
content: `// package_json: explicit\nexport async function main() { return "uses explicit bun deps"; }`,
|
||||
language: "bun",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "python_script",
|
||||
data: {
|
||||
type: "buttoncomponent",
|
||||
componentInput: {
|
||||
type: "runnable",
|
||||
runnable: {
|
||||
type: "runnableByName",
|
||||
inlineScript: {
|
||||
content: `def main():\n return "python script"`,
|
||||
language: "python3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
hiddenInlineScripts: [],
|
||||
css: {},
|
||||
norefreshbar: false,
|
||||
};
|
||||
|
||||
// Create app via API
|
||||
const createResponse = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/apps/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: appPath,
|
||||
value: appValue,
|
||||
summary: "Multi-script app for workspace deps testing",
|
||||
policy: {
|
||||
on_behalf_of: null,
|
||||
on_behalf_of_email: null,
|
||||
triggerables: {},
|
||||
execution_mode: "viewer",
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
assertEquals(createResponse.ok, true, `Failed to create app: ${await createResponse.text()}`);
|
||||
|
||||
// Pull the app to disk
|
||||
const pullResult = await backend.runCLICommand(
|
||||
["sync", "pull", "--yes"],
|
||||
tempDir,
|
||||
"workspace_deps_app_test"
|
||||
);
|
||||
assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`);
|
||||
|
||||
// Setup workspace dependencies
|
||||
await ensureDir(`${tempDir}/dependencies`);
|
||||
const defaultBunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
|
||||
const explicitBunDep = `{"dependencies": {"axios": "1.6.0"}}`;
|
||||
const pythonDep = "requests==2.31.0";
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/package.json`, defaultBunDep);
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/explicit.package.json`, explicitBunDep);
|
||||
await Deno.writeTextFile(`${tempDir}/dependencies/requirements.in`, pythonDep);
|
||||
|
||||
// Read the pulled app.yaml
|
||||
const { yamlParseFile } = await import("../deps.ts");
|
||||
const appFilePath = `${tempDir}/${appPath}.app/app.yaml`;
|
||||
const appFile = await yamlParseFile(appFilePath);
|
||||
|
||||
const rawWorkspaceDeps: Record<string, string> = {
|
||||
"dependencies/package.json": defaultBunDep,
|
||||
"dependencies/explicit.package.json": explicitBunDep,
|
||||
"dependencies/requirements.in": pythonDep,
|
||||
};
|
||||
|
||||
// Call the app-specific filtering API
|
||||
const filteredDeps = await filterWorkspaceDependenciesForApp(
|
||||
appFile.value,
|
||||
rawWorkspaceDeps,
|
||||
`${tempDir}/${appPath}.app`
|
||||
);
|
||||
|
||||
// Verify all 3 dep types are included
|
||||
assertEquals(Object.keys(filteredDeps).length, 3,
|
||||
`App with bun (default), bun (explicit), and python should have 3 filtered deps, got: ${JSON.stringify(filteredDeps)}`);
|
||||
assert("dependencies/package.json" in filteredDeps,
|
||||
`Should include default package.json for default bun script`);
|
||||
assert("dependencies/explicit.package.json" in filteredDeps,
|
||||
`Should include explicit.package.json for annotated bun script`);
|
||||
assert("dependencies/requirements.in" in filteredDeps,
|
||||
`Should include requirements.in for python script`);
|
||||
|
||||
// Verify hash changes when deps change
|
||||
const hash1 = await generateHash(JSON.stringify(filteredDeps));
|
||||
|
||||
const newDefaultBunDep = `{"dependencies": {"lodash": "4.17.22"}}`;
|
||||
const newRawDeps: Record<string, string> = {
|
||||
"dependencies/package.json": newDefaultBunDep,
|
||||
"dependencies/explicit.package.json": explicitBunDep,
|
||||
"dependencies/requirements.in": pythonDep,
|
||||
};
|
||||
|
||||
const filteredDeps2 = await filterWorkspaceDependenciesForApp(
|
||||
appFile.value,
|
||||
newRawDeps,
|
||||
`${tempDir}/${appPath}.app`
|
||||
);
|
||||
|
||||
const hash2 = await generateHash(JSON.stringify(filteredDeps2));
|
||||
assert(hash1 !== hash2, `Hash should change when filtered deps change`);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import { newPathAssigner, PathAssigner } from "../path-utils/path-assigner.ts";
|
||||
import { FlowModule, RawScript } from "../gen/types.gen.ts";
|
||||
import { FlowModule, RawScript, ScriptLang } from "../gen/types.gen.ts";
|
||||
|
||||
/**
|
||||
* Represents an inline script extracted from a flow module
|
||||
*/
|
||||
interface InlineScript {
|
||||
export interface InlineScript {
|
||||
/** File path where the script content should be written */
|
||||
path: string;
|
||||
/** The actual script content */
|
||||
content: string;
|
||||
/** The script language */
|
||||
language: ScriptLang;
|
||||
is_lock: boolean;
|
||||
}
|
||||
|
||||
function extractRawscriptInline(
|
||||
@@ -21,14 +24,15 @@ function extractRawscriptInline(
|
||||
): InlineScript[] {
|
||||
const [basePath, ext] = assigner.assignPath(summary ?? id, rawscript.language);
|
||||
const path = mapping[id] ?? basePath + ext;
|
||||
const language = rawscript.language;
|
||||
const content = rawscript.content;
|
||||
const r = [{ path: path, content: content }];
|
||||
const r = [{ path: path, content: content, language, is_lock: false}];
|
||||
rawscript.content = "!inline " + path.replaceAll(separator, "/");
|
||||
const lock = rawscript.lock;
|
||||
if (lock && lock != "") {
|
||||
const lockPath = basePath + "lock";
|
||||
rawscript.lock = "!inline " + lockPath.replaceAll(separator, "/");
|
||||
r.push({ path: lockPath, content: lock });
|
||||
r.push({ path: lockPath, content: lock, language, is_lock: true});
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user