diff --git a/docs/api.md b/docs/api.md index b511d473d..205d4d92a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -174,3 +174,22 @@ export default function () { [config]: ../README.md#%EF%B8%8F-configuration [docs-collections]: ./collections.md [rss]: #-rss-feed + +### Node builtins + +Astro aims to be compatible with multiple JavaScript runtimes in the future. This includes [Deno](https://deno.land/) and [Cloudflare Workers](https://workers.cloudflare.com/) which do not support Node builtin modules such as `fs`. We encourage Astro users to write their code as cross-environment as possible. + +Due to that, you cannot use Node modules that you're familiar with such as `fs` and `path`. Our aim is to provide alternative built in to Astro. If you're use case is not covered please let us know. + +However, if you *really* need to use these builtin modules we don't want to stop you. Node supports the `node:` prefix for importing builtins, and this is also supported by Astro. If you want to read a file, for example, you can do so like this: + +```jsx +--- +import fs from 'node:fs/promises'; + +const url = new URL('../../package.json', import.meta.url); +const json = await fs.readFile(url, 'utf-8'); +const data = JSON.parse(json); +--- + +Version: {data.version} \ No newline at end of file diff --git a/package.json b/package.json index f836f2974..c19e80a6f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "tools/*", "scripts", "www", - "docs-www" + "docs-www", + "packages/astro/test/fixtures/builtins/packages/*" ], "volta": { "node": "14.16.1", diff --git a/packages/astro/package.json b/packages/astro/package.json index df3f8541d..9d0ce7336 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -92,7 +92,7 @@ "sass": "^1.32.13", "shorthash": "^0.0.2", "slash": "^4.0.0", - "snowpack": "^3.6.0", + "snowpack": "^3.6.1", "source-map-support": "^0.5.19", "string-width": "^5.0.0", "tiny-glob": "^0.2.8", diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 145c20576..96de21796 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -22,6 +22,7 @@ import { renderMarkdown } from '@astrojs/markdown-support'; import { transform } from '../transform/index.js'; import { PRISM_IMPORT } from '../transform/prism.js'; import { positionAt } from '../utils'; +import { nodeBuiltinsSet } from '../../node_builtins.js'; import { readFileSync } from 'fs'; const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default; @@ -327,6 +328,9 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp for (const componentImport of componentImports) { const importUrl = componentImport.source.value; + if(nodeBuiltinsSet.has(importUrl)) { + throw new Error(`Node builtins must be prefixed with 'node:'. Use node:${importUrl} instead.`); + } for (const specifier of componentImport.specifiers) { const componentName = specifier.local.name; state.components.set(componentName, { diff --git a/packages/astro/src/external.ts b/packages/astro/src/external.ts index c3f99281f..c48d6d4e1 100644 --- a/packages/astro/src/external.ts +++ b/packages/astro/src/external.ts @@ -1,4 +1,5 @@ import { createRequire } from 'module'; +import { nodeBuiltinsMap } from './node_builtins.js'; const require = createRequire(import.meta.url); const pkg = require('../package.json'); @@ -17,7 +18,13 @@ const isAstroRenderer = (name: string) => { // These packages should NOT be built by `esinstall` // But might not be explicit dependencies of `astro` -const denyList = ['prismjs/components/index.js', '@vue/server-renderer', '@astrojs/markdown-support']; +const denyList = [ + 'prismjs/components/index.js', + '@vue/server-renderer', + '@astrojs/markdown-support', + 'node:fs/promises', + ...nodeBuiltinsMap.values() +]; export default Object.keys(pkg.dependencies) // Filter out packages that should be loaded threw Snowpack diff --git a/packages/astro/src/node_builtins.ts b/packages/astro/src/node_builtins.ts new file mode 100644 index 000000000..dad2afd96 --- /dev/null +++ b/packages/astro/src/node_builtins.ts @@ -0,0 +1,5 @@ + +import {builtinModules} from 'module'; + +export const nodeBuiltinsSet = new Set(builtinModules); +export const nodeBuiltinsMap = new Map(builtinModules.map(bareName => [bareName, 'node:' + bareName])); \ No newline at end of file diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts index db3940b92..477da6df8 100644 --- a/packages/astro/src/runtime.ts +++ b/packages/astro/src/runtime.ts @@ -22,6 +22,7 @@ import { debug, info } from './logger.js'; import { configureSnowpackLogger } from './snowpack-logger.js'; import { searchForPage } from './search.js'; import snowpackExternals from './external.js'; +import { nodeBuiltinsMap } from './node_builtins.js'; import { ConfigManager } from './config_manager.js'; interface RuntimeConfig { @@ -389,6 +390,9 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO knownEntrypoints, external: snowpackExternals, }, + alias: { + ...Object.fromEntries(nodeBuiltinsMap) + } }); snowpack = await startSnowpackServer( diff --git a/packages/astro/test/builtins.test.js b/packages/astro/test/builtins.test.js new file mode 100644 index 000000000..23625e6f7 --- /dev/null +++ b/packages/astro/test/builtins.test.js @@ -0,0 +1,26 @@ +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import { doc } from './test-utils.js'; +import { setup } from './helpers.js'; + +const Builtins = suite('Node builtins'); + +setup(Builtins, './fixtures/builtins'); + +Builtins('Can be used with the node: prefix', async ({ runtime }) => { + const result = await runtime.load('/'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + + assert.equal($('#version').text(), '1.2.0'); + assert.equal($('#dep-version').text(), '0.0.1'); +}); + +Builtins('Throw if using the non-prefixed version', async ({ runtime }) => { + const result = await runtime.load('/bare'); + assert.ok(result.error, 'Produced an error'); + assert.ok(/Use node:fs instead/.test(result.error.message)); +}); + +Builtins.run(); diff --git a/packages/astro/test/fixtures/builtins/package.json b/packages/astro/test/fixtures/builtins/package.json new file mode 100644 index 000000000..8923fbd49 --- /dev/null +++ b/packages/astro/test/fixtures/builtins/package.json @@ -0,0 +1,7 @@ +{ + "name": "@astrojs/astro-test-builtins", + "version": "1.2.0", + "dependencies": { + "@astrojs/astro-test-builtins-dep": "file:./packages/dep" + } +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/builtins/packages/dep/main.js b/packages/astro/test/fixtures/builtins/packages/dep/main.js new file mode 100644 index 000000000..577976428 --- /dev/null +++ b/packages/astro/test/fixtures/builtins/packages/dep/main.js @@ -0,0 +1,10 @@ +import fs from 'fs'; + +const readFile = fs.promises.readFile; + +export async function readJson(path) { + const json = await readFile(path, 'utf-8'); + const data = JSON.parse(json); + return data; +} + diff --git a/packages/astro/test/fixtures/builtins/packages/dep/package.json b/packages/astro/test/fixtures/builtins/packages/dep/package.json new file mode 100644 index 000000000..808f1aa4a --- /dev/null +++ b/packages/astro/test/fixtures/builtins/packages/dep/package.json @@ -0,0 +1,6 @@ +{ + "name": "@astrojs/astro-test-builtins-dep", + "version": "0.0.1", + "module": "main.js", + "main": "main.js" +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/builtins/snowpack.config.json b/packages/astro/test/fixtures/builtins/snowpack.config.json new file mode 100644 index 000000000..8f034781d --- /dev/null +++ b/packages/astro/test/fixtures/builtins/snowpack.config.json @@ -0,0 +1,3 @@ +{ + "workspaceRoot": "../../../../../" +} diff --git a/packages/astro/test/fixtures/builtins/src/components/Version.astro b/packages/astro/test/fixtures/builtins/src/components/Version.astro new file mode 100644 index 000000000..2f37ba69f --- /dev/null +++ b/packages/astro/test/fixtures/builtins/src/components/Version.astro @@ -0,0 +1,9 @@ +--- +import fs from 'node:fs/promises'; + +const url = new URL('../../package.json', import.meta.url); +const json = await fs.readFile(url, 'utf-8'); +const data = JSON.parse(json); +--- + +{data.version} \ No newline at end of file diff --git a/packages/astro/test/fixtures/builtins/src/pages/bare.astro b/packages/astro/test/fixtures/builtins/src/pages/bare.astro new file mode 100644 index 000000000..32af9fd04 --- /dev/null +++ b/packages/astro/test/fixtures/builtins/src/pages/bare.astro @@ -0,0 +1,12 @@ +--- +import fs from 'fs'; +--- + + +
+