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
253 changes: 152 additions & 101 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions packages/edge-bundler/node/bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,26 @@ describe.skipIf(lt(denoVersion, '2.4.3'))(
await rm(vendorDirectory.path, { force: true, recursive: true })
})

test('Tarball bundling succeeds when edge functions use import assertions', async () => {
const fixtures = ['with_deno_1x_features', 'with_import_assertions'] as const

for (const fixtureName of fixtures) {
const { basePath, cleanup, distPath } = await useFixture(fixtureName, { copyDirectory: true })
const sourceDirectory = join(basePath, 'functions')

await expect(
bundle([sourceDirectory], distPath, [], {
basePath,
featureFlags: {
edge_bundler_generate_tarball: true,
},
}),
).resolves.toBeDefined()

await cleanup()
}
})

describe('Dry-run tarball generation flag enabled', () => {
test('Logs success message when tarball generation succeeded', async () => {
const systemLogger = vi.fn()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { describe, test, expect } from 'vitest'
import dedent from 'dedent'
import { transformImportAssertionsToAttributes } from './import-assertions-to-attributes.js'

describe('transformImportAssertionsToAttributes', () => {
const cases = [
{
name: 'rewrites static import assertions to `with` syntax',
input: "import data from './data.json' assert { type: 'json' }",
expected: "import data from './data.json' with { type: 'json' };",
},
{
name: 'rewrites export named assertions',
input: "export { data } from './data.json' assert { type: 'json' }",
expected: "export { data } from './data.json' with { type: 'json' };",
},
{
name: 'rewrites export all assertions',
input: "export * from './data.json' assert { type: 'json' }",
expected: "export * from './data.json' with { type: 'json' };",
},
{
name: 'rewrites dynamic import assertions with identifier keys',
input: "await import('./foo.json', { assert: { type: 'json' } })",
expected: dedent`
await import('./foo.json', {
with: {
type: 'json'
}
});
`,
},
{
name: 'rewrites dynamic import assertions with string literal keys',
input: 'await import("./foo.json", { "assert": { type: "json" } })',
expected: dedent`
await import("./foo.json", {
"with": {
type: "json"
}
});
`,
},
{
name: 'leaves dynamic imports without options untouched',
input: "await import('./foo.json');",
expected: "await import('./foo.json');",
},
{
name: 'skips dynamic imports when options are not an object expression',
input: "await import('./foo.json', null);",
expected: "await import('./foo.json', null);",
},
{
name: 'leaves static imports without assertions untouched',
input: "import data from './data.json';",
expected: "import data from './data.json';",
},
{
name: 'leaves static imports already using `with` syntax untouched',
input: "import data from './data.json' with { type: 'json' };",
expected: "import data from './data.json' with { type: 'json' };",
},
{
name: 'rewrites multiple import assertions',
input: "import data from './data.json' assert { type: 'json', foo: 'bar' }",
expected: "import data from './data.json' with { type: 'json', foo: 'bar' };",
},
{
name: 'leaves export named without assertions untouched',
input: "export { data } from './data.json';",
expected: "export { data } from './data.json';",
},
{
name: 'leaves export all without assertions untouched',
input: "export * from './data.json';",
expected: "export * from './data.json';",
},
{
name: 'handles dynamic imports with `with` already present',
input: "await import('./foo.json', { with: { type: 'json' } });",
expected: dedent`
await import('./foo.json', {
with: {
type: 'json'
}
});
`,
},
{
name: 'preserves other properties alongside rewritten assert in dynamic imports',
input: "await import('./foo.json', { assert: { type: 'json' }, cache: true })",
expected: dedent`
await import('./foo.json', {
with: {
type: 'json'
},
cache: true
});
`,
},
{
name: 'skips spread elements in dynamic import options',
input: "const opts = { type: 'json' }; await import('./foo.json', { ...opts });",
expected: dedent`
const opts = {
type: 'json'
};
await import('./foo.json', {
...opts
});
`,
},
{
name: 'handles TypeScript code with import assertions',
input: "import data from './data.json' assert { type: 'json' };\nconst x: string = data.name;",
expected: dedent`
import data from './data.json' with { type: 'json' };
const x: string = data.name;
`,
},
{
name: 'handles JSX code with import assertions',
input: "import data from './data.json' assert { type: 'json' };\nconst el = <div>{data.name}</div>;",
expected: dedent`
import data from './data.json' with { type: 'json' };
const el = <div>{data.name}</div>;
`,
},
{
name: 'handles multiple imports in the same file',
input: dedent`
import a from './a.json' assert { type: 'json' };
import b from './b.json' assert { type: 'json' };
import c from './c.js';
`,
expected: dedent`
import a from './a.json' with { type: 'json' };
import b from './b.json' with { type: 'json' };
import c from './c.js';
`,
},
{
name: 'handles mixed static and dynamic imports',
input: dedent`
import data from './data.json' assert { type: 'json' };
const other = await import('./other.json', { assert: { type: 'json' } });
`,
expected: dedent`
import data from './data.json' with { type: 'json' };
const other = await import('./other.json', {
with: {
type: 'json'
}
});
`,
},
{
name: 'does not modify non-assert properties in dynamic import options',
input: "await import('./foo.json', { cache: 'force-cache' });",
expected: dedent`
await import('./foo.json', {
cache: 'force-cache'
});
`,
},
{
name: 'handles computed property keys in dynamic import options (skips them)',
input: "const key = 'assert'; await import('./foo.json', { [key]: { type: 'json' } });",
expected: dedent`
const key = 'assert';
await import('./foo.json', {
[key]: {
type: 'json'
}
});
`,
},
{
name: 'rewrites default export with assertions',
input: "export { default } from './data.json' assert { type: 'json' };",
expected: "export { default } from './data.json' with { type: 'json' };",
},
] as const

test.each(cases)('$name', ({ input, expected }) => {
expect(transformImportAssertionsToAttributes(input)).toBe(expected)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { parse } from '@babel/parser'
import traverse, { type NodePath } from '@babel/traverse'
import generate, { type GeneratorResult } from '@babel/generator'
import {
isExpression,
isIdentifier,
isImport,
isObjectExpression,
isObjectProperty,
isStringLiteral,
} from '@babel/types'
import type {
CallExpression,
Expression,
ExportAllDeclaration,
ExportNamedDeclaration,
Identifier,
ImportAttribute,
ImportDeclaration,
ImportExpression,
ObjectExpression,
ObjectMember,
ObjectProperty,
SpreadElement,
StringLiteral,
} from '@babel/types'

type HasImportAssertions = {
assertions?: ImportAttribute[] | null
attributes?: ImportAttribute[] | null
}

type ImportExpressionWithOptions = ImportExpression & {
options?: Expression | null
}

const isImportAttributesObject = (value: Expression | null | undefined): value is ObjectExpression =>
isObjectExpression(value)

const isImportAttributesProperty = (
value: ObjectMember | SpreadElement,
): value is ObjectProperty & { key: Identifier | StringLiteral } =>
isObjectProperty(value) && (isIdentifier(value.key) || isStringLiteral(value.key))

const rewriteImportOptions = (options: Expression | null | undefined): void => {
if (!isImportAttributesObject(options)) return

for (const prop of options.properties) {
if (!isImportAttributesProperty(prop)) continue

if (isIdentifier(prop.key) && prop.key.name === 'assert') {
prop.key.name = 'with'
} else if (isStringLiteral(prop.key) && prop.key.value === 'assert') {
prop.key.value = 'with'
}
}
}

const moveAssertionsToAttributes = (node: HasImportAssertions): void => {
const { assertions, attributes } = node
if (!assertions?.length) return

node.attributes = attributes?.length
? // Preserve any existing attributes before appending the migrated assertions.
[...attributes, ...assertions]
: Array.from(assertions)

node.assertions = []
}

/**
* Transform `assert` import attributes to `with` import attributes.
*
* - Static imports / re-exports: uses Babel's `assertions` -> `attributes` fields.
* - Dynamic imports: rewrites `{ assert: { ... } }` to `{ with: { ... } }`.
*/
export function transformImportAssertionsToAttributes(code: string): string {
const ast = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript', ['importAttributes', { deprecatedAssertSyntax: true }]],
})

traverse(ast, {
ImportDeclaration(path: NodePath<ImportDeclaration>) {
moveAssertionsToAttributes(path.node)
},

ExportNamedDeclaration(path: NodePath<ExportNamedDeclaration>) {
moveAssertionsToAttributes(path.node)
},

ExportAllDeclaration(path: NodePath<ExportAllDeclaration>) {
moveAssertionsToAttributes(path.node)
},

ImportExpression(path: NodePath<ImportExpressionWithOptions>) {
rewriteImportOptions(path.node.options)
},

CallExpression(path: NodePath<CallExpression>) {
if (!isImport(path.node.callee)) return

const [, options] = path.node.arguments

if (isExpression(options)) {
rewriteImportOptions(options)
}
},
})

const output = generate(
ast,
{
importAttributesKeyword: 'with',
},
code,
) as GeneratorResult

return output.code
}
Loading
Loading